REST API · v1

Routella API for developers

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.

Server-to-server Bearer key auth Delivery webhooks
Base URLhttps://routella.appAll endpoints are relative to this host and return JSON.

Quickstart

New here? These five steps take you from zero to a live order with delivery updates flowing back to your own server.

  1. 1Create a Routella accountSign up and open the dashboard. The API and webhooks are free on every plan.
  2. 2Generate an API keyGo to Settings → Integrations → API access and select Generate API key. Copy it — the full key is shown only once.
  3. 3Send your first orderFrom your server, POST an order to /api/v1/orders with the key in the Authorization header. Only customerName and address are required.
  4. 4See it in your dashboardThe order appears in Routella right away, ready to drop into a delivery round.
  5. 5Receive events backAdd your server URL under Settings → Integrations → Webhooks and tick the events you care about. Routella POSTs you as the delivery moves.
Your first order — cURL
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.

Three ways to connect

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.

You → Routella

Push orders in

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 → You

Receive webhooks back

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.

Routella ← You

Let Routella pull (Custom API)

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.

Get your API key

Every API request is authenticated with a secret key tied to your Routella account. You generate one inside the dashboard.

  1. Open the dashboard and go to Settings → API.
  2. Select Generate API key. The key looks like rtl_3f9c1a7b2e8d4056af1c9b3e7d2a6f08 the prefix rtl_ followed by a hex string.
  3. Copy the key right away. The full key is shown only once. If you lose it, generate a new one and update your integration.
Keep the key on your server. Treat it like a password. Never put it in browser code, mobile apps, or any place a customer could see it. Anyone with the key can create and read your orders.

Authentication

Send your API key on every request in the Authorization header as a Bearer token:

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

Create an order

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.

POST/api/v1/orders
Request body fields
FieldTypeRequirementDescription
customerNamestringRequiredName of the person receiving the order.
addressstringRequiredStreet address. Routella geocodes this if no coordinates are supplied.
citystringOptionalCity or town.
phonestringOptionalCustomer phone number. Recommended so the driver and notifications can reach them.
emailstringOptionalCustomer email address.
itemsarrayOptionalLine items. Each item is an object with title, quantity, price, and sku.
totalnumberOptionalOrder total.
currencystringOptionalISO currency code, for example GBP, USD, or EUR.
notestringOptionalFree-text delivery note shown to the driver.
paymentMethodstringOptionalOne of online, cash, card, check, transfer. Use cash for cash-on-delivery.
codAmountnumberOptionalAmount the driver must collect on delivery (cash-on-delivery).
countrystringOptionalCountry name.
countryCodestringOptionalTwo-letter country code.
stopTypestringOptionaldelivery or pickup. Defaults to delivery.
latitudenumberOptionalOptional. Skip geocoding by supplying coordinates directly.
longitudenumberOptionalOptional. Supply together with latitude.
packageCountnumberOptionalNumber of packages in the order.
deliveryMethodstringOptionalDelivery method label for the order.
discountAmountnumberOptionalDiscount applied to the order.
Example request — cURL
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"
  }'
Example request — Node.js fetch
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);
Example response
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"
  }
}
On success the API returns HTTP 201 with the created order. A free plan that has hit its monthly order cap returns 402 quota_exceeded — see the Errors section below.

Pickup + delivery jobs

Use two-stop mode when the same job must be collected from one place and delivered to another. Send exactly two ordered stops: pickup first, then delivery. This is separate from the legacy single-stop request above.

POST/api/v1/orders

One job, two physical stops

The pickup and delivery stay in one route with one driver. Pickup must remain before delivery, although unrelated stops may sit between them.

One quota item and create event

The parent counts as one order against quota and the first create sends one order.created webhook.

Collection event once

The first successful collection sends order.collected once. Repeated taps, undo, and collecting again do not send another collection event.

Delivery-only effects

Only delivery can send customer messages, expose the customer tracking link, collect POD or COD, fulfill the store order, update the commerce provider, or send delivery lifecycle events.

Required headers
Authorization: Bearer rtl_3f9c1a7b2e8d4056af1c9b3e7d2a6f08
Idempotency-Key: shopify-order-12345-pickup-delivery
Content-Type: application/json

