PristineSend.ai
Get started
API Reference

Webhooks

PristineSend forwards Resend webhook events to your endpoint so you can react to delivery status changes in real time.

Overview

Webhooks are HTTP POST requests sent to a URL you configure. They are triggered by events in Resend and proxied through your PristineSend workspace. To set up a webhook endpoint, go to Settings → Webhooks in your dashboard and enter your endpoint URL.

Tip: During development, use ngrok or localtunnel to expose your local server to the internet.

Supported events

Event typeDescription
email.sentResend accepted the message for delivery.
email.deliveredThe receiving mail server confirmed delivery.
email.delivery_delayedDelivery is being retried due to a temporary failure.
email.complainedRecipient marked the email as spam.
email.bouncedThe email could not be delivered (hard or soft bounce).
email.clickedRecipient clicked a tracked link (requires click tracking).
email.openedRecipient opened the email (requires open tracking).

Payload format

All events share a common envelope with a type field and a data object:

{
  "type": "email.delivered",
  "data": {
    "email_id": "re_abc123xyz",
    "from": "orders@yourdomain.com",
    "to": ["recipient@example.com"],
    "subject": "Your order has shipped!",
    "created_at": "2026-05-16T12: 34: 56.000Z"
  }
}

Verifying signatures

Resend signs webhook payloads using Svix. Each request includes three headers — svix-id, svix-timestamp, and svix-signature — that you should verify using the svix npm package or equivalent before processing the event.

Your webhook signing secret is available in Resend under Webhooks → your endpoint → Signing Secret.

Example handler

A minimal Next.js App Router webhook handler that verifies the signature and dispatches on event type:

"color:#ff7b72">import { NextRequest, NextResponse } "color:#ff7b72">from "next/server"
"color:#ff7b72">import { Webhook } "color:#ff7b72">from "svix"

"color:#ff7b72">const secret = process.env.RESEND_WEBHOOK_SECRET!

"color:#ff7b72">export "color:#ff7b72">async "color:#ff7b72">function POST(req: NextRequest) {
  "color:#ff7b72">const body = "color:#ff7b72">await req.text()
  "color:#ff7b72">const headers = {
    "svix-id": req.headers.get("svix-id") ?? "",
    "svix-timestamp": req.headers.get("svix-timestamp") ?? "",
    "svix-signature": req.headers.get("svix-signature") ?? "",
  }

  "color:#ff7b72">let event: { type: string; data: unknown }
  try {
    "color:#ff7b72">const wh = new Webhook(secret)
    event = wh.verify(body, headers) as { type: string; data: unknown }
  } catch {
    "color:#ff7b72">return NextResponse.json({ error: "Invalid signature" }, { status: 400 })
  }

  switch (event.type) {
    case "email.delivered":
      // mark contact as active
      break
    case "email.bounced":
      // mark contact as bounced
      break
    case "email.complained":
      // unsubscribe contact
      break
  }

  "color:#ff7b72">return NextResponse.json({ received: true })
}