Email webhooks

Signed webhooks for every email event

In short

Sendpository posts a signed JSON webhook when an email is sent, delivered, delayed, bounced, complained about, opened, clicked or received. Each request carries an HMAC-SHA256 signature with a timestamp, so your endpoint can reject forged and replayed calls.

Every event

email.sent, delivered, delayed, bounced, complained, failed, opened, clicked, scheduled and received.

Verified in one line

The SDK's verifyWebhook checks the signature and the timestamp and returns a typed event.

Every attempt logged

Each delivery's status code and response is visible on the Webhooks page; an endpoint that keeps failing is paused, not hammered.

Choose what you get

Subscribe each endpoint to only the events it handles.

Example

Next.js route handler
import { verifyWebhook } from "sendpository";

export async function POST(req: Request) {
  const event = verifyWebhook({
    body: await req.text(),
    signature: req.headers.get("sendpository-signature") ?? "",
    secret: process.env.SENDPOSITORY_WEBHOOK_SECRET!,
  });
  if (event.type === "email.bounced") await markUndeliverable(event.data.to);
  return new Response("ok");
}

How a delivery is signed, and why that shape

Every request carries a single `Sendpository-Signature` header: an HMAC over the timestamp and the exact raw body, using the endpoint's own secret. Verify it before parsing - a handler that reads JSON first and checks the signature afterwards has already acted on unverified input.

The timestamp is inside the signed content rather than beside it. That is what makes a replay detectable: an attacker who captures a valid delivery cannot change its age without breaking the signature, so rejecting anything older than a few minutes is sufficient.

Compare with a constant-time comparison. A naive `==` on the signature leaks, through timing, how much of the value was correct, and that is enough to recover it a byte at a time.

Failure behaviour you should plan for

Deliveries are retried with backoff. Your endpoint should answer 2xx quickly and do the work afterwards - a handler that performs a slow database write before responding will time out and be retried, which turns one event into several.

An endpoint that fails 15 times consecutively is disabled automatically, and the events are still in the log. That is deliberate: a dead endpoint that keeps being retried forever is a queue nobody is draining.

What it does not do

Read the Webhooks guide →

Questions

How do I verify an email webhook signature?

Recompute HMAC-SHA256 over the timestamp and the raw request body with your endpoint secret and compare it, in constant time, to the v1 value in the Sendpository-Signature header. The SDK's verifyWebhook does this and rejects stale timestamps.

Other features

Send your first email in minutes

6,000 emails a month (up to 200 a day), free forever. No card needed.