Idempotency-Key is required in two-stop mode. It must already be trimmed, contain 1 to 200 visible characters, and contain no control or invisible characters. The first successful create returns HTTP 201. Sending the same normalized request with the same key returns HTTP 200 and the header Idempotent-Replay: 1. Reusing the key with a different request returns HTTP 409 idempotency_conflict.

Parent job fields
FieldTypeRequirementDescription
stopsarrayRequiredExactly two ordered objects: pickup first, delivery second. Its presence selects pickup-delivery mode.
externalIdstringOptionalYour account-visible reference, up to 200 characters.
itemsarrayOptionalParent line items. Each item may use title, quantity, price, sku, and variantTitle.
totalnumberOptionalParent order total before discountAmount is subtracted.
currencystringOptionalCurrency code. When omitted, the account currency is used.
notestringOptionalParent job note.
paymentMethodstringOptionalonline, cash, card, check, or transfer.
codAmountnumberOptionalDelivery-only cash amount. It is used only when paymentMethod is cash.
packageCountnumberOptionalNumber of packages on the one parent job.
deliveryMethodstringOptionalDelivery method label on the parent job.
discountAmountnumberOptionalNon-negative discount subtracted from total.
Fields inside each stop
FieldTypeRequirementDescription
typestringRequiredpickup for stops[0]; delivery for stops[1].
addressobjectOptionalRequired for delivery and for a custom pickup. A pickup must use address or warehouseId, never both.
warehouseIdstringOptionalPickup alternative only: a 24-character id for an active warehouse owned by this API account.
address.line1stringOptionalRequired inside every custom address.
address.line2stringOptionalOptional second address line.
address.citystringOptionalOptional city or town.
address.regionstringOptionalOptional region, county, or state.
address.postalCodestringOptionalOptional postal or ZIP code.
address.countrystringOptionalOptional country name.
address.countryCodestringOptionalOptional country code; Routella stores it in uppercase.
address.latitudenumberOptionalOptional finite latitude from -90 through 90. Supply it together with longitude.
address.longitudenumberOptionalOptional finite longitude from -180 through 180. Supply it together with latitude.
contact.namestringOptionalRequired for delivery; optional for pickup.
contact.phonestringOptionalOptional stop contact phone.
contact.emailstringOptionalOptional stop contact email.
instructionsstringOptionalOptional instructions for this physical stop.
serviceMinutesintegerOptionalWhole minutes from 0 through 180. Defaults to 10.

Custom pickup and custom delivery

This complete request uses a custom address for both stops. The coordinates are optional; omit both latitude and longitude when Routella should geocode an address.

Request JSON
{
  "externalId": "shopify-order-12345",
  "items": [
    {
      "title": "Parcel",
      "quantity": 1,
      "price": 42.5,
      "sku": "PARCEL-1"
    }
  ],
  "total": 42.5,
  "currency": "GBP",
  "paymentMethod": "online",
  "packageCount": 1,
  "note": "Shopify order #12345",
  "stops": [
    {
      "type": "pickup",
      "address": {
        "line1": "10 Collection Road",
        "line2": "Unit 2",
        "city": "Manchester",
        "region": "Greater Manchester",
        "postalCode": "M1 1AA",
        "country": "United Kingdom",
        "countryCode": "GB",
        "latitude": 53.4808,
        "longitude": -2.2426
      },
      "contact": {
        "name": "Collection contact",
        "phone": "+441234567890",
        "email": "collection@example.com"
      },
      "instructions": "Ring the bell at the side entrance",
      "serviceMinutes": 10
    },
    {
      "type": "delivery",
      "address": {
        "line1": "25 Delivery Street",
        "city": "London",
        "postalCode": "SW1A 1AA",
        "country": "United Kingdom",
        "countryCode": "GB",
        "latitude": 51.501,
        "longitude": -0.1416
      },
      "contact": {
        "name": "Delivery recipient",
        "phone": "+449876543210",
        "email": "recipient@example.com"
      },
      "instructions": "Leave with reception",
      "serviceMinutes": 10
    }
  ]
}
Copy-paste request — cURL
cat > pickup-delivery.json <<'JSON'
{
  "externalId": "shopify-order-12345",
  "items": [
    {
      "title": "Parcel",
      "quantity": 1,
      "price": 42.5,
      "sku": "PARCEL-1"
    }
  ],
  "total": 42.5,
  "currency": "GBP",
  "paymentMethod": "online",
  "packageCount": 1,
  "note": "Shopify order #12345",
  "stops": [
    {
      "type": "pickup",
      "address": {
        "line1": "10 Collection Road",
        "line2": "Unit 2",
        "city": "Manchester",
        "region": "Greater Manchester",
        "postalCode": "M1 1AA",
        "country": "United Kingdom",
        "countryCode": "GB",
        "latitude": 53.4808,
        "longitude": -2.2426
      },
      "contact": {
        "name": "Collection contact",
        "phone": "+441234567890",
        "email": "collection@example.com"
      },
      "instructions": "Ring the bell at the side entrance",
      "serviceMinutes": 10
    },
    {
      "type": "delivery",
      "address": {
        "line1": "25 Delivery Street",
        "city": "London",
        "postalCode": "SW1A 1AA",
        "country": "United Kingdom",
        "countryCode": "GB",
        "latitude": 51.501,
        "longitude": -0.1416
      },
      "contact": {
        "name": "Delivery recipient",
        "phone": "+449876543210",
        "email": "recipient@example.com"
      },
      "instructions": "Leave with reception",
      "serviceMinutes": 10
    }
  ]
}
JSON

