Skip to content

For developers · Private beta

API and webhooks

The Teams API lets you send orders from your shop or order system straight into the dispatcher website (Inbox in Routes), read their status and get a webhook when a driver delivers — or connect any service with Zapier.

  1. Create an API key in Team → Integrations.
  2. Your system sends each order with POST /orders. It appears in Inbox on its delivery date.
  3. A dispatcher adds the orders to drivers' routes, as with any other stops.
  4. When a driver marks a stop, the order gets its status, time and proof of delivery, and your webhooks are called.

The API is in private beta. If Integrations says the API is not enabled for your team yet, write to support@courierrouteplanner.com to join the beta. No developer? Use the private Zapier integration (Zapier).

Authentication

Create a key on the dispatcher website: Team → Integrations → Create API key, and give it a name of up to 40 characters (for example, the shop it is for). The team owner and dispatchers can create and revoke keys.

  • The key is shown once, right after you create it. Copy it into your server's secrets. We keep only a hash of it, so we can't show it again: if you lose it, revoke it and create a new one.
  • Keys start with rpt_live_. Keep them secret: anyone with a key can add orders to your team and read orders, recipient details and proof of delivery.
  • Call the API from your server only — never from a browser or a mobile app. Browser (cross-origin) requests are refused.
  • Revoke a key at any time in Integrations; requests with it stop working straight away.

Send the key in the Authorization header of every request:

Header
Authorization: Bearer rpt_live_…
Request
curl https://courierrouteplanner.com/api/v1/me \
  -H "Authorization: Bearer $RPT_API_KEY"

Requests and errors

Base URL
https://courierrouteplanner.com/api/v1

Request bodies and responses are JSON (send Content-Type: application/json). Times are ISO 8601 in UTC, for example 2026-10-23T10:42:28.000Z; delivery dates are YYYY-MM-DD. Field names are snake_case.

MethodPathWhat it does
GET/meTeam, key, plan and usage
POST/ordersCreate an order
GET/ordersList orders
GET/orders/{id}Get an order
DELETE/orders/{id}Cancel an order
POST/hooksSubscribe a webhook
GET/hooksList webhooks
DELETE/hooks/{id}Delete a webhook

Errors come with an HTTP status and a JSON body with a short code and a message for people:

Error
{
  "error": "address-not-found",
  "message": "The address could not be found. Check it, add the city and postcode, or send lat and lng."
}
ErrorWhen
invalidHTTP 400The body or a parameter is not valid. The message names the field.
address-not-foundHTTP 400The address could not be found. Add the city and postcode, or send lat and lng.
unauthorizedHTTP 401No key, a malformed key, or a revoked key.
team-lapsedHTTP 403The team's subscription is not active (the trial ended or a payment failed). Pay in Subscription to continue.
api-disabledHTTP 403The API is not enabled.
team-not-foundHTTP 403The team of this key no longer exists.
not-foundHTTP 404No such order or webhook, or an unknown path.
order-plannedHTTP 409The order is already on a route, delivered or failed, so it can't be cancelled through the API.
too-many-hooksHTTP 409The team already has 20 webhooks. Delete one first.
rate-limitedHTTP 429Too many requests. When the daily limit is reached, the body also has "reason": "daily-limit" and reset_at.
quota-exceededHTTP 429The team's monthly allowance of address lookups is used up ("reason": "monthly-limit"). It resets on the 1st of the month (UTC).
internalHTTP 500Something went wrong on our side. Try again later.

Retry 429 and 500 after a pause. Don't repeat other errors without changing the request. To retry POST /orders safely, send external_id: the same order is never created twice.

Limits

  • 600 requests per 10 minutes per key, and 5,000 requests per day per key (UTC day). Over the limit you get 429 rate-limited.
  • Address lookups. Each order sent with an address (and without lat / lng) uses one address lookup from the team's monthly allowance — the same allowance as addresses imported from files and lists on the website. Orders sent with lat and lng, repeats with the same external_id and addresses that can't be found don't use it. GET /me shows how much is used.
  • Stops. An order becomes a stop of the monthly stops allowance only when a dispatcher adds it to a route, as with any other stop.
  • Up to 10 active API keys and 20 webhooks per team.

