A Node.js invoice email service that generates PDF invoices and sends them via Resend. Built as an interview demo project to showcase Resend's transactional email, scheduling, and webhook capabilities.
- PDF invoice generation with PDFKit
- Transactional email via Resend with PDF attachment
- Optional scheduled receipt email (using Resend's
scheduled_at) - Webhook handling for delivery events (delivered, bounced, complained)
- Svix signature verification for webhooks
- A Resend Account. Sign up here.
- A Resend API Key.
- A real email to send from.
onboarding@resend.devcan be used for the sender email. The resend.dev domain is available for testing, but with a restriction: it can only send to your own email address.- Resend provides test addresses like
delivered@resend.dev,bounced@resend.dev,complained@resend.dev, andsuppressed@resend.devto simulate different delivery scenarios.
Package manager: pnpm is recommended; npm can also be used.
git clone <repo>
cd resend-demo
pnpm install # or: npm install
cp .env.example .env
# Edit .env with your values
pnpm start # or: npm startConfirm the server is working by running:
curl http://localhost:{port}A successful response will look like:
{"status":"ok"}
Set the clientEmail to a real email address.
curl -X POST http://localhost:{port}/invoice \
-H "Content-Type: application/json" \
-d '{
"lineItems": [
{ "description": "Consulting", "quantity": 5, "rate": 150 },
{ "description": "Design review", "quantity": 2, "rate": 200 }
],
"clientName": "Acme Corp",
"clientEmail": "you@youremail.com",
"schedule_receipt": true,
"delay_minutes": 1
}'Use delay_minutes to control when the payment receipt is sent. Defaults to 1 if omitted.
| Field | Type | Default | Description |
|---|---|---|---|
delay_minutes |
number | 1 |
Minutes to wait before sending the payment receipt email |
Response:
{
"success": true,
"invoiceId": "INV-20260217-4823",
"invoice_total": 1150,
"from": "Resend <onboarding@resend.dev>",
"to": "you@youremail.com"
}| Method | Path | Description |
|---|---|---|
GET |
/ |
Health check |
POST |
/invoice |
Generate and send invoice email |
POST |
/webhooks/resend |
Receive Resend delivery events |
Resend uses Svix to sign webhook payloads. Every request to POST /webhooks/resend includes svix-id, svix-timestamp, and svix-signature headers. The server verifies these headers against WEBHOOK_SIGNING_SECRET before processing any event.
Subscribe to these in the Resend dashboard when adding your webhook endpoint:
| Event | Description |
|---|---|
email.bounced |
Permanently rejected by recipient server |
email.clicked |
Recipient clicked a link in the email |
email.complained |
Recipient marked as spam |
email.delivered |
Successfully delivered to recipient's mail server |
email.delivery_delayed |
Temporary delivery issue (inbox full, server transient) |
email.failed |
Send failed (invalid recipient, API issues, etc.) |
email.opened |
Recipient opened the email |
email.received |
Resend successfully received the email |
email.scheduled |
Email scheduled to be sent |
email.sent |
API accepted the request |
email.suppressed |
Email suppressed by Resend |
- Open the Resend Dashboard
- Go to Webhooks → Add Endpoint
- Enter your endpoint URL (e.g.
https://your-domain.com/webhooks/resend) - Copy the Signing Secret shown after creation
- Add it to your
.env:WEBHOOK_SECRET=whsec_...
401 Invalid signature? The signing secret is unique per webhook endpoint. If you changed your ngrok URL, you created a new endpoint—use the secret from that endpoint. No spaces/newlines when copying.
Use the port you've set in .env
ngrok http {port}Use the port your app runs on (see PORT in .env). Add the HTTPS URL + /webhooks/resend as your webhook endpoint in the Resend dashboard.
src/
index.js # Express app and routes
invoice.js # PDF generation (generateInvoicePDF, generateInvoiceId)
email.js # Email sending (sendInvoiceEmail, scheduleReceiptEmail)
.env.example # Environment variable template