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 id field). 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:

  1. msg_id — string from webhook-id (same as JSON id).

  2. ts — integer parsed from webhook-timestamp.

  3. raw_body — the exact POST body as received (bytes or string your stack gives you before json parsing). 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-id and webhook-timestamp must be the ones paired with that exact body.

  • Clock skew: widen the allowed timestamp window slightly if needed.

After verification

  • Treat JSON id as your idempotency key.

  • Return 2xx quickly 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.