Account

GET /me

Checks the key and shows the team, the key, the plan, this month's usage (UTC month) and the API settings. Handy as the first call and for a health check.

Request
curl https://courierrouteplanner.com/api/v1/me \
  -H "Authorization: Bearer $RPT_API_KEY"
Response 200
{
  "team": {
    "id": "Tk7Qp2Wm9xLs",
    "name": "Northside Couriers"
  },
  "key": {
    "id": "k_Qm3xT8vLp2aZ",
    "prefix": "rpt_live_x7Gk2PqA",
    "name": "Online shop"
  },
  "plan": {
    "status": "active",
    "seats": 5,
    "paid": true,
    "lapsed": false,
    "graceUntil": null,
    "lapsedReason": null,
    "retainUntil": null
  },
  "limits": {
    "month": "2026-10",
    "resets_at": "2026-11-01T00:00:00.000Z",
    "stops": {
      "used": 1240,
      "cap": 15000
    },
    "optimizations": {
      "used": 96,
      "cap": 5000
    },
    "address_lookups": {
      "used": 310,
      "cap": 30000
    },
    "requests_per_10min": 600,
    "requests_per_day": 5000
  },
  "settings": {
    "timezone": "Europe/London",
    "share_location": false
  }
}
  • limits — stops added to routes, route optimisations and address lookups this month, each with its cap, and the request limits of the key.
  • settings.timezone — the team's time zone for the default date of new orders. It is taken from the browser of the person who creates the team's first API key.
  • settings.share_location — whether orders include the position where the driver marked the stop (Share the driver’s position at the stop in Integrations).

Orders

An order is a delivery (or pickup) you send from your system. It arrives in Inbox in Routes on its delivery date, and a dispatcher adds it to a driver's route from there.

StatusMeaning
inboxWaiting in Inbox. If the dispatcher removes the stop from its route, or deletes the route, the order comes back here.
plannedOn a route: route_id, stop_id and planned_at are set.
deliveredThe driver marked the stop as delivered (delivered_at, pod).
failedThe driver marked the stop as failed (failed_at, fail_reason). If the driver undoes a mark, the order is planned again.
cancelledCancelled with DELETE /orders/{id} or dismissed in Inbox.

POST /orders

Creates an order in Inbox. Send an address, or lat and lng.

Request
curl https://courierrouteplanner.com/api/v1/orders \
  -H "Authorization: Bearer $RPT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "SHOP-10482",
    "address": "221B Baker Street, London NW1 6XE",
    "country": "GB",
    "name": "Sam Taylor",
    "phone": "+44 20 7946 0958",
    "note": "Leave with the concierge",
    "date": "2026-10-23",
    "time_window": {
      "from": "09:00",
      "to": "12:00"
    },
    "service_minutes": 10,
    "parcel_code": "1Z999AA10123456784",
    "signature_required": true
  }'
FieldDescription
addressstringDelivery address. Required unless you send lat and lng. Include the city and postcode for the best match. Up to 300 characters.
lat
lngnumber
Coordinates of the stop. With both, the address isn't looked up and doesn't use the allowance; address is then only the label the dispatcher and driver see.
namestringRecipient's name, up to 80 characters.
phonestringRecipient's phone, up to 40 characters.
notestringNote for the driver, up to 250 characters.
external_idstringYour order number, up to 128 characters. Makes the request idempotent: the same external_id again returns the existing order with status 200 and "created": false, and changes nothing.
typestringdelivery (default) or pickup.
time_windowobjectDelivery window { "from": "HH:MM", "to": "HH:MM" } in local time; from must be before to.
service_minutesnumberTime the driver spends at this stop, in minutes: 0 to 120, for example 10 or 7.5. It is used for the arrival times and when the route is optimised. Without it the stop gets the route's Minutes per stop.
datestringDelivery date YYYY-MM-DD — the day in Inbox where the order appears. Default: today in the team's time zone (settings.timezone in GET /me).
parcel_codestringParcel barcode or label number, up to 128 characters.
signature_requiredbooleanWhether the recipient's signature is required at this stop. Default: false.
countrystringTwo-letter country code (ISO 3166-1), for example GB, to help find the address.

