# Webhook

每当订单或退款发生变化时，在您的服务器上接收带签名的 POST。这样您就不必每隔几秒查询一次 API。

- [在控制台管理 Webhook](https://bytemax.exchange/zh-CN/webhooks)
- [下载 OpenAPI 3.1](https://bytemax.exchange/api-v1.openapi.json)
- [文档页面](https://bytemax.exchange/zh-CN/docs/webhooks)

## 工作原理

1. **注册 Webhook**
   在[Webhook 面板](https://bytemax.exchange/zh-CN/webhooks)中或通过 `POST /webhooks` 注册，填写您的服务器 URL 和希望接收的事件。您的账户需要有有效的 API 密钥。URL 必须以 `https://` 开头，并使用可通过 DNS 解析的公共域名。IP 地址和包含用户名或密码的 URL 会被拒绝，返回 `INVALID_INPUT`。

2. **保存签名密钥**
   它随创建响应返回，格式为 `whsec_…`，之后可用 `GET /webhooks/{id}?include_signing_secret=true` 再次查看。无法重新生成：如果泄露，请删除该 Webhook 并创建一个新的。不要与 API 密钥混淆。

3. **接收 POST**
   每个事件都带有 `svix-id`、`svix-timestamp` 和 `svix-signature` 请求头，正文为 JSON 格式的事件。

4. **验证、保存并响应 2xx**
   先核对签名并保存事件，然后再响应。任何非 2xx 的响应或没有响应都会触发新的重试。

## 事件

| 事件 | 何时发送 |
| --- | --- |
| `order.created` | 订单已创建。查询订单可获取最终金额和支付信息。 |
| `order.completed` | 订单已支付并完成。在向您的客户交付商品之前，请先查询确认 `status` 为 `completed`。 |
| `order.expired` | 订单未付款，已过期。 |
| `order.canceled` | ByteMax 取消了订单，例如付款无法被接受并已退还给付款人。 |
| `order.updated` | 订单数据变化但状态未变：已退款金额、已冻结金额或支付信息。这不表示已支付：订单支付后会发送 `order.completed`。 |
| `refund.created` | 您的账户（通过 API 或控制台）发起的退款已登记。客服发起的退款只会体现在 `order.updated` 中。 |
| `refund.succeeded` | 退款已完成：款项已退还给付款人。 |
| `refund.failed` | 退款未能完成。 |
| `webhook.test` | 测试事件，只发送到您测试的那个 Webhook。 |

每个事件包含 `type`（说明发生了什么）和 `related_object`：对象的 `type`（`order`、`refund` 或 `webhook`）、`id` 和 `url`（`https://bytemax.exchange` 上的查询路径）。处理前请查询该 `url`：即使事件延迟、重复或乱序到达，查询始终返回当前状态。

**示例：order.completed**

```json
{
  "id": "evt_example",
  "type": "order.completed",
  "created_at": "2026-09-10T13:05:00.000Z",
  "related_object": {
    "type": "order",
    "id": "ord_example",
    "url": "/api/v1/orders/ord_example"
  }
}
```

### 发布状态

`GET /events` 和 `GET /events/{id}` 会返回 `publication_status`，表示事件是否已从 ByteMax 发出。事件在您服务器上的结果见投递记录。

- `pending`: 已记录，正在等待发送或重试。ByteMax 会尝试发布约 2 小时；之后事件变为 `not_published`。
- `published`: 已提交投递。每次尝试的结果见投递记录。
- `not_published`: 未发送：当时您的账户没有有效的 API 密钥、没有启用的 Webhook 订阅该事件类型，或发布失败。此事件无法重新发送；请查询订单或退款的当前状态。

## 接收并验证

在解析 JSON 之前，用密钥和原始请求正文进行验证。使用您所用语言的 Svix 官方库：它会核对签名以及 `svix-timestamp` 的时间容差。不要自行实现验证。

[如何使用官方库验证签名（docs.svix.com）](https://docs.svix.com/receiving/verifying-payloads/how)

**到达您服务器的内容**

```http
POST /webhooks/bytemax HTTP/1.1
Content-Type: application/json
svix-id: msg_2gkQ7Wc9cD6yYq1xkKz7GJb3wQm
svix-timestamp: 1789045500
svix-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhUjM8G3xRk2w1vNq4T0=

{ "id": "evt_example", "type": "order.completed", ... }
```

## 避免重复处理

事件可能多次到达，也可能乱序。五条规则可避免重复效果：

- 把事件的 `id` 作为唯一键：响应前先保存，并忽略重复的事件。
- 不要依赖到达顺序：每收到一个事件，都查询订单或退款，并按查询返回的状态处理。
- 在同一个事务中保存事件的效果和已处理标记。
- 保存后才响应 2xx。如果保存失败，响应 5xx 以便再次接收。
- 耗时的工作放在响应之外：先入队，快速响应。

## 测试与重发

`POST /webhooks/{id}/test-events`，带上 `Content-Type: application/json` 并以 `{}` 作为正文，会向该 Webhook 发送一个 `webhook.test`；该 Webhook 必须处于启用状态。用 `GET /events/{id}` 跟踪发布情况，并在您的服务器上确认事件已到达且签名已通过。测试只证明投递和签名有效；要测试订单和退款的处理，请跟踪一笔真实订单。

投递失败会自动重试，最多 7 次：分别在 5 秒、5 分钟、30 分钟、2 小时、5 小时、10 小时和 10 小时后。连续失败 5 天的 Webhook 会被自动停用。每次尝试的历史记录在控制台和 `GET /webhooks/{id}/deliveries` 中。要重发特定事件，使用 `POST /webhooks/{id}/redeliveries`，最多 100 个 ID。即使部分事件被拒绝，该调用也会返回 200：请查看每一项的 `status`。

- `queued`: 重发已被接受。
- `rejected`: 未重发任何内容。原因见 `code`：`EVENT_NOT_FOUND`（ID 不存在）、`EVENT_NOT_ACCEPTED`（该 Webhook 未订阅此事件类型，或该测试事件属于另一个 Webhook）、`EVENT_NOT_PUBLISHED`（事件仍为 `pending` 或已是 `not_published`）或 `REDELIVERY_REJECTED`（投递服务拒绝）。`pending` 事件在 `GET /events/{id}` 显示 `published` 后即可重发。
- `retry`: 重发未执行或无法确认：调用已超时，或投递服务繁忙或出错。请重新发送；如果事件到达两次，事件的 `id` 可以避免重复处理。

## 限制

- 每个账户最多 200 个 Webhook，每个 URL 一个。
- 事件在发布后 30 天内可查询。
- 列表使用 `items` 和 `next_cursor`：每页 30 条，最多 100 条。游标只能用于同一个列表和相同的筛选条件。

## 错误

身份验证、通用错误和调用限制见 [API 页面](https://bytemax.exchange/zh-CN/docs/api)。以下代码为 Webhook 专有：

- `WEBHOOK_URL_EXISTS` (HTTP 409): 您已有使用此 URL 的 Webhook。
- `WEBHOOK_LIMIT_REACHED` (HTTP 409): 您已有 200 个 Webhook。请先删除一个，再创建新的。
- `WEBHOOK_NOT_ENABLED_FOR_V1` (HTTP 409): 测试和重发需要已启用的 v1 Webhook。请启用它；如果它来自旧版 API，请用 `PATCH` 迁移至 v1。
- `WEBHOOK_VERSION_REQUIRED` (HTTP 400): 来自旧版 API 的 Webhook 只能在迁移至 v1 的同时修改：请发送 `api_version: "v1"` 和新的 `events`。
- `WEBHOOK_NOT_FOUND` (HTTP 404): 您的账户中没有该 ID 的 Webhook。
- `EVENT_NOT_FOUND` (HTTP 404): 没有该 ID 的事件。事件在发布后保留 30 天。
- `INVALID_CURSOR` (HTTP 400): 游标不属于当前列表和筛选条件。请去掉 `cursor` 重新开始。

返回 503 `SERVICE_UNAVAILABLE` 表示投递服务暂时不可用。请几秒后重试。

## 参考

共十个操作，全部需要 `Authorization: Bearer <API_KEY>`。最常用的三个完整列出；其余格式相同。

### 创建 Webhook

`POST /webhooks`

创建 Webhook 并返回签名密钥。

**JSON 正文**

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `url` | `string` | 是 | 您的服务器接收事件的 `https://` 地址，须使用公共域名。 |
| `events` | `array` | 是 | Webhook 接收的事件类型，不可重复：从事件中的 8 种订单和退款事件里选择一个或多个。`webhook.test` 不能订阅：它只会通过测试发送。 |
| `enabled` | `boolean` | 否 | Webhook 是否接收事件。默认为 `true`。 |

**请求**

```bash
curl --request POST "https://bytemax.exchange/api/v1/webhooks" \
  --header "Authorization: Bearer <API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{"url":"https://merchant.example/webhooks/bytemax","events":["order.created","order.completed","order.expired","order.canceled","order.updated","refund.created","refund.succeeded","refund.failed"],"enabled":true}'
```

**响应 201**

```json
{
  "id": "ep_example",
  "url": "https://merchant.example/webhooks/bytemax",
  "events": [
    "order.created",
    "order.completed",
    "order.expired",
    "order.canceled",
    "order.updated",
    "refund.created",
    "refund.succeeded",
    "refund.failed"
  ],
  "enabled": true,
  "api_version": "v1",
  "signing_secret": "<WEBHOOK_SIGNING_SECRET>"
}
```

### 发送测试

`POST /webhooks/{id}/test-events`

向此 Webhook 发送一个 `webhook.test`。正文为空的 JSON 对象。

**请求参数**

| 参数 | 位置 | 必填 |
| --- | --- | --- |
| `id` | 路径 | 是 |

**请求**

```bash
curl --request POST "https://bytemax.exchange/api/v1/webhooks/ep_example/test-events" \
  --header "Authorization: Bearer <API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{}'
```

**响应 202**

```json
{
  "id": "evt_test_example",
  "type": "webhook.test",
  "created_at": "2026-09-10T13:05:00.000Z",
  "related_object": {
    "type": "webhook",
    "id": "ep_example",
    "url": "/api/v1/webhooks/ep_example"
  },
  "publication_status": "pending"
}
```

### 重发事件

`POST /webhooks/{id}/redeliveries`

向此 Webhook 重发最多 100 个事件。响应包含每个事件的结果。

**请求参数**

| 参数 | 位置 | 必填 |
| --- | --- | --- |
| `id` | 路径 | 是 |

**JSON 正文**

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `event_ids` | `array` | 是 | 要重发的事件 ID，1 到 100 个，不可重复。 |

**请求**

```bash
curl --request POST "https://bytemax.exchange/api/v1/webhooks/ep_example/redeliveries" \
  --header "Authorization: Bearer <API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{"event_ids":["evt_example"]}'
```

**响应 200**

```json
{
  "items": [
    {
      "event_id": "evt_example",
      "status": "queued"
    }
  ]
}
```

### 其他操作

| 操作 | 名称 | 作用 |
| --- | --- | --- |
| `GET /webhooks` | 列出 Webhook | 列出您账户中的所有 Webhook，包括通过旧版 API 创建的 Webhook（`api_version: "legacy"`）。 |
| `GET /webhooks/{id}` | 查询 Webhook | 单个 Webhook。带 `include_signing_secret=true` 时包含签名密钥。 |
| `PATCH /webhooks/{id}` | 修改 Webhook | URL、事件或启用状态。要把在旧版 API 中创建的 Webhook 迁移至 v1，请同时发送 `api_version: "v1"` 和新的 `events`。 |
| `DELETE /webhooks/{id}` | 删除 Webhook | 删除该 Webhook。 |
| `GET /events` | 列出事件 | 最近 30 天的事件及其发布状态。 |
| `GET /events/{id}` | 查询事件 | 单个事件及其发布状态。 |
| `GET /webhooks/{id}/deliveries` | 列出投递 | 某个 Webhook 的投递尝试：您的服务器返回 2xx 时为 `succeeded`，没有响应或返回其他状态码时为 `failed`。可按状态和类型筛选。 |
