Connect your own website or store to Routella for a full two-way integration. Push orders straight into dispatch, read their live status, and receive a webhook on every step of the delivery — created, picked, dispatched, delivered, and more.
https://routella.appAll endpoints are relative to this host and return JSON.New here? These five steps take you from zero to a live order with delivery updates flowing back to your own server.
curl -X POST https://routella.app/api/v1/orders \
-H "Authorization: Bearer rtl_3f9c1a7b2e8d4056af1c9b3e7d2a6f08" \
-H "Content-Type: application/json" \
-d '{ "customerName": "Jane Doe", "address": "221B Baker Street", "city": "London" }'Every endpoint and every webhook is free on all plans — building a full integration costs nothing.
Routella connects to your systems in three directions. Most stores combine the first two — push orders in, receive webhooks back — for a complete two-way integration.
Your website or backend calls Routella whenever a customer places an order. You decide exactly when an order enters Routella. Uses your API key and POST /api/v1/orders.
Routella calls your server on every delivery event — created, picked, dispatched, delivered, and more — so your own system stays in sync without polling. Set it up under Settings → Integrations → Webhooks.
Already expose an orders endpoint? Add a Custom API integration in the dashboard. Routella reads orders from your own server on a schedule, with field mapping for any JSON shape — no pushing required.
Every API request is authenticated with a secret key tied to your Routella account. You generate one inside the dashboard.
rtl_3f9c1a7b2e8d4056af1c9b3e7d2a6f08 — the prefix rtl_ followed by a hex string.Send your API key on every request in the Authorization header as a Bearer token:
Authorization: Bearer rtl_3f9c1a7b2e8d4056af1c9b3e7d2a6f08
A request with a missing, malformed, or invalid key is rejected with 401 unauthorized. Because the key is a secret, all calls must be made from your server — never from a browser.
Push a new order into Routella. The order shows up in your dashboard ready to be added to a delivery round. Routella geocodes the address automatically unless you pass coordinates yourself.
curl -X POST https://routella.app/api/v1/orders \
-H "Authorization: Bearer rtl_3f9c1a7b2e8d4056af1c9b3e7d2a6f08" \
-H "Content-Type: application/json" \
-d '{
"customerName": "Jane Doe",
"address": "221B Baker Street",
"city": "London",
"phone": "+447700900123",
"email": "jane@example.com",
"items": [
{ "title": "Wireless Mouse", "quantity": 1, "price": 24.99, "sku": "WM-001" }
],
"total": 24.99,
"currency": "GBP",
"paymentMethod": "cash",
"codAmount": 24.99,
"stopType": "delivery",
"note": "Leave with the concierge"
}'const res = await fetch("https://routella.app/api/v1/orders", {
method: "POST",
headers: {
"Authorization": "Bearer rtl_3f9c1a7b2e8d4056af1c9b3e7d2a6f08",
"Content-Type": "application/json",
},
body: JSON.stringify({
customerName: "Jane Doe",
address: "221B Baker Street",
city: "London",
phone: "+447700900123",
items: [
{ title: "Wireless Mouse", quantity: 1, price: 24.99, sku: "WM-001" },
],
total: 24.99,
currency: "GBP",
paymentMethod: "cash",
codAmount: 24.99,
stopType: "delivery",
}),
});
if (!res.ok) {
const err = await res.json();
throw new Error(`Routella API error: ${err.error}`);
}
const { order } = await res.json();
console.log("Created order", order.id, order.orderNumber);HTTP/1.1 201 Created
Content-Type: application/json
{
"order": {
"id": "6711e2a4c9d3f80012ab34cd",
"orderNumber": "M-0042",
"status": "unfulfilled",
"customerName": "Jane Doe",
"phone": "+447700900123",
"email": "jane@example.com",
"address": {
"line1": "221B Baker Street",
"city": "London",
"full": "221B Baker Street, London",
"country": "United Kingdom",
"latitude": 51.523767,
"longitude": -0.1585557
},
"items": [
{
"title": "Wireless Mouse",
"quantity": 1,
"price": 24.99,
"sku": "WM-001",
"variantTitle": ""
}
],
"total": 24.99,
"currency": "GBP",
"note": "Leave with the concierge",
"paymentMethod": "cash",
"codAmount": 24.99,
"stopType": "delivery",
"delivery": {
"status": "unfulfilled",
"driverName": null,
"roundId": null
},
"createdAt": "2026-05-19T09:14:22.000Z",
"updatedAt": "2026-05-19T09:14:22.000Z"
}
}402 quota_exceeded — see the Errors section below.Return your orders, newest first. Use the query parameters to page through results and filter by fulfillment status.
curl -X GET "https://routella.app/api/v1/orders?status=unfulfilled&limit=25" \ -H "Authorization: Bearer rtl_3f9c1a7b2e8d4056af1c9b3e7d2a6f08"
HTTP/1.1 200 OK
Content-Type: application/json
{
"orders": [
{
"id": "6711e2a4c9d3f80012ab34cd",
"orderNumber": "M-0042",
"status": "unfulfilled",
"customerName": "Jane Doe",
"phone": "+447700900123",
"address": {
"line1": "221B Baker Street",
"city": "London",
"full": "221B Baker Street, London",
"country": "United Kingdom",
"latitude": 51.523767,
"longitude": -0.1585557
},
"total": 24.99,
"currency": "GBP",
"paymentMethod": "cash",
"codAmount": 24.99,
"stopType": "delivery",
"delivery": { "status": "unfulfilled", "driverName": null, "roundId": null },
"createdAt": "2026-05-19T09:14:22.000Z",
"updatedAt": "2026-05-19T09:14:22.000Z"
}
],
"count": 1
}Fetch a single order by its id, including its current delivery status — the assigned driver and round, if any.
curl -X GET https://routella.app/api/v1/orders/6711e2a4c9d3f80012ab34cd \ -H "Authorization: Bearer rtl_3f9c1a7b2e8d4056af1c9b3e7d2a6f08"
HTTP/1.1 200 OK
Content-Type: application/json
{
"order": {
"id": "6711e2a4c9d3f80012ab34cd",
"orderNumber": "M-0042",
"status": "partial",
"customerName": "Jane Doe",
"phone": "+447700900123",
"email": "jane@example.com",
"address": {
"line1": "221B Baker Street",
"city": "London",
"full": "221B Baker Street, London",
"country": "United Kingdom",
"latitude": 51.523767,
"longitude": -0.1585557
},
"items": [
{ "title": "Wireless Mouse", "quantity": 1, "price": 24.99, "sku": "WM-001", "variantTitle": "" }
],
"total": 24.99,
"currency": "GBP",
"note": "Leave with the concierge",
"paymentMethod": "cash",
"codAmount": 24.99,
"stopType": "delivery",
"delivery": {
"status": "partial",
"driverName": "Marcus Lane",
"roundId": "6711f0b8c9d3f80012ab35ef"
},
"createdAt": "2026-05-19T09:14:22.000Z",
"updatedAt": "2026-05-19T11:02:47.000Z"
}
}404 not_found.All three order endpoints return the same order shape. The status field is one of unfulfilled, fulfilled, or partial. The delivery object reflects the current dispatch state — driver and round.
{
"id": "string",
"orderNumber": "string",
"status": "unfulfilled | fulfilled | partial",
"customerName": "string",
"phone": "string",
"email": "string",
"address": {
"line1": "string",
"city": "string",
"full": "string",
"country": "string",
"latitude": "number",
"longitude": "number"
},
"items": [
{
"title": "string",
"quantity": "number",
"price": "number",
"sku": "string",
"variantTitle": "string"
}
],
"total": "number",
"currency": "string",
"note": "string",
"paymentMethod": "online | cash | card | check | transfer",
"codAmount": "number",
"stopType": "delivery | pickup",
"delivery": {
"status": "unfulfilled | fulfilled | partial",
"driverName": "string",
"roundId": "string"
},
"createdAt": "ISO 8601 string",
"updatedAt": "ISO 8601 string"
}Instead of polling, let Routella tell you when something happens. Open Settings → Integrations → Webhooks, add your server URL, and tick the events you want. Routella then sends an HTTP POST to that URL every time one of those events occurs. The picker in Settings lists exactly the events below.
Every webhook POST has the same three top-level fields: event, timestamp (ISO 8601), and a data object whose contents depend on the event.
POST https://your-server.com/webhooks/routella
Content-Type: application/json
X-Webhook-Signature: 9a1f3c0b7e2d... (HMAC-SHA256 hex — only when a secret is set)
{
"event": "order.delivered",
"timestamp": "2026-05-19T13:48:10.000Z",
"data": { ... }
}The Payload column says which data shape the event ships — see the two reference tables below.
Every order.* event carries this exact data object. The eta field is present only on order.arriving.
{
"id": "6711e2a4c9d3f80012ab34cd",
"orderNumber": "M-0042",
"customerName": "Jane Doe",
"phone": "+447700900123",
"email": "jane@example.com",
"address": {
"line1": "221B Baker Street",
"city": "London",
"full": "221B Baker Street, London",
"country": "United Kingdom",
"latitude": 51.523767,
"longitude": -0.1585557
},
"driver": "Marcus Lane",
"round": "1747645200000"
}The two round.* events carry a short round summary instead of an order.
{
"round": "1747645200000",
"driver": "Marcus Lane",
"stops": 5
}{
"event": "order.created",
"timestamp": "2026-05-19T09:14:22.000Z",
"data": {
"id": "6711e2a4c9d3f80012ab34cd",
"orderNumber": "M-0042",
"customerName": "Jane Doe",
"phone": "+447700900123",
"email": "jane@example.com",
"address": {
"line1": "221B Baker Street",
"city": "London",
"full": "221B Baker Street, London",
"country": "United Kingdom",
"latitude": 51.523767,
"longitude": -0.1585557
},
"driver": "",
"round": ""
}
}{
"event": "order.arriving",
"timestamp": "2026-05-19T13:31:50.000Z",
"data": {
"id": "",
"orderNumber": "M-0042",
"customerName": "Jane Doe",
"phone": "+447700900123",
"email": "jane@example.com",
"address": {
"line1": "",
"city": "London",
"full": "221B Baker Street, London",
"country": "",
"latitude": null,
"longitude": null
},
"driver": "Marcus Lane",
"round": "1747645200000",
"eta": "16:47 (12 minutes)"
}
}{
"event": "round.completed",
"timestamp": "2026-05-19T15:02:00.000Z",
"data": {
"round": "1747645200000",
"driver": "Marcus Lane",
"stops": 5
}
}orderNumber or round so a repeated delivery is harmless.If you set a webhook secret in Settings, Routella signs each request. The X-Webhook-Signature header holds an HMAC-SHA256 hex digest of the raw request body, keyed with your secret. Recompute it and compare to confirm the request really came from Routella.
// Node.js — verify the X-Webhook-Signature header.
// Use the RAW request body, not a re-serialized JSON object.
const crypto = require("crypto");
function verifyRoutellaWebhook(rawBody, signatureHeader, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody, "utf8")
.digest("hex");
// Constant-time compare to avoid timing attacks.
const a = Buffer.from(expected, "hex");
const b = Buffer.from(signatureHeader || "", "hex");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Express example (raw body required):
// app.post("/webhooks/routella",
// express.raw({ type: "application/json" }),
// (req, res) => {
// const ok = verifyRoutellaWebhook(
// req.body, // Buffer — the raw body
// req.get("X-Webhook-Signature"),
// process.env.ROUTELLA_WEBHOOK_SECRET
// );
// if (!ok) return res.status(401).send("bad signature");
// const event = JSON.parse(req.body.toString("utf8"));
// // ... handle event.event / event.data ...
// res.sendStatus(200);
// });Errors come back with the matching HTTP status code and a JSON body containing an error string. Validation errors also include a fields object naming each problem.
unauthorizedThe API key is missing, malformed, or invalid. Check the Authorization header.
HTTP/1.1 401 Unauthorized
{ "error": "unauthorized" }validation_failedOne or more body fields are missing or invalid. The fields object names each problem.
HTTP/1.1 400 Bad Request
{
"error": "validation_failed",
"fields": {
"customerName": "required",
"address": "required"
}
}quota_exceededThe free-plan monthly order cap was reached. Upgrade the plan to keep creating orders.
HTTP/1.1 402 Payment Required
{ "error": "quota_exceeded" }not_foundNo order with that id exists for this merchant.
HTTP/1.1 404 Not Found
{ "error": "not_found" }