API · Webhooks
Postern can push inbound mail to your endpoint as HMAC-signed, timestamped webhooks, so you don’t have to poll. The signature lets you verify every delivery with Web Crypto — in Node or at the edge, with no dependency.
POST /v1/webhooks — register
Section titled “POST /v1/webhooks — register”Request
Section titled “Request”{ "url": "https://my-agent.example.com/inbound", "events": ["message.received"], "address": "agent7@x4p.mszazu.com" // optional — scope to one mailbox; omit for all}curl -sS -X POST "$POSTERN_API_BASE_URL/v1/webhooks" \ -H "Authorization: Bearer $POSTERN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://my-agent.example.com/inbound", "events": ["message.received"] }'Response 201
Section titled “Response 201”{ "id": "wh_2Lp", "url": "https://my-agent.example.com/inbound", "events": ["message.received"], "secret": "whsec_…" // shown once — store it now}The delivery
Section titled “The delivery”Each delivery POSTs a JSON body and carries the signature headers:
POST /inbound HTTP/1.1Content-Type: application/jsonX-Postern-Signature: t=1749751542,v1=3b2a… // timestamp + HMAC-SHA256 over `t.body`X-Postern-Event: message.received{ "event": "message.received", "inbox": "agent7@x4p.mszazu.com", "message": { "id": "msg_8Tz", "thread_id": "thread_9aB", "from": "no-reply@acme.test", "subject": "Verify your email", "received_at": "2026-06-12T18:05:42Z" }}Verifying a delivery
Section titled “Verifying a delivery”The signature is HMAC-SHA256(secret, "{t}.{raw_body}"), hex-encoded, with a timestamp to defeat
replay. Verify the raw body — don’t re-serialize the JSON first.
import { verifyWebhookSignature } from "@postern/sdk";
export async function POST(req: Request) { const payload = await req.text(); // raw body — do not re-serialize const ok = await verifyWebhookSignature({ payload, signature: req.headers.get("x-postern-signature")!, secret: process.env.POSTERN_WEBHOOK_SECRET!, }); if (!ok) return new Response("bad signature", { status: 400 }); // ... handle the verified message.received event ... return new Response("ok");}Or parseWebhook({ ... }) to verify and JSON-parse in one step (returns null on a bad signature).
async function verify(raw: string, header: string, secret: string) { const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("="))); const data = `${parts.t}.${raw}`; const key = await crypto.subtle.importKey( "raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"], ); const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(data)); const hex = [...new Uint8Array(sig)].map((b) => b.toString(16).padStart(2, "0")).join(""); return hex === parts.v1; // also reject if `t` is too old (replay window)}- wait_for_email — pull the next message instead of receiving a push.
- Inboxes —
inbound_webhook_urlregisters a webhook at create time. - Errors — status codes.