camelCase names (externalId, timeWindow, serviceMinutes, parcelCode, signatureRequired) are accepted too.

Response 201
{
  "order": {
    "id": "x_3f9c0e1d2b7a4c58e6f1a0b9c8d7e6f5a4b3c2d1",
    "external_id": "SHOP-10482",
    "status": "inbox",
    "type": "delivery",
    "address": "221B Baker Street, London NW1 6XE",
    "formatted_address": "221B Baker Street, London NW1 6XE",
    "name": "Sam Taylor",
    "phone": "+44 20 7946 0958",
    "note": "Leave with the concierge",
    "lat": 51.523767,
    "lng": -0.158555,
    "date": "2026-10-23",
    "time_window": {
      "from": "09:00",
      "to": "12:00"
    },
    "service_minutes": 10,
    "parcel_code": "1Z999AA10123456784",
    "signature_required": true,
    "review": false,
    "review_reasons": [],
    "source": "api",
    "created_at": "2026-10-22T16:05:12.481Z",
    "updated_at": "2026-10-22T16:05:12.481Z",
    "route_id": null,
    "stop_id": null,
    "planned_at": null,
    "delivered_at": null,
    "failed_at": null,
    "fail_reason": null,
    "cancelled_at": null,
    "location": null,
    "pod": null
  },
  "created": true
}

If the address was found with low confidence, review is true and review_reasons says why — check the pin on the website before planning. An address that can't be found returns 400 address-not-found and no order is created.

GET /orders

Lists orders. Without parameters — today's orders (in the team's time zone).

ParameterDescription
dateDelivery date YYYY-MM-DD.
statusinbox, planned, delivered, failed, cancelled. With date — orders of that day with this status; without date — orders with this status on any day.
Request
curl "https://courierrouteplanner.com/api/v1/orders?date=2026-10-23&status=delivered" \
  -H "Authorization: Bearer $RPT_API_KEY"
Response 200
{
  "orders": [
    { …order… }
  ],
  "truncated": false
}

Orders are sorted newest first. A response holds up to 500 orders; "truncated": true means there are more — narrow the query by date and status.

GET /orders/{id}

Returns one order by its id (the one returned when you created it).

Request
curl https://courierrouteplanner.com/api/v1/orders/x_3f9c0e1d2b7a4c58e6f1a0b9c8d7e6f5a4b3c2d1 \
  -H "Authorization: Bearer $RPT_API_KEY"
Response 200 — a delivered order
{
  "order": {
    "id": "x_3f9c0e1d2b7a4c58e6f1a0b9c8d7e6f5a4b3c2d1",
    "external_id": "SHOP-10482",
    "status": "delivered",
    "type": "delivery",
    "address": "221B Baker Street, London NW1 6XE",
    "formatted_address": "221B Baker Street, London NW1 6XE",
    "name": "Sam Taylor",
    "phone": "+44 20 7946 0958",
    "note": "Leave with the concierge",
    "lat": 51.523767,
    "lng": -0.158555,
    "date": "2026-10-23",
    "time_window": {
      "from": "09:00",
      "to": "12:00"
    },
    "service_minutes": 10,
    "parcel_code": "1Z999AA10123456784",
    "signature_required": true,
    "review": false,
    "review_reasons": [],
    "source": "api",
    "created_at": "2026-10-22T16:05:12.481Z",
    "updated_at": "2026-10-23T10:42:36.017Z",
    "route_id": "Vb3kQ9xLm2PzR7tW1cYe",
    "stop_id": "st_1761213600412",
    "planned_at": "2026-10-22T18:03:10.215Z",
    "delivered_at": "2026-10-23T10:42:28.000Z",
    "failed_at": null,
    "fail_reason": null,
    "cancelled_at": null,
    "location": {
      "lat": 51.523812,
      "lng": -0.158491,
      "accuracy_m": 9
    },
    "pod": {
      "photos": [
        "https://courierrouteplanner.com/api/v1/media/eyJ0IjoiVGs3UXA…"
      ],
      "signature": "https://courierrouteplanner.com/api/v1/media/eyJ0IjoiVGs3UXB…",
      "note": "Left with the concierge"
    }
  }
}