curl -X POST https://routella.app/api/v1/orders \
  -H "Authorization: Bearer rtl_3f9c1a7b2e8d4056af1c9b3e7d2a6f08" \
  -H "Idempotency-Key: shopify-order-12345-pickup-delivery" \
  -H "Content-Type: application/json" \
  --data-binary @pickup-delivery.json
Copy-paste request — Node.js fetch
async function main() {
  const pickupDeliveryJob = {
  "externalId": "shopify-order-12345",
  "items": [
    {
      "title": "Parcel",
      "quantity": 1,
      "price": 42.5,
      "sku": "PARCEL-1"
    }
  ],
  "total": 42.5,
  "currency": "GBP",
  "paymentMethod": "online",
  "packageCount": 1,
  "note": "Shopify order #12345",
  "stops": [
    {
      "type": "pickup",
      "address": {
        "line1": "10 Collection Road",
        "line2": "Unit 2",
        "city": "Manchester",
        "region": "Greater Manchester",
        "postalCode": "M1 1AA",
        "country": "United Kingdom",
        "countryCode": "GB",
        "latitude": 53.4808,
        "longitude": -2.2426
      },
      "contact": {
        "name": "Collection contact",
        "phone": "+441234567890",
        "email": "collection@example.com"
      },
      "instructions": "Ring the bell at the side entrance",
      "serviceMinutes": 10
    },
    {
      "type": "delivery",
      "address": {
        "line1": "25 Delivery Street",
        "city": "London",
        "postalCode": "SW1A 1AA",
        "country": "United Kingdom",
        "countryCode": "GB",
        "latitude": 51.501,
        "longitude": -0.1416
      },
      "contact": {
        "name": "Delivery recipient",
        "phone": "+449876543210",
        "email": "recipient@example.com"
      },
      "instructions": "Leave with reception",
      "serviceMinutes": 10
    }
  ]
};

  const res = await fetch("https://routella.app/api/v1/orders", {
    method: "POST",
    headers: {
      "Authorization": "Bearer rtl_3f9c1a7b2e8d4056af1c9b3e7d2a6f08",
      "Idempotency-Key": "shopify-order-12345-pickup-delivery",
      "Content-Type": "application/json",
    },
    body: JSON.stringify(pickupDeliveryJob),
  });

  const payload = await res.json();
  if (!res.ok) {
    throw new Error("Routella API error: " + payload.error);
  }

  console.log(res.status, payload.order.id, payload.order.stops);
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

Active warehouse pickup and custom delivery

Replace the example warehouseId with the 24-character id of an active warehouse owned by the same Routella account as the API key. Routella copies that warehouse address and coordinates into the job when it is created. A malformed, missing, inactive, or different-account warehouse returns the same private HTTP 404 warehouse_not_found response.

Request JSON
{
  "externalId": "shopify-order-12346",
  "items": [
    {
      "title": "Parcel",
      "quantity": 1,
      "price": 25,
      "sku": "PARCEL-2"
    }
  ],
  "total": 25,
  "currency": "GBP",
  "stops": [
    {
      "type": "pickup",
      "warehouseId": "66b0f40f1d6a0e0012abcdef",
      "instructions": "Collect from loading bay 2",
      "serviceMinutes": 10
    },
    {
      "type": "delivery",
      "address": {
        "line1": "25 Delivery Street",
        "city": "London",
        "postalCode": "SW1A 1AA",
        "country": "United Kingdom",
        "countryCode": "GB",
        "latitude": 51.501,
        "longitude": -0.1416
      },
      "contact": {
        "name": "Delivery recipient",
        "phone": "+449876543210",
        "email": "recipient@example.com"
      }
    }
  ]
}

Two-stop response

Two-stop orders add externalId, jobType, and the ordered stops array to the normal public order object. Each stop contains its public address, contact, instructions, service time, and completion state. The response never exposes the idempotency key, request hash, internal account id, warehouse record, or warehouseId.

Example response
{
  "order": {
    "id": "66c0f40f1d6a0e0012abcdef",
    "orderNumber": "M-0042",
    "status": "unfulfilled",
    "customerName": "Delivery recipient",
    "phone": "+449876543210",
    "email": "recipient@example.com",
    "address": {
      "line1": "25 Delivery Street",
      "city": "London",
      "full": "25 Delivery Street, London",
      "country": "United Kingdom",
      "latitude": 51.501,
      "longitude": -0.1416
    },
    "items": [
      {
        "title": "Parcel",
        "quantity": 1,
        "price": 42.5,
        "sku": "PARCEL-1",
        "variantTitle": ""
      }
    ],
    "total": 42.5,
    "currency": "GBP",
    "note": "Shopify order #12345",
    "paymentMethod": "online",
    "codAmount": 0,
    "stopType": "delivery",
    "delivery": {
      "status": "unfulfilled",
      "driverName": null,
      "roundId": null
    },
    "createdAt": "2026-08-15T09:14:22.000Z",
    "updatedAt": "2026-08-15T09:14:22.000Z",
    "externalId": "shopify-order-12345",
    "jobType": "pickup_delivery",
    "stops": [
      {
        "id": "66c0f40f1d6a0e0012abc001",
        "type": "pickup",
        "status": "pending",
        "addressSource": "custom",
        "address": {
          "line1": "10 Collection Road",
          "line2": "Unit 2",
          "city": "Manchester",
          "region": "Greater Manchester",
          "postalCode": "M1 1AA",
          "full": "10 Collection Road, Manchester",
          "country": "United Kingdom",
          "countryCode": "GB",
          "latitude": 53.4808,
          "longitude": -2.2426
        },
        "contact": {
          "name": "Collection contact",
          "phone": "+441234567890",
          "email": "collection@example.com"
        },
        "instructions": "Ring the bell at the side entrance",
        "serviceMinutes": 10,
        "completedAt": null
      },
      {
        "id": "66c0f40f1d6a0e0012abc002",
        "type": "delivery",
        "status": "pending",
        "addressSource": "custom",
        "address": {
          "line1": "25 Delivery Street",
          "line2": "",
          "city": "London",
          "region": "",
          "postalCode": "SW1A 1AA",
          "full": "25 Delivery Street, London",
          "country": "United Kingdom",
          "countryCode": "GB",
          "latitude": 51.501,
          "longitude": -0.1416
        },
        "contact": {
          "name": "Delivery recipient",
          "phone": "+449876543210",
          "email": "recipient@example.com"
        },
        "instructions": "Leave with reception",
        "serviceMinutes": 10,
        "completedAt": null
      }
    ]
  }
}

Validation and side-effect safety

  • A present stops property always selects two-stop mode. null, an empty array, or any length other than two is rejected instead of falling back to a legacy order.
  • stops[0].type must be pickup and stops[1].type must be delivery. Both entries must be objects.
  • A pickup uses exactly one location source: address or warehouseId. Delivery requires address and cannot use warehouseId.
  • Custom addresses require address.line1. Coordinates may be omitted for geocoding; when either coordinate is present, both must be finite numbers in range.
  • Delivery requires contact.name. Stop address fields belong under address and contact fields belong under contact.
  • serviceMinutes is optional, defaults to 10, and must be a whole number from 0 through 180.
  • Idempotency-Key must already be trimmed, contain 1 to 200 visible characters, and contain no control or invisible characters.
  • Validation, owned-warehouse lookup, quota checks, and geocoding finish before the write. A failure creates no order, uses no quota, and sends no webhook.
Legacy self-collection is different. Top-level stopType: "pickup" still means customer self-collection. It cannot be combined with stops.
Pickup details stay private. Public customer tracking may show only a generic collection milestone. It never exposes the pickup address, warehouse location, coordinates, contact, phone, email, instructions, internal stop key, ETA, or map marker.

List orders

Return your orders, newest first. Use the query parameters to page through results and filter by fulfillment status.

GET/api/v1/orders
Query parameters
ParameterTypeDescription
limitnumberHow many orders to return. 1 to 100. Defaults to 50.
statusstringFilter by order status: unfulfilled, fulfilled, or partial.
Example request — cURL
curl -X GET "https://routella.app/api/v1/orders?status=unfulfilled&limit=25" \
  -H "Authorization: Bearer rtl_3f9c1a7b2e8d4056af1c9b3e7d2a6f08"
Example response
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
}

