v0:{timestamp}:{raw_body} using your webhook’s signing secret. The signature lives in the Anchor-Signature header:
- Read the
Anchor-TimestampandAnchor-Signatureheaders, plus the raw request body bytes (do not parse the JSON first - re-serializing changes whitespace and breaks the signature). - Reject the request if the timestamp is more than 2 minutes old (replay protection).
- Compute
HMAC_SHA256(secret, "v0:" + timestamp + ":" + raw_body).hex(). - Compare it against the
v1=value inAnchor-Signatureusing a constant-time compare (hmac.compare_digest,crypto.timingSafeEqual, etc.). - Only accept the event if the signatures match.
Code examples
Each example below performs four checks in order: presence of the headers, freshness (timestamp within 2 minutes of the time you received the request), HMAC signature, and finally a duplicate-id check so a retry doesn’t re-trigger your business logic.The dedupe maps below are kept in-process for clarity. In production you should swap them for a persistent store with a TTL of at least your retry budget (24 hours is safe) — Redis, DynamoDB, or a
seen_event_ids (id PRIMARY KEY, seen_at) table all work. An in-memory map won’t survive a restart and won’t dedupe across replicas behind a load balancer.Rotating your signing secret
Click Rotate signing secret in the dashboard (or callPOST /v1/webhooks/{id}/rotate-secret) to issue a new secret. The previous secret remains valid for 24 hours so you can roll your verification code without dropping events:
- Receive the new secret from the rotate-secret response. Anchor returns it once.
- Update your verifier so it accepts deliveries signed by either secret during the overlap window.
- Deploy.
- After 24 hours, remove the old secret from your verifier - Anchor stops accepting it automatically.
Common verification pitfalls
- Re-serialized body. Frameworks like Express’s
body-parseror FastAPI’sBody(...)decode the JSON before you see it. Capture the raw bytes explicitly (express.raw,request.get_data(),io.ReadAll(r.Body)). - Wrong timestamp source. Use the
t=value fromAnchor-Signature(equivalent to theAnchor-Timestampheader). Do not use the time you received the request - clock skew between your machine and Anchor will break the HMAC base string. - String comparison. Use a constant-time compare (
hmac.compare_digest,crypto.timingSafeEqual,hmac.Equal) to avoid timing-side-channel attacks. - Missing replay check. Reject anything older than 2 minutes.

