# Webhook events

> Register an HTTPS endpoint, choose delivery events, and read the signed event envelope Maxmove sends for every delivery change.

Webhooks tell your system about every delivery change as it happens, so you don't need to poll. Maxmove sends a signed `POST` with a JSON body to your endpoint for each event.

## Register an endpoint

Register a public HTTPS URL with [`POST /v1/webhooks`](https://api.maxmove.com/v1/docs#tag/webhooks/POST/v1/webhooks). The key needs the `webhooks:manage` permission. List the events you want in `events`, or leave it empty to receive all of them.

```bash cURL
curl -X POST https://api.maxmove.com/v1/webhooks \
  -H "x-api-key: $MAXMOVE_KEY" -H "content-type: application/json" \
  -d '{
    "url": "https://shop.example.com/maxmove/webhooks",
    "events": ["delivery.status_updated", "delivery.delivered", "delivery.cancelled"]
  }'
```

```ts Node.js
const res = await fetch('https://api.maxmove.com/v1/webhooks', {
  method: 'POST',
  headers: { 'x-api-key': process.env.MAXMOVE_KEY!, 'content-type': 'application/json' },
  body: JSON.stringify({
    url: 'https://shop.example.com/maxmove/webhooks',
    events: ['delivery.status_updated', 'delivery.delivered', 'delivery.cancelled'],
  }),
});
const endpoint = await res.json();
// Store endpoint.secret now. It is not shown again.
```

```python Python
import os
import requests

endpoint = requests.post(
    "https://api.maxmove.com/v1/webhooks",
    headers={"x-api-key": os.environ["MAXMOVE_KEY"]},
    json={
        "url": "https://shop.example.com/maxmove/webhooks",
        "events": ["delivery.status_updated", "delivery.delivered", "delivery.cancelled"],
    },
).json()
# Store endpoint["secret"] now. It is not shown again.
```

The response contains the endpoint id (`we_…`) and the signing secret (`whsec_…`). The secret is shown only in this response. Store it to [verify signatures](https://maxmove.com/en/developers/docs/webhooks/verify-signatures).

- The URL must be a public HTTPS URL. Private and reserved network targets are rejected with `400 invalid_webhook_url`.
- Endpoints belong to the mode of the key that registered them. An endpoint registered with a test key only receives test deliveries, and the other way round.
- A workspace can register a limited number of endpoints per mode. Above that, registration answers `409 endpoint_limit_reached`.

## Events

| Event | Sent when |
| --- | --- |
| `delivery.created` | The delivery was created. `data.status` is `pending`. |
| `delivery.status_updated` | The status changed, for example to `picked_up` or `in_transit`. |
| `delivery.courier_assigned` | A courier accepted the delivery. |
| `delivery.courier_arrived` | The courier arrived at a waypoint: `data.status` is `at_pickup` or `at_dropoff`. At an intermediate stop, the status doesn't change. |
| `delivery.eta_updated` | The schedule or the route changed, so the expected arrival moved. |
| `delivery.pod_submitted` | The courier submitted proof at pickup or dropoff. |
| `delivery.delivered` | The delivery was completed. `delivered_at` is set. |
| `delivery.cancelled` | The delivery was cancelled by you or by Maxmove, also when no courier was found. `cancelled_at` is set. |

Events are sent for deliveries created with `POST /v1/deliveries`. [Fleet orders](https://maxmove.com/en/developers/docs/fleet/order-ingestion) don't send webhook events.

The [API reference](https://api.maxmove.com/v1/docs#tag/webhook-events) lists the request Maxmove sends for each event type.

## How to read events

- **One event per status change.** Each forward status change produces one event. `delivery.eta_updated`, `delivery.pod_submitted`, and `delivery.courier_arrived` at intermediate stops don't change the status.
- **Always read `data.status`.** Statuses that have their own event (`courier_assigned`, `at_pickup`, `at_dropoff`, `delivered`, `cancelled`) can also arrive as `delivery.status_updated`, depending on how the underlying updates are ordered. Use `data.status` from any event as the current status, rather than the event type.
- **`delivery.courier_arrived` without a status change.** At intermediate stops, the courier's arrival is reported, but `data.status` stays the same.
- **`delivery.eta_updated` carries no ETA.** It tells you that the schedule or route changed. Read the new ETA from [`GET /v1/deliveries/{deliveryId}/tracking`](https://api.maxmove.com/v1/docs#tag/deliveries/GET/v1/deliveries/{deliveryId}/tracking).
- **`delivery.pod_submitted` per checkpoint.** It is sent once for proof at pickup and once for proof at dropoff, so up to twice per delivery. Fetch the files from [`GET /v1/deliveries/{deliveryId}/proof-of-delivery`](https://api.maxmove.com/v1/docs#tag/deliveries/GET/v1/deliveries/{deliveryId}/proof-of-delivery).

## Event body

```json
{
  "id": "evt_4c1f9e2b7a3d8c5e6f0a1b2c3d4e5f60",
  "type": "delivery.status_updated",
  "version": "2026-10",
  "created_at": "2026-10-07T09:42:11Z",
  "data": { "id": "del_…", "status": "picked_up", "pickup": { … }, "dropoff": { … }, … }
}
```

`data` is the delivery as returned by [`GET /v1/deliveries/{deliveryId}`](https://api.maxmove.com/v1/docs#tag/deliveries/GET/v1/deliveries/{deliveryId}), without the courier's phone number and position: `data.courier` carries only the courier's name.

`version` changes only with breaking payload changes. Additive changes, such as new fields in `data`, keep the version. Ignore fields you don't know. See [Versioning](https://maxmove.com/en/developers/docs/versioning).

## Test your endpoint

[`POST /v1/webhooks/{webhookId}/test`](https://api.maxmove.com/v1/docs#tag/webhooks/POST/v1/webhooks/{webhookId}/test) sends a signed sample `delivery.status_updated` event to the endpoint. Its `data.id` is `del_sample` and `data.test` is `true`, so your handler can recognize and ignore it. Disabled endpoints answer `409 endpoint_disabled`.

With a [test key](https://maxmove.com/en/developers/docs/test-mode), every test delivery also sends real events as the Robocourier moves it along.

## Manage endpoints

| Task | Request |
| --- | --- |
| List endpoints | [`GET /v1/webhooks`](https://api.maxmove.com/v1/docs#tag/webhooks/GET/v1/webhooks) |
| Change URL, events, or description | [`PATCH /v1/webhooks/{webhookId}`](https://api.maxmove.com/v1/docs#tag/webhooks/PATCH/v1/webhooks/{webhookId}) |
| Pause or resume | `PATCH` with `"status": "disabled"` or `"status": "active"` |
| Rotate the signing secret | `PATCH` with `"rotate_secret": true` |
| Delete an endpoint | [`DELETE /v1/webhooks/{webhookId}`](https://api.maxmove.com/v1/docs#tag/webhooks/DELETE/v1/webhooks/{webhookId}) |

A disabled endpoint receives no new events. See [Retries and replay](https://maxmove.com/en/developers/docs/webhooks/retries) for events that were already queued.

---

Source: https://maxmove.com/en/developers/docs/webhooks/events
