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.
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.
// app/api/webhooks/nuntly/route.tsimport 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.
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.