Skip to main content
Add a transactional email to a Next.js App Router project: a Server Action sends it with the Nuntly TypeScript SDK, and a Route Handler receives the signed webhook events that tell your app whether it was delivered or bounced. The samples use the standard 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 that create-next-app configures.

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
Do not prefix the variable with NEXT_PUBLIC_. Next.js inlines variables with that prefix into the JavaScript bundle sent to the browser (Next.js environment variables). An unprefixed variable stays on the server.
3

Create the client

Create the client once, in a module that your server code imports.
lib/nuntly.ts
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 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
The 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:
Run 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.
Route Handlers are public HTTP endpoints (Next.js backend for frontend guide). Never expose a Route Handler that reads a recipient address from the request body and sends to it without authentication: anyone who finds the URL can send email from your domain. An exported Server Action is also reachable with a direct POST request (Next.js data security guide). Check the session inside any action that sends on behalf of a signed-in user.

Observe what happened

A 202 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 through queued, 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 a webhook-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 handler relies on four details:
  • The raw body. The signature covers the exact bytes Nuntly sent, so read them with request.text(). Parsing with request.json() and serializing again breaks verification. Route Handlers expose the raw body without any bodyParser configuration.
  • A 401 on a bad signature. verifyWebhook throws WebhookVerificationError when 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 inside case 'email.bounced' TypeScript knows that event.data.bounce exists. A Permanent bounce type means the address cannot receive mail: stop sending to it.
  • A fast 2xx. Any response other than 2xx counts as a failed delivery. Respond first and move slow work out of the request path.
Nuntly retries failed deliveries and records each attempt; see webhook observability. After an outage on your side, you can replay a missed event from the dashboard or with 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.
For every event type and payload, see sending events.

Production notes

Idempotency keys

The SDK sends an Idempotency-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:
Nuntly keeps a key for 24 hours, and a key must be 8 to 255 characters long. A repeat with the same key and the same body returns the original response with an 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 and 408, 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 your catch 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, the evt_ event ID and the requestId. They identify the email without identifying the person.
  • Do not log to, from, subject, the HTML body, or a full webhook payload. A console.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