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.
Register your endpoint
Create the subscription withPOST /webhooks. url must be a public HTTPS endpoint. subscriptions takes event type values from the catalog or "*" for all of them. The response returns a signingKey exactly once.
Verify the signature
Every delivery carries aWebhook-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
tdiffers from your current time by more than 5 minutes in either direction (|now - t| > 300seconds). A future-datedtis rejected the same way as a stale one, so clock skew on your side cannot admit a replay. OMS regeneratestand the signature on every attempt, so a legitimate retry always carries a fresh timestamp.
Respond before you process
Return a2xx 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.
Read the envelope
Every delivery body has the same six fields:type, then read the resource from data.resource. It has the same shape GET returns for that resource, minus empty lists. A *.statusChanged event adds data.previousStatus, and a *.updated event adds data.changedFields. See the webhook events catalog for every type.
Write the handler to tolerate values it does not know:
- Unknown
type: acknowledge with2xxand ignore it. New types can appear without a version bump. - Unknown
subStatus: fall back to the part before the dot (processing.payoutHeldfalls back toprocessing). schemais not"v1": the event predates the current format. Ignore it or handle it separately.
Deduplicate on id and idempotencyKey
Delivery is at-least-once: OMS may deliver successfully, fail to record it, and send again. id (evt_) is the same on every retry of an event, so store it with a unique constraint and drop a delivery whose id you have already applied.
Occasionally the same change is emitted twice with two different id values. Both carry the same idempotencyKey, so skip a delivery whose idempotencyKey you have already applied too.
Order by createdAt
OMS delivers in parallel, so events arrive in any order, even for the same resource. There is no sequence number. createdAt is strictly increasing per resource, so apply an event only when its createdAt is newer than the last one you applied for that resource:
createdAt is UTC with exactly three fractional digits, so the ISO strings compare correctly as text.
Events about related resources can also arrive out of order: a transfer.created can land before the transaction.created it belongs to. If you receive a transfer for a transaction you have not seen, hold it until the transaction arrives, or fetch it with GET /transactions/{transactionId}. When a gap matters, read the resource from the API instead of inferring the missing step.
Test your endpoint
POST /webhooks/{webhookId}/test sends a signed test delivery to your endpoint, regardless of your subscriptions, so you can check signing and reachability before any real event fires.
GET /webhooks/{webhookId}/deliveries. Filter by status, test, a createdAfter/createdBefore window, or eventId (the envelope id). Each delivery record has its own whd_ ID and nests the exact body OMS sent under event: its id, idempotencyKey, type, createdAt, schema, and data. GET /webhooks/{webhookId}/deliveries/{deliveryId} adds the HTTP attempts, with the response status, duration, and request body of each, which tells you 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.
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.
Delivery history is retained for 30 days, and it is the only way to recover missed events: there is no GET /events. For anything older, read the resource’s current state from the API. DELETE /webhooks/{webhookId} is a soft delete: the webhook disappears from list and get, but its history is kept for the retention window.