Webhook signatures
Every webhook the Gateway delivers is cryptographically signed with an Ed25519 cluster key so you can verify it really came from us and was not tampered with in transit. Signing is cluster-wide and is always active — there is no per-app shared secret.
Signature Header
Every delivery carries all three headers below. The application UUID and idempotency key are part of the signed pre-image, so verify the signature before trusting either value:
Signature Fields
Fields may appear in any order. Require exactly one each of t, akid, and v1a, reject duplicate known fields, and ignore unknown extension fields for forward compatibility. Webhook endpoints should enforce an application-appropriate request-body limit before buffering the raw bytes.
The Signing Input
v1a signs this exact byte sequence, with one LF byte (\n, byte 0x0a) after each of the first four fields and the raw request bytes appended without alteration:
Use the exact textual t field from X-Webhook-Signature and the exact X-Webhook-App-UUID and X-Idempotency-Key header values. Do not add a trailing newline after the body.
Use the raw bytes. Compute the signature over the request body exactly as received, before any JSON parse/re-serialize. Re-encoding (whitespace, key order, unicode escaping) changes the bytes and breaks verification. Read the raw body first, verify, then parse.
Fetching the Public Key
The cluster’s Ed25519 public key(s) are published at an unauthenticated endpoint:
The endpoint is unauthenticated and returns key IDs, algorithm names, and base64-encoded public keys.
The keys array always contains at least one active key and may contain two keys during rotation.
Match the delivery’s akid= field against a key’s akid, then verify v1a
with that key only. Periodically refresh the complete key set and atomically
replace the cached keyring so retired keys are evicted. Also refresh immediately
when you encounter an akid you do not recognise, and reject the delivery if
no exact match exists after refresh.
Verifying the Ed25519 Signature (v1a)
- Read the raw request body plus
X-Webhook-Signature,X-Webhook-App-UUID, andX-Idempotency-Key. Reject the request if any are missing or empty. - Compare
X-Webhook-App-UUIDwith your configured application UUID and reject a mismatch. - Parse the signature header. Require exactly one each of
t,akid, andv1a; reject duplicate known fields and ignore unknown extension fields. - Reject the delivery if
tis too far from your current time (for example, more than 5 minutes) to limit replay. - Fetch the public key for this
akidfromGET /.well-known/webhook-public-keys. Refresh periodically by atomically replacing the full keyring, and immediately on an unknownakid. - Strictly decode the
public_keyfrom canonical standard base64 and require exactly 32 bytes. - Reconstruct the exact LF-delimited pre-image, strictly decode
v1afrom canonical standard base64, require exactly 64 signature bytes, and verify it with the Ed25519 public key. - Only after verification succeeds, parse and validate the JSON body. Then atomically claim the signed application UUID plus
X-Idempotency-Keyin a durable deduplication store. Use a short processing lease and an opaque ownership token that fences every renew, complete, and release operation. While processing, renew the lease with margin before it expires and pass a cancellation signal into external operations; if renewal fails, cancel or stop side effects so a stale owner cannot keep mutating state after takeover. Mark the claim complete only when side effects commit, and release it on failure. If another handler still owns the lease, return425 Too EarlywithRetry-Afterequal to the 30-second lease duration so the delivery is deferred without spending its failure budget. A completed claim should suppress later deliveries for at least 24 hours.
Redirects are not followed. Configure the final HTTPS webhook URL directly: a 3xx response is a retryable redirect-policy failure under the normal delivery budget, and the signed body and headers are never forwarded to its target.
Reject present-but-invalid signatures. If v1a is present but fails verification, treat the delivery as unauthenticated and reject it. Do not fall through to any other check.
Node.js
Python
Go
Key Rotation
During a cluster key rotation, two public keys are published at GET /.well-known/webhook-public-keys simultaneously (current and previous). Deliveries signed under the previous akid remain verifiable for an overlap window. Periodically fetch the full list, build a new keyring, and atomically replace the old one so a retired key cannot remain trusted indefinitely. Also refresh immediately when a delivery uses an unknown akid.