> ## Documentation Index
> Fetch the complete documentation index at: https://docs.brale.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive signed webhook events from Brale when transfers and payments complete.

## Brale webhooks

Brale webhooks let your application receive real-time notifications when important events happen in Brale.

Instead of polling Brale APIs to check whether a transfer or payment has completed, you can register an HTTPS endpoint and Brale will `POST` a signed event to your application as soon as the event occurs.

Use webhooks to:

* Update your internal ledger when a transfer completes
* Notify your users when money movement settles
* Reconcile completed transfers or payments
* Prompt customers to upload KYB documents when verification review requires them
* Trigger downstream workflows without polling
* Build real-time dashboards for operations and treasury teams

## How webhooks work

At a high level:

1. You create a webhook subscription for your Brale account.
2. Brale returns a `sharedSecret` one time.
3. You store the `sharedSecret` securely.
4. When a subscribed event occurs, Brale sends an HTTPS `POST` to your endpoint.
5. Your application verifies the HMAC signature using the exact raw request body.
6. Your application processes the event idempotently.
7. Your endpoint returns a `2xx` response to acknowledge delivery.

```
Brale transfer completes
        ↓
Brale builds event payload
        ↓
Brale signs raw JSON body with your shared secret
        ↓
Brale POSTs the event to your webhook URL
        ↓
Your app verifies, deduplicates, and updates state
```

## Webhook subscription API

Webhook subscriptions are managed under a Brale account:

```
/accounts/{account_id}/webhooks
```

| Method | Path | Purpose |
| - | - | - |
| GET | `/accounts/{account_id}/webhooks/event_types` | Discover supported event types |
| POST | `/accounts/{account_id}/webhooks` | Create a webhook subscription |
| GET | `/accounts/{account_id}/webhooks` | List webhook subscriptions |
| GET | `/accounts/{account_id}/webhooks/{subscription_id}` | Get one webhook subscription |
| PATCH | `/accounts/{account_id}/webhooks/subscriptions/{subscription_id}` | Update a webhook subscription |
| DELETE | `/accounts/{account_id}/webhooks/subscriptions/{subscription_id}` | Archive a webhook subscription |
| GET | `/accounts/{account_id}/webhooks/deliveries` | List delivery attempts |

### Create a subscription

```bash theme={null}
curl -X POST https://api.brale.xyz/accounts/{account_id}/webhooks \
  -H "Authorization: Bearer {access_token}" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: {uuid}" \
  -d '{
    "url": "https://your-app.example.com/webhooks/brale",
    "events": ["transfer.completed", "transfer.failed"]
  }'
```

> [!NOTE]
>
> The `sharedSecret` is returned only once when the subscription is created. Store it immediately. List and get subscription endpoints do not return the secret again.

## Choose the subscription owner

The `account_id` in `POST /accounts/{account_id}/webhooks` identifies the account that owns the subscription. It is not necessarily the account each event concerns.

* A subscription owned by a **managing account** receives subscribed events for the managing account and the accounts it manages.
* A subscription owned by a **managed account** receives subscribed events for that account only.