Get an order

Fetch a single order by its id, including its current delivery status — the assigned driver and round, if any.

GET/api/v1/orders/{id}
Example request — cURL
curl -X GET https://routella.app/api/v1/orders/6711e2a4c9d3f80012ab34cd \
  -H "Authorization: Bearer rtl_3f9c1a7b2e8d4056af1c9b3e7d2a6f08"
Example response
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"
  }
}
If no order with that id belongs to your account, the API returns 404 not_found.

The order object

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. A pickup-delivery job always includes externalId as a string (empty when omitted), jobType set to pickup_delivery, and its two ordered public stops.

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

Webhooks

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.

The envelope

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": { ... }
}
Events

The Payload column says which data shape the event ships — see the two reference tables below.

EventPayloadWhen it fires
order.createdorderA new order was created — for example through POST /api/v1/orders.
order.assignedorderThe order was added to a delivery round.
order.collectedorderThe order’s items were collected from its custom pickup or warehouse pickup location.
order.pickedorderThe order was picked up — by the driver, or handed to the delivery company.
order.dispatchedorderThe order is out for delivery and on its way to the customer.
order.arrivingorder + etaThe driver is approaching the customer. The payload carries an estimated arrival time.
order.deliveredorderThe order was delivered to the customer.
order.missedorderA delivery attempt failed — no answer, wrong address, refused, and so on.
order.feedback_requestedorderA review request was sent to the customer after delivery.
round.createdroundA delivery round was created.
round.completedroundEvery stop in a delivery round reached a final state — the round is finished.
Order event payload

