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

# Webhooks

> Register HTTPS endpoints that receive signed events when your orders and limit orders change.

> [!WARNING]
>
> **Not available yet.** Webhooks are implemented but switched off for every account, in both the test and the production environment: every webhook endpoint answers `404` and no events are sent. These pages describe exactly how they work, so you can build your receiver now. Your Bullionwale relationship manager will tell you when they are turned on.

A webhook is an HTTPS endpoint of yours that Bullionwale calls when something happens on your account: an order is booked, closed or rejected, or a limit order is placed, triggered or cancelled. Instead of polling `GET /orders`, your program is told. This page covers the webhook object and the calls that manage it; the [Webhooks guide](/webhooks/overview) covers events, signatures and retries.

| Call | Endpoint | Scope |
| - | - | - |
| [Create a webhook](#create-a-webhook) | `POST /webhooks` | `webhooks:write` |
| [List webhooks](#list-and-read) | `GET /webhooks` | `webhooks:read` |
| [One webhook](#list-and-read) | `GET /webhooks/{webhook_id}` | `webhooks:read` |
| [Change or pause a webhook](#change-or-pause-a-webhook) | `PATCH /webhooks/{webhook_id}` | `webhooks:write` |
| [Delete a webhook](#delete-a-webhook) | `DELETE /webhooks/{webhook_id}` | `webhooks:write` |
| [Send a test event](#send-a-test-event) | `POST /webhooks/{webhook_id}/test` | `webhooks:write` |
| [Rotate the signing secret](#rotate-the-signing-secret) | `POST /webhooks/{webhook_id}/rotate-secret` | `webhooks:write` |

Each path sits under the test base path or the production base path:

| Endpoint | Test | Production |
| - | - | - |
| Create | `POST /api/trading/test/v1/webhooks` | `POST /api/trading/v1/webhooks` |
| List | `GET /api/trading/test/v1/webhooks` | `GET /api/trading/v1/webhooks` |
| One | `GET /api/trading/test/v1/webhooks/{webhook_id}` | `GET /api/trading/v1/webhooks/{webhook_id}` |
| Change | `PATCH /api/trading/test/v1/webhooks/{webhook_id}` | `PATCH /api/trading/v1/webhooks/{webhook_id}` |
| Delete | `DELETE /api/trading/test/v1/webhooks/{webhook_id}` | `DELETE /api/trading/v1/webhooks/{webhook_id}` |
| Test event | `POST /api/trading/test/v1/webhooks/{webhook_id}/test` | `POST /api/trading/v1/webhooks/{webhook_id}/test` |
| Rotate secret | `POST /api/trading/test/v1/webhooks/{webhook_id}/rotate-secret` | `POST /api/trading/v1/webhooks/{webhook_id}/rotate-secret` |

Neither scope is on a key by default: ask for `webhooks:read` and `webhooks:write` when you ask for the key (see the [Scope reference](/scopes)). Every webhook call counts against the key's read budget (see [Rate limits](/rate-limits)).

**A webhook belongs to one environment.** One created on the test path receives only test events and is visible only on the test path. One created on the production path receives only production events (your real orders and limit orders, from every channel) and is visible only there. The id of a webhook from the other environment answers `404 webhook_not_found`.

## The webhook object

| Field | Meaning |
| - | - |
| `id` | The webhook's id (a UUID). |
| `environment` | `test` or `production`: the events it receives. |
| `url` | Your endpoint. See [URL rules](#url-rules). |
| `events` | The [event types](/webhooks/event-catalog) it receives, in catalog order. |
| `description` | Your own note, up to 255 characters, or `null`. |
| `active` | `true` while it receives events; `false` while it is paused. |
| `secret_last4` | The last 4 characters of its signing secret, to tell secrets apart. |
| `created_at`, `updated_at` | When it was created and last changed. |

```json 200 OK theme={null}
{
  "id": "5b0e8f2c-7a41-4d7e-9c3a-2f6d1e8b4a90",
  "environment": "test",
  "url": "https://hooks.example.com/bullionwale",
  "events": ["order.booked", "order.closed"],
  "description": "Order bot",
  "active": true,
  "secret_last4": "7iQx",
  "created_at": "2026-10-21T09:30:00+00:00",
  "updated_at": "2026-10-21T09:30:00+00:00"
}
```

## Create a webhook

`POST /webhooks` takes:

| Field | Type | Required | Meaning |
| - | - | - | - |
| `url` | string | yes | Your endpoint, up to 2048 characters. See [URL rules](#url-rules). |
| `events` | array of strings | yes | At least one [event type](/webhooks/event-catalog), at most 20 entries. Duplicates are dropped. `webhook.test` cannot be subscribed to. |
| `description` | string | no | Your own note, up to 255 characters. |

```bash curl theme={null}
curl -s -X POST https://api.bullionwale.com/api/trading/test/v1/webhooks \
  -H "X-API-Key: $BULLIONWALE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://hooks.example.com/bullionwale", "events": ["order.booked", "order.closed"], "description": "Order bot"}'
```

```python Python theme={null}
import os
import requests

BASE = "https://api.bullionwale.com/api/trading/test/v1"
HEADERS = {"X-API-Key": os.environ["BULLIONWALE_API_KEY"]}

r = requests.post(
    f"{BASE}/webhooks",
    headers=HEADERS,
    json={"url": "https://hooks.example.com/bullionwale",
          "events": ["order.booked", "order.closed"], "description": "Order bot"},
    timeout=10,
)
webhook = r.json()
print(r.status_code, webhook["id"])  # store webhook["secret"] in your secret manager now
```

The answer is `201 Created` with the webhook and its signing **`secret`**:

```json 201 Created theme={null}
{
  "id": "5b0e8f2c-7a41-4d7e-9c3a-2f6d1e8b4a90",
  "environment": "test",
  "url": "https://hooks.example.com/bullionwale",
  "events": ["order.booked", "order.closed"],
  "description": "Order bot",
  "active": true,
  "secret_last4": "7iQx",
  "secret": "whsec_Rk2v9QpZt7vK2rX8bN1cL5dF0gH4jW6yA2sE9uR7iQx",
  "created_at": "2026-10-21T09:30:00+00:00",
  "updated_at": "2026-10-21T09:30:00+00:00"
}
```

> [!WARNING]
>
> **The secret is shown only in this response** (and in the answer to a [rotation](#rotate-the-signing-secret)); it is never returned again. Store it with your other credentials. It is `whsec_` followed by 43 characters, and the whole string, prefix included, is the key you [verify signatures](/webhooks/signature-verification) with.

Each account can have at most **5 webhooks per environment**, paused ones included. A sixth is refused with `409 webhook_limit_reached`.

### URL rules

The URL is checked when you create or change a webhook, and the address it resolves to is checked again on every delivery. It must:

* use `https`;
* carry no user name, password or `#fragment`;
* resolve, and resolve **only** to public internet addresses: private, loopback, link-local, shared and cloud-metadata addresses are refused, as are local network names.

A URL that breaks a rule is refused with `422 invalid_webhook_url`. Put any token of your own in the path or query string, not in a user name or password.

## List and read

`GET /webhooks` returns `items`, every webhook of your account in the calling environment, oldest first, and `total`. `GET /webhooks/{webhook_id}` returns one.

```bash theme={null}
curl -s https://api.bullionwale.com/api/trading/test/v1/webhooks \
  -H "X-API-Key: $BULLIONWALE_API_KEY"
```

```json 200 OK theme={null}
{
  "items": [
    {
      "id": "5b0e8f2c-7a41-4d7e-9c3a-2f6d1e8b4a90",
      "environment": "test",
      "url": "https://hooks.example.com/bullionwale",
      "events": ["order.booked", "order.closed"],
      "description": "Order bot",
      "active": true,
      "secret_last4": "7iQx",
      "created_at": "2026-10-21T09:30:00+00:00",
      "updated_at": "2026-10-21T09:30:00+00:00"
    }
  ],
  "total": 1
}
```

## Change or pause a webhook

`PATCH /webhooks/{webhook_id}` changes `url`, `events`, `description` or `active`. Send only the fields to change; a new `url` is checked against the [URL rules](#url-rules) again.

```bash theme={null}
curl -s -X PATCH https://api.bullionwale.com/api/trading/test/v1/webhooks/5b0e8f2c-7a41-4d7e-9c3a-2f6d1e8b4a90 \
  -H "X-API-Key: $BULLIONWALE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"active": false}'
```

```json 200 OK theme={null}
{
  "id": "5b0e8f2c-7a41-4d7e-9c3a-2f6d1e8b4a90",
  "environment": "test",
  "url": "https://hooks.example.com/bullionwale",
  "events": ["order.booked", "order.closed"],
  "description": "Order bot",
  "active": false,
  "secret_last4": "7iQx",
  "created_at": "2026-10-21T09:30:00+00:00",
  "updated_at": "2026-10-21T11:02:40+00:00"
}
```

**While a webhook is paused**, no new events are owed to it: events that happen during the pause are not sent later. Deliveries it was already owed are held, and go out once you set `active` back to `true`.

## Delete a webhook

`DELETE /webhooks/{webhook_id}` answers `204 No Content`. The webhook is gone, and so is every delivery it was still owed.

```bash theme={null}
curl -s -o /dev/null -w "%{http_code}\n" -X DELETE \
  https://api.bullionwale.com/api/trading/test/v1/webhooks/5b0e8f2c-7a41-4d7e-9c3a-2f6d1e8b4a90 \
  -H "X-API-Key: $BULLIONWALE_API_KEY"
```

## Send a test event

`POST /webhooks/{webhook_id}/test` queues one [`webhook.test`](/webhooks/event-catalog#the-test-event) event to that webhook, signed and retried like any other delivery, whatever its `events` list. Use it to check your receiver end to end. A paused webhook answers `409 webhook_inactive`. At most **5 test events per minute** per webhook; more answer `429 rate_limited` with `Retry-After`.

```bash theme={null}
curl -s -X POST https://api.bullionwale.com/api/trading/test/v1/webhooks/5b0e8f2c-7a41-4d7e-9c3a-2f6d1e8b4a90/test \
  -H "X-API-Key: $BULLIONWALE_API_KEY"
```

```json 202 Accepted theme={null}
{
  "event_id": "9a4f2d61-3c7b-4e85-b1d0-6e2a8f5c7b13",
  "delivery_id": "e2c71b5a-0d94-4f3e-8a6b-1f7c9d2e4a58",
  "status": "pending"
}
```

`event_id` is the `id` your endpoint receives in the body, and `delivery_id` the `X-Bullionwale-Delivery` header.

## Rotate the signing secret

`POST /webhooks/{webhook_id}/rotate-secret` replaces the signing secret **at once** and returns the webhook with the new `secret`, shown only in this response. Every delivery signed from then on, including retries of earlier events, uses the new secret; the old one stops verifying immediately, with no overlap. Switch your receiver to the new secret straight away.

```bash theme={null}
curl -s -X POST https://api.bullionwale.com/api/trading/test/v1/webhooks/5b0e8f2c-7a41-4d7e-9c3a-2f6d1e8b4a90/rotate-secret \
  -H "X-API-Key: $BULLIONWALE_API_KEY"
```

The answer has the same shape as the answer to [create](#create-a-webhook), with a new `secret` and `secret_last4`.

## Errors

| Code | Status | Meaning |
| - | - | - |
| `invalid_webhook_url` | 422 | The URL breaks a [URL rule](#url-rules). |
| `invalid_event_type` | 422 | `events` is empty or names an unknown type. |
| `webhook_limit_reached` | 409 | Your account already has 5 webhooks in this environment. |
| `webhook_not_found` | 404 | No such webhook on your account in this environment. |
| `webhook_inactive` | 409 | A test event for a paused webhook. |
| `rate_limited` | 429 | Over the test-event limit for this webhook, or over the key's read budget. |
| `insufficient_scope` | 403 | The key lacks `webhooks:read` or `webhooks:write`. |

The full list is in the [Error reference](/api-reference/error-reference).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.