Events
A provisional decision is normally finalized within 30 seconds. If the asynchronous backends have not answered by
then, Specter fails open: it finalizes the decision as
ALLOW and sends decision.async.allow.
Simulations do not send webhooks.
Payload
Each notification is aPOST request with a JSON body:
See Event notification for the full schema.
Responding to Webhooks
Specter counts a delivery as successful when your endpoint answers with any2xx status code, for example 200,
202 or 204, within 15 seconds.
- Acknowledge first, process later. Store the event and return
2xxright away, then do the work, such as capturing or cancelling a payment, asynchronously. A slow response counts as failed and the event is sent again. - Keep the response body empty. Specter ignores the body. If you do return one with
content-type: application/json, it must be valid JSON; otherwise the delivery counts as failed. - Every other outcome is a failure. Any status outside
2xx, including4xx, a timeout, and a connection or TLS error, causes a retry. There is no status code that stops retries. - Register the final URL. Specter follows redirects, but a
301,302or303redirect turns the request into aGETwithout the payload.
Retries
Specter makes up to five attempts per event and endpoint. After a failed attempt, it waits before trying again:
Each wait can be up to 10% longer so retries don’t arrive in lockstep. The last attempt happens about a minute and a
half after the first.
If the fifth attempt fails, Specter drops the event. The endpoint stays active for later events, but failed
deliveries cannot be listed or sent again. To recover a missed outcome, fetch the decision with
Get decision details.
Updating or
deleting an endpoint affects only events that occur
afterwards. Deliveries that are already queued, including their retries, still go to the previous URL.
Duplicates and Ordering
The same event can arrive more than once, for example when your endpoint processed it but answered too late. Retries carry the sameid, the same body and the same signature, so store the id of each processed event and
skip events you have already seen.
Events are not guaranteed to arrive in order:
- A
decision.async.*event can arrive before thedecision.provisionalevent of the same decision. - It can even arrive before Specter’s response to your
POST /api/decisionsrequest, when the backends answer quickly.
decision_id and treat a decision.async.* event as the final outcome, whenever it arrives. A
decision.provisional event never replaces it.
Verifying Signatures
Every notification carries anx-hmac-signature header: the HMAC-SHA256 of the raw request body, encoded as
lowercase hex.
Specter generates the HMAC key when you register the endpoint and returns it only once, as hmac_key in the
creation response. Store it securely; later responses show only a masked version. The key cannot be changed: to
rotate it, register a new endpoint and then delete the old one.
Use the key exactly as returned, as a string. It looks like base64, but must not be decoded.
To verify a notification:
- Calculate the HMAC-SHA256 of the raw request body, before any JSON parsing, with your
hmac_key. - Compare the result with the
x-hmac-signatureheader, using a constant-time comparison.
Example
For the HMAC keyq3Vt8hZ2mN0xYcLr5pW7sKfA1eJ9uDbG4oTnXiRvEyQ and this raw body:
x-hmac-signature header is:
Buffer from a raw body parser. Re-serializing parsed JSON can
change the bytes and break the signature.