Every order.* event carries the base data object below. The eta field is present only on order.arriving. For a pickup-delivery job, externalId, jobType, and the two ordered stops are also present.

FieldTypeDescription
idstringRoutella order id. Pickup-delivery events raised from a saved route use the saved parent order id. For other event sources, this is an empty string only when no saved order id is available; then orderNumber identifies the order.
orderNumberstringThe order number, for example M-0042.
customerNamestringName of the person receiving the order.
phonestringCustomer phone number, or an empty string.
emailstringCustomer email address, or an empty string.
addressobjectDelivery address, or null. Holds line1, city, full, country, latitude, and longitude. Some fields may be empty depending on where the order came from.
driverstringName of the assigned driver, or an empty string if no driver is assigned yet.
roundstringId of the delivery round, or an empty string if the order is not in a round yet.
etastringHuman-readable estimated arrival, for example "16:47 (12 minutes)". Included only on order.arriving.
externalIdstringIncluded only for a pickup-delivery job. The caller’s external reference, or an empty string when none was supplied.
jobTypestringIncluded only for a pickup-delivery job. Its value is pickup_delivery.
stopsarrayIncluded only for a pickup-delivery job. The two public physical stops stay ordered: pickup first, delivery second.
data — order events
{
  "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"
}
Round event payload

