Skip to main content

The request

Every delivery is an HTTPS POST 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

  • secret is the whsec_… value returned once by create or rotate-secret.
  • t is 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
These mirror 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.
Do not put business logic in front of the signature check. An endpoint that parses the body first and verifies later is trivially spoofable.