Every email event, pushed to you.
Delivered, opened, clicked, bounced, or received — we POST a signed event to your endpoint the moment it happens. HMAC-signed, retried with backoff, logged per delivery.
Signed by default
Trust every request you receive.
Each delivery is signed with HMAC-SHA256 over the raw body using a per-endpoint secret — the svix header convention, so any compatible library verifies it.
Per-endpoint signing secret
A whsec_ secret is returned once on create — used to verify every request from that endpoint.
Constant-time verification
Signatures are compared in constant time; the SDK checks the timestamp to reject replays.
Timestamped & replay-safe
A signed svix-timestamp lets you reject anything outside a tolerance window (±5 min 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 individual events or a whole category with a wildcard. Every payload shares the same envelope, so one handler covers them all.
email.*email.sentemail.deliveredemail.delivery_delayedemail.bouncedemail.complainedemail.openedemail.clickedemail.failedemail.scheduledemail.suppressedemail.received
Domain
domain.*domain.createddomain.updateddomain.deleted
Contact
contact.*contact.createdcontact.updatedcontact.deleted
Segment
segment.*segment.joinedsegment.left
Topic
topic.*topic.subscribedtopic.unsubscribed
Same 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.
Hand the SDK the raw body, the secret, and the headers — it returns the parsed, typed event or throws on a bad signature. No manual HMAC, no timing bugs.
Typed events
Narrow on event.type and get the exact data shape for that event.
Fails closed
Missing, malformed, or stale signatures throw before your handler runs.
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.
Each endpoint logs every attempt with the response code your server returned. Inspect one delivery or watch success roll up across your fleet.
whsec_••••••••Attemptsauto-retry ×5- email.delivered1s200
- email.opened2h200
- email.bounced5h429
- email.clicked8h200
Built to survive a bad day
Retries and backoff, handled for you.
Your endpoint will have an outage. Ours plans for it: failed deliveries retry on a schedule, 4xx errors fail fast, and a broken endpoint is suspended — with the backlog captured for replay.
Automatic retries
A failed delivery retries up to seven times over ~19 hours on a fixed backoff — 30s, 2.5m, 10m, and on out to 10h.
Smart failure handling
Any 2xx succeeds. 4xx client errors (400, 401, 404, 422…) fail fast; 429, 5xx, and timeouts retry.
Auto-suspend, never hammer
After 20 consecutive failures an endpoint is suspended, not spammed — re-enable it once you've fixed the receiver.
Ordered & timestamped
Each request carries a svix-timestamp and a stable svix-id you can use to dedupe and order events.
Backlog & replay
Events for a suspended endpoint are captured as a backlog you can replay or flush from the dashboard.
Per-delivery log
Thirty days of deliveries with status codes and attempts — inspect exactly what your server returned.
Fully programmable
Manage endpoints over the API.
Create, update, test, and delete webhook endpoints with the same scoped keys you send with — per environment, in code, or from the dashboard.
import { Unitpost } from "unitpost";
const unitpost = new Unitpost(process.env.UNITPOST_API_KEY);
const { data: endpoint } = await unitpost.webhooks.create({
url: "https://acme.com/api/webhooks",
events: ["email.delivered", "email.bounced", "email.complained"],
});
// The signing secret is returned exactly once.
console.log(endpoint.signing_secret); // "whsec_…"What you can build
Wire email into everything you run
Webhooks turn a one-way send into a two-way loop. React to every delivery, click, or bounce.
Sync delivery status
Update your own records the moment an email is delivered, bounced, or marked as spam.
Auto-suppress on bounce
React to email.bounced and email.complained to prune bad addresses from your own lists.
Trigger downstream flows
Kick off a Slack alert, a support ticket, or a retry when a critical email fails.
Track engagement
Pipe email.opened and email.clicked into your analytics or CRM in real time.
Keep contacts in sync
Mirror contact.created / updated / deleted into your own database or data warehouse.
Monitor domains
Get domain.updated when verification state changes, so infra alerts fire automatically.
Receive inbound mail
Subscribe to email.received to build support inboxes, reply handling, and parsers.
Feed a data warehouse
Stream every event into your ETL for a complete, queryable history of your email.
Included free
Webhooks on every plan, from day one
$0/ mo
- 1 endpoint free — up to 25 on paid plans
- Every event type on every endpoint
- HMAC-signed, svix-compatible headers
- 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.