DELETE /orders/{id}

Cancels an order that is still in Inbox: its status becomes cancelled and it leaves Inbox. Cancelling an order that is already cancelled returns it unchanged. Once the order is on a route (planned, delivered or failed), you get 409 order-planned: ask the dispatcher to remove it from the route on the website.

Request
curl -X DELETE https://courierrouteplanner.com/api/v1/orders/x_3f9c0e1d2b7a4c58e6f1a0b9c8d7e6f5a4b3c2d1 \
  -H "Authorization: Bearer $RPT_API_KEY"
Response 200
{
  "order": {
    "id": "x_3f9c0e1d2b7a4c58e6f1a0b9c8d7e6f5a4b3c2d1",
    "external_id": "SHOP-10482",
    "status": "cancelled",
    …,
    "cancelled_at": "2026-10-22T17:30:02.914Z"
  }
}

The order object

FieldDescription
idOrder id. An order sent with external_id always has the same id.
external_idYour order number, or null.
statusinbox, planned, delivered, failed, cancelled (see Orders).
typedelivery or pickup.
addressThe address as you sent it.
formatted_addressThe address as it was found (for orders sent with lat and lng — the address you sent).
name
phone
note
As you sent them (empty strings if not sent).
lat
lng
Coordinates of the stop.
dateDelivery date, YYYY-MM-DD.
time_window{ "from", "to" } or null.
service_minutesTime at the stop in minutes, or null — then the stop gets the route's Minutes per stop.
parcel_codeParcel code, or null.
signature_requiredtrue or false.
review
review_reasons
review: true — the address was found with low confidence; check it on the website. review_reasons lists why.
sourceWhere the order came from: api.
created_at
updated_at
When the order was created and last changed.
route_id
stop_id
The route and the stop the order is on, or null.
planned_atWhen the order was added to a route.
delivered_at
failed_at
When the driver marked the stop.
fail_reasonThe driver's note when the delivery failed, or null.
cancelled_atWhen the order was cancelled.
location{ "lat", "lng", "accuracy_m" } — where the driver marked the stop. Only when Share the driver’s position at the stop is on in Integrations; otherwise null.
podProof of delivery: { "photos": [url, …], "signature": url | null, "note": … }, or null. Photos are JPEG, the signature is SVG. Links work without the API key and are valid for 7 days — treat them as private, and fetch the order again for fresh links.

Webhook subscriptions

A webhook sends an HTTPS request to your server when something happens. Each webhook is one event and one URL; subscribe several webhooks to get several events.

EventWhen
order.createdA new order arrived in Inbox.
order.deliveredThe driver marked the order's stop as delivered.
order.failedThe driver marked the order's stop as failed.
route.completedA driver finished a team route (any team route, also one without API orders).

POST /hooks

Subscribes target_url to an event. The URL must use https and a public host name: not an IP address, not localhost or a private network, and without a user name or password (up to 2,048 characters).

Request
curl https://courierrouteplanner.com/api/v1/hooks \
  -H "Authorization: Bearer $RPT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "event": "order.delivered", "target_url": "https://example.com/webhooks/route-planner" }'