The two round.* events carry a short round summary instead of an order.

FieldTypeDescription
roundstringId of the delivery round.
driverstringName of the round’s driver, or an empty string.
stopsnumberNumber of stops (orders) in the round.
data — round events
{
  "round": "1747645200000",
  "driver": "Marcus Lane",
  "stops": 5
}
Example — order.created
{
  "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": ""
  }
}
Example — order.arriving (carries an ETA)
{
  "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)"
  }
}
Example — round.completed
{
  "event": "round.completed",
  "timestamp": "2026-05-19T15:02:00.000Z",
  "data": {
    "round": "1747645200000",
    "driver": "Marcus Lane",
    "stops": 5
  }
}
Routella delivers each webhook as soon as the event happens, and retries transient failures — timeouts, 429, and 5xx responses. A brief blip is retried within seconds; if your endpoint stays unreachable, Routella keeps retrying with backoff for up to 24 hours before dropping the event. Treat your handler as idempotent anyway: return a 2xx quickly and key off orderNumber or round so a repeated delivery is harmless.
Verifying the signature

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

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.

400idempotency_key_required

A request with a stops property must include Idempotency-Key.

HTTP/1.1 400 Bad Request

{ "error": "idempotency_key_required" }
400idempotency_key_invalid

The idempotency key is empty, too long, untrimmed, or contains an invisible or control character.

HTTP/1.1 400 Bad Request

{ "error": "idempotency_key_invalid" }
400validation_failed

One or more request fields are missing, ambiguous, or invalid. The fields object names each problem.

HTTP/1.1 400 Bad Request

{
  "error": "validation_failed",
  "fields": {
    "stops.0.address.line1": "required"
  }
}
401unauthorized

The API key is missing, malformed, invalid, or revoked. Check the Authorization header.

HTTP/1.1 401 Unauthorized

{
  "error": "unauthorized",
  "message": "Missing API key. Send the header: Authorization: Bearer <your key>"
}
402quota_exceeded

The monthly order cap was reached. The response also includes a plan-specific message explaining the current cap; that message can change with the account plan and usage. Treat quota_exceeded as the stable code. The failed request creates no order and uses no quota.

HTTP/1.1 402 Payment Required

{
  "error": "quota_exceeded",
  "message": "<plan-specific message>"
}
403account_suspended

The API-key owner is suspended or no longer open for API access.

HTTP/1.1 403 Forbidden

{
  "error": "account_suspended",
  "message": "This account is suspended."
}
404warehouse_not_found

The warehouse id is malformed, missing, inactive, or belongs to another account. All four cases use the same private response.

HTTP/1.1 404 Not Found

{
  "error": "warehouse_not_found",
  "fields": {
    "stops.0.warehouseId": "not_found"
  }
}
404not_found

GET could not find that order inside the authenticated account.

HTTP/1.1 404 Not Found

{
  "error": "not_found",
  "message": "No order with that id."
}
405method_not_allowed

The endpoint does not accept that HTTP method. The Allow response header lists GET and POST for the collection, or GET for one order.

HTTP/1.1 405 Method Not Allowed

{ "error": "method_not_allowed" }
409idempotency_conflict

The same account and Idempotency-Key were already used with different request data.

HTTP/1.1 409 Conflict

{ "error": "idempotency_conflict" }
409account_deletion_in_progress

Account deletion began after authentication and before the order write could finish.

HTTP/1.1 409 Conflict

{ "error": "account_deletion_in_progress" }
422stop_geocode_failed

A custom address or warehouse snapshot has no usable coordinates. The fields object identifies the failed stop.

HTTP/1.1 422 Unprocessable Entity

{
  "error": "stop_geocode_failed",
  "fields": {
    "stops.1.address": "unusable"
  }
}
500server_error

An unexpected create, list, or get failure occurred. Retry only with the same Idempotency-Key and body.

HTTP/1.1 500 Internal Server Error

{
  "error": "server_error",
  "message": "Failed to create the order."
}
Built for your backend

The Routella API is server-to-server

Call it from your own backend, never from a browser or mobile app. Your API key is a secret — keep it server-side and rotate it from Settings → API if it is ever exposed.

View pricing