The signature scheme
| Property | Value |
|---|---|
| Algorithm | HMAC-SHA256 |
| Encoding | Lowercase hex |
| Signed string | {timestamp}.{body} — Unix timestamp, a literal dot, and the raw request body |
| Header carrying the signature | X-Webhook-Signature, prefixed with v1= |
| Header carrying the timestamp | X-Webhook-Timestamp (Unix seconds, UTC) |
| Secret format | whsec_ followed by 64 hex characters |
X-Webhook-ID (the event id, useful for deduplication) and X-Webhook-Event (the event type). The User-Agent is AntonPayments-Webhook/1.0.
Verification steps
Read the raw request body
Capture the body as bytes before any JSON parsing or framework normalisation. Parsed-and-re-serialised JSON will not byte-match the string Anton signed.
Read the timestamp and signature headers
Pull
X-Webhook-Timestamp and X-Webhook-Signature from the request. Strip the v1= prefix on the signature.Reject old timestamps
If the timestamp is more than 5 minutes (300 seconds) away from your server’s current time, reject the request. This prevents replays of captured deliveries.
Compute the expected signature
Concatenate
{timestamp}.{body}, compute HMAC-SHA256 over it using your subscription’s signing secret, and hex-encode the result.Compare in constant time
Compare the expected signature to the header value using a constant-time comparison (
hmac.compare_digest, crypto.timingSafeEqual, hash_equals, hmac.Equal). Never use == — it leaks timing information.Verification code
All examples read the raw body, enforce a 5-minute freshness window, and use a constant-time comparison.Rotating secrets
Every subscription has one signing secret. The secret is returned once — in the response toPOST /v1/webhooks — and never again. Store it in your secrets manager at creation time.
Retrieve the current secret
Retrieve the current secret
Rotate the secret
Rotate the secret
Common pitfalls
- Parsed JSON instead of raw bytes. Frameworks that parse and re-serialise the body (Express’s default
json()middleware, some ASP.NET pipelines) will break verification — whitespace, key ordering, and number formatting all change. Capture the raw body before parsing. - Wrong header case. HTTP headers are case-insensitive, but some frameworks expose them under specific casing. Read
X-Webhook-Signature/X-Webhook-Timestampvia your framework’s header API, not by exact-match dictionary lookup. - Forgetting to strip
v1=. The signature header isv1={hex}. Strip the prefix before hex-decoding. - Trailing newlines or encoding changes. A proxy or middleware that appends
\nor transcodes the body will invalidate the signature. - Using
==orstrcmp. Byte-by-byte equality checks leak timing information. Always use a constant-time comparison. - Not enforcing the timestamp window. Without a freshness check, an attacker who captures one valid delivery can replay it indefinitely. Reject anything older than 5 minutes.
- Replying slowly. Deliveries time out after 30 seconds. Acknowledge first, process asynchronously.