ZevSend Docs
Sign up

Webhooks

Receiving webhooks

Subscribe to message lifecycle events so your application stays in sync.

Webhooks let ZevSend push lifecycle events to your application in real time. Every state transition — sent, delivered, bounced, read, failed — fires an event you can subscribe to.

How it works

  1. Create an endpoint in the dashboard with the URL you want events POSTed to.
  2. Pick which events the endpoint subscribes to.
  3. We sign every delivery with a shared secret. Your app verifies the signature and returns 2xx quickly.

We retry failed deliveries with exponential backoff for up to 24 hours. After that, the event is logged as undeliverable and surfaced in the dashboard.

Setting up an endpoint

Open Settings → Webhooks → Add endpoint in the dashboard. Paste the public HTTPS URL of your handler and pick the events you want.

We’ll show your signing secret once. Copy it immediately — it’s the only secret that proves a delivery is genuinely from us.

whsec_a1b2c3d4e5f6...

Endpoints must be HTTPS in live mode. HTTP is allowed in sandbox so you can point at localhost via a tunnel like ngrok during development.

Event delivery

Every event arrives as a POST with this body shape:

{
  "id": "whe_01HXYZ...",
  "type": "email.delivered",
  "created_at": "2025-05-24T12:34:56.789Z",
  "data": {
    "message": {
      "public_id": "eml_01HXYZ...",
      "from_address": "noreply@acme.com",
      "to_address": "ada@example.com",
      "subject": "Your receipt",
      "status": "delivered",
      "sandbox": false,
      "sent_at": "2025-05-24T12:34:55.000Z"
    }
  }
}
HeaderValue
Content-Typeapplication/json
X-Zev-Signaturet=<unix-ms>,v1=<hmac-sha256-hex> (see Signatures)
X-Zev-EventThe event type, e.g. email.delivered.
X-Zev-Event-IdThe unique event id (whe_…).
X-Zev-Delivery-IdThe unique id of this delivery attempt.

What to do in your handler

Three rules:

  1. Verify the signature before doing anything with the payload. An unsigned payload is just internet noise.
  2. Return 2xx quickly. Do the actual work async if it takes more than a couple of seconds. We retry anything non-2xx, which can amplify a slow handler.
  3. Be idempotent. We retry on failure, and at-least-once delivery means you may see the same event twice. Use the X-Zev-Event-Id header as your dedupe key.
app.post('/webhooks/zevsend', async (req, res) => {
  if (!verifyZevSignature(req)) return res.sendStatus(400);

  const { id, type, data } = req.body;

  // Idempotent — skip if we've seen this event before.
  if (await alreadyProcessed(id)) return res.sendStatus(200);

  // Queue the real work. Return fast.
  await queue.add('zevsend.event', { type, data });
  await markProcessed(id);

  res.sendStatus(200);
});

Retries

If your endpoint doesn’t return 2xx within 10 seconds we treat the delivery as failed and retry on this schedule:

+30s, +2m, +10m, +1h, +6h, +24h

After the final attempt the event is parked. The dashboard shows the last-attempted timestamp and the response body so you can debug. You can replay a parked event from the dashboard once your endpoint is back.

Sandbox vs live

Endpoints created with a sandbox key only receive sandbox: true events. Endpoints created with a live key only receive sandbox: false events. Same URL? Create one endpoint in each mode.

Testing without a public URL

The dashboard has a Send test event button per endpoint. It dispatches a synthetic email.delivered event with a recognisable eml_test_… id. Useful for proving signature verification works before you wire production traffic.

For local development, use a tunneling service (ngrok, cloudflared, etc.) and point the endpoint at the tunnel URL.

Updated at, Thursday, October 1, 2026