Skip to main content

Delivery webhooks

Instead of polling GET /v1/orders/{id}, pass a webhook_url (https only) when creating an order. paperplane POSTs a signed event on each status transition.

Events

Fired on the same transitions the email notifications cover:
  • submitted
  • refused
  • held
  • mailed
  • delivered
  • failed

Request

Every event is a POST with a Webhook-Id header (for idempotency) and this body:
version is the payload schema version, currently always 1. It only changes if data’s shape changes in a way your parser could break on (a field renamed or removed); we’ll bump it and note the change here when that happens — adding a new optional field does not bump it.

Verify it came from us

Set OUTBOUND_WEBHOOK_SECRET on the server. Every request carries:
where v1 = HMAC-SHA256(secret, t + "." + rawBody). Reject events older than a few minutes (replay window). If the secret is unset, paperplane refuses to send unsigned events — you can always prove a status push came from us. A runnable Node example that verifies this exact scheme against a real payload shape (no dependencies, no network) lives at scripts/verify-webhook-example.mjs in the repo:
It signs a sample event with a throwaway secret, verifies it the same way your endpoint should, then shows a tampered body and a stale timestamp both failing verification — copy the verify() function straight into your handler.

Delivery

Fire-and-forget with one retry after a short backoff. If your endpoint is down, the reconcile cron re-converges order state and re-fires on the next poll.
Last modified on September 17, 2026