# Manage webhooks with the API

The webhooks feature must be enabled using a feature flag on your Ripple Custody instance. Contact your Ripple liaison if you're unable to access the feature.

Use this page to manage webhook channels with the Event Distribution Service (EDS) API. Webhooks push event notifications to an HTTPS endpoint that you control. To pull event history for reconciliation or audit workflows, see [Manage events with the API](/products/custody/v1.36/operations-and-maintenance/events-and-webhooks/events-api).

## Webhook actions

| Action | API procedure | API reference |
|  --- | --- | --- |
| Create a webhook channel | [Create a webhook channel](#create-a-webhook-channel) | [Create a channel](/products/custody/v1.36/reference/api/openapi/event-distribution-service/createchannel) |
| List webhook channels | [List webhook channels](#list-webhook-channels) | [Get all channels](/products/custody/v1.36/reference/api/openapi/event-distribution-service/getallchannels) |
| Get webhook channel details | [Get webhook channel details](#get-webhook-channel-details) | [Get channel details](/products/custody/v1.36/reference/api/openapi/event-distribution-service/getchannel) |
| Update a webhook channel | [Update a webhook channel](#update-a-webhook-channel) | [Update a channel](/products/custody/v1.36/reference/api/openapi/event-distribution-service/updatechannel) |
| Test a webhook channel | [Test a webhook channel](#test-a-webhook-channel) | [Test a channel](/products/custody/v1.36/reference/api/openapi/event-distribution-service/testchannel) |
| Delete a webhook channel | [Delete a webhook channel](#delete-a-webhook-channel) | [Delete a channel](/products/custody/v1.36/reference/api/openapi/event-distribution-service/deletechannel) |
| List delivery events across channels | [Track webhook delivery events](#track-webhook-delivery-events) | [Get all events](/products/custody/v1.36/reference/api/openapi/event-distribution-service/getallevents) |
| List delivery events for a channel | [Track webhook delivery events](#track-webhook-delivery-events) | [Get all events for a channel](/products/custody/v1.36/reference/api/openapi/event-distribution-service/getallchannelevents) |
| Get delivery event details | [Track webhook delivery events](#track-webhook-delivery-events) | [Get channel event details](/products/custody/v1.36/reference/api/openapi/event-distribution-service/getevent) |


## Before you start

Prepare the following values:

| Value | Description |
|  --- | --- |
| `domainId` | Domain that owns the webhook channel. |
| `JWT_TOKEN` | JWT for an authenticated API user. |
| `createdBy` or `lastUpdatedBy` | User ID for the user creating or updating the channel. |
| Webhook URL | HTTPS endpoint where Ripple Custody sends event notifications. |
| Event types | Event `payload.type` values to subscribe to, such as `IntentCreated`, `IntentUpdated`, or `IntentClosed`. |


You must be a user with the `eds-manager` role to create and manage webhook channels for your domain.

## Choose event types

You can subscribe to many event types when configuring a webhook. For detailed information about each event type and complete payload structures, see [Event payload reference](/products/custody/v1.36/reference/events-and-webhooks/event-payload-reference).

For intent state changes, subscribe to `IntentUpdated` to receive updates when an intent is approved or rejected. Subscribe to `IntentClosed` to receive updates when an intent reaches its final state.

## Webhook callback payload

When a subscribed event occurs, Ripple Custody sends a minimal event payload to your webhook URL. The callback body uses the event shape returned by the Events API and is designed for easy parsing.


```json
{
  "domainId": "25aaec0d-e8dc-44b6-8070-9231f1ddadf0",
  "payload": {
    "id": "35e4d8d9-f943-484b-865c-c736679ba0cc",
    "type": "IntentCreated"
  },
  "id": "03424a3f-bdfc-4521-b8b2-933a8ff13cce",
  "sequenceNumber": 369,
  "savedAt": "2025-03-07T14:22:37.389Z"
}
```

| Field | Description |
|  --- | --- |
| `domainId` | Domain where the event was generated. |
| `payload` | Event-specific payload. The payload includes a `type` discriminator. |
| `id` | Unique event ID. |
| `sequenceNumber` | Event sequence number. Sequence numbers are strictly increasing, but may not be contiguous. |
| `savedAt` | Timestamp when the event was saved. |


For event-specific payload fields and event type names, see [Event payload reference](/products/custody/v1.36/reference/events-and-webhooks/event-payload-reference).

## Create a webhook channel

Call `POST /v1/domains/{domainId}/channels` to create a webhook channel.


```sh
curl -X POST "${CUSTODY_API_URL}/v1/domains/${DOMAIN_ID}/channels" \
  -H "Authorization: Bearer ${JWT_TOKEN}" \
  -H "Content-Type: application/json" \
  -d @create-webhook-channel.json
```

The request body uses the `EDS_WebhookChannelCreate` schema:


```json
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "name": "My Webhook Channel",
  "supportedEventTypes": [
    "IntentCreated",
    "IntentUpdated",
    "IntentClosed"
  ],
  "createdBy": "b7c8d9e0-f1a2-3456-bcde-f23456789012",
  "type": "WEBHOOK",
  "url": "https://your-domain.com/webhook-endpoint"
}
```

### Create request fields

| Field | Type | Required | Description |
|  --- | --- | --- | --- |
| `id` | string, UUID | Yes | Unique identifier for the webhook channel. |
| `name` | string | Yes | Descriptive channel name. |
| `supportedEventTypes` | array of strings | Yes | Event `payload.type` values to subscribe to. |
| `createdBy` | string, UUID | Yes | User ID for the user creating the channel. |
| `type` | string | Yes | Channel type. For webhooks, use `WEBHOOK`. |
| `url` | string | Yes | Webhook endpoint URL where events are sent. |
| `status` | string | No | Initial channel status. Supported values are `ACTIVE` and `DISABLED`. |
| `maxRetries` | integer | No | Maximum number of retry attempts for failed webhook deliveries. |


The API returns the created channel:


```json
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "domainId": "25aaec0d-e8dc-44b6-8070-9231f1ddadf0",
  "name": "My Webhook Channel",
  "supportedEventTypes": [
    "IntentCreated",
    "IntentUpdated",
    "IntentClosed"
  ],
  "type": "WEBHOOK",
  "status": "ACTIVE",
  "maxRetries": 3,
  "url": "https://your-domain.com/webhook-endpoint",
  "createdBy": "b7c8d9e0-f1a2-3456-bcde-f23456789012",
  "lastUpdatedBy": "b7c8d9e0-f1a2-3456-bcde-f23456789012",
  "createdAt": "2025-03-07T14:22:37.389Z",
  "lastUpdatedAt": "2025-03-07T14:22:37.389Z",
  "errorRate": 0
}
```

### Channel response fields

| Field | Description |
|  --- | --- |
| `id` | Unique channel ID. |
| `domainId` | Domain that owns the channel. |
| `name` | Channel name. |
| `supportedEventTypes` | Event types subscribed by the channel. |
| `type` | Channel type. Webhook channel responses use `WEBHOOK`. |
| `status` | Channel status. Supported response values are `ACTIVE` and `DISABLED`. |
| `maxRetries` | Maximum number of retry attempts for failed webhook deliveries. |
| `url` | Webhook endpoint URL. |
| `createdBy` | User ID for the user who created the channel. |
| `lastUpdatedBy` | User ID for the user who last updated the channel. |
| `createdAt` | Timestamp when the channel was created. |
| `lastUpdatedAt` | Timestamp when the channel was last updated. |
| `errorRate` | Webhook delivery error rate. |


## List webhook channels

Call `GET /v1/domains/{domainId}/channels` to retrieve all webhook channels for a domain.


```sh
curl -X GET "${CUSTODY_API_URL}/v1/domains/${DOMAIN_ID}/channels" \
  -H "Authorization: Bearer ${JWT_TOKEN}"
```

The response is an array of channel objects.

## Get webhook channel details

Call `GET /v1/domains/{domainId}/channels/{channelId}` to retrieve one webhook channel.


```sh
curl -X GET "${CUSTODY_API_URL}/v1/domains/${DOMAIN_ID}/channels/${CHANNEL_ID}" \
  -H "Authorization: Bearer ${JWT_TOKEN}"
```

The response is a channel object.

## Update a webhook channel

Call `PATCH /v1/domains/{domainId}/channels/{channelId}` to update a webhook channel. Include only the fields you need to change.


```sh
curl -X PATCH "${CUSTODY_API_URL}/v1/domains/${DOMAIN_ID}/channels/${CHANNEL_ID}" \
  -H "Authorization: Bearer ${JWT_TOKEN}" \
  -H "Content-Type: application/json" \
  -d @update-webhook-channel.json
```

The request body uses the `EDS_ChannelUpdate` schema:


```json
{
  "name": "Operations Webhook Channel",
  "supportedEventTypes": [
    "IntentCreated",
    "IntentUpdated",
    "IntentClosed"
  ],
  "status": "active",
  "maxRetries": 3,
  "lastUpdatedBy": "b7c8d9e0-f1a2-3456-bcde-f23456789012"
}
```

| Field | Type | Description |
|  --- | --- | --- |
| `name` | string | New channel name. |
| `supportedEventTypes` | array of strings | New list of event types to subscribe to. |
| `status` | string | New channel status. The update schema supports `active` and `disabled`. |
| `maxRetries` | integer | New maximum number of retry attempts for failed webhook deliveries. |
| `lastUpdatedBy` | string, UUID | User ID for the user updating the channel. |


The update request schema uses lowercase `active` and `disabled` values. Channel responses use uppercase `ACTIVE` and `DISABLED` values.

The API returns the updated channel object.

## Test a webhook channel

Call `POST /v1/domains/{domainId}/channels/{channelId}/test` to test a webhook channel. The operation does not define a request body.


```sh
curl -X POST "${CUSTODY_API_URL}/v1/domains/${DOMAIN_ID}/channels/${CHANNEL_ID}/test" \
  -H "Authorization: Bearer ${JWT_TOKEN}"
```

The API returns `200 OK` when the test request is accepted.

## Delete a webhook channel

Call `DELETE /v1/domains/{domainId}/channels/{channelId}` to delete a webhook channel. The operation does not define a request body.


```sh
curl -X DELETE "${CUSTODY_API_URL}/v1/domains/${DOMAIN_ID}/channels/${CHANNEL_ID}" \
  -H "Authorization: Bearer ${JWT_TOKEN}"
```

The API returns `200 OK` when the channel is deleted.

## Track webhook delivery events

EDS delivery events are operational records for webhook delivery attempts. They are different from the event payload sent to your webhook endpoint.

| Task | Endpoint |
|  --- | --- |
| List delivery events across all channels in a domain | `GET /v1/domains/{domainId}/channels/events` |
| List delivery events for one channel | `GET /v1/domains/{domainId}/channels/{channelId}/events` |
| Get one delivery event | `GET /v1/domains/{domainId}/channels/{channelId}/events/{eventId}` |


List all delivery events across channels:


```sh
curl -X GET "${CUSTODY_API_URL}/v1/domains/${DOMAIN_ID}/channels/events" \
  -H "Authorization: Bearer ${JWT_TOKEN}"
```

List delivery events for one channel:


```sh
curl -X GET "${CUSTODY_API_URL}/v1/domains/${DOMAIN_ID}/channels/${CHANNEL_ID}/events" \
  -H "Authorization: Bearer ${JWT_TOKEN}"
```

Get one delivery event:


```sh
curl -X GET "${CUSTODY_API_URL}/v1/domains/${DOMAIN_ID}/channels/${CHANNEL_ID}/events/${EVENT_ID}" \
  -H "Authorization: Bearer ${JWT_TOKEN}"
```

Delivery event list operations return an array of `EDS_Event` objects. The delivery event detail operation returns one `EDS_Event` object.


```json
{
  "id": "c0c54215-77e8-4e8f-8b0f-b3d60269f2f7",
  "channelId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "channelName": "My Webhook Channel",
  "type": "IntentCreated",
  "status": "FAILED",
  "createdAt": "2025-03-07T14:22:37.389Z",
  "lastUpdatedAt": "2025-03-07T14:22:40.889Z",
  "retriedCount": 3,
  "payload": "{\"id\":\"35e4d8d9-f943-484b-865c-c736679ba0cc\",\"type\":\"IntentCreated\"}"
}
```

| Field | Description |
|  --- | --- |
| `id` | Unique delivery event ID. |
| `channelId` | Webhook channel ID. |
| `channelName` | Webhook channel name. |
| `type` | Event type associated with the delivery event. |
| `status` | Delivery status. Supported values are `PENDING`, `SUCCESS`, and `FAILED`. |
| `createdAt` | Timestamp when the delivery event was created. |
| `lastUpdatedAt` | Timestamp when the delivery event was last updated. |
| `retriedCount` | Number of retry attempts recorded for the delivery event. |
| `payload` | Event payload stored as a string. |


## Delivery behavior

If a webhook delivery fails, for example if your endpoint is unavailable or returns an error, Ripple Custody retries delivery using exponential backoff.

### Retry behavior

1. Ripple Custody sends the event to your webhook endpoint.
2. If delivery fails, the system waits and retries using exponential backoff.
3. After the maximum number of retry attempts is exhausted, the delivery event is marked as `FAILED` and automatic retries stop.


The default retry behavior is:

| Parameter | Default | Description |
|  --- | --- | --- |
| Max retries | 3 | Number of retry attempts before marking the delivery event as `FAILED`. |
| Initial interval | 500 ms | Wait time before the first retry. |
| Multiplier | 2 | Factor applied to increase wait time between retries. |
| Max interval | 5000 ms | Upper limit for wait time between retries. |


With the default values, if delivery fails, the system retries after 500 ms, then 1000 ms, then 2000 ms.

The EDS channel schema exposes `maxRetries` for channel configuration. Other retry timing settings are instance-level operational configuration and require coordination with Ripple teams to modify.

### Timeout configuration

| Parameter | Default | Description |
|  --- | --- | --- |
| Request timeout | 30 seconds | Maximum time to wait for your endpoint to respond. |
| Connection timeout | 10 seconds | Maximum time to establish a connection to your endpoint. |


If your endpoint does not respond within the timeout period, the delivery is considered failed and is retried.

### HTTP status code handling

| Status code range | Behavior |
|  --- | --- |
| `2xx` | Delivery successful, no retry. |
| `3xx` | Redirects are not followed. Delivery is treated as failed and retried. |
| `4xx` | Delivery failed and retried, except for `410 Gone`. |
| `410` | Delivery failed, no retry. The endpoint is treated as permanently unavailable. |
| `5xx` | Delivery failed and retried. |
| Timeout | Delivery failed and retried. |
| Connection error | Delivery failed and retried. |


To prevent retries for a specific event, return `410 Gone`. For all other cases, return `2xx` to acknowledge successful receipt or return another status code to allow retries.

### Rate limiting

Webhook deliveries are not rate-limited by default. Design your endpoint to handle:

- Multiple concurrent webhook deliveries.
- Bursts of events during high activity periods.
- Retry attempts for failed deliveries.


If your endpoint cannot keep up with event volume, consider:

- Implementing a queue to buffer incoming webhooks.
- Scaling your webhook receiver horizontally.
- Using AMQP queues instead for guaranteed delivery and backpressure handling.


## Handle failed deliveries

Use EDS delivery events, the Events API, and any configured AMQP integration when recovering from webhook delivery failures:

1. Check delivery event records for `FAILED` status.
2. Use `retriedCount` and `lastUpdatedAt` to understand retry activity.
3. Use [List events](/products/custody/v1.36/reference/api/openapi/events/getevents) with `sortBy=sequenceNumber` to reconcile the event history your system processed.
4. Page through Events API results with `startingAfter`; the Events API schema does not define time-range filters.
5. If your deployment includes an AMQP queue integration, inspect the queue to recover missed events. Contact [Ripple support](/products/custody/v1.36/support/get-support) for help with AMQP-based recovery.
6. Process any missing event IDs or sequence numbers in your integration.


## Secure your webhook endpoint

Secure the endpoint that receives webhook callbacks:

- Use HTTPS endpoints.
- Validate incoming requests.
- Implement authentication or authorization for your receiver.
- Use IP allowlisting if required by your environment.


Want to manage webhooks with the UI instead? See [Manage webhooks in the UI](/products/custody/v1.36/operations-and-maintenance/events-and-webhooks/webhooks-ui).