Skip to main content
To create and manage webhooks, see the webhooks guide.
This guide shows how to set up an HTTPS endpoint in your application to receive webhook events from Nuntly, verify their authenticity using the signing secret, and handle each event type.

Prerequisites

  • A webhook created in your Nuntly account. See the webhooks guide.
  • The signing secret for that webhook (displayed once at creation)
  • An HTTPS endpoint on a publicly accessible server

Verify the webhook signature

Every request Nuntly sends includes an x-nuntly-signature header. This is an HMAC-SHA256 signature computed from the raw request body using your webhook’s signing secret. You must verify this signature before processing the event. This prevents your endpoint from acting on forged requests.
Use a timing-safe comparison function (such as crypto.timingSafeEqual in Node.js) when comparing signatures to avoid timing attacks.

Implement the endpoint

// app/api/webhooks/nuntly/route.ts
import type { NextRequest } from 'next/server';
import crypto from 'crypto';

export async function POST(req: NextRequest) {
  const payload = await req.text();
  const signature = req.headers.get('x-nuntly-signature') || '';
  const secret = process.env.NUNTLY_WEBHOOK_SIGNING_SECRET!;

  // Verify the HMAC-SHA256 signature
  const expected = crypto.createHmac('sha256', secret).update(payload).digest('hex');

  if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
    return new Response('Invalid signature', { status: 401 });
  }

  const event = JSON.parse(payload);

  switch (event.type) {
    case 'email.delivered':
      console.log(`Email ${event.data.id} was delivered`);
      break;
    case 'email.bounced':
      // Consider removing the address from your sending list
      console.log(`Email ${event.data.id} bounced`);
      break;
    case 'email.complained':
      // Stop sending to this address
      console.log(`Email ${event.data.id} was marked as spam`);
      break;
    case 'email.opened':
      console.log(`Email ${event.data.id} was opened`);
      break;
    case 'email.clicked':
      console.log(`Email ${event.data.id} had a link clicked`);
      break;
    default:
      console.log(`Unhandled event type: ${event.type}`);
  }

  // Return 200 immediately — do heavy processing asynchronously
  return new Response(JSON.stringify({ received: true }), {
    status: 200,
    headers: { 'Content-Type': 'application/json' },
  });
}
If you use express.json() as global middleware, it will consume the raw body before your route handler runs. Apply express.text() specifically to your webhook route, as shown above.

Best practices

  • Respond quickly. Return a 200 response as soon as you receive and verify the event. If processing takes time, queue the work for background execution.
  • Always verify the signature. Never skip signature verification, even in development.
  • Handle duplicates. Nuntly may retry failed deliveries. Use the event ID to deduplicate if needed.
  • Log raw payloads. Store the raw event body for debugging and auditing.
  • Handle bounces and complaints. When you receive an email.bounced or email.complained event, stop sending to that address to protect your sender reputation.

Monitor webhook delivery

You can see whether your webhook events are being delivered successfully in the Webhooks tab of the Observability page.

Learn more