@nuntly/sdk, edit four files from the nest new scaffold, and create one controller. The Nuntly client goes straight into the NestJS dependency injection container. The samples use the standard Nuntly client, which throws typed exceptions, and catch them with try/catch.
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, stored in the
NUNTLY_API_KEYenvironment variable. See API keys. - Node.js 20 or later, which the SDK requires.
Install and configure the SDK
1
Install the SDK
From your NestJS project root:The SDK has zero runtime dependencies and ships both ES module and CommonJS entry points.
2
Add your secrets to a .env file
Create The Nest CLI loads this file with the
.env at the project root and add it to .gitignore:.env
--env-file flag:3
Register the client as a provider
Register The factory runs when Nest boots the module. If the key is missing, the SDK throws
Nuntly with a factory provider in src/app.module.ts. The class is its own injection token, so any constructor can ask for it by type:src/app.module.ts
API key is required at startup, so a misconfigured deployment fails before the first send. See custom providers in the NestJS documentation for the factory pattern.NestJS 12 scaffolds an ES module project, so relative imports end in
.js and main.ts uses a top-level await. The NestJS 11 scaffold uses CommonJS: drop the .js suffix from relative imports and call bootstrap() without await.Send your first email
Put the send in a service and keep the controller thin.AppService receives the client through its constructor:
src/app.service.ts
frommust use your verified domain. It accepts a display name (Acme <hello@...>) in the RFC 5322 format.htmlandtexttogether give mail clients that block HTML a readable version.tagslabel the email. They show on its detail page in the dashboard and in every webhook event for it.idempotencyKeybinds the send to one user. See idempotency keys.
src/app.controller.ts
The scaffold’s
src/app.controller.spec.ts and test/app.e2e-spec.ts test the getHello() method this page replaces. Update or delete them before you run npm test.queued means Nuntly accepted the email. Delivery events arrive afterwards.
Observe what happened
After the API accepts an email, it moves through statuses such asqueued, processed, and sent, then reaches an outcome such as delivered, bounced, or complained.
Email timeline in the dashboard
In the dashboard, open Emails, select the email, and click Email Timeline to see each event with its timestamp. From code,nuntly.emails.events.list(id) returns the same history. It needs a Full access API key: a Sending only key cannot read emails.
Nuntly keeps emails and their events for your organization’s retention window, 30 days by default.
Receive webhooks in a NestJS controller
1
Create the webhook
Create a webhook in the dashboard as described in the webhooks guide. Use
https://your-api.example.com/webhooks/nuntly as the endpoint URL and select the email.* events you need. Copy the signing secret into NUNTLY_WEBHOOK_SECRET. The dashboard shows it only once.2
Keep the raw request body
Nuntly signs each request with HMAC-SHA256 over the signing timestamp, a dot, and the raw request body, and sends the result in the Nest still parses
webhook-signature header. NestJS parses JSON bodies before your controller runs, so enable rawBody in src/main.ts to keep the original bytes:src/main.ts
req.body for every route and also exposes the unparsed bytes as req.rawBody. See raw body in the NestJS documentation.3
Verify and handle events
verifyWebhook from the SDK checks the signature, rejects timestamps more than 5 minutes away from the current time, and returns a typed event:src/webhooks.controller.ts
WebhookEvent is a discriminated union, so TypeScript narrows event.data in each case. A missing or invalid signature returns 401 and logs the reason, for example Missing webhook signing secret or Webhook signature verification failed.2xx response as a successful delivery. NestJS answers POST routes with 201 by default; @HttpCode(200) makes the response explicit. Return quickly and queue any slow processing.
Nuntly retries failed deliveries, and the Replay button in the Webhooks tab of the observability dashboard sends an event again. Both can deliver the same event more than once, so deduplicate on the top-level event.id before acting on it.
For local testing, expose port 3000 through a tunnel such as ngrok and use the tunnel URL as the endpoint. The full list of events and payloads is in sending events.
Production notes
Idempotency keys
An idempotency key lets the API recognize a repeated request: a second request with the same key within 24 hours returns the first response instead of sending another email. The SDK generates a random key for eachemails.send call, which makes its own retries safe.
Your own code can still call send twice, for example when a job runs again after a restart, and each call gets a new random key. Derive the key from the business event instead, like welcome-${userId}. Keys are 8 to 255 characters. See idempotency keys for other strategies.
Retries
The SDK retries a failed request 2 times by default, with exponential backoff from 500 ms to 8 s plus jitter. It follows theRetry-After header when the API sends one.
A
409 is either idempotency_key_in_progress, which a retry resolves, or idempotency_key_payload_mismatch, which needs a new key. Change the defaults with the maxRetries and retry client options, globally or per request. Avoid adding your own retry loop on top of the SDK’s.
Error handling
EveryAPIError exposes status, code, title, and requestId. APIConnectionError covers timeouts and network failures, which have no HTTP status. The controller above logs these fields and returns 502 Bad Gateway for any failed send. Include the requestId when you contact support.
Separate environments
- Create one API key per environment, named after it (“Production”, “Staging”).
- In production, use a Sending only key restricted to your production domain. A leaked key can then send email but cannot read emails, change domains, or configure webhooks. See permissions.
- Create a separate webhook endpoint and signing secret per environment.
What not to log
Recipient email addresses are personal data. Your logs live in a separate system with its own retention and access rules, so keep addresses out of them.- Log the email ID, the event ID, the event type, the HTTP status, the error code, and the request ID. The email ID leads to everything else in the dashboard.
- Do not log
to,cc,bcc,from, subjects, message bodies, or raw webhook payloads.
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 Express
Send from an Express route and verify webhooks on the raw body
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
