Maxmove LogoDocs

Test webhooks locally

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

View as Markdown

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 is computed over the exact bytes.

    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);
  2. Open a tunnel

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

    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 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.

    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 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.

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 sends all of them again. See Retries.

Quick tunnel URLs change on every start. Update the endpoint with 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}, 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.

{
  "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
  }
}

Did this answer your question?