Mailpark
Guides

Verifying webhooks

Mailpark POSTs events to your endpoint as they happen. Each request is signed with your webhook's secret, so you can check it really came from Mailpark.

Add an endpoint

Create a webhook with your URL and the event kinds you want. The response holds signing_secret, shown only once: save it as MAILPARK_WEBHOOK_SECRET. Then send yourself a sample with the test endpoint.

const wh = await mailpark.webhooks.create({
  url: "https://acme.dev/api/mailpark-webhook",
  event_kinds: ["email.bounced", "email.complained", "email.unsubscribed"],
});
// Save wh.signing_secret, then:
await mailpark.webhooks.test(wh.id);

The payload

POST to your URL
{
  "id": "evt_5Rc2",
  "kind": "email.bounced",
  "created_at": "2026-10-22T10:02:11Z",
  "data": {
    "message_id": "msg_01JB4Q7Z2K",
    "type": "transactional",
    "to": "jane@example.com",
    "contact_id": "con_7Yt1",
    "template": "password-reset",
    "bounce_type": "hard",
    "environment": "live"
  }
}
HeaderValue
X-Signaturet=<unix seconds>,v1=<hex>
X-Mailpark-Event-IdThe event id, same as the body's `id`.
X-Mailpark-Event-KindThe event kind.
X-Mailpark-Delivery-IdThis delivery attempt.

Verify the signature

v1 is HMAC-SHA256 of `${t}.${rawBody}` with your signing secret, as hex. Check it against the raw request body, compare in constant time, and reject timestamps more than five minutes from now.

import { verifyWebhook } from "mailpark";

// app/api/mailpark-webhook/route.ts
export async function POST(req: Request) {
  const raw = await req.text(); // the raw body, not re-serialised JSON
  let event;
  try {
    event = await verifyWebhook(raw, req.headers.get("x-signature"), process.env.MAILPARK_WEBHOOK_SECRET!);
  } catch {
    return new Response("bad signature", { status: 400 });
  }

  // Retries can deliver the same event twice: skip ids you've already handled.
  if (await alreadyHandled(event.id)) return new Response("ok");

  if (event.kind === "email.bounced") {
    await markEmailInvalid(event.data.to);
  }
  return new Response("ok");
}

Retries

  • Answer with any 2xx within a few seconds. Do slow work after responding.
  • Anything else is retried with backoff for up to 24 hours. A webhook that keeps failing is marked failing, and the team is told.
  • Deduplicate on the event id: a retry carries the same id.
  • See every attempt with GET /v1/webhooks/{id}/deliveries, and resend one by hand.

Event kinds

KindWhen
email.sentHanded to the inbox provider.
email.deliveredThe provider accepted it.
email.bouncedIt bounced. bounce_type is hard or soft.
email.complainedThe recipient marked it as spam.
email.openedOpened. likely_automatic marks machine opens such as Apple Mail Privacy Protection.
email.clickedA link was clicked. link is the URL.
email.unsubscribedThe recipient unsubscribed.
sequence.enteredA contact entered a sequence.
sequence.exitedA contact left a sequence.
broadcast.heldA broadcast was held for review.
account.pausedSending was paused on the account.
Test mode
Events from test keys are delivered too, with environment: "test" in data. Send to bounce@example.com to get an email.bounced.