Response 201
{
  "hook": {
    "id": "h_V2n8Rk4qLx0s",
    "event": "order.delivered",
    "target_url": "https://example.com/webhooks/route-planner",
    "secret": "5d41a7c9e0b84f2a93c61e7d08f5b2c4a6e9d1f3b7c05a8e2d4f6b9c1e3a5d70",
    "created_at": "2026-10-22T16:20:00.000Z",
    "disabled": false,
    "failures": 0
  }
}

Store secret: it signs every delivery, and it is returned only by POST /hooks (GET /hooks doesn't show it). Sending the same event and target_url again doesn't create a duplicate: it returns the existing webhook (status 200, same secret) and turns it back on if it was disabled.

GET /hooks

Lists the team's webhooks, oldest first, without secrets. disabled: true means deliveries stopped (see Retries); failures counts failed deliveries in a row.

Request
curl https://courierrouteplanner.com/api/v1/hooks \
  -H "Authorization: Bearer $RPT_API_KEY"
Response 200
{
  "hooks": [
    {
      "id": "h_V2n8Rk4qLx0s",
      "event": "order.delivered",
      "target_url": "https://example.com/webhooks/route-planner",
      "created_at": "2026-10-22T16:20:00.000Z",
      "disabled": false,
      "failures": 0
    }
  ]
}

DELETE /hooks/{id}

Deletes a webhook; deliveries to it stop.

Request
curl -X DELETE https://courierrouteplanner.com/api/v1/hooks/h_V2n8Rk4qLx0s \
  -H "Authorization: Bearer $RPT_API_KEY"
Response 200
{
  "deleted": true,
  "id": "h_V2n8Rk4qLx0s"
}

Webhook deliveries

Each delivery is a POST with a JSON body: a delivery id, the event, created_at, team_id and either order or route.

order.delivered
{
  "id": "d_Jm4cX0pQe7Ys",
  "event": "order.delivered",
  "created_at": "2026-10-23T10:42:31.904Z",
  "team_id": "Tk7Qp2Wm9xLs",
  "order": {
    "id": "x_3f9c0e1d2b7a4c58e6f1a0b9c8d7e6f5a4b3c2d1",
    "external_id": "SHOP-10482",
    "status": "delivered",
    "type": "delivery",
    "date": "2026-10-23",
    "service_minutes": 10,
    "route_id": "Vb3kQ9xLm2PzR7tW1cYe",
    "stop_id": "st_1761213600412",
    "planned_at": "2026-10-22T18:03:10.215Z",
    "delivered_at": "2026-10-23T10:42:28.000Z",
    "failed_at": null,
    "fail_reason": null,
    "cancelled_at": null,
    "location": {
      "lat": 51.523812,
      "lng": -0.158491,
      "accuracy_m": 9
    },
    "pod": {
      "photos": [
        "https://courierrouteplanner.com/api/v1/media/eyJ0IjoiVGs3UXA…"
      ],
      "signature": "https://courierrouteplanner.com/api/v1/media/eyJ0IjoiVGs3UXB…",
      "note": "Left with the concierge"
    },
    "created_at": "2026-10-22T16:05:12.481Z"
  }
}

The order in a webhook is shorter than in the API: it has no recipient name, phone, address or note. Match it by id or external_id, or fetch GET /orders/{id} for the full order.

route.completed
{
  "id": "d_Rt8wB3nLs1Kc",
  "event": "route.completed",
  "created_at": "2026-10-23T15:12:47.310Z",
  "team_id": "Tk7Qp2Wm9xLs",
  "route": {
    "id": "Vb3kQ9xLm2PzR7tW1cYe",
    "name": "North — Thursday",
    "date": "2026-10-23T07:30:00.000Z",
    "finished_at": "2026-10-23T15:12:44.000Z",
    "stops_total": 38,
    "delivered": 36,
    "failed": 2,
    "order_ids": [
      "x_3f9c0e1d2b7a4c58e6f1a0b9c8d7e6f5a4b3c2d1",
      "o_b2Vq9LmX4kPz7RtW1cYe"
    ]
  }
}

route.date is the planned start time of the route (or null), stops_total counts the route's stops without the start, and order_ids lists the API orders on the route.

Headers

HeaderValue
Content-Typeapplication/json; charset=utf-8
User-AgentRoutePlannerTeams-Webhooks/1.0
X-RoutePlanner-EventThe event, for example order.delivered.
X-RoutePlanner-DeliveryThe delivery id — the same as id in the body, and the same on every retry of this delivery. Use it to skip duplicates.
X-RoutePlanner-Signaturet=<unix time in ms>,v1=<signature>

Verifying the signature

v1 is the hex HMAC-SHA256 of the string <t>.<raw body>, with the webhook's secret as the key. Compute it from the raw body exactly as received (before parsing the JSON), compare in constant time, and reject the request if t is more than 5 minutes away from your clock.

Node.js (Express)
const crypto = require('crypto');
const express = require('express');

const app = express();
const SECRET = process.env.RPT_WEBHOOK_SECRET; // the webhook's "secret"
const TOLERANCE_MS = 5 * 60 * 1000;

function verify(header, rawBody, secret) {
  const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header || '');
  if (!m) return false;
  const t = Number(m[1]);
  if (Math.abs(Date.now() - t) > TOLERANCE_MS) return false; // too old or from the future
  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(m[2], 'hex'));
}

