# Webhooks API

Register webhook endpoints and inspect or replay deliveries.

> **Scope of this file.** It is the OPERATION reference for THIS resource, generated from the BillerAPI OpenAPI document served at [/docs/openapi.json](/docs/openapi.json): each API-key endpoint the resource owns, with its parameters, request body, responses and schemas. Narrative and catalog sections the rendered page also carries (walkthroughs, event taxonomies, payload catalogs) are NOT reproduced here — read [/docs/api/webhooks](/docs/api/webhooks) for those. Endpoints the hosted flow drives from inside the iframe, and dashboard-session endpoints, are absent by design: they are not API-key operations. [/docs/api.md](/docs/api.md) enumerates the full API-key surface, including the endpoints that have no reference page yet.

All request and response field names are snake_case. Errors use the coded envelope described in [/docs/concepts/errors.md](/docs/concepts/errors.md).

## Endpoints

- [`GET /v1/webhook-endpoints`](#get-v1webhook-endpoints)
- [`POST /v1/webhook-endpoints`](#post-v1webhook-endpoints)
- [`GET /v1/webhook-endpoints/{id}`](#get-v1webhook-endpointsid)
- [`PATCH /v1/webhook-endpoints/{id}`](#patch-v1webhook-endpointsid)
- [`DELETE /v1/webhook-endpoints/{id}`](#delete-v1webhook-endpointsid)
- [`POST /v1/webhook-endpoints/{id}/rotate-secret`](#post-v1webhook-endpointsidrotate-secret)

## GET /v1/webhook-endpoints

List webhook endpoints

Lists every webhook endpoint registered for the authenticated environment. Signing secrets are NEVER included in list responses.

Operation ID: `list`

Auth: `client-api-key`

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | List of endpoints (no secrets). | — |

## POST /v1/webhook-endpoints

Create a webhook endpoint

Registers a new webhook endpoint for the authenticated environment. Returns the whsec_ signing secret ONCE in the response — it is never shown again (rotate to get a new one). N endpoints per environment are supported; each has its own event filter and secret.

Operation ID: `create`

Auth: `client-api-key`

**Parameters**

| Parameter | In | Required | Type | Notes | Description |
| --- | --- | --- | --- | --- | --- |
| `Idempotency-Key` | header | yes | string | — | Stable printable-ASCII key (1-255 characters) for this registration. |

**Request body** (required)

Schema: `CreateWebhookEndpointDto`

| Field | Required | Type | Notes | Description |
| --- | --- | --- | --- | --- |
| `url` | yes | string | — | Destination URL BillerAPI POSTs signed events to. |
| `events` | yes | array<string> | — | Per-endpoint event filter. Event types this subscription delivers. Must be deliverable event types — one of: bill.created, bill.deleted, bill.updated, bill_import.updated, biller.unsupported, connection.ready, customer.message.complaint, customer.message.created, insight.created, insight.updated, link.completed, link.disconnected, link.failure_reason_corrected, link.session_finished, link_request.cancelled, link_request.created, link_request.updated, link_token.completed, pay.escalated, pay.failed, pay.scheduled, pay.succeeded, payment.observed, sandbox.claimed, sandbox.expiring. Legacy collapse/rename sources (e.g. `bill.paid`, `link.expired`, `request-to-link.updated`, and the historic PascalCase forms like `BillCreated`) are ACCEPTED and normalized to the name they are actually delivered under — the stored + echoed value is the normalized one, so the response shows exactly what was saved. Any other string is rejected with a 400 (`error_code: VALIDATION_ERROR`, `errors[].code: unknown_event_type`) rather than silently stored as a subscription that can never fire. `*` and `all` subscribe to everything. |
| `description` | no | string | — | Human-readable label for the endpoint. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 201 | Endpoint created; secret returned once. May carry an additive `warnings` array — WEBHOOK_URL_NOT_PUBLICLY_DELIVERABLE means the URL does not resolve to a public address, so deliveries to it will be refused. The endpoint is still created (#6361); a future API version will reject instead (#6472). | — |
| 400 | Missing or malformed Idempotency-Key. | — |
| 401 | Missing/invalid client credentials. | — |
| 409 | Idempotency-Key reused with different parameters. | — |
| 503 | Outcome unknown; retry with the same Idempotency-Key. | — |

## GET /v1/webhook-endpoints/{id}

Get a webhook endpoint

Fetches a single webhook endpoint by id. No signing secret in the response.

Operation ID: `getOne`

Auth: `client-api-key`

**Parameters**

| Parameter | In | Required | Type | Notes | Description |
| --- | --- | --- | --- | --- | --- |
| `id` | path | yes | string | — | — |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | The endpoint (no secret). | — |
| 404 | Unknown or cross-client id (WEBHOOK_ENDPOINT_NOT_FOUND). | — |

## PATCH /v1/webhook-endpoints/{id}

Update a webhook endpoint

Updates a webhook endpoint (url, events filter, enabled state, description). Only supplied fields change. No secret in the response.

Operation ID: `update`

Auth: `client-api-key`

**Parameters**

| Parameter | In | Required | Type | Notes | Description |
| --- | --- | --- | --- | --- | --- |
| `id` | path | yes | string | — | — |
| `Idempotency-Key` | header | yes | string | — | Stable printable-ASCII key (1-255 characters) for this update. |

**Request body** (required)

Schema: `UpdateWebhookEndpointDto`

| Field | Required | Type | Notes | Description |
| --- | --- | --- | --- | --- |
| `url` | no | string | — | Updated destination URL. |
| `events` | no | array<string> | — | Replacement event filter for this endpoint. Event types this subscription delivers. Must be deliverable event types — one of: bill.created, bill.deleted, bill.updated, bill_import.updated, biller.unsupported, connection.ready, customer.message.complaint, customer.message.created, insight.created, insight.updated, link.completed, link.disconnected, link.failure_reason_corrected, link.session_finished, link_request.cancelled, link_request.created, link_request.updated, link_token.completed, pay.escalated, pay.failed, pay.scheduled, pay.succeeded, payment.observed, sandbox.claimed, sandbox.expiring. Legacy collapse/rename sources (e.g. `bill.paid`, `link.expired`, `request-to-link.updated`, and the historic PascalCase forms like `BillCreated`) are ACCEPTED and normalized to the name they are actually delivered under — the stored + echoed value is the normalized one, so the response shows exactly what was saved. Any other string is rejected with a 400 (`error_code: VALIDATION_ERROR`, `errors[].code: unknown_event_type`) rather than silently stored as a subscription that can never fire. `*` and `all` subscribe to everything. |
| `enabled` | no | boolean | — | Enable or disable deliveries to this endpoint. |
| `description` | no | string | — | Updated human-readable label. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | Updated endpoint (no secret). | — |
| 400 | Missing or malformed Idempotency-Key. | — |
| 404 | Unknown or cross-client id (WEBHOOK_ENDPOINT_NOT_FOUND). | — |
| 409 | Concurrent update or key reuse with different parameters. | — |
| 503 | Outcome unknown; retry with the same Idempotency-Key. | — |

## DELETE /v1/webhook-endpoints/{id}

Delete a webhook endpoint

Removes a webhook endpoint. Idempotent from the caller's view (404 on unknown id).

Operation ID: `remove`

Auth: `client-api-key`

**Parameters**

| Parameter | In | Required | Type | Notes | Description |
| --- | --- | --- | --- | --- | --- |
| `id` | path | yes | string | — | — |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | Endpoint deleted. | — |
| 404 | Unknown or cross-client id (WEBHOOK_ENDPOINT_NOT_FOUND). | — |

## POST /v1/webhook-endpoints/{id}/rotate-secret

Rotate a webhook endpoint signing secret

Generates a NEW whsec_ signing secret for the endpoint and returns it ONCE. The previous secret stays valid until previous_secret_expires_at (a 24h grace window). During that window every delivery carries one BillerAPI-Signature header holding a signature per live secret — t=<ts>,v1=<new>,v1=<previous> — and a receiver accepts the request if its own secret matches ANY v1 entry, so verification can roll over to the new secret at any point in the window without dropped events. After previous_secret_expires_at passes, deliveries carry a single v1 signed with the new secret only. Retries must reuse the same Idempotency-Key so an ambiguous outcome can be recovered while the receipt and current endpoint state still agree.

Operation ID: `rotateSecret`

Auth: `client-api-key`

**Parameters**

| Parameter | In | Required | Type | Notes | Description |
| --- | --- | --- | --- | --- | --- |
| `id` | path | yes | string | — | — |
| `Idempotency-Key` | header | yes | string | — | Stable printable-ASCII key (1-255 characters) for this rotation. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | Rotated; new secret returned once, previous_secret_expires_at is the grace deadline. | — |
| 400 | Missing or malformed Idempotency-Key. | — |
| 404 | Unknown or cross-client id (WEBHOOK_ENDPOINT_NOT_FOUND). | — |
| 409 | Idempotency-Key reused for a different rotation. | — |
| 503 | Outcome unknown; retry with the same Idempotency-Key. | — |

## Schemas

### CreateWebhookEndpointDto

| Field | Always present | Type | Notes | Description |
| --- | --- | --- | --- | --- |
| `url` | yes | string | — | Destination URL BillerAPI POSTs signed events to. |
| `events` | yes | array<string> | — | Per-endpoint event filter. Event types this subscription delivers. Must be deliverable event types — one of: bill.created, bill.deleted, bill.updated, bill_import.updated, biller.unsupported, connection.ready, customer.message.complaint, customer.message.created, insight.created, insight.updated, link.completed, link.disconnected, link.failure_reason_corrected, link.session_finished, link_request.cancelled, link_request.created, link_request.updated, link_token.completed, pay.escalated, pay.failed, pay.scheduled, pay.succeeded, payment.observed, sandbox.claimed, sandbox.expiring. Legacy collapse/rename sources (e.g. `bill.paid`, `link.expired`, `request-to-link.updated`, and the historic PascalCase forms like `BillCreated`) are ACCEPTED and normalized to the name they are actually delivered under — the stored + echoed value is the normalized one, so the response shows exactly what was saved. Any other string is rejected with a 400 (`error_code: VALIDATION_ERROR`, `errors[].code: unknown_event_type`) rather than silently stored as a subscription that can never fire. `*` and `all` subscribe to everything. |
| `description` | no | string | — | Human-readable label for the endpoint. |

### UpdateWebhookEndpointDto

| Field | Always present | Type | Notes | Description |
| --- | --- | --- | --- | --- |
| `url` | no | string | — | Updated destination URL. |
| `events` | no | array<string> | — | Replacement event filter for this endpoint. Event types this subscription delivers. Must be deliverable event types — one of: bill.created, bill.deleted, bill.updated, bill_import.updated, biller.unsupported, connection.ready, customer.message.complaint, customer.message.created, insight.created, insight.updated, link.completed, link.disconnected, link.failure_reason_corrected, link.session_finished, link_request.cancelled, link_request.created, link_request.updated, link_token.completed, pay.escalated, pay.failed, pay.scheduled, pay.succeeded, payment.observed, sandbox.claimed, sandbox.expiring. Legacy collapse/rename sources (e.g. `bill.paid`, `link.expired`, `request-to-link.updated`, and the historic PascalCase forms like `BillCreated`) are ACCEPTED and normalized to the name they are actually delivered under — the stored + echoed value is the normalized one, so the response shows exactly what was saved. Any other string is rejected with a 400 (`error_code: VALIDATION_ERROR`, `errors[].code: unknown_event_type`) rather than silently stored as a subscription that can never fire. `*` and `all` subscribe to everything. |
| `enabled` | no | boolean | — | Enable or disable deliveries to this endpoint. |
| `description` | no | string | — | Updated human-readable label. |


## See also

- [API reference index](/docs/api.md)
- [Authentication](/docs/guides/authentication.md)
- [Pagination](/docs/concepts/pagination.md)
- [Error handling](/docs/concepts/errors.md)
