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
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
- Delivery is at-least-once. Build the handler to be idempotent on the event id rather than assuming exactly-once.
- Events are not ordered. A `delivered` can arrive after a `bounced` for a different recipient of the same message, so reconcile on the event rather than on arrival order.
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
Transactional email API
Sendpository sends password resets, receipts, sign-in links and notifications with one HTTPS request.
Inbound email API
Sendpository receives email for addresses on your own domain - support@, replies@, or a catch-all - and delivers each message to your inbox, the API and an email.received webhook.
Domain authentication (DKIM, SPF, DMARC)
Sendpository generates a 2048-bit DKIM key for every domain, signed as your own domain so it passes strict DMARC alignment, and puts its SPF and bounce records on a dedicated subdomain so your existing company email is never touched.
Scheduled email
Add scheduled_at with an ISO 8601 time to any send and Sendpository delivers it then.
Send your first email in minutes
6,000 emails a month (up to 200 a day), free forever. No card needed.