Webhook security¶
Each delivery is an HTTP POST with a JSON body. ALPHA signs the request
with HMAC-SHA256 using a shared secret and the scheme identifier v1.
Your handler must verify the signature using the raw request bytes and the
same headers ALPHA sent—do not trust the body until verification passes.
Verify before you parse the payload¶
Important
If verification fails, return 401 or 403 and do not use the JSON
body for any business logic.
Headers (every request)¶
-
webhook-id -
Message id for this delivery (same value as the JSON
idfield). Use it for idempotency. -
webhook-timestamp -
Unix time in seconds when this delivery attempt was signed.
-
webhook-signature -
One or more space-separated entries. ALPHA sends a single entry of the form
v1,<base64>where<base64>is the base64 encoding of the raw 32-byte HMAC digest.
Endpoint secret¶
Webhook secret format: whsec_<base64>.
Remove the whsec_ prefix, base64-decode the remaining text, and use those
decoded bytes as the HMAC key.
What to sign¶
Use these values from the same request you are verifying:
-
msg_id— string fromwebhook-id(same as JSONid). -
ts— integer parsed fromwebhook-timestamp. -
raw_body— the exact POST body as received (bytes or string your stack gives you beforejsonparsing). ALPHA serializes JSON with sorted keys and compact separators(",", ":"); re-serializing from a dict will usually break the signature.
Join them with . separators — no spaces, no newlines:
signed_input = f"{msg_id}.{ts}.{raw_body}"
Then compute the HMAC over the UTF-8 bytes of signed_input:
digest = HMAC_SHA256(key, signed_input.encode("utf-8"))
Base64-encode digest (standard base64, 32 raw bytes). Your expected header
fragment is:
v1,<that_base64_string>
Compare that to the v1,... entry in webhook-signature using a
constant-time comparison. If ALPHA ever sends multiple signatures, accept
the request if any entry matches.
Replay protection¶
Compare webhook-timestamp to your server time. Reject requests outside a
small window (for example ±5 minutes) so old deliveries cannot be replayed.
If verification fails¶
-
Wrong secret, or invalid base64 after the
whsec_prefix. -
Body mismatch: you must use the raw HTTP body, not a pretty-printed or re-ordered JSON string.
-
Header mismatch:
webhook-idandwebhook-timestampmust be the ones paired with that exact body. -
Clock skew: widen the allowed timestamp window slightly if needed.
After verification¶
-
Treat JSON
idas your idempotency key. -
Return
2xxquickly after you have safely queued or stored the work; run heavy processing asynchronously. -
Keep the secret out of logs and source control; rotate if it leaks.
Transport¶
-
Expose HTTPS URLs only for webhook endpoints.
-
Avoid logging full payloads or secrets in plaintext.