# Webhook 通知

> 通过 HMAC 签名的 webhook 实时接收支付与提现状态更新。

每当支付状态变更时，2328.io 会向您配置的 `url_callback` 发送 webhook。这是获取支付状态通知的推荐方式。

## 请求格式

- **方法：** `POST`
- **Content-Type：** `application/json`
- **签名：** 请求体中的 `sign` 字段

## 载荷

webhook 请求体与 `/v1/payment/info` 响应一致，并额外包含一个用于签名校验的 `sign` 字段。

### 支付成功

```json
{
  "uuid": "db17d490-15b6-47b9-9015-91d1d8b119f2",
  "order_id": "ORDER-12345",
  "amount": "180.00000000",
  "currency": "RUB",
  "url": "https://go.2328.io/db17d490-15b6-47b9-9015-91d1d8b119f2",
  "expires_at": "2026-05-09T16:56:58+03:00",
  "created_at": "2026-05-09T15:56:58+03:00",
  "payer_currency": "TON",
  "payer_amount": "0.95256917",
  "network": "TON",
  "address": "UQA0RevhkCQx-EltyNgPPeG8dqtnCz7ZslOzMdNQlLxVaNBb",
  "payment_status": "paid",
  "txid": "41c2a327323480af8e705d05deb09c238a41779928832abef4bb77c862357b11",
  "payment_amount": "0.95256917",
  "merchant_amount": "0.949711462490000000",
  "amount_usd": "2.41324380",
  "exchange_rate": "0.01340691",
  "sign": "6f8c15b6e53b506d5bfa38ed3fb3b50697af73434262153c02e412541372f04d"
}
```

### 已取消 / 失败的支付

未到达终态 `paid` 时，`txid`、`payment_amount` 和 `merchant_amount` 为 `null`：

```json
{
  "uuid": "48edaf2d-2c49-4638-8f86-88636f661c1f",
  "order_id": "ORDER-12345",
  "amount": "2800.00000000",
  "currency": "RUB",
  "url": "https://go.2328.io/48edaf2d-2c49-4638-8f86-88636f661c1f",
  "expires_at": "2026-05-09T06:19:04+03:00",
  "created_at": "2026-05-09T05:19:04+03:00",
  "payer_currency": "ETH",
  "payer_amount": "0.01620968",
  "network": "ETH-ERC20",
  "address": "0x37c20d6d96d130Bc5B33D832e43b8e16aACe0c59",
  "payment_status": "cancel",
  "txid": null,
  "payment_amount": null,
  "merchant_amount": null,
  "amount_usd": "37.53934800",
  "exchange_rate": "0.01340691",
  "sign": "40ce68ad9691ad54e684329d75ab5adaf5b01409a2d18d3e0110b8c1be605342"
}
```

### 字段说明

| 字段 | 类型 | 说明 |
|------|------|------|
| `uuid` | string | 支付 UUID |
| `order_id` | string | 您的订单 ID |
| `amount` | decimal (8 位小数) | `currency` 对应的法币金额 |
| `currency` | string | 商户请求的法币币种 |
| `url` | string | 托管支付页面 URL |
| `expires_at` | string (ISO 8601) | 支付会话的过期时间 |
| `created_at` | string (ISO 8601) | 支付会话的创建时间 |
| `payer_currency` | string | 付款人使用的加密货币 |
| `payer_amount` | decimal (8 位小数) | 预期支付的加密货币数量 |
| `network` | string | 区块链网络 |
| `address` | string | 收款地址 |
| `payment_status` | string | 取值之一：`pending`、`check`、`paid`、`underpaid_check`、`underpaid`、`overpaid`、`cancel`、`aml_lock`（详见 [References](/docs/references)） |
| `txid` | string \| null | 链上交易哈希，仅在支付确认后才有值 |
| `payment_amount` | decimal \| null | 实际支付金额，仅在支付到账后才有值 |
| `merchant_amount` | decimal (18 位小数) \| null | 扣除手续费后入账给商户的金额 |
| `amount_usd` | decimal (8 位小数) | 创建时的美元等值 |
| `exchange_rate` | decimal | 创建时使用的加密货币 / 法币汇率 |
| `sign` | string (hex) | 载荷的 HMAC-SHA256 签名 |

## 验证签名

验证 webhook 签名的步骤：

