Skip to main content
Before you start: the OMS API is in early access. Every endpoint, including the ones in this guide, requires an early-access API key. Request access before you begin.
OMS delivers state changes to an HTTPS endpoint you own. Delivery is at-least-once and ordering is best effort, so a correct handler does four things: verify the signature, respond quickly, ignore deliveries it has already applied, and refuse to apply state older than what it already has. This guide covers the handler itself. For the events you can subscribe to and the envelope they arrive in, see the webhook events catalog.

Register your endpoint

Create the subscription with POST /webhooks. The response returns a signingKey exactly once.
Store signingKey the moment you receive it. It is never returned again. If you lose it, call POST /webhooks/{webhookId}/rotate-key for a new one, which invalidates the old key.

Verify the signature

Every delivery carries a Webhook-Signature header:
v1 is a hex HMAC-SHA256 over the string <t>.<raw request body>, keyed with your signing key. Two details matter:
  • Hash the raw body, before any JSON parsing. Re-serializing the parsed object changes the bytes and the signature will not match.
  • Compare in constant time, and reject deliveries whose t is too old to limit replay. OMS regenerates t and the signature on every attempt, so a legitimate retry always carries a fresh timestamp.

Respond before you process

Return a 2xx as soon as you have verified and persisted the delivery, then do the real work asynchronously. Each attempt is bounded by the webhook’s timeoutMs (default 5000, hard cap 10000), measured from connect through response. Anything slower is recorded as a failed attempt and retried, even if your handler eventually succeeded, which turns slow processing into duplicate work. Any non-2xx response, timeout, or network error is retried. Status codes are not special-cased: returning 410 Gone does not stop delivery.

Deduplicate on id

The envelope’s id is the delivery ID (whd_). It is one per event and endpoint, and it stays the same across that delivery’s retries, which makes it your deduplication key. Because delivery is at-least-once, the same id can arrive more than once: OMS may succeed in sending and fail to record it, then send again. Persist id with a unique constraint and drop a delivery you have already applied.
The five *.statusChanged events also carry eventId, the producer event ID (evt_). That ID is shared across the fan-out to every endpoint subscribed to the same event, so it is not a per-endpoint dedup key. Deduplicate on id.

Order by sequence

Ordering is best effort, not a guarantee. Retries and concurrency mean an older event can arrive after a newer one for the same resource. sequence is a per-resource monotonic counter. Apply a delivery only when its sequence is newer than the last one you applied for that resource, so a late arrival cannot overwrite newer state. Where a gap matters, refetch the resource from the API instead of inferring the missing step.
If you need strict in-order delivery, webhooks are the wrong transport. Reconcile against the API instead.

Read the event type

Most events name the type in event. The five *.statusChanged events use eventType. Read whichever is present:
The changed resource is under data in both shapes, so branch on data.status rather than parsing the event name for a status suffix.

Test your endpoint

POST /webhooks/{webhookId}/test sends a webhook.test event. It is delivered regardless of your subscriptions, so it exercises signing and reachability before any real event fires.
Inspect what OMS actually sent with GET /webhooks/{webhookId}/deliveries, filtering by status, eventId, test, or a createdAfter/createdBefore window. GET /webhooks/{webhookId}/deliveries/{deliveryId} expands a single delivery with its HTTP attempts, including the response status and duration OMS recorded for each one, which is the fastest way to confirm whether a failure was your endpoint or the network.

When deliveries fail

A failed delivery is retried on a fixed schedule: That is 10 attempts across roughly 3 days, after which the delivery is marked failed. Requeue failed deliveries yourself with POST /webhooks/{webhookId}/deliveries/retry, passing explicit deliveryIds, or a status plus a createdAfter/createdBefore range. A manual retry resets the attempt count. If your endpoint stays down, two things happen on separate clocks:
  • Notification after the endpoint has been failing longer than notifyThresholdSecs (10 minutes to 1 day, default 1 hour). Sent once per down episode, not per failed attempt.
  • Auto-suspension after roughly 3 days of continuous failure.
A suspended webhook keeps creating delivery rows and records them as skipped rather than queued, so nothing is silently dropped while you fix the endpoint. Only OMS sets suspended, and it never clears it on its own: call POST /webhooks/{webhookId}/enable to return the webhook to enabled and reset the down clock, then requeue the skipped deliveries with POST /webhooks/{webhookId}/deliveries/retry.
A disabled webhook behaves differently: it creates no delivery rows at all, so events that fire while it is disabled cannot be recovered. Prefer fixing a suspended endpoint over disabling it.
Delivery history is retained for 30 days by default. DELETE /webhooks/{webhookId} is a soft delete: the webhook disappears from list and get, but its history is kept for the retention window.