Events you can trust.
14 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.
One signed POST, then a fixed retry ladder.
Every event takes the same path to your endpoint. The numbers in the diagram are the ones the sender uses.
Fourteen 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 a connected mailbox and for the inbound webhook alike, with the same shape.Liveemail.sentAn outbound email was accepted by the relay.Liveemail.openedA tracking pixel fired. Display only: Apple Mail pre-fetches images, so don't treat it as engagement.Liveemail.clickedA tracked link was followed.Liveemail.bouncedAn outbound email failed at the relay.Liveconversion.recordedA conversion was ingested via the Conversions API.Livesuppression.createdAn address became suppressed. Mirror these into your own do-not-send list.Livedomain.verifiedA sending domain finished verifying.Livedomain.failedA previously-verified sending domain stopped verifying.Livedomain.driftA monitored domain's deliverability score dropped 10 points or more between two checks.Livemailbox.connectedA mailbox was connected for reply ingestion (Gmail / Microsoft / IMAP).Liveautomation.firedAn automation action ran.Livetest.pingSent when you click Send test in the dashboard.Livewebhook.verifySent when you add an endpoint or change its URL. Echo data.challenge in a 2xx response; nothing else is delivered until you do.Live
What we POST to your endpoint.
One envelope shape across every event type. Headers carry routing metadata; body carries the data.
# 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"
}
}Prove the URL is yours, once.
Before any event is delivered we send one signed webhook.verify request. Answer 2xx with its data.challenge in the body. Until then the endpoint shows Not verified and its events wait in the queue; nothing is lost. A plain 200 does not count, because any URL can answer 200.
const event = JSON.parse(req.body.toString("utf8"));
if (event.type === "webhook.verify") {
return res.json({ challenge: event.data.challenge });
}Ten lines, any language.
We sign sha256({timestamp}.body) using your subscription's signing secret. Reject anything older than 5 minutes.
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");
});Exponential backoff, then dead.
2xx: done. 4xx (except 429): permanent failure, no retry. Anything else: retry.
- attempt 1, next try +1m
- attempt 2, next try +5m
- attempt 3, next try +30m
- attempt 4, next try +2h
- attempt 5, next try +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.

Background reading.
- How to classify email replies: six readings, not one labelWhy a reply needs six separate readings, the vocabulary Email Digit uses for each, and what happens when AI classifications run out.
- How a reply priority score works, and why it is a formulaThe five weighted inputs behind a 0 to 100 reply priority, worked through on a real-looking message, with the floors and the cap.
- Unsubscribe or spam complaint: two levels of do-not-sendWhy an unsubscribe and a spam complaint need different blocks, and how Email Digit keeps the two apart.
- Why security scanners inflate your email click rateHow security scanners fake clicks, what that breaks, and how Email Digit keeps them out of your numbers.