# 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.

important

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 the `eventType` and on the subscriber's [payload version](#payload-versions).

The fields around `data` are the same in every payload version, and so is the [signature](#validating-authenticity).

Example webhook payload

```
{

  "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](https://docs.odus.com/docs/guides/upgrading/webhook-payload-v2.md#fields-by-resource) |
| 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](#when-version-2-cannot-capture-the-event)                                                                                 |
| 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}`](https://docs.odus.com/docs/api/core/v2/payment-controller-find-one).

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.

warning

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](https://docs.odus.com/docs/guides/upgrading/webhook-payload-v2.md#fields-by-resource) 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.

Deletion events

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](#when-version-2-cannot-capture-the-event) 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](https://docs.odus.com/docs/guides/dashboard/webhook-subscriber.md) 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:

1. Retrieve the `X-Webhook-HMAC` header from the incoming request.
2. Compute the HMAC of the request body using the shared secret and the SHA256 algorithm.
3. Compare the computed HMAC with the `X-Webhook-HMAC` header value.

Example of validating webhook authenticity

```
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.

important

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](https://docs.odus.com/docs/guides/upgrading/webhook-payload-v2.md#move-a-subscriber-to-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 `eventId` to 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](https://docs.odus.com/docs/api/core/v2/webhook-customer-created) 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](#version-2).
