POST /signupsends a welcome email with the Nuntly SDK.POST /webhooks/nuntlyreceives signed delivery events (email.delivered,email.bounced,email.complained).
@nuntly/sdk package, and the body parsers built into Express. They use the standard Nuntly client, which throws on failure, and let Express 5 forward the error.
Prerequisites
- A Nuntly account. The free plan includes 3,000 emails per month (100 per day) and needs no credit card. See account setup.
- A verified sending domain. See sending domains.
- An API key with the Sending only permission. See API keys. You store it in an environment variable in the next section.
- Node.js 24 or later. It runs TypeScript files through type stripping and loads
.envfiles with the built-in--env-fileflag. You don’t needtsx,ts-node, ordotenv.
Install and configure the SDK
Create a new project with ES modules enabled:.env file at the project root:
.env
src/nuntly.ts
Send your first email
In Express, send a transactional email from the route handler that triggers it. HerePOST /signup creates a user and sends a welcome email:
src/server.ts
yourdomain.com with your verified domain. The import path ends in .ts because Node.js loads the file as written, without a build step.
Start the server and call the route:
202 and the SDK returns its ID and first status:
queued status means Nuntly accepted the email for delivery. The recipient’s server answers later, and you see its answer in the delivery events below.
Errors without try/catch
Express 5 forwards a rejected promise from anasync handler to your error middleware (Express error handling), so it acts as the try/catch for every route. When nuntly.emails.send() fails with an HTTP error, the SDK throws a typed APIError subclass with status, code, and requestId. The error middleware turns it into a single 502 response and logs the request ID you can share with Nuntly support. See errors for the error codes.
Express 4 doesn’t forward rejected promises. Wrap each handler body in
try/catch and call next(err).Observe what happened
Delivery, bounces, and spam complaints happen after the202, on the recipient’s side. You can follow them in the dashboard for a single email, and receive them in your application with a webhook.
Email timeline in the dashboard
In the dashboard, open Emails, select the email by its ID, then click Email Timeline. The panel lists every event for that email in order, with its timestamp. The Events tab of the observability dashboard shows events across all your emails. Nuntly keeps email records and their events for 30 days by default. See data retention.Receive webhooks in Express
Nuntly signs every webhook request with HMAC-SHA256 (RFC 2104) over the signing timestamp and the raw request body, using your webhook signing secret. The SDK’sverifyWebhook function checks that signature and returns a typed event.
1
Create the webhook
In the dashboard, go to Webhooks and click Create Webhook. Enter your endpoint URL and select
email.delivered, email.bounced, and email.complained. See set up webhooks for every field.2
Save the signing secret
Copy the signing secret into
NUNTLY_WEBHOOK_SECRET in your .env file. The dashboard shows it once.3
Expose your local server
Nuntly sends webhooks to a public HTTPS URL. During development, expose
localhost:3000 with a tunnel such as ngrok or cloudflared, and use the tunnel URL as the endpoint.src/server.ts, above app.use(express.json()):
src/server.ts
verifyWebhook rejects a request when the signature doesn’t match or when its timestamp is more than 5 minutes away from the current time. Inside each case, TypeScript narrows event.data, so event.data.bounce.bounceType type-checks. event.data.id is the email ID that nuntly.emails.send() returned.
What each event means and what to do with it:
Return a
2xx before any slow work, and move that work to a queue. When your endpoint fails, Nuntly retries the delivery, and you can replay a failed delivery from the webhooks observability view. Retries mean the same event can arrive more than once, so deduplicate on the event id (prefixed evt_), not on event.data.id.
The full payload of every event is in sending events. The bounce details follow the delivery status notification format.
Production notes
Idempotency keys
An idempotency key is a unique value you send with a request. The API processes that request once, even when you or the SDK retry it. The SDK sends one for you. Eachnuntly.emails.send() call generates an Idempotency-Key and reuses it across its own retries, so a timeout followed by an automatic retry doesn’t send twice. Two separate calls get two different keys. A double-clicked signup button or a job that runs twice still sends two emails.
To deduplicate across calls, derive the key from your own data:
409 with the code idempotency_key_payload_mismatch. See idempotency keys.
Retries
By default, the SDK retries a failed request up to 2 times on408, 409, 429, 500, 502, 503, 504, and timeouts. It uses exponential backoff from 500 ms to 8 seconds and honors the Retry-After header. Other 4xx errors, such as a 422 for an invalid payload, mean the request itself is wrong, and the SDK doesn’t retry them.
To change the policy, set maxRetries or retry on the client:
src/nuntly.ts
Error handling
The error middleware above handlesAPIError, which covers every HTTP error from the API. A network failure throws APIConnectionError instead, and a timeout (60 seconds by default) throws its subclass APIConnectionTimeoutError. Neither extends APIError, so add a branch for them:
src/server.ts
Separate environments
Use a separate API key and a separate webhook for development, staging, and production. Each webhook has its own signing secret. A staging key then can’t send production traffic, and a staging endpoint never receives production events. Keep the values in each environment’s.env file or secret manager.
What not to log
Recipient addresses are personal data. Every address you write to your logs is another copy you have to protect and delete. The code above logs identifiers only: the email ID, the event type, and the SDKrequestId.
- Don’t log
req.bodyon the signup route. It contains the user’s address. - Don’t log a full webhook event.
event.data.to,event.data.from, andevent.data.bounce.bouncedRecipientscontain addresses. - Log
event.id,event.type, andevent.data.id. They are enough to find the email in the dashboard.
Next steps
Handle webhook events
Signature verification details and webhook best practices
Sending events
Payload reference for every sending event
Idempotency keys
Key formats, strategies, and error codes
SDK reference
Install and configure the Nuntly SDKs
Other frameworks
Send email from NestJS
Inject the client as a provider and send from a service
Send email from Next.js
Send from a Server Action and receive webhooks in a Route Handler
Send email from Spring Boot
Use the Spring Boot starter on Java 21