// Read the body raw: the signature covers the exact bytes we sent.
app.post('/webhooks/route-planner', express.raw({ type: 'application/json' }), (req, res) => {
  const raw = req.body.toString('utf8');
  if (!verify(req.get('X-RoutePlanner-Signature'), raw, SECRET)) return res.status(401).end();
  res.status(200).end(); // answer first, do the work afterwards

  const delivery = JSON.parse(raw);
  // delivery.id (= X-RoutePlanner-Delivery) is the same on retries: skip ids you have already handled.
  if (delivery.event === 'order.delivered') {
    // delivery.order.external_id — your order number; delivery.order.pod — proof of delivery links
  }
});

app.listen(3000);

Retries

  • Answer with any 2xx status within 8 seconds, and do slow work afterwards.
  • Each delivery is tried up to 3 times: right away, then 3 seconds and 10 seconds after the previous attempt. Each attempt waits up to 8 seconds for your answer.
  • Timeouts, network errors, 408, 429 and 5xx are retried; other 4xx answers are not. Redirects are not followed (a 3xx counts as a failure) — use the final URL.
  • 410 Gone unsubscribes: the webhook is disabled at once.
  • After 10 failed deliveries in a row the webhook is disabled. To turn it back on, send POST /hooks with the same event and target_url, or create a new one.
  • At delivery time the host must resolve to public addresses; otherwise the delivery fails.
  • Deliveries are not guaranteed to arrive in order, and one may arrive twice — rely on the delivery id and on the order's status and times.

Proof of delivery: when photos or a signature are required at the stop, we wait a few seconds for them before sending order.delivered. Otherwise the driver's photos can arrive a few seconds after the event. If pod is null or photos are missing, fetch GET /orders/{id} a little later for the final proof of delivery.

Zapier

No developer? Connect your shop, forms or spreadsheets with Zapier. Route Planner Teams has a private Zapier integration:

  • Triggers: Order Delivered, Order Failed, Route Completed.
  • Action: Create Order.
  • Search: Find Order.

It is not in Zapier's app directory yet: you add it with an invite link. The integration signs in with an API key from Integrations. Step by step, with the invite link and example Zaps: Connect Route Planner Teams to Zapier.

Changelog

VersionChanges
v1 — October 2026First version (private beta): orders, webhooks, Zapier integration.

The API is in beta and may still change. We'll announce changes that affect existing integrations in advance where we can. Questions and feedback: support@courierrouteplanner.com.

Guides for dispatchers