The request
Every delivery is an HTTPSPOST with a JSON body and these headers:
Body
The body is intentionally thin: it tells you what changed, not how.
Read the current resource through the REST API with your own credentials —
that also guarantees you see the latest state rather than the state at
publish time.
How the signature is computed
secretis thewhsec_…value returned once by create or rotate-secret.tis the Unix time (seconds) when the worker signed the request.- The raw body is the exact byte sequence sent; whitespace or key order changes invalidate the digest.
Verification rules
1
Read the raw body
Capture the body before any JSON middleware parses it. Re-serialising
the parsed object changes the bytes.
2
Parse the header
Expect exactly
t=<digits>,v1=<64 hex chars>. Reject anything else.3
Check the timestamp
Reject when
|now - t| > 300 seconds. This bounds replay of a captured
request; keep your server clock in sync (NTP).4
Compare in constant time
Recompute the HMAC and compare with a timing-safe function.
5
Deduplicate and acknowledge
Retries and operator replays reuse
id. Record processed ids, return
2xx within 10 seconds, and do the real work asynchronously.Reference implementations
- Node.js
- Python
Node.js
verifyOutboundWebhookSignature in
backend/horizon-api/lib/webhook/outbound/webhook-signature.ts, which is
also what the built-in test sink uses.
Responding
Details in Retries and failures.