Guides

Webhooks

Subscribe to delivery events, verify HMAC signatures, and handle retries and replays.

emitd POSTs an event to your endpoint at every stage of a message's life — each one HMAC-signed so you can trust it.

Subscribing

Create a webhook in the console or via POST /v1/webhooks with a URL and the events you want. emitd returns a signing secret exactly once — store it.

Eleven event types are available:

GroupEvents
Delivery lifecyclesent, delivery, delivery_delayed, failed
Reputationbounce, complaint, suppressed
Engagement (when tracking is on)open, click
Scheduled sendsscheduled, cancelled

Full CRUD + replay: /v1/webhooks. Event semantics: Webhook events.

Event payload

POST https://your-app/webhooks/emitd
{
  "type": "bounce",
  "message_id": "msg_2h8Kd0Rk9Qa",
  "ses_message_id": "0100018f...",
  "recipients": ["ada@lovelace.io"],
  "timestamp": 1783386190864
}

Verifying the signature

Every delivery carries an X-Webhook-Signature: sha256=<hex> header — the HMAC-SHA256 of the raw request body under your webhook secret. Compute the same and compare in constant time before trusting the payload.

verify.ts
import crypto from "node:crypto";

export function verify(rawBody: Buffer, header: string, secret: string) {
  const expected =
    "sha256=" +
    crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  // constant-time compare
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(header));
}

// in your handler:
if (!verify(rawBody, req.headers["x-webhook-signature"], SECRET)) {
  return new Response(null, { status: 401 });
}

Sign the raw body

Verify against the exact bytes you received. If your framework re-parses and re-serialises the body first, the HMAC will never match.

Retries and replays

  • Return a 2xx quickly (process asynchronously if your handler is slow). Non-2xx counts as a failure.
  • Failures retry with delays of 1 min, 5 min, 30 min, 2 hr, then every 8 hr — after 8 total attempts the delivery is marked failed.
  • Every attempt is recorded; POST /v1/webhooks/{id}/replay re-delivers an event on demand (useful after an outage on your side).
  • Endpoints can be limited per plan — webhook_limit_reached means you're at your cap.

Webhook creation requires an HTTPS URL (url_must_be_https).

On this page