Webhooks
ODUS webhooks allow you to receive real-time notifications about events that occur in your ODUS account. This enables you to build integrations that can react to events such as successful payments, recurring payments, subscription updates, and more.
It is important to understand that webhook delivery guarantees are as follows:
- Webhooks are delivered at least once, meaning that you may receive duplicate events. Use Event IDs to deduplicate events on your end.
- Webhooks are sent in order of event creation, however, it doesn't mean that they will be delivered in the same order, as they may be processed in parallel and there's network latency involved.
- Webhooks are sent asynchronously, meaning that you may receive events after a delay. The delay is usually less than 5 seconds, but it can be longer in some cases.
A single action can dispatch multiple events
One action on a resource can produce more than one event, and you will receive a separate webhook for each event you are subscribed to. The events share no parent — each has its own eventId, is delivered independently, and counts as its own delivery for retries.
The most common example is an automatically captured payment: it is authorized and captured in a single step, so it emits both a payment.succeeded event and a payment.captured event at the same time. If you subscribe to both, you will receive two webhooks for that one payment. For a payment that is captured later (delayed or manual capture), the two events arrive separately: payment.succeeded at authorization, and payment.captured once the funds are collected.
Because these events are delivered independently, do not assume they will arrive in a fixed order, and do not assume one webhook implies another has already been delivered. Subscribe only to the events you act on, deduplicate by eventId, and keep each handler idempotent so that receiving related events in any order — or more than once — has no unintended effect.
Webhook payload
The webhook payload is a JSON object that contains the following fields:
eventId: A unique identifier for the event.eventType: The type of the event, e.g.payment.created,payment.updated, etc.profile: The webhook subscriber profile that the webhook is sent to.merchantId: The identifier of the merchant account the event belongs to.timestamp: The timestamp of the event creation in ISO 8601 format.data: The event data, which is a JSON object that contains the event-specific data. The structure of this object depends on theeventTypeand on the subscriber's payload version.
The fields around data are the same in every payload version, and so is the signature.
{
"eventId": "evt_abc",
"eventType": "payment.created",
"profile": "whs_xyz",
"merchantId": "acct_xyz",
"timestamp": "2023-10-01T12:00:00Z",
"data": {
// Payment object data
}
}
Payload versions
Webhook payloads have a version, which is separate from the API version. The version decides the shape of data. Each subscriber has a payloadVersion. You choose it when you create the subscriber, and you cannot change it afterwards. If you don't choose one, the subscriber uses version 1.
| Version 1 | Version 2 | |
|---|---|---|
What data contains | The resource as its retrieve endpoint returns it | A fixed set of fields per resource, listed in Upgrading webhooks to payload version 2 |
When data is captured | When the webhook is first built, which can be some seconds after the event | At the moment of the event, with a rare exception |
| Related objects | Expanded into full objects | Only their ID |
| Deletion events | Only the ID: { "id": "<resource_id>" } | Normally the resource as it was when it was removed |
<resource>.updated events | Sent for a narrower set of changes | Sent for every change to a version 2 field |
Version 1 continues to be delivered. If support for version 1 is going to end, we will announce it in advance.
Version 1
data follows the response of the resource's retrieve endpoint, including sensitive fields. For example, for a payment.created event, data contains the payment object described in GET /v2/payments/{id}.
Version 1 predates API v2 and shapes data the way the v1 retrieve endpoints do. For most resources that is the shape v2 returns too. The exceptions are the fields v2 dropped: a subscription's invoices, a customer's paymentMethods and subscriptions, and a secret key's active. A payload may still carry them, so don't build on them. Read those collections from their own list endpoints, as you would on v2.
On version 1, data shows the resource when the webhook is built, not when the event happened. It can include changes made after the event. For example, a payment.created webhook can carry a payment that has already been attempted. Use eventType to know what happened, and don't treat data as the state at that moment.
Version 2
data is a snapshot of the resource, taken at the same moment as the change that caused the event. It never changes afterwards. The webhook defines the shape, not the API response, so it stays the same when the API response changes. Each resource has a fixed set of fields. Related objects are sent as their ID only. Upgrading webhooks to payload version 2 lists the fields for each resource.
On version 2, <resource>.updated is sent whenever an existing resource changes in any version 2 field other than updatedAt. It can also be sent for a change to a field that version 2 does not include, and it can arrive alongside the more specific event or on its own. For example, a payment that succeeds produces payment.succeeded and can also produce payment.updated. This applies to payments, customers, subscriptions, invoices, products and prices. If you don't need these events, don't subscribe to .updated. On version 1, .updated keeps its narrower set of changes.
When version 2 cannot capture the event
Rarely, the snapshot cannot be taken at the moment of the event. data then has the same version 2 fields, but they are read when the webhook is first built, from the resource as it is at that time. If the resource no longer exists by then, data contains only its identifier: { "id": "<resource_id>" }. Make your handler accept a payload that contains only id, and use eventType to know what happened.
For events that signal the removal of a resource — such as price.deleted — the resource no longer exists when the webhook is delivered. On version 1, data contains only its identifier: { "id": "<resource_id>" }. On version 2, data normally contains the resource as it was when it was removed, and rarely only its identifier. In both versions, use the eventType to recognize these events and the id to reconcile against your own records.
Receiving webhooks
To start receiving webhooks, you first need to create a webhook subscriber and save the generated shared secret. This secret is used to sign the webhook payloads, allowing you to verify that the payloads are sent by ODUS.
Once you have created a webhook subscriber, saved the shared secret, and exposed a publicly accessible URL to receive webhooks, you will now receive webhooks for all events that you have subscribed to.
Validating authenticity
Each webhooks comes with an HMAC in the X-Webhook-HMAC header, which is a SHA256 hash of the payload using the shared secret. To validate the authenticity of the webhook, you should:
- Retrieve the
X-Webhook-HMACheader from the incoming request. - Compute the HMAC of the request body using the shared secret and the SHA256 algorithm.
- Compare the computed HMAC with the
X-Webhook-HMACheader value.
import crypto from 'node:crypto';
const sharedSecret = 'your_shared_secret';
const hmac = req.headers['x-webhook-hmac'];
const computedHmac = crypto.createHmac('sha256', sharedSecret).update(req.body).digest('hex');
if (hmac !== computedHmac) {
// Request did not come from ODUS, reject.
return res.status(401).send();
}
// Request is valid, process the webhook.
Responding to webhooks
When you receive a webhook, you should respond with a 2xx status code to acknowledge that you have received the webhook. If you do not respond with a 2xx status code, the webhook will be retried.
If you receive a webhook that you cannot process, you should respond with a 4xx or 5xx status code to indicate that there was an error.
Each webhook will be attempted up to 10 times over a period of 3 days with an exponential backoff strategy. If the webhook fails to deliver after 10 attempts, it will be marked as failed and will not be retried again.
Long-running webhook handlers
If your webhook handler takes a long time to process the event, you should respond with a 202 Accepted status code as soon as you start processing the event. This will acknowledge the receipt of the webhook and prevent it from being retried.
Webhooks are considered as failed if they exceed the timeout of 15 seconds. If you need more time to process the event, you can use a background job or a message queue to handle the event asynchronously.
Moving to a new payload version
You cannot change the payload version of an existing subscriber. To move, run a second subscriber on the new version next to the one you have, then deactivate the old one. Upgrading webhooks to payload version 2 has the steps.
Best Practices
- Security: Always validate the incoming requests using the shared secret to ensure that the requests are coming from ODUS.
- Least Privilege: Only subscribe to the events that you need. This reduces the amount of data you receive and helps you focus on the events that are relevant to your application.
- Idempotency: Design your webhook handlers to be idempotent, meaning that processing the same event multiple times will not have unintended side effects. Use the
eventIdto deduplicate events if necessary. - Logging: Implement logging for your webhook handlers to track incoming events and any errors that occur during processing. This will help you troubleshoot issues.
Supported Events
The Webhooks section of the API reference lists every event we send, what it means, and the shape of its body on payload version 2.
On payload version 2, the .updated events are sent for more changes than on version 1. See Version 2.