# Thông báo Webhook

> Nhận cập nhật trạng thái thanh toán và rút tiền theo thời gian thực qua webhook được ký HMAC.

Hệ thống 2328.io gửi webhook đến `url_callback` của bạn mỗi khi trạng thái thanh toán thay đổi. Đây là cách được khuyến nghị để nhận thông báo về các thanh toán thành công.

## Định dạng yêu cầu

- **Method:** `POST`
- **Content-Type:** `application/json`
- **Chữ ký:** trường `sign` trong thân yêu cầu

## Payload

Thân webhook giống hệt phản hồi `/v1/payment/info`, kèm theo một trường `sign` dùng cho việc xác minh chữ ký.

### Thanh toán thành công

```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"
}
```

### Thanh toán bị hủy / thất bại

Khi thanh toán không ở trạng thái cuối `paid`, các trường `txid`, `payment_amount` và `merchant_amount` là `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"
}
```

### Tham chiếu trường

| Field | Type | Description |
|-------|------|-------------|
| `uuid` | string | UUID thanh toán |
| `order_id` | string | ID đơn hàng của bạn |
| `amount` | decimal (8 dp) | Số tiền pháp định theo `currency` |
| `currency` | string | Đồng tiền pháp định mà merchant yêu cầu |
| `url` | string | URL trang checkout được hosted |
| `expires_at` | string (ISO 8601) | Thời điểm phiên thanh toán hết hạn |
| `created_at` | string (ISO 8601) | Thời điểm phiên thanh toán được tạo |
| `payer_currency` | string | Crypto mà người trả đang dùng để thanh toán |
| `payer_amount` | decimal (8 dp) | Số crypto dự kiến |
| `network` | string | Mạng blockchain |
| `address` | string | Địa chỉ nạp tiền |
| `payment_status` | string | Một trong: `pending`, `check`, `paid`, `underpaid_check`, `underpaid`, `overpaid`, `cancel`, `aml_lock` (xem [References](/docs/references)) |
| `txid` | string \| null | Hash giao dịch blockchain, chỉ có sau khi thanh toán được xác nhận |
| `payment_amount` | decimal \| null | Số tiền thực sự đã trả, chỉ có sau khi thanh toán |
| `merchant_amount` | decimal (18 dp) \| null | Số tiền được ghi có cho merchant sau khi trừ phí |
| `amount_usd` | decimal (8 dp) | Số tiền quy ra USD tại thời điểm tạo |
| `exchange_rate` | decimal | Tỷ giá Crypto / fiat đã sử dụng |
| `sign` | string (hex) | Chữ ký HMAC-SHA256 của payload |

## Xác minh chữ ký

Để xác minh chữ ký webhook:

1. Trích xuất trường `sign` từ payload
2. Loại bỏ trường `sign` khỏi object
3. Mã hóa các trường còn lại thành JSON
4. Mã hóa Base64 chuỗi JSON
5. Tính HMAC-SHA256 của chuỗi Base64 bằng API_KEY của bạn
6. So sánh chữ ký vừa tính với giá trị `sign` bằng so sánh thời gian không đổi

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

> **DANGER:** **Luôn xác minh chữ ký** trước khi ghi có bất kỳ khoản tiền nào cho người dùng. Một webhook không có chữ ký hoặc có chữ ký sai có thể là một yêu cầu giả mạo.

## Webhook rút tiền

Khi `status` của một rút tiền thay đổi, hệ thống gửi webhook `POST` đến URL `url_callback` được truyền khi tạo rút tiền. Nếu không cung cấp `url_callback`, sẽ không có webhook nào được gửi cho rút tiền đó.

> **WARNING:** Webhook rút tiền phải được xác minh bằng **Payout API key** — không phải API key thông thường. Thuật toán ký giống hệt webhook thanh toán (loại bỏ `sign`, mã hóa JSON, base64, HMAC-SHA256), chỉ khác key.

### Payload

```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"
}
```

### Tham chiếu trường

| Field | Type | Description |
|-------|------|-------------|
| `uuid` | string | UUID rút tiền |
| `order_id` | string | ID idempotency / tham chiếu của bạn, nếu bạn có cung cấp |
| `status` | string | `pending`, `completed`, `failed`, `cancelled` (xem [References](/docs/references)) |
| `currency` | string | Đồng tiền rút |
| `network` | string | Mạng blockchain |
| `amount` | decimal | Số tiền rút (theo `currency`) |
| `merchant_amount` | decimal | Số tiền bị trừ từ số dư merchant |
| `network_amount` | decimal | Số tiền thực sự được gửi trên chuỗi |
| `amount_usd` | decimal | Giá trị USD tại thời điểm rút tiền |
| `to_address` | string | Địa chỉ blockchain nhận tiền |
| `memo` | string \| null | Memo / destination tag, nếu có dùng |
| `txid` | string \| null | Hash giao dịch blockchain, được đặt khi `completed` |
| `block_number` | integer \| null | Chiều cao block của giao dịch trên chuỗi |
| `error_type` | string \| null | Lý do khi `status = failed` (ví dụ `aml_risk`, xem [References](/docs/references)) |
| `created_at` | string (ISO 8601) | Thời điểm rút tiền được tạo |
| `updated_at` | string (ISO 8601) | Thời điểm trạng thái thay đổi gần nhất |
| `from_currency` | string | Số dư nguồn mà khoản rút tiền đã bị trừ khi sử dụng quy đổi tự động (ví dụ `USDT` cho khoản rút `BTC`) |
| `debited_amount` | decimal | Số tiền bị trừ từ số dư `from_currency` |
| `debited_currency` | string | Đồng tiền của khoản trừ |
| `sign` | string (hex) | Chữ ký HMAC-SHA256 của payload, ký bằng **Payout API key** |

## Best practices

- **Idempotency** — Luôn kiểm tra xem thanh toán đã được xử lý hay chưa (theo `order_id` hoặc `uuid`). Webhook có thể đến nhiều lần.
- **Phản hồi nhanh** — Trả về HTTP 200 càng nhanh càng tốt. Đẩy các công việc nặng sang một queue chạy nền.
- **Retry** — Nếu hệ thống không nhận được HTTP 200, webhook sẽ được gửi lại sau 2 phút. Tối đa 5 lần thử lại.
- **Xử lý bất đồng bộ** — Xử lý sự kiện webhook bất đồng bộ để tránh chặn phản hồi.
- **Bảo mật** — LUÔN xác minh chữ ký `sign` trước khi tin tưởng payload.

> **WARNING:** Webhook có thể đến không theo thứ tự. Đừng cho rằng webhook đầu tiên bạn nhận được là trạng thái cuối cùng — luôn lấy lại qua `/v1/payment/info` (hoặc `/v1/payout/status/{uuid}`) nếu cần chắc chắn.