Skip to main content
When an event you’ve subscribed to occurs, Spirii sends it to your endpoint as a signed JSON POST. Every delivery carries the same envelope and the same three signing headers, whatever the event type. Subscriptions themselves are managed through the Webhooks endpoints. This page covers what arrives afterwards.

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 a POST 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:
The secret is the one returned when you created the subscription, or the most recent one if you’ve rotated it. The digest is lowercase hex, prefixed with 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.
Compare signatures with a constant-time function, timingSafeEqual in Node.js or hmac.compare_digest in Python. A plain == leaks how much of the signature matched through its timing, which is enough to forge one given sufficient attempts.
The five-minute tolerance above is a suggestion, not something Spirii enforces. Pick a window that suits your infrastructure: too tight and a slow queue rejects legitimate deliveries, too loose and a captured request stays replayable for longer. Because 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 any 2xx 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.
Five consecutive failures disable the subscription. failure_count counts consecutive failed attempts across all events and resets to 0 on any success, so a single event failing all five attempts is enough to move the subscription from ACTIVE to FAILED. Deliveries stop at that point, and events that occur while it’s disabled are not queued for later.
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, event and a data object holding a fixed message, with no event_id, api_version, occurred_at or published_at. X-Webhook-Id is test_{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.
Signing is identical, so a test is a sound way to check your signature verification end to end.

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.

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.