Skip to main content
Anchor signs every delivery with HMAC-SHA256 over v0:{timestamp}:{raw_body} using your webhook’s signing secret. The signature lives in the Anchor-Signature header:
To verify a request:
  1. Read the Anchor-Timestamp and Anchor-Signature headers, plus the raw request body bytes (do not parse the JSON first - re-serializing changes whitespace and breaks the signature).
  2. Reject the request if the timestamp is more than 2 minutes old (replay protection).
  3. Compute HMAC_SHA256(secret, "v0:" + timestamp + ":" + raw_body).hex().
  4. Compare it against the v1= value in Anchor-Signature using a constant-time compare (hmac.compare_digest, crypto.timingSafeEqual, etc.).
  5. Only accept the event if the signatures match.
Always compute the HMAC over the raw request body bytes. If your framework parses JSON before you read the body (e.g. body-parser, FastAPI’s Body(...), gin’s BindJSON), you must opt out of that for the webhook route - re-serializing the parsed object will produce different bytes and the signature will fail.

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 call POST /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:
  1. Receive the new secret from the rotate-secret response. Anchor returns it once.
  2. Update your verifier so it accepts deliveries signed by either secret during the overlap window.
  3. Deploy.
  4. 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-parser or FastAPI’s Body(...) 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 from Anchor-Signature (equivalent to the Anchor-Timestamp header). 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.