# Receiving webhooks

A webhook has us call your server when one of your shipments changes, so you don't have to keep asking. Set one up with [Create a webhook](/reference/create-a-webhook): give us a `target_url` and the `events` you want. The answer includes a `token`. Keep it, because every event we send carries it.

## The events

| Event | Sent when |
|---|---|
| `shipment.updated.status` | A shipment's status changes to `order_received`, `in_transit` or `delivered`. Other statuses send nothing; see [Order statuses](/order-statuses). |
| `shipment.updated.waybill` | A shipment's waybill number changes: first made, made again, or cleared (`updated_value` is then `null`). |

Subscribe to the exact strings above. A [hub order](/guides/hub-shipments) has two shipments, so it sends events for each leg, under the same order.

## What we send


A `POST` to your `target_url`, with `Content-Type: application/json` and this body:

```json
{
  "object_type": "order",
  "object_id": 48213,
  "event_type": "shipment.updated.status",
  "original_value": "awaiting_pickup",
  "updated_value": "in_transit",
  "updated_at_utc": "2026-11-11T09:30:12.345Z",
  "token": "q7Vt2mX9pL4sKe8RzW1nYg"
}
```

| Field | What it is |
|---|---|
| `object_type` | `order`. |
| `object_id` | The order's `id`, the one [Get an order](/reference/get-an-order) takes. |
| `event_type` | Which event this is. |
| `original_value` | The value before the change. `null` for a first waybill. |
| `updated_value` | The value after the change. |
| `updated_at_utc` | When the shipment changed, in UTC, ISO 8601 with milliseconds. |
| `token` | Your webhook's token. |

The body has no other fields, and no event id.


## Checking it came from us

Compare the body's `token` with the token you kept from Create a webhook, and ignore any request where they differ. The token travels in the body, not in a header, and there is no signature. Use an `https` address so the token can't be read on the way.

The token never changes. To replace it, delete the webhook and create a new one.

## Answering

Answer any `2xx` status, as soon as you can. We ignore what you send back. Store the event and do the work afterwards, because we give up waiting after 60 seconds and count it as a failure.

Use the address you registered as it is. If it redirects with `301`, `302` or `303`, the event arrives at the new address as a `GET` with no body.

## When your server doesn't answer

- **Retries:** we try five times in all, waiting longer each time, and stop after about five minutes. Anything other than a `2xx` counts as a failure, and so does no answer in 60 seconds.
- **Failing:** if all five tries fail, the webhook's `status` becomes `failing` and it gets no more events. We email your company's email address to say so.
- **Missed events aren't sent again.** Events from while a webhook was failing are lost.
- **Getting it working again:** the API can't switch a failing webhook back on. Delete it and create a new one, which gives you a new token.

Because events can be lost, check your open orders now and then with [List orders](/reference/list-orders), and don't rely on webhooks alone.

## Duplicates and order

- **The same event can arrive twice**, for example if your server answered slowly and we tried again. To spot a repeat, compare `object_id`, `event_type`, `updated_value` and `updated_at_utc`.
- **Events can arrive out of order.** Each one is sent on its own, and a retried event arrives after later ones. Use `updated_at_utc` to tell which is newer.
- **Read the order when an event arrives.** Treat the event as a prompt: call [Get an order](/reference/get-an-order) for the current state. Compare `shipments[].status` and `shipments[].waybill`, not the order's own `status` and `waybill`: on a [hub order](/guides/hub-shipments) those belong to the last shipment, and the event doesn't say which leg changed. An event can reach you a moment before the change shows there, so if the order still shows the old value, wait a few seconds and read it again.

## Testing the address

Our team can test your address from the portal, and that sends an empty `POST`. Answer it with a `2xx` too.
