Unitpost
Webhooks

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 https://acme.com/api/webhooks
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.*
  • email.sent
  • email.delivered
  • email.delivery_delayed
  • email.bounced
  • email.complained
  • email.opened
  • email.clicked
  • email.failed
  • email.scheduled
  • email.suppressed
  • email.received

Domain

domain.*
  • domain.created
  • domain.updated
  • domain.deleted

Contact

contact.*
  • contact.created
  • contact.updated
  • contact.deleted

Segment

segment.*
  • segment.joined
  • segment.left

Topic

topic.*
  • topic.subscribed
  • topic.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.

Verify a webhook
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.

POST https://acme.com/api/webhooksEnabled
Listening foremail.*Signing keywhsec_••••••••Attemptsauto-retry ×5
  • email.delivered
    1s200
  • email.opened
    2h200
  • email.bounced
    5h429
  • email.clicked
    8h200

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.

Create a webhook
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.