Skip to main content
email·digit
Docs · Webhooks

Events you can trust.

15 event types, all live today. Every payload is HMAC-SHA256-signed; verifying takes ten lines in any language. Exponential-backoff retry, and failed deliveries can be replayed from the dashboard.

Event catalogue

Fifteen live events.

Every one of these is emitted today. Nothing here is a placeholder — and you can pull a generated sample payload for any of them from GET /api/webhooks/events/samples.

  • reply.receivedA reply landed and was classified. Fires for every channel — email, WhatsApp, SMS, inbound webhook — with the same shape.Live
  • email.sentAn outbound email was accepted by the relay.Live
  • email.openedA tracking pixel fired. Display only — Apple Mail pre-fetches images, so don't treat it as engagement.Live
  • email.clickedA tracked link was followed.Live
  • email.bouncedAn outbound email failed at the relay.Live
  • conversion.recordedA conversion was ingested via the Conversions API.Live
  • suppression.createdAn address became suppressed. Mirror these into your own do-not-send list.Live
  • domain.verifiedA sending domain finished verifying.Live
  • domain.failedA previously-verified sending domain stopped verifying.Live
  • domain.driftA verified domain's DNS records changed underneath it.Live
  • mailbox.connectedA mailbox was connected for reply ingestion (Gmail / Microsoft / IMAP).Live
  • whatsapp.receivedAn inbound WhatsApp message arrived (also fires reply.received).Live
  • sms.receivedAn inbound SMS arrived (also fires reply.received).Live
  • automation.firedAn automation action ran.Live
  • test.pingSent when you click Send test in the dashboard.Live
Payload

What we POST to your endpoint.

One envelope shape across every event type. Headers carry routing metadata; body carries the data.

POST /your-endpoint
# What we POST to your endpoint
# X-Email-Digit-Version: 2026-07-30
# X-Email-Digit-Signature: sha256=abc...,t=1748459269
# X-Email-Digit-Event: reply.received
# X-Email-Digit-Mode: live
# X-Email-Digit-Event-Id: 00000000-0000-0000-0000-000000000000
# X-Email-Digit-Delivery-Id: 00000000-0000-0000-0000-000000000000
# X-Email-Digit-Attempt: 1

{
  "version": "2026-07-30",
  "id": "00000000-0000-0000-0000-000000000000",
  "type": "reply.received",
  "mode": "live",
  "created_at": "2026-05-28T14:21:09Z",
  "data": {
    "reply_id": "00000000-0000-0000-0000-000000000000",
    "channel": "email",
    "from_email": "sample@example.com",
    "from_name": "Sample Sender",
    "phone": null,
    "subject": "Re: your demo",
    "party_type": "lead",
    "category": "sales",
    "intent": "interested",
    "sentiment": "positive",
    "urgency": "normal",
    "intensity": 62,
    "priority_score": 74,
    "risk_flags": [],
    "needs_review": false,
    "confidence": 0.91,
    "received_at": "2026-05-28T14:20:55+00:00",
    "contact_id": "00000000-0000-0000-0000-000000000000"
  }
}
Verifying signatures

Ten lines, any language.

We sign sha256({timestamp}.body) using your subscription's signing secret. Reject anything older than 5 minutes.

verify(body, header)
import crypto from "crypto";
import express from "express";

const SECRET = process.env.ED_WEBHOOK_SECRET;

function verify(rawBody, header) {
  const [sigPart, tsPart] = header.split(",");
  const sig = sigPart.slice("sha256=".length);
  const ts = tsPart.slice("t=".length);
  // Reject anything older than 5 minutes (replay defense)
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const expected = crypto.createHmac("sha256", SECRET)
    .update(`${ts}.`).update(rawBody).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}

// IMPORTANT: use express.raw — JSON parsers break the MAC
app.post("/webhooks/email-digit",
  express.raw({ type: "application/json" }),
  (req, res) => {
    if (!verify(req.body, req.get("X-Email-Digit-Signature"))) {
      return res.status(401).send("invalid");
    }
    const event = JSON.parse(req.body.toString("utf8"));
    // dispatch on event.type
    res.status(200).send("ok");
  });
Retry policy

Exponential backoff, then dead.

2xx → done. 4xx (except 429) → permanent failure, no retry. Anything else → retry.

attempt 1 → +1m
attempt 2 → +5m
attempt 3 → +30m
attempt 4 → +2h
attempt 5 → +12h
attempt 6 → dead (terminal)

After 25 consecutive failures across all events the subscription auto-pauses so we don't hammer a permanently-broken endpoint. Resume it from the dashboard once you've fixed things. Failed deliveries can be replayed individually from /dashboard/webhooks.

Docs · Webhooks

Sign. Send. Retry.
All the way down.

Subscribe an endpoint in the dashboard, click Send test, watch the verification pattern work in your code in two minutes.