Email webhooks are just HTTP POSTs.
When a message is delivered, bounced, opened, or clicked, we POST signed JSON to your URL. HMAC, retries, a log per delivery — not polling, not IMAP.
Signed by default
Trust every request you receive.
HMAC-SHA256 over the raw body. Any Svix-compatible library verifies it.
Per-endpoint secret
A whsec_ key, returned once on create.
Constant-time
Timestamp checked. Replays rejected.
Replay-safe
±5 min window by default.
POST /api/webhooks HTTP/1.1
Host: acme.com
Content-Type: application/json
svix-id: msg_2a9f1c…
svix-timestamp: 1767258124
svix-signature: v1,g0Xm9c8Qk2Jp7yq3rN4bR1sT6uV8wXyZ…
{
"type": "email.delivered",
"created_at": "2026-01-01T09:00:00.000Z",
"data": {
"email_id": "email_9f2c…",
"to": ["user@example.com"],
"from": "you@acme.com",
"subject": "Your receipt from Acme"
}
}One event for every state
Subscribe to exactly what you need.
Pick events or a wildcard. Same envelope, one handler.
email.*email.sentemail.deliveredemail.delivery_delayedemail.bouncedemail.complainedemail.openedemail.clickedemail.failedemail.scheduledemail.suppressedemail.receivedSame delivery system powers inbound email: subscribe to email.received to parse and handle mail your users send you.
Developer-first
Verify a request in one call.
Raw body, secret, headers. Typed event — or it throws.
Typed events
Narrow on event.type.
Fails closed
Bad signatures never reach your handler.
import { verifyWebhook } from "unitpost";
// Pass the RAW request body — re-serializing breaks the signature.
export async function POST(req: Request) {
const payload = await req.text();
const event = verifyWebhook({
payload,
secret: process.env.UNITPOST_WEBHOOK_SECRET, // whsec_…
headers: {
"svix-id": req.headers.get("svix-id"),
"svix-timestamp": req.headers.get("svix-timestamp"),
"svix-signature": req.headers.get("svix-signature"),
},
});
if (event.type === "email.bounced") {
// …handle the bounce
}
return new Response("ok");
}Full visibility
See every delivery, trace every event.
Every attempt, with the response code your server returned.
whsec_••••••••- email.delivered1s200
- email.opened2h200
- email.bounced5h429
- email.clicked8h200
Built to survive a bad day
Retries and backoff, handled for you.
Retries on a schedule. 4xx fails fast. A broken endpoint is suspended.
Automatic retries
Up to seven times over ~19 hours. Fixed backoff.
Smart failure handling
2xx succeeds. 4xx fails fast. 429 and 5xx retry.
Auto-suspend, never hammer
20 failures in a row and we pause it. Re-enable when fixed.
Ordered & timestamped
svix-timestamp and svix-id. Dedupe and order.
Backlog & replay
Suspended events stay queued. Replay from the dashboard.
Per-delivery log
30 days of status codes and attempts.
Fully programmable
Manage endpoints over the API.
Create, test, and delete endpoints with the same scoped keys.
curl -X POST 'https://www.unitpost.com/webhooks' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'User-Agent: my-app/1.0' \
-H 'Content-Type: application/json' \
-d '{
"url": "https://example.com/webhooks/unitpost",
"events": [
"email.delivered",
"email.bounced"
]
}'What you can build
Wire email into everything you run
React to every delivery, click, or bounce.
Sync delivery status
Update records on delivered, bounced, or complained.
Auto-suppress on bounce
Prune bounced and complained addresses.
Trigger downstream flows
Alert, ticket, or retry when a send fails.
Track engagement
Opens and clicks into your analytics or CRM.
Keep contacts in sync
Mirror contact changes into your own store.
Monitor domains
domain.updated when verification changes.
Receive inbound mail
email.received for inboxes, replies, and parsers.
Feed a data warehouse
Stream every event into your warehouse.
Included free
Webhooks on every plan, from day one
$0/ mo
- Included on the free plan — no extra charge
- 2 endpoints on free · 5 on Starter · 25 on Growth
- HMAC-signed, svix-compatible headers
- Delivery, engagement & inbound events
- 7 automatic retries over ~19 hours
- Test, replay & flush from the dashboard
Questions
Common questions
How do I verify a Unitpost webhook signature?
Each delivery carries svix-id, svix-timestamp, and svix-signature headers, where the signature is an HMAC-SHA256 over the raw request body keyed with your endpoint's signing secret (whsec_…). Compute the same HMAC over the unparsed body and compare in constant time — or call verifyWebhook from the SDK, which does it in one line. Always verify against the raw bytes: parsing and re-serializing the JSON changes the payload and the signature will not match.
Why are the headers svix-prefixed?
Because that header format is the de-facto standard many receivers already parse, so an existing svix-compatible verifier works against Unitpost unchanged. We are not routed through svix — we sign and deliver ourselves; only the header shape is shared.
What happens if my endpoint is down?
Deliveries retry automatically — 7 attempts with exponential backoff spread over roughly 19 hours, so a short outage resolves itself with no lost events. After 20 consecutive failures the endpoint is suspended rather than hammered, and events queue as a backlog you can replay or flush from the dashboard once the receiver is fixed.
Which email events can I subscribe to?
Delivery lifecycle (sent, delivered, bounced, complained, failed), engagement (opened, clicked), inbound (email.received), plus contact, domain, and campaign events. Subscribe an endpoint to only the events you need — each endpoint has its own event list, so you can route engagement and infrastructure events to different services.
How do I avoid processing the same event twice?
Dedupe on svix-id, which is stable per delivery across retries. Treat your handler as idempotent and return 2xx as soon as you have durably recorded the event; do the slow work afterwards, because a timeout counts as a failure and triggers a retry.
Can I test a webhook before going live?
Yes. Send a test event to any endpoint from the dashboard or the API and it exercises your real handler and signature check end to end. Every attempt is kept in a 30-day per-delivery log with the status code your server returned, so you can see exactly what happened rather than guessing.