Nuntly client, which throws typed errors, 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. See API keys. You store it in an environment variable in the next section.
- A Next.js project that uses the App Router. The samples use the
app/directory and the@/import alias thatcreate-next-appconfigures.
Install and configure the SDK
1
Install the SDK
The package has zero runtime dependencies and ships its own type definitions.
2
Store the API key in an environment variable
Add the key to
.env.local at the root of the project. Next.js reads this file at startup, and create-next-app excludes it from Git..env.local
3
Create the client
Create the client once, in a module that your server code imports.The defaults suit a web request: a 60-second timeout and 2 automatic retries with exponential backoff (500 ms initial delay, capped at 8 seconds). If you omit
lib/nuntly.ts
apiKey, the SDK reads NUNTLY_API_KEY from the environment on its own.If you prefer
{ data, error } results to try/catch, create the client with createSafeNuntly instead. See the SDK README.Send your first email
A Server Action runs only on the server, and your forms call it without an HTTP endpoint. That makes it the place to send an email triggered by your own UI, such as a signup form.app/actions/send-welcome-email.ts
from address must use your verified domain. Sending both html and text gives mail clients that block HTML a readable fallback.
The API answers 202 Accepted. The SDK returns the email id (prefixed em_) and its status, which is queued: Nuntly has accepted the email and delivers it after the call returns. Delivery is reported later through events, covered in Observe what happened.
Call the action from a Client Component with React’s useActionState hook, as in the Next.js forms guide:
npm run dev, open /signup, and submit your own address. The email arrives in your inbox, and its em_ ID appears in your server logs.
Server Action or Route Handler
Use a Server Action when your own UI triggers the send. Use a Route Handler (app/**/route.ts) when the trigger comes from outside your UI: a cron job, a queue worker, or another service calling your app over HTTP. Both run the same nuntly.emails.send call.
Observe what happened
A202 confirms that Nuntly accepted the email. Delivery comes afterwards, and two places report it: the dashboard, for one email at a time, and webhooks, for your code.
Email timeline in the dashboard
In the dashboard, open Emails, select the email, then click Email Timeline. The timeline lists every event recorded for that email with its timestamp. A typical send goes throughqueued, processed, sending and sent, then reaches an outcome such as delivered, bounced, rejected or failed.
A deliveryDelayed event marks a delay in delivery to the recipient. opened and clicked events appear when open and click tracking is enabled on the domain. Events are kept for 30 days by default; see data retention.
Receive webhooks in a Route Handler
Receiving webhooks is a job for a Route Handler, because Nuntly calls your app over HTTP. Each request carries awebhook-signature header: an HMAC-SHA256 signature of the timestamp and the raw body, computed with your webhook’s signing secret. Recomputing it proves the request came from Nuntly and was not altered.
1
Create the webhook
Create a webhook in the dashboard (see webhooks) that points to
https://yourapp.com/api/webhooks/nuntly, and select the events you need, for example email.delivered, email.bounced and email.complained.2
Store the signing secret
Copy the signing secret, which starts with
whsec_, into .env.local..env.local
3
Create the Route Handler
The SDK’s
verifyWebhook function checks the signature and returns a typed event.app/api/webhooks/nuntly/route.ts
- The raw body. The signature covers the exact bytes Nuntly sent, so read them with
request.text(). Parsing withrequest.json()and serializing again breaks verification. Route Handlers expose the raw body without anybodyParserconfiguration. - A
401on a bad signature.verifyWebhookthrowsWebhookVerificationErrorwhen the signature does not match, the header is malformed, or the timestamp is more than 5 minutes away from the current time, which blocks replayed requests. - Typed events. The event is a union typed on
event.type, so insidecase 'email.bounced'TypeScript knows thatevent.data.bounceexists. APermanentbounce type means the address cannot receive mail: stop sending to it. - A fast
2xx. Any response other than2xxcounts as a failed delivery. Respond first and move slow work out of the request path.
POST /webhooks/{id}/events/{eventId}/replay.
Because of retries and replays, the same event can arrive more than once. Deduplicate on the root
id (prefixed evt_). Do not use data.id: it is the email ID, shared by every event of that email. A unique constraint on a table of processed event IDs is enough.Production notes
Idempotency keys
The SDK sends anIdempotency-Key header on every emails.send call. It generates a UUID v4 and reuses it across its own automatic retries, so a retry after a 503 cannot send a second email.
That automatic key does not cover a Server Action that runs twice: a double click, a browser resubmitting after a dropped connection, a server restart during the request. Each run is a new send call with a new random key. Derive the key from your own data to make the operation itself idempotent:
Idempotent-Replayed: true header. The same key with a different body returns 409 idempotency_key_payload_mismatch. See idempotency keys for key strategies.
Retries
The SDK retries timeouts and408, 409, 429, 500, 502, 503 and 504 responses, up to 2 times by default with exponential backoff from 500 ms to 8 seconds, and honors the Retry-After header. Change the policy with the maxRetries and retry client options.
Error handling
An error that reaches yourcatch block is either a client error that a retry would not fix, or a failure that outlasted the retries. Every API error is an APIError subclass with status, code and requestId.
Return a generic message to the browser, as the Server Action above does, and log the
requestId so a failed request can be traced. See errors.
Separate environments
Use one API key per environment:.env.local for development, your hosting provider’s environment settings for preview and production. Revoking a leaked development key then has no effect on production. You can restrict a key to sending only; see API keys.
What not to log
Recipient email addresses are personal data, and logs are a separate store with their own retention and access rules.- Log the
em_email ID, theevt_event ID and therequestId. They identify the email without identifying the person. - Do not log
to,from,subject, the HTML body, or a full webhook payload. Aconsole.log(event)in the webhook handler writes a recipient address to your logs on every delivery. - To investigate a support request, take the email ID from your logs and open 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 Express
Send from an Express route and verify webhooks on the raw body
Send email from NestJS
Inject the client as a provider and send from a service
Send email from Spring Boot
Use the Spring Boot starter on Java 21
