# Test webhooks locally

> Receive Maxmove webhooks on your laptop through a tunnel, trigger every event with a test key, and replay failed events.

Maxmove only sends webhooks to public HTTPS URLs. Private, local, and reserved addresses such as `localhost` or `192.168.x.x` are rejected, both when you register the endpoint and before every send. To test on your machine, put a tunnel in front of your local server.

**1. Run a local receiver**

Read the raw body before parsing it: the [signature](https://maxmove.com/en/developers/docs/webhooks/verify-signatures) is computed over the exact bytes.

```ts Node.js
import { createServer } from 'node:http';

createServer((req, res) => {
  const chunks: Buffer[] = [];
  req.on('data', (chunk) => chunks.push(chunk));
  req.on('end', () => {
    const rawBody = Buffer.concat(chunks);
    console.log(req.headers['webhook-id'], req.headers['webhook-signature']);
    console.log(rawBody.toString('utf8'));
    res.writeHead(200).end();
  });
}).listen(4242);
```

```python Python
from flask import Flask, request

app = Flask(__name__)

@app.post("/maxmove/webhooks")
def maxmove_webhook():
    raw_body = request.get_data()  # exact bytes, for signature checks
    print(request.headers.get("webhook-id"), request.headers.get("webhook-signature"))
    print(raw_body.decode("utf-8"))
    return "", 200

app.run(port=4242)
```

**2. Open a tunnel**

Any HTTPS tunnel works, for example Cloudflare's quick tunnel or ngrok:

```bash
cloudflared tunnel --url http://localhost:4242
```

Note the public `https://…` URL it prints.

**3. Register the endpoint with your test key**

Register the tunnel URL with [`POST /v1/webhooks`](https://api.maxmove.com/v1/docs#tag/webhooks/POST/v1/webhooks) using an `mm_test_…` key. Store the `secret` from the response: it is shown only once. An endpoint registered with a test key receives events of test deliveries only.

```bash
curl -X POST https://api.maxmove.com/v1/webhooks \
  -H "x-api-key: $MAXMOVE_TEST_KEY" -H "content-type: application/json" \
  -d '{ "url": "https://your-tunnel.trycloudflare.com/maxmove/webhooks" }'
```

**4. Send a sample event**

[`POST /v1/webhooks/{webhookId}/test`](https://api.maxmove.com/v1/docs#tag/webhooks/POST/v1/webhooks/{webhookId}/test) sends one signed `delivery.status_updated` event with `data.test: true`. Use it to check your signature verification.

**5. Run a full lifecycle**

Create a delivery with the test key. The Robocourier moves it through every status, about 20 seconds per step, and you receive the same signed events as for a live delivery. Add `test_scenario` (`courier_cancelled` or `delivery_failed`) to get the failure paths; see [Cancellations](https://maxmove.com/en/developers/docs/guides/cancellations).

## When your receiver was down

Maxmove retries failed sends up to 8 times with growing intervals, at most 15 minutes apart. After that the events are kept as failed. Once your receiver is back, [`POST /v1/webhooks/{webhookId}/replay`](https://api.maxmove.com/v1/docs#tag/webhooks/POST/v1/webhooks/{webhookId}/replay) sends all of them again. See [Retries](https://maxmove.com/en/developers/docs/webhooks/retries).

> **Warning:**
> Quick tunnel URLs change on every start. Update the endpoint with [`PATCH /v1/webhooks/{webhookId}`](https://api.maxmove.com/v1/docs#tag/webhooks/PATCH/v1/webhooks/{webhookId}) instead of registering a new one each time, or delete old endpoints: a workspace can have at most 10 per mode (live and test).

## Example payloads

One example per event type, for a live delivery from Köln to Düsseldorf. `data` is the delivery as returned by [`GET /v1/deliveries/{deliveryId}`](https://api.maxmove.com/v1/docs#tag/deliveries/GET/v1/deliveries/{deliveryId}), with `courier` reduced to the courier's name. Read `data.status` rather than the event type alone: events can arrive out of order, and a status change can arrive as `delivery.status_updated`.

```json delivery.created
{
  "id": "evt_014c1f9e2b7a3d8c5e6f0a1b2c3d4e5f",
  "type": "delivery.created",
  "version": "2026-10",
  "created_at": "2026-10-07T09:30:00Z",
  "data": {
    "id": "del_0c9b0f6e2d8a4e1f9b7c5a3d1e2f4a6b",
    "status": "pending",
    "mode": "live",
    "external_id": "order-4711",
    "quote_id": "qt_8f14e45fceea167a5a36dedd4bea2543",
    "amount_cents": 4890,
    "currency": "EUR",
    "pickup": {
      "address": "Ehrenstraße 15, 50672 Köln",
      "latitude": 50.9385,
      "longitude": 6.9469,
      "contact": {
        "name": "Feinkost Ehrenfeld",
        "phone": "+49 221 1234567"
      },
      "instructions": "Pick up at the back entrance."
    },
    "dropoff": {
      "address": "Königsallee 27, 40212 Düsseldorf",
      "latitude": 51.2233,
      "longitude": 6.7768,
      "contact": {
        "name": "Erika Muster",
        "phone": "+49 170 1234567"
      },
      "instructions": "Ring at reception, 2nd floor."
    },
    "stops": [],
    "vehicle_type_id": "courier",
    "notes": null,
    "scheduled_at": null,
    "tracking_available_at": null,
    "tracking_url": "https://maxmove.com/de/track/3J8kQ2pVx9",
    "courier": null,
    "created_at": "2026-10-07T09:30:00Z",
    "updated_at": "2026-10-07T09:30:00Z",
    "cancelled_at": null,
    "delivered_at": null
  }
}
```

```json delivery.courier_assigned
{
  "id": "evt_024c1f9e2b7a3d8c5e6f0a1b2c3d4e5f",
  "type": "delivery.courier_assigned",
  "version": "2026-10",
  "created_at": "2026-10-07T09:32:10Z",
  "data": {
    "id": "del_0c9b0f6e2d8a4e1f9b7c5a3d1e2f4a6b",
    "status": "courier_assigned",
    "mode": "live",
    "external_id": "order-4711",
    "quote_id": "qt_8f14e45fceea167a5a36dedd4bea2543",
    "amount_cents": 4890,
    "currency": "EUR",
    "pickup": {
      "address": "Ehrenstraße 15, 50672 Köln",
      "latitude": 50.9385,
      "longitude": 6.9469,
      "contact": {
        "name": "Feinkost Ehrenfeld",
        "phone": "+49 221 1234567"
      },
      "instructions": "Pick up at the back entrance."
    },
    "dropoff": {
      "address": "Königsallee 27, 40212 Düsseldorf",
      "latitude": 51.2233,
      "longitude": 6.7768,
      "contact": {
        "name": "Erika Muster",
        "phone": "+49 170 1234567"
      },
      "instructions": "Ring at reception, 2nd floor."
    },
    "stops": [],
    "vehicle_type_id": "courier",
    "notes": null,
    "scheduled_at": null,
    "tracking_available_at": null,
    "tracking_url": "https://maxmove.com/de/track/3J8kQ2pVx9",
    "courier": {
      "name": "Jonas"
    },
    "created_at": "2026-10-07T09:30:00Z",
    "updated_at": "2026-10-07T09:32:10Z",
    "cancelled_at": null,
    "delivered_at": null
  }
}
```

```json delivery.courier_arrived
{
  "id": "evt_034c1f9e2b7a3d8c5e6f0a1b2c3d4e5f",
  "type": "delivery.courier_arrived",
  "version": "2026-10",
  "created_at": "2026-10-07T09:48:05Z",
  "data": {
    "id": "del_0c9b0f6e2d8a4e1f9b7c5a3d1e2f4a6b",
    "status": "at_pickup",
    "mode": "live",
    "external_id": "order-4711",
    "quote_id": "qt_8f14e45fceea167a5a36dedd4bea2543",
    "amount_cents": 4890,
    "currency": "EUR",
    "pickup": {
      "address": "Ehrenstraße 15, 50672 Köln",
      "latitude": 50.9385,
      "longitude": 6.9469,
      "contact": {
        "name": "Feinkost Ehrenfeld",
        "phone": "+49 221 1234567"
      },
      "instructions": "Pick up at the back entrance."
    },
    "dropoff": {
      "address": "Königsallee 27, 40212 Düsseldorf",
      "latitude": 51.2233,
      "longitude": 6.7768,
      "contact": {
        "name": "Erika Muster",
        "phone": "+49 170 1234567"
      },
      "instructions": "Ring at reception, 2nd floor."
    },
    "stops": [],
    "vehicle_type_id": "courier",
    "notes": null,
    "scheduled_at": null,
    "tracking_available_at": null,
    "tracking_url": "https://maxmove.com/de/track/3J8kQ2pVx9",
    "courier": {
      "name": "Jonas"
    },
    "created_at": "2026-10-07T09:30:00Z",
    "updated_at": "2026-10-07T09:48:05Z",
    "cancelled_at": null,
    "delivered_at": null
  }
}
```

```json delivery.status_updated
{
  "id": "evt_044c1f9e2b7a3d8c5e6f0a1b2c3d4e5f",
  "type": "delivery.status_updated",
  "version": "2026-10",
  "created_at": "2026-10-07T09:52:40Z",
  "data": {
    "id": "del_0c9b0f6e2d8a4e1f9b7c5a3d1e2f4a6b",
    "status": "picked_up",
    "mode": "live",
    "external_id": "order-4711",
    "quote_id": "qt_8f14e45fceea167a5a36dedd4bea2543",
    "amount_cents": 4890,
    "currency": "EUR",
    "pickup": {
      "address": "Ehrenstraße 15, 50672 Köln",
      "latitude": 50.9385,
      "longitude": 6.9469,
      "contact": {
        "name": "Feinkost Ehrenfeld",
        "phone": "+49 221 1234567"
      },
      "instructions": "Pick up at the back entrance."
    },
    "dropoff": {
      "address": "Königsallee 27, 40212 Düsseldorf",
      "latitude": 51.2233,
      "longitude": 6.7768,
      "contact": {
        "name": "Erika Muster",
        "phone": "+49 170 1234567"
      },
      "instructions": "Ring at reception, 2nd floor."
    },
    "stops": [],
    "vehicle_type_id": "courier",
    "notes": null,
    "scheduled_at": null,
    "tracking_available_at": null,
    "tracking_url": "https://maxmove.com/de/track/3J8kQ2pVx9",
    "courier": {
      "name": "Jonas"
    },
    "created_at": "2026-10-07T09:30:00Z",
    "updated_at": "2026-10-07T09:52:40Z",
    "cancelled_at": null,
    "delivered_at": null
  }
}
```

```json delivery.eta_updated
{
  "id": "evt_054c1f9e2b7a3d8c5e6f0a1b2c3d4e5f",
  "type": "delivery.eta_updated",
  "version": "2026-10",
  "created_at": "2026-10-07T10:05:12Z",
  "data": {
    "id": "del_0c9b0f6e2d8a4e1f9b7c5a3d1e2f4a6b",
    "status": "in_transit",
    "mode": "live",
    "external_id": "order-4711",
    "quote_id": "qt_8f14e45fceea167a5a36dedd4bea2543",
    "amount_cents": 4890,
    "currency": "EUR",
    "pickup": {
      "address": "Ehrenstraße 15, 50672 Köln",
      "latitude": 50.9385,
      "longitude": 6.9469,
      "contact": {
        "name": "Feinkost Ehrenfeld",
        "phone": "+49 221 1234567"
      },
      "instructions": "Pick up at the back entrance."
    },
    "dropoff": {
      "address": "Königsallee 27, 40212 Düsseldorf",
      "latitude": 51.2233,
      "longitude": 6.7768,
      "contact": {
        "name": "Erika Muster",
        "phone": "+49 170 1234567"
      },
      "instructions": "Ring at reception, 2nd floor."
    },
    "stops": [],
    "vehicle_type_id": "courier",
    "notes": null,
    "scheduled_at": null,
    "tracking_available_at": null,
    "tracking_url": "https://maxmove.com/de/track/3J8kQ2pVx9",
    "courier": {
      "name": "Jonas"
    },
    "created_at": "2026-10-07T09:30:00Z",
    "updated_at": "2026-10-07T10:05:12Z",
    "cancelled_at": null,
    "delivered_at": null
  }
}
```

```json delivery.pod_submitted
{
  "id": "evt_064c1f9e2b7a3d8c5e6f0a1b2c3d4e5f",
  "type": "delivery.pod_submitted",
  "version": "2026-10",
  "created_at": "2026-10-07T10:31:58Z",
  "data": {
    "id": "del_0c9b0f6e2d8a4e1f9b7c5a3d1e2f4a6b",
    "status": "at_dropoff",
    "mode": "live",
    "external_id": "order-4711",
    "quote_id": "qt_8f14e45fceea167a5a36dedd4bea2543",
    "amount_cents": 4890,
    "currency": "EUR",
    "pickup": {
      "address": "Ehrenstraße 15, 50672 Köln",
      "latitude": 50.9385,
      "longitude": 6.9469,
      "contact": {
        "name": "Feinkost Ehrenfeld",
        "phone": "+49 221 1234567"
      },
      "instructions": "Pick up at the back entrance."
    },
    "dropoff": {
      "address": "Königsallee 27, 40212 Düsseldorf",
      "latitude": 51.2233,
      "longitude": 6.7768,
      "contact": {
        "name": "Erika Muster",
        "phone": "+49 170 1234567"
      },
      "instructions": "Ring at reception, 2nd floor."
    },
    "stops": [],
    "vehicle_type_id": "courier",
    "notes": null,
    "scheduled_at": null,
    "tracking_available_at": null,
    "tracking_url": "https://maxmove.com/de/track/3J8kQ2pVx9",
    "courier": {
      "name": "Jonas"
    },
    "created_at": "2026-10-07T09:30:00Z",
    "updated_at": "2026-10-07T10:31:58Z",
    "cancelled_at": null,
    "delivered_at": null
  }
}
```

```json delivery.delivered
{
  "id": "evt_074c1f9e2b7a3d8c5e6f0a1b2c3d4e5f",
  "type": "delivery.delivered",
  "version": "2026-10",
  "created_at": "2026-10-07T10:33:20Z",
  "data": {
    "id": "del_0c9b0f6e2d8a4e1f9b7c5a3d1e2f4a6b",
    "status": "delivered",
    "mode": "live",
    "external_id": "order-4711",
    "quote_id": "qt_8f14e45fceea167a5a36dedd4bea2543",
    "amount_cents": 4890,
    "currency": "EUR",
    "pickup": {
      "address": "Ehrenstraße 15, 50672 Köln",
      "latitude": 50.9385,
      "longitude": 6.9469,
      "contact": {
        "name": "Feinkost Ehrenfeld",
        "phone": "+49 221 1234567"
      },
      "instructions": "Pick up at the back entrance."
    },
    "dropoff": {
      "address": "Königsallee 27, 40212 Düsseldorf",
      "latitude": 51.2233,
      "longitude": 6.7768,
      "contact": {
        "name": "Erika Muster",
        "phone": "+49 170 1234567"
      },
      "instructions": "Ring at reception, 2nd floor."
    },
    "stops": [],
    "vehicle_type_id": "courier",
    "notes": null,
    "scheduled_at": null,
    "tracking_available_at": null,
    "tracking_url": "https://maxmove.com/de/track/3J8kQ2pVx9",
    "courier": {
      "name": "Jonas"
    },
    "created_at": "2026-10-07T09:30:00Z",
    "updated_at": "2026-10-07T10:33:20Z",
    "cancelled_at": null,
    "delivered_at": "2026-10-07T10:33:20Z"
  }
}
```

```json delivery.cancelled
{
  "id": "evt_084c1f9e2b7a3d8c5e6f0a1b2c3d4e5f",
  "type": "delivery.cancelled",
  "version": "2026-10",
  "created_at": "2026-10-07T09:31:15Z",
  "data": {
    "id": "del_0c9b0f6e2d8a4e1f9b7c5a3d1e2f4a6b",
    "status": "cancelled",
    "mode": "live",
    "external_id": "order-4711",
    "quote_id": "qt_8f14e45fceea167a5a36dedd4bea2543",
    "amount_cents": 4890,
    "currency": "EUR",
    "pickup": {
      "address": "Ehrenstraße 15, 50672 Köln",
      "latitude": 50.9385,
      "longitude": 6.9469,
      "contact": {
        "name": "Feinkost Ehrenfeld",
        "phone": "+49 221 1234567"
      },
      "instructions": "Pick up at the back entrance."
    },
    "dropoff": {
      "address": "Königsallee 27, 40212 Düsseldorf",
      "latitude": 51.2233,
      "longitude": 6.7768,
      "contact": {
        "name": "Erika Muster",
        "phone": "+49 170 1234567"
      },
      "instructions": "Ring at reception, 2nd floor."
    },
    "stops": [],
    "vehicle_type_id": "courier",
    "notes": null,
    "scheduled_at": null,
    "tracking_available_at": null,
    "tracking_url": "https://maxmove.com/de/track/3J8kQ2pVx9",
    "courier": null,
    "created_at": "2026-10-07T09:30:00Z",
    "updated_at": "2026-10-07T09:31:15Z",
    "cancelled_at": "2026-10-07T09:31:15Z",
    "delivered_at": null
  }
}
```

---

Source: https://maxmove.com/en/developers/docs/guides/webhooks-local-testing
