# Callback trạng thái đơn (Webhook)

> **Chiều: GHN → server của bạn.** Khi đơn hàng thay đổi, GHN POST một callback JSON đến webhook URL bạn đã đăng ký.

## Thiết lập

Bạn tự bật webhook được ngay, không cần chờ GHN cấu hình hộ.

1. Đăng nhập Developer Portal.
2. Mở menu tài khoản ở góc phải header, chọn [Cấu hình webhook](/account/webhook).
3. Ở tab **Order**, dán URL endpoint của bạn vào. Chỉ vậy là đủ để bắt đầu — mọi thứ còn lại đều đã có giá trị mặc định hợp lý.
4. Bấm **Tạo webhook**. Xong.

Khi cần tinh chỉnh, bạn có thêm:

- **Header tùy chỉnh** — nếu endpoint của bạn cần token xác thực hoặc metadata định tuyến.
- **Timeout** (1–60 giây) và **retry** (1–5) — GHN chờ bao lâu, và thử lại mấy lần nếu endpoint của bạn chưa phản hồi.
- **Quyền · Order** — chọn đúng những sự kiện bạn muốn nhận, thay vì nhận tất cả.

Vài điều thường thắc mắc:

- **Tên client và Mã Client** portal tự điền theo tài khoản của bạn, không cần nhập.
- **Staging và Production là hai tài khoản riêng.** Cấu hình ở `developer.ghn.dev` không tự chuyển sang `developer.ghn.vn` — làm xong ở Staging thì nhớ làm lại ở Production.
- **Đổi cấu hình cần ~15 phút mới có hiệu lực** vì GHN cache lại. Nếu callback chưa đổi ngay thì bạn cứ chờ thêm một chút.

## Gửi (Delivery)

- Method: **POST**, body JSON, đến URL bạn đăng ký.
- **Xác nhận bằng HTTP `2xx`.** Bất kỳ mã non-2xx (hoặc timeout) đều bị coi là thất bại và sẽ retry (xem Retry).
- Timeout mỗi lần gửi có thể cấu hình (mặc định **5 giây**).
- Hãy làm endpoint **idempotent** — cùng một sự kiện có thể được gửi nhiều lần. Khử trùng lặp theo `OrderCode` + `Type` + `Time`.

## Loại sự kiện

Một thay đổi đơn có thể kích hoạt **nhiều** callback cùng lúc (trừ `create`, gửi một mình). Mỗi loại chỉ được gửi nếu GHN đã bật cho client của bạn.

| Type | Kích hoạt khi | Giá trị Description |
| --- | --- | --- |
| `create` | Đơn vừa được tạo (gửi một mình) | Tạo đơn hàng |
| `switch_status` | Trạng thái đơn thay đổi | Cập nhật trạng thái đơn hàng |
| `update_weight` | Khối lượng/kích thước thay đổi | Cập nhật kích thước khối lượng |
| `update_cod` | Số tiền COD thay đổi | Cập nhật COD |
| `update_fee` | Cước phí thay đổi | Cập nhật cước |
| `update_payment_type` | Bên trả phí thay đổi | Cập nhật hình thức thanh toán |
| `cod` | Tiền COD đã chuyển cho bạn | Chuyển tiền COD |
| `update_partial_return` | Có yêu cầu giao 1 phần trên đơn | Cập nhật đơn giao 1 phần |

## Payload

Khóa dạng **PascalCase** (đúng như bên dưới). **Mọi trường luôn có mặt** — trường không được đặt sẽ ở giá trị mặc định (`""`, `0`, `null`), không bị bỏ.

```json
{
  "ShopID": 196560,
  "Time": "2026-07-15T04:49:47.811Z",
  "OrderCode": "LADFYR",
  "ClientOrderCode": "SHOP-2026-00193",
  "Type": "switch_status",
  "Description": "Cập nhật trạng thái đơn hàng",
  "Status": "delivered",
  "Reason": "",
  "ReasonCode": "",
  "CODAmount": 285000,
  "CODTransferDate": null,
  "Weight": 600,
  "ConvertedWeight": 800,
  "Length": 25,
  "Width": 20,
  "Height": 8,
  "PaymentType": 2,
  "IsPartialReturn": false,
  "PartialReturnCode": "",
  "Fee": {},
  "TotalFee": 0,
  "Warehouse": "",
  "ShipperName": "",
  "ShipperPhone": "",
  "PodURL": ""
}
```