1. 从载荷中提取 `sign` 字段
2. 从对象中移除 `sign` 字段
3. 将剩余字段编码为 JSON
4. 将 JSON 进行 Base64 编码
5. 使用您的 API_KEY 对 Base64 字符串计算 HMAC-SHA256
6. 使用恒定时间比较函数（如 `hash_equals`）将计算结果与收到的 `sign` 比较

<CodeSnippet name="verifyWebhookSign" langs="php,js,python,go,ruby" />

> **DANGER:** **为用户入账资金前务必校验签名。** 未签名或签名错误的 webhook 可能是伪造请求。

## 提现 webhook

当提现的 `status` 发生变化时，系统会向创建提现时传入的 `url_callback` 发送 `POST` webhook。如果未提供 `url_callback`，则不会为该提现发送任何 webhook。

> **WARNING:** 提现 webhook 必须使用 **Payout API key** 校验签名，**不是**普通 API key。签名算法与支付 webhook 完全相同（移除 `sign` → JSON → Base64 → HMAC-SHA256），仅签名密钥不同。

### 载荷

```json
{
  "uuid": "019dff1f-0dbd-7277-8d45-271e7775388f",
  "order_id": "4dfdcc84402b1185b71cbe399321533e",
  "status": "completed",
  "currency": "TRX",
  "network": "TRX-TRC20",
  "amount": "3.00",
  "merchant_amount": "3.00",
  "network_amount": "3.00",
  "amount_usd": "1.04",
  "to_address": "THauRv5tcucQRohXg8NiyGTk16DX1XQG5x",
  "memo": null,
  "txid": "9242e533703704ef3eaba840f70b4a26333e72c943377ee375fea17badb53def",
  "block_number": null,
  "error_type": null,
  "created_at": "2026-05-07T00:08:38+03:00",
  "updated_at": "2026-05-07T00:08:54+03:00",
  "from_currency": "USDT",
  "debited_amount": "1.050735",
  "debited_currency": "USDT",
  "sign": "925ad7bf3d6841864101f7cc2c7e30652e70a06cdb04dbe07a0129480000ce4a"
}
```

### 字段说明

| 字段 | 类型 | 说明 |
|------|------|------|
| `uuid` | string | 提现 UUID |
| `order_id` | string | 您提供的幂等性 / 业务 ID（如有） |
| `status` | string | `pending`、`completed`、`failed`、`cancelled`（详见 [References](/docs/references)） |
| `currency` | string | 提现币种 |
| `network` | string | 区块链网络 |
| `amount` | decimal | 提现金额（以 `currency` 为单位） |
| `merchant_amount` | decimal | 从商户余额扣除的金额 |
| `network_amount` | decimal | 实际链上发送的金额 |
| `amount_usd` | decimal | 提现时的美元等值 |
| `to_address` | string | 收款方区块链地址 |
| `memo` | string \| null | 备注 / destination tag（如使用） |
| `txid` | string \| null | 链上交易哈希，`completed` 时设置 |
| `block_number` | integer \| null | 链上交易所在区块高度 |
| `error_type` | string \| null | `status = failed` 时的失败原因（例如 `aml_risk`，详见 [References](/docs/references)） |
| `created_at` | string (ISO 8601) | 提现创建时间 |
| `updated_at` | string (ISO 8601) | 状态最近一次变更时间 |
| `from_currency` | string | 使用自动兑换时，提现实际扣款的源余额（例如 `BTC` 提现的 `USDT`） |
| `debited_amount` | decimal | 从 `from_currency` 余额扣除的金额 |
| `debited_currency` | string | 扣款币种 |
| `sign` | string (hex) | 载荷的 HMAC-SHA256 签名，使用 **Payout API key** 计算 |

## 最佳实践

- **幂等性** — 务必检查支付是否已被处理（按 `order_id` 或 `uuid`）。webhook 可能被多次发送。
- **快速响应** — 尽快返回 HTTP 200。耗时操作转交后台队列。
- **重试** — 若系统未收到 HTTP 200，2 分钟后会重新发送，最多重试 5 次。
- **异步处理** — 异步处理 webhook 事件，避免阻塞响应。
- **安全** — 在信任载荷之前务必验证 `sign` 签名。

> **WARNING:** webhook 可能乱序到达。不要假设第一条 webhook 即是最终状态 — 如需确认，可通过 `/v1/payment/info`（或 `/v1/payout/status/{uuid}`）重新拉取。