If you onboard customers as [managed accounts](https://docs.brale.xyz/guides/managed-accounts.md) or [individual accounts](https://docs.brale.xyz/guides/individual-accounts.md), you don't need a subscription or signing secret per customer. One subscription on your managing account covers them. Coverage comes from the management relationship. It is not an org-wide catch-all, and there is no scope parameter to set.

Separate Brale accounts that don't share a managing account need their own subscriptions.

| Your setup | Subscribe on |
| - | - |
| One Brale account | That account |
| Platform with managed customer accounts | The managing account |
| You only want one managed customer's events | That managed account |

To identify which account an event concerns, read `data.account_id` from the payload. See [Managing accounts and event fan-in](https://docs.brale.xyz/webhooks/webhook-events.md#managing-accounts-and-event-fan-in).

For testnet and mainnet, create webhook subscriptions in the account/environment you want to receive events from. If you have separate testnet and mainnet accounts, create separate subscriptions for those accounts.

## Supported events

Use the event type discovery endpoint to see currently available events:

```
GET /accounts/{account_id}/webhooks/event_types
```

Current supported events:

| Event type | When it fires | Recommended use |
| - | - | - |
| `transfer.created` | A transfer is created | Record transfer intents and surface in-flight state to customers |
| `transfer.completed` | A transfer reaches `complete` status | Replace polling transfer status; update ledgers, balances, and customer-facing status |
| `transfer.canceled` | A transfer is canceled | Reverse provisional ledger entries and notify customers of cancellations |
| `transfer.failed` | A transfer reaches `failed` status | React to permanent transfer failures (e.g., ACH returns or returned outbound wires, including wires returned after `complete`); inspect `data.failure` for details |
| `transfer.refunding` | A refund process starts | Mark a refund as in progress. This event does not confirm that the refund is complete. |
| `transfer.refunded` | A refund completes | Mark the refund as complete and update ledgers and customer-facing status |
| `payment.completed` | A payment reaches `complete` status | React to completed fiat payments, including inbound wire, ACH, and FedNow deposits. Payment IDs are not Transfer IDs, and this event does not confirm the stablecoin mint. |
| `address.created` | A wallet or bank account is added to an account, including a custodial wallet that is still `pending` | Sync new addresses into your records. Do not send transfers to the address until it is ready. |
| `address.ready` | A wallet or bank account reaches `active` status and can be used in transfers | Enable the address as a transfer source or destination |
| `account.verification.documents_required` | KYB review requires one or more documents | Notify customers to upload documents; see [Stage a Verification Document](https://docs.brale.xyz/api-reference/brale/stage-verification-document.md) and [Link Verification Documents](https://docs.brale.xyz/api-reference/brale/link-verification-documents.md) |
| `account.verification.completed` | Account verification passes | Enable downstream flows when account `status` becomes `complete` |

Use `transfer.completed` when your integration is tracking Brale Transfer objects and wants to know when a transfer has completed. Subscribe to `transfer.failed` as well, or use `transfer.*`. An outbound domestic wire can be returned after `transfer.completed`, and Brale reports the return with [`transfer.failed`](https://docs.brale.xyz/webhooks/webhook-events#returned-outbound-wires).

To track refunds, subscribe to `transfer.refunding` and `transfer.refunded`, or use `transfer.*`. `transfer.failed` reports that a payout failed or was returned. It does not mean a refund has started or completed. Not every failed or returned payout is refunded. See [Refund events](https://docs.brale.xyz/webhooks/webhook-events.md#refund-events).

### Wildcard subscriptions

The `events` array on create and update subscription requests accepts namespace wildcards of the form `<namespace>.*`. For example, subscribing to `transfer.*` gives you a single subscription that receives every event in the transfer family (`transfer.created`, `transfer.completed`, `transfer.canceled`, `transfer.failed`, `transfer.refunding`, `transfer.refunded`, and any future `transfer.*` events).

Wildcards work for every major event family — not just transfers:

* `transfer.*` — all transfer events
* `payment.*` — all payment events
* `address.*` — all address events (`address.created` and `address.ready`)
* `account.*` — all account events, including nested types like `account.verification.completed`

You can combine wildcards and concrete event types in the same subscription (e.g., `["transfer.*", "payment.completed"]`). New events added to a family in the future are automatically delivered to any matching wildcard subscription.

Example create request using a wildcard:

```json theme={null}
{
  "url": "https://your-app.example.com/webhooks/brale",
  "events": ["transfer.*"]
}
```

See [Create Webhook Subscription — Wildcard event subscriptions](https://docs.brale.xyz/api-reference/brale/create-webhook-subscription.md#wildcard-event-subscriptions) for the full reference.

## Requirements

Your webhook endpoint must:

* Be publicly reachable over HTTPS
* Accept `POST` requests
* Accept `Content-Type: application/json`
* Read the raw request body before parsing JSON
* Verify the `x-request-signature-sha-256` header
* Return a `2xx` response quickly
* Deduplicate events by event `id` and/or `idempotency-key`

## Local development

For local development, expose your local server with a tool like ngrok or Cloudflare Tunnel.

Example local tunnel:

```bash theme={null}
ngrok http 4000
```

If ngrok gives you:

```
https://example.ngrok-free.app -> http://localhost:4000
```

your webhook URL might be:

```
https://example.ngrok-free.app/webhooks/brale
```
