The signing secret
Each endpoint has its own secret, shown once when the endpoint is created. Store it as you would any secret: in an environment variable or a secrets manager, never in client-side code or a repository. If it leaks, delete the endpoint and add it again; that mints a new secret. Deliveries do not come from a fixed set of IP addresses, so allowlisting is not a substitute for verifying the signature.The signature header
Every request carries:t is the time the request was signed, in seconds since the Unix epoch. v1 is the hex HMAC-SHA256, keyed with the signing secret, of the string <t>.<raw body>: the timestamp, a full stop, and the request body exactly as received.
Verifying it
- Read the raw body as bytes. Do not parse and re-serialise it first; any change to whitespace or key order changes the digest.
- Split the header on
,and each part on=to gettandv1. - Check that
v1is exactly 64 lowercase hex characters. Anything else is not a digest and is rejected before comparing, so a malformed header cannot make the comparison throw. - Compute HMAC-SHA256 of
<t>.<raw body>with the secret and compare it tov1with a constant-time comparison. - Reject the request if
tis more than a few minutes from now. This stops a captured request from being replayed later.
After verifying
- Deduplicate on the body’s
id. Delivery is at least once, and retries of one event share it. - Treat the body as a notification, not as authority over your data. When an action depends on the poll’s current state, fetch
GET /polls/{pollId}with your API key rather than trusting a snapshot. - Respond
2xxas soon as the request is stored. Work that takes longer than 10 seconds should happen after the response, or the delivery is retried.