# Order Status Callback (Webhook)

> **Direction: GHN → your server.** When an order changes, GHN POSTs a JSON callback to the webhook URL you registered.

## Setup

Webhooks are configured by GHN, per client. **Contact GHN support/admin** and provide:

- your **Client ID**
- your **callback URL** (and HTTP method, default `POST`)
- **environment** (Staging or Production)
- any **auth** your endpoint needs (GHN can send Basic auth or an API-key header)
- which **event types** you want to receive, and your **retry count** (`num_retry`)

GHN stores this per client. **A config change can take up to ~15 minutes to take effect** (config is cached).

## Delivery

- Method: **POST**, JSON body, to your registered URL.
- **Acknowledge with HTTP `2xx`.** Any non-2xx (or a timeout) is treated as a failure and retried (see Retry).
- Per-delivery timeout is configurable (default **5 seconds**).
- Make your endpoint **idempotent** — the same event can be delivered more than once. Deduplicate on `OrderCode` + `Type` + `Time`.

## Event types

One order change can fire **several** callbacks at once (except `create`, which fires alone). Each type is delivered only if GHN enabled it for your client.

| Type | Fires when | Payload Description (Vietnamese) |
| --- | --- | --- |
| `create` | The order was just created (fires alone) | Tạo đơn hàng |
| `switch_status` | Order status changed | Cập nhật trạng thái đơn hàng |
| `update_weight` | Weight/dimensions changed | Cập nhật kích thước khối lượng |
| `update_cod` | COD amount changed | Cập nhật COD |
| `update_fee` | Shipping fee changed | Cập nhật cước |
| `update_payment_type` | Fee payer changed | Cập nhật hình thức thanh toán |
| `cod` | COD money transferred to you | Chuyển tiền COD |
| `update_partial_return` | A partial-return was raised on the order | Cập nhật đơn giao 1 phần |

## Payload

Keys are **PascalCase** (exactly as below). **Every field is always present** — an unset field is at its zero value (`""`, `0`, `null`), not omitted.

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

_(Illustrative values; field names and types are exact.)_

| Field | Type | Meaning |
| --- | --- | --- |
| `ShopID` | Int | Shop that owns the order |
| `Time` | String | Order's last-updated time (ISO 8601) |
| `OrderCode` | String | GHN tracking code |
| `ClientOrderCode` | String | Your own order code |
| `Type` | String | Event type (see table above) |
| `Description` | String | Vietnamese label of the event |
| `Status` | String | New order status (see [Order Info](https://developer.ghn.vn/en/docs/order/info.md) for the status list) |
| `Reason` | String | Failure reason text — set only for `ready_to_pick`, `delivery_fail`, `return_fail`, `damage`, `lost`, `cancel` |
| `ReasonCode` | String | Failure reason code — same statuses as `Reason` |
| `CODAmount` | Int | COD amount (VND) |
| `CODTransferDate` | String/null | When COD was transferred to you |
| `Weight` | Int | Declared weight (gram) |
| `ConvertedWeight` | Int | Volumetric weight (gram) |
| `Length`, `Width`, `Height` | Int | Dimensions (cm) |
| `PaymentType` | Int | Fee payer: `1` if the shop pays, `2` if the buyer pays |
| `IsPartialReturn` | Bool | Whether this order is itself a partial-return |
| `PartialReturnCode` | String | Set when a partial-return was raised against this order |
| `Fee` | Object | Fee breakdown — populated only if the `fee` event is enabled for you |
| `TotalFee` | Int | Total fee (VND) — with `fee` enabled |
| `Warehouse` | String | Current warehouse name — with `warehouse` enabled |
| `ShipperName`, `ShipperPhone` | String | Assigned driver — with `shipper` enabled |
| `PodURL` | String | Proof-of-delivery URL — with `pod` enabled, only on the transition into `delivered` |

## Retry

GHN retries failed deliveries **per order, per client**, on a fixed backoff curve.

**Which responses retry:**

| Response from your server | Outcome |
| --- | --- |
| `2xx` | Success — done |
| `4xx` (except 408, 429) | Permanent failure — **dropped, not retried** (fix your endpoint) |
| `5xx`, `408`, `429` | Retried |
| Timeout / no response | Retried |

**Backoff schedule** (wait before each retry):

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

- **Total attempts = 1 + `num_retry`** (your configured cap). After the cap is reached, the callback is **given up** (no more attempts).
- A queued retry becomes due within ~10 seconds of its backoff expiring.

## Ordering

- Deliveries are **FIFO per order** (`OrderCode` + your client). Callbacks for one order are delivered in the order the changes happened.
- **A failing callback blocks the ones behind it for the same order**: the queue drains head-first, so a callback that keeps failing holds up later callbacks for that same order until it succeeds, is permanently dropped, or is given up. Different orders are independent.
- When your endpoint recovers, the whole backlog for that order is flushed in one pass, in order.
