Your endpoint
Your endpoint has to be reachable over HTTPS at a publicly resolvable address. Spirii resolves the hostname before each delivery and refuses any that points at a private, loopback, link-local, metadata or otherwise reserved address, on both IPv4 and IPv6. The check runs against the resolved addresses rather than the hostname, so a public name pointing at an internal IP is refused too. A consequence worth knowing during development:localhost, 127.0.0.1 and private-range addresses can never receive deliveries. Use a public tunnel endpoint while you build.
The delivery request
Each delivery is aPOST with a JSON body. Spirii waits 10 seconds for your response.
Any custom headers you configured on the subscription are sent alongside these. The three signing headers always take precedence, so a custom header cannot overwrite them.
The body is the same envelope for every event type:
data is shown empty here because its contents differ per event type. Fetch the schema for the types you subscribe to from GET /emsp/v1/webhooks/event_types/{type}, which returns the versions available and the latest one. Those schemas are served live, so read them from the API rather than copying them into your code.
Verify the signature
Spirii signs the timestamp and the body together, so a captured delivery can’t be replayed later with a fresh timestamp:sha256=.
Sign over the raw bytes you received. Parsing the JSON and re-serialising it changes whitespace and key order, and the signature will not match.
event_id and X-Webhook-Id are stable across retries, use either as an idempotency key. A retry after your handler succeeded but its response was lost is indistinguishable from a first delivery, so the same event will sometimes arrive twice.
Respond to a delivery
Return any2xx status to acknowledge a delivery. Anything else, including a timeout, a connection error or a redirect, counts as a failure and is retried.
Acknowledge first and do the work afterwards. With a 10-second timeout, a handler that writes to a slow downstream system before responding will start failing under load, and those failures count against your subscription.
Retries and failures
A failed delivery is retried up to five attempts, on a fixed schedule:
After the fifth attempt the delivery is dead-lettered and that event is not sent again.
To bring a disabled subscription back, fix the endpoint, then
PATCH /emsp/v1/webhooks/{webhook_id} with status: ACTIVE. Confirm the endpoint works before you do: failure_count only clears on a successful delivery, so re-enabling with the count still at five means the next failure disables it again immediately. Send a test delivery first.
Pausing is different from failing. A subscription you set to PAUSED stays paused until you set it back, and is not counted as unhealthy.
Test deliveries
POST /emsp/v1/webhooks/{webhook_id}/test sends a one-off signed payload to your endpoint and reports whether it was accepted.
The test leaves your subscription alone. It never changes failure_count or status, so a failing test costs you nothing, and a passing one does not clear a count that has already climbed. Its value is confirming the endpoint is healthy before you re-enable and spend the one delivery you have left.
Two things differ from a real delivery:
- The body is not the standard envelope. A test sends
type,webhook_id,eventand adataobject holding a fixed message, with noevent_id,api_version,occurred_atorpublished_at.X-Webhook-Idistest_{webhook_id}rather than an event id. A handler that validates the envelope strictly will reject a test and look broken when it isn’t. - The timeout is 5 seconds, against 10 for a real delivery. A handler responding close to that limit can fail a test it would have passed in production.
Monitor your subscription
GET /emsp/v1/webhooks/{webhook_id} returns five fields describing delivery health:
Alert on
failure_count rather than on status. By the time status reads FAILED you’ve already lost the events that occurred during the outage, whereas a count climbing above one or two gives you a window to act.
A last_status_code of null alongside a rising failure_count points at a connection-level problem, such as an expired certificate or a DNS change, rather than an error your application returned.
Related
Authentication
Get a token and the scopes needed to manage subscriptions.
API overview
The Spirii APIs and their path prefixes.
Errors
The status codes the management endpoints return.