Hosted/webhook mode rename
Hosted/webhook mode rename
2026-04-20 · v0.3.0.0 · breaking
Hosted/webhook mode rename
2026-04-20 · v0.3.0.0 · breaking
v0.3.0.0 signs every outbound Saperly webhook with HMAC-SHA256 and adds line_id to every payload body. Receivers that ignore unknown fields and headers keep working. Receivers with strict-schema validation need three small changes.
x-saperly-timestamp — unix secondsx-saperly-delivery-id — UUID v4x-saperly-signature — v1=<hex> HMAC-SHA256 over ${timestamp}.${delivery_id}.${rawBody}line_id: "<uuid>" in every payload body.POST /v1/lines/:id/webhook-secret/rotate for two-phase secret rotation.line_id in your request-body schemaStrict schemas (Zod, Pydantic, etc.) reject unknown fields. Add the field.
Single-secret receivers keep working — just set the env var to the line’s current secret. Receivers that handle more than one line need a lookup keyed on line_id.
verifyWebhook before trusting the bodyUse the SDK helper. The signing primitives are inlined into the SDK, so there is no separate package to install.
The helpers already enforce the 5-minute clock skew window. You still need to cache x-saperly-delivery-id for 5 minutes and reject duplicates — that cache is receiver-side state.
Rotate a fresh secret so you know which value is live.
Capture the returned webhook_secret — it is returned exactly once.
Deploy your new receiver with that secret wired into lookupSecretForLine.
Send a test delivery. The body is a signed test event.
Confirm the delivery record shows status: success and HTTP 200.
If valid is false, check the reason — common failures are clock_skew (your server clock is off), bad_signature (wrong secret), or missing_header (reverse proxy is stripping the x-saperly-* headers).
If your receiver can’t be updated in time, there is no app-side rollback — signed traffic is on. Your options:
Prefer the proxy.