_(Giá trị minh họa; tên trường và kiểu là chính xác.)_

| Trường | Kiểu | Ý nghĩa |
| --- | --- | --- |
| `ShopID` | Int | Shop sở hữu đơn |
| `Time` | String | Thời điểm cập nhật cuối của đơn (ISO 8601) |
| `OrderCode` | String | Mã vận đơn GHN |
| `ClientOrderCode` | String | Mã đơn riêng của bạn |
| `Type` | String | Loại sự kiện (xem bảng trên) |
| `Description` | String | Nhãn tiếng Việt của sự kiện |
| `Status` | String | Trạng thái đơn mới (xem [Mã trạng thái đơn hàng](https://developer.ghn.dev/vi/docs/master-data/order-status.md)) |
| `Reason` | String | Text lý do thất bại — chỉ có với `ready_to_pick`, `delivery_fail`, `return_fail`, `damage`, `lost`, `cancel` |
| `ReasonCode` | String | Mã lý do thất bại — cùng các trạng thái với `Reason`. Danh mục đầy đủ: [Mã lý do](https://developer.ghn.dev/vi/docs/master-data/reason-code.md) |
| `CODAmount` | Int | Số tiền COD (VND) |
| `CODTransferDate` | String/null | Thời điểm COD đã chuyển cho bạn |
| `Weight` | Int | Khối lượng khai báo (gram) |
| `ConvertedWeight` | Int | Khối lượng quy đổi thể tích (gram) |
| `Length`, `Width`, `Height` | Int | Kích thước (cm) |
| `PaymentType` | Int | Bên trả phí: là `1` nếu shop trả, là `2` nếu người mua trả |
| `IsPartialReturn` | Bool | Đơn này có phải là đơn giao 1 phần hay không |
| `PartialReturnCode` | String | Có giá trị khi có yêu cầu giao 1 phần trên đơn này |
| `Fee` | Object | Chi tiết phí — chỉ có khi sự kiện `fee` được bật cho bạn |
| `TotalFee` | Int | Tổng phí (VND) — khi bật `fee` |
| `Warehouse` | String | Tên kho hiện tại — khi bật `warehouse` |
| `ShipperName`, `ShipperPhone` | String | Tài xế được gán — khi bật `shipper` |
| `PodURL` | String | URL bằng chứng giao hàng — khi bật `pod`, chỉ khi chuyển sang `delivered` |

## Retry

GHN retry các lần gửi thất bại **theo từng đơn, từng client**, trên một đường cong backoff cố định.

**Response nào sẽ retry:**

| Response từ server của bạn | Kết quả |
| --- | --- |
| `2xx` | Thành công — xong |
| `4xx` (trừ 408, 429) | Thất bại vĩnh viễn — **bỏ, không retry** (hãy sửa endpoint) |
| `5xx`, `408`, `429` | Retry |
| Timeout / không phản hồi | Retry |

**Lịch backoff** (chờ trước mỗi lần retry):

| Retry # | Chờ | Retry # | Chờ |
| --- | --- | --- | --- |
| 1 | 30s | 7 | 1h |
| 2 | 2m | 8 | 2h |
| 3 | 5m | 9 | 3h |
| 4 | 10m | 10 | 6h |
| 5 | 20m | 11 | 12h |
| 6 | 30m |  |  |

- **Tổng số lần gửi = 1 + `num_retry`** (mức bạn cấu hình). Sau khi đạt mức này, callback bị **bỏ cuộc** (không gửi nữa).
- Một retry trong hàng đợi trở nên "đến hạn" trong khoảng ~10 giây sau khi backoff hết hạn.

## Thứ tự (Ordering)

- Gửi theo **FIFO cho từng đơn** (`OrderCode` + client của bạn). Các callback của một đơn được gửi theo đúng thứ tự thay đổi đã xảy ra.
- **Một callback thất bại sẽ chặn các callback phía sau của cùng đơn đó**: hàng đợi rút từ đầu, nên một callback liên tục thất bại sẽ giữ lại các callback sau của cùng đơn cho tới khi nó thành công, bị bỏ vĩnh viễn, hoặc bị bỏ cuộc. Các đơn khác nhau độc lập với nhau.
- Khi endpoint của bạn phục hồi, toàn bộ tồn đọng của đơn đó được đẩy hết trong một lượt, đúng thứ tự.
