View Categories

Launching soon on the Shopify App StoreAbout Delivery RoutingJoin the waitlist

REST API reference: Delivery Routing

16 min read

The REST API v1 endpoints of SI Delivery Routing: read and change your stops and vehicles, plan and send a day, read routes, proof of delivery and reports, and get a signed webhook when a stop changes status. The key, limits, pagination and error format are the same as for all our apps: see the API overview. The same operations are in GraphQL: GraphQL API reference: Delivery Routing.

Needs the SI Delivery Routing app. It works on a store without Rentals & Bookings. On a store without Delivery Routing, every endpoint here answers 403 with the code rentals_rest_app_not_installed and data.app: "routes".

Keys, plans and conventions #

Make a key in Delivery Routing under Settings › API. Keys belong to the store: a key made in Rentals & Bookings or in any other of our apps works here too. GET needs a read key; everything else needs a key with Allow changes (write). A read key gets 403 rentals_rest_forbidden on a write.

The API is on every plan. Three things need a higher plan and answer 403 rentals_rest_plan_required (with data.feature and data.required_plan) below it:

FeaturePlanEndpoints
Plan a day, accept a planRegular and upPOST /days/{date}/plan, POST /plans/{id}/accept
Route reportPro and upGET /reports/routes
The day’s CSVPro and upGET /days/{date}/export

The examples use two shell variables, API="https://YOUR-API-ADDRESS/api/v1" and KEY="sirb_your_secret_key". Every path starts with /api/v1/routes.

  • Days are YYYY-MM-DD in the store’s time zone, times are HH:MM, and timestamps (at, sent_at) are ISO 8601 in UTC.
  • location takes a Shopify location id (801) or its gid (gid://shopify/Location/801). Without it, the store’s first depot is used.
  • Request bodies are JSON with snake_case names (service_min, shift_from); camelCase names work too.
  • Lists of stops are paged with page and per_page (at most 200); the totals are in the X-Total and X-Total-Pages headers. The other lists come whole.

Stops #

A stop is one delivery or collection at one address on one day, made from an order (source: "order"), a rental booking ("rental") or added by hand ("manual"). A later order to the same address and day joins the stop, so orders can list several orders.

statusMeaning
needs_dayThe order has no delivery day yet.
unplannedIt has a day, but it isn’t on a saved route.
plannedOn a saved route that isn’t sent yet.
sentOn a route sent to the driver.
on_the_wayThe driver is on the way to it.
arrivedThe driver is there.
doneDelivered or collected.
failedThe driver couldn’t complete it: fail_reason says why.
cancelledThe order was cancelled, or the stop was added by hand and deleted.

GET /api/v1/routes/stops #

Stops without a day first, then by day and slot. Paged. Cancelled stops come only when you ask for status=cancelled.

ParameterNotes
dateOne day, YYYY-MM-DD.
statusOne status or several, comma-separated: needs_day,unplanned.
locationA depot (Shopify location).
sourceorder, rental or manual.
qAn order name (#1042) or a postcode.
needs_daytrue: only stops that still need a day.
unroutedtrue: only stops that aren’t on a route.
curl -s "$API/routes/stops?date=2026-11-20&per_page=50" -H "Authorization: Bearer $KEY"
# 200, X-Total: 22, X-Total-Pages: 1
[{"id": 10, "source": "order", "kind": "delivery", "status": "sent", "date": "2026-11-20", "from": "08:00", "to": "12:00",
  "day_source": "picker", "order_gid": "gid://shopify/Order/5550001", "order_id": 5550001, "order_name": "#1042",
  "orders": [{"gid": "gid://shopify/Order/5550001", "id": 5550001, "name": "#1042"}],
  "location_gid": "gid://shopify/Location/801", "area_id": 1, "name": "Jordan Lee",
  "address_line": "18 Elm Street, Columbus 43215", "zip": "43215", "units": 1, "kg": 62, "m3": 0, "service_min": 10,
  "geo": "shopify", "needs_pin": false, "added_by_hand": false,
  "route": {"id": 1, "name": "Box truck 1", "status": "sent"},
  "vehicle": {"key": "v1", "name": "Box truck 1", "colour": "#2563eb"},
  "eta": {"eta": "09:15", "from": "09:00", "to": "09:30"},
  "arrived_at": null, "done_at": null, "attempts": 0, "fail_reason": null,
  "address": {"address1": "18 Elm Street", "city": "Columbus", "province": "Ohio", "province_code": "OH", "zip": "43215",
              "country": "United States", "country_code": "US"},
  "phone": "6145550100", "email": "[email protected]", "instructions": null, "note": null, "lat": 39.9612, "lng": -82.9988,
  "items": [{"kg": 62, "qty": 1, "line": "gid://shopify/LineItem/13002", "name": "3-seat sofa", "order": "#1042"}]}]
FieldNotes
kinddelivery or collection.
from, toThe time slot the customer has; null when there is none.
day_sourceWho set the day, for example picker (the customer, at checkout), staff or rental; null without a day.
units, kg, m3The load, from the items. service_min is the minutes spent at the stop.
geo, needs_pinWhere the map position came from (shopify: the order’s address, pin: set by hand); needs_pin: true when the address couldn’t be placed and needs a pin.
route, vehicle, etaThe route the stop is on, its vehicle and the arrival estimate with the window the customer was told; null when it isn’t on a route.
arrived_at, done_at, attempts, fail_reasonWhat the driver recorded.
itemsThe order lines to deliver or collect.

GET /api/v1/routes/stops/{id} #

One stop: the fields above plus events, its history, oldest first: [{at, actor, action, data}]. actor says who did it: shopify (an order change), staff:<id>, api:<key prefix> for a change through the API, or the driver, the customer or the app. 404 rentals_rest_not_found (“No such stop.”) otherwise.

curl -s "$API/routes/stops/10" -H "Authorization: Bearer $KEY"
# 200 (trimmed)
{"id": 10, "status": "sent", "date": "2026-11-20", "name": "Jordan Lee", "…": "…",
 "events": [{"at": "2026-11-05T22:14:45+00:00", "actor": "shopify", "action": "created", "data": {"via": "customer", "order": "#1042"}},
            {"at": "2026-11-05T22:43:18+00:00", "actor": "staff:7001", "action": "planned", "data": {"route": "Box truck 1", "from": "09:00", "to": "09:30"}}]}

GET /api/v1/routes/stops/{id}/pod #

The proof of delivery: who signed, the driver’s note, photos and the signature. The image links work for a limited time without a key. null when the driver recorded none.

curl -s "$API/routes/stops/1/pod" -H "Authorization: Bearer $KEY"
# 200
{"signed_by": "Jordan Lee", "note": null, "at": "2026-11-05T22:44:34+00:00", "lat": null, "lng": null,
 "photos": [{"url": "https://YOUR-API-ADDRESS/a/routes/pod/01m6…?expires=…&signature=…", "taken_at": "2026-11-05T22:44:34+00:00"}],
 "signature_url": "https://YOUR-API-ADDRESS/a/routes/pod/01m6…?expires=…&signature=…"}

POST /api/v1/routes/stops #

Adds a stop by hand (source: "manual"). name and address.address1 are required. Answers 201 with the stop, as GET /stops/{id} does.

Body fieldNotes
nameWho the stop is for. Required.
address{company, address1, address2, city, province, zip, country, country_code}; address1 is required.
dateYYYY-MM-DD. Without it the stop needs a day.
from, toA time slot, HH:MM, both or neither.
kinddelivery (default) or collection.
location_idThe depot it leaves from.
phone, emailThe contact the driver and the messages use.
instructionsFor the driver (gate code, floor). note is for your team.
units, kg, m3, service_minThe load and the minutes at the stop.
lat, lngA map pin, when the address alone isn’t enough.
curl -s -X POST "$API/routes/stops" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"name": "Jordan Lee", "address": {"address1": "18 Elm Street", "city": "Columbus", "zip": "43215", "country_code": "US"}, "date": "2026-11-20", "from": "08:00", "to": "12:00", "kg": 40}'
# 201: the stop
# 400, without a name:
{"code": "rentals_rest_invalid", "message": "Who is the stop for?", "data": {"field": "name", "status": 400}}

PATCH or PUT /api/v1/routes/stops/{id} #

Changes a stop; only the fields you send change. Any stop can change its date, from and to, instructions, note and its pin (lat, lng); "date": null puts it back to needing a day. A stop added by hand can also change its name, contact, address and load. Answers the stop.

Refusals: 400 rentals_rest_not_movable (the stop is already with a driver or finished), 400 rentals_rest_rental_day (a rental’s delivery or collection day is changed in Rentals & Bookings), 400 rentals_rest_invalid with data.field.

curl -s -X PATCH "$API/routes/stops/10" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"date": "2026-11-21", "from": "12:00", "to": "16:00", "instructions": "Side door, ring twice"}'

DELETE /api/v1/routes/stops/{id} #

Deletes a stop added by hand: it becomes cancelled and keeps its history. Answers the stop. A stop made from an order follows its order (cancel or edit the order in Shopify), and a stop already with a driver is part of the day’s record: both answer 400 rentals_rest_not_deletable.

Routes and the day’s plan #

A route is one vehicle’s run on one day: its stops in order, with arrival estimates. The day is planned by the app’s planner, the plan is accepted as the day’s routes, and the routes are sent to the drivers. These are the same steps as Plan the day and Send to drivers in the app.

Route statusMeaning
draftSaved, not sent.
sentSent to the driver.
startedThe driver started it.
endedFinished.

GET /api/v1/routes/routes #

A day’s routes with their stops in order. Parameters: date (default: today), location, status (comma-separated).

curl -s "$API/routes/routes?date=2026-11-20" -H "Authorization: Bearer $KEY"
# 200
[{"id": 1, "date": "2026-11-20", "location_gid": "gid://shopify/Location/801", "name": "Box truck 1", "status": "sent",
  "version": 5, "position": 1, "vehicle": {"key": "v1", "name": "Box truck 1", "colour": "#2563eb"},
  "driver": {"id": 3, "name": "Sam Rivera"}, "km": 76.47, "drive_min": 131, "sent_at": "2026-11-19T22:43:21+00:00",
  "stops": [{"stop_id": 3, "position": 1, "status": "sent", "eta": "07:39", "from": "07:25", "to": "07:55"},
            {"stop_id": 10, "position": 2, "status": "sent", "eta": "09:15", "from": "09:00", "to": "09:30"}]}]

GET /api/v1/routes/routes/{id} #

One route, as above. 404 rentals_rest_not_found otherwise.

POST /api/v1/routes/days/{date}/plan #

Plans the day: the planner shares the day’s stops among the vehicles, in order, within their shifts and loads. Nothing changes until you accept the plan. Body (optional): location, and locks to fix stops in place: {"<stop id>": {"vehicle": "v1", "position": 1, "time": "09:00"}}. Regular plan and up.

curl -s -X POST "$API/routes/days/2026-11-20/plan" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{}'
# 200
{"id": 44, "date": "2026-11-20", "location_gid": "gid://shopify/Location/801", "version": 5, "travel": 7860, "planner_note": null,
 "routes": [{"vehicle_key": "v1", "km": 76.47, "drive_min": 131,
             "stops": [{"stop_id": 3, "eta": "07:39", "from": null, "to": null}, {"stop_id": 10, "eta": "09:15", "from": null, "to": null}]}],
 "unassigned": [],
 "totals": {"vehicles": 1, "placed": 2, "km": 76.47, "drive_min": 131}}

travel is the total driving time in seconds. unassigned lists the stops the planner couldn’t place, each with stop_id, reason and message; plan again after changing them, or lock them. Refusals: 409 rentals_rest_not_ready (“There are no stops to plan on this day.”), 400 rentals_rest_invalid_param for a date that isn’t a day.

POST /api/v1/routes/plans/{id}/accept #

Saves a plan as the day’s routes. Body (optional): version, the day’s version when it was planned (the default). If the day changed since (a new order, a stop moved), the answer is 409 rentals_rest_version_conflict: plan it again. Regular plan and up.

curl -s -X POST "$API/routes/plans/44/accept" -H "Authorization: Bearer $KEY"
# 200
{"ok": true, "plan_id": 44, "date": "2026-11-20", "location_gid": "gid://shopify/Location/801", "version": 6, "routes": [{"id": 1, "…": "…"}]}

404 rentals_rest_not_found (“No such plan.”), 409 rentals_rest_not_ready when the plan has no proposal.

POST /api/v1/routes/days/{date}/send #

Sends the day’s saved routes to the drivers (on every plan). Body (optional): location. Answers {"ok": true, "date", "location_gid", "sent", "version", "routes"}.

curl -s -X POST "$API/routes/days/2026-11-20/send" -H "Authorization: Bearer $KEY"
# 409, nothing saved yet:
{"code": "rentals_rest_not_ready", "message": "There are no saved routes to send. Accept a plan first.", "data": {"status": 409}}

GET /api/v1/routes/days/{date}/export #

The day’s stops as the CSV the Schedule page downloads, with the columns ID, Name, Address, Latitude, Longitude, From, To, Duration, Notes, Phone, Email, Types, Load, Order, Items. With format=json (or Accept: application/json) the same rows come as objects keyed by those column names, all values as strings. Parameter: location. Pro plan and up.

curl -s "$API/routes/days/2026-11-20/export?format=json" -H "Authorization: Bearer $KEY"
# 200
[{"ID": "3", "Name": "Jordan Lee #1042", "Address": "18 Elm Street, Columbus 43215, United States", "Latitude": "39.9612",
  "Longitude": "-82.9988", "From": "", "To": "", "Duration": "10", "Notes": "Deliver", "Phone": "6145550100",
  "Email": "[email protected]", "Types": "Box truck 1", "Load": "1", "Order": "#1042", "Items": "3-seat sofa × 1"}]

GET /api/v1/routes/reports/routes #

The route report per day and vehicle for up to 62 days (default: the last 7): stops, done, failed, km, minutes driving and on the road, stops per hour and the share on time. Parameters: from, to, location; format=csv (or Accept: text/csv) gives a CSV file. Pro plan and up.

curl -s "$API/routes/reports/routes?from=2026-11-01&to=2026-11-10" -H "Authorization: Bearer $KEY"
# 200
{"from": "2026-11-01", "to": "2026-11-10", "days": 10,
 "rows": [{"date": "2026-11-07", "vehicle": "Box truck 1", "stops": 7, "done": 6, "failed": 1, "km": 76.5, "driving_min": 131,
           "on_road_min": 260, "stops_per_hour": 1.6, "on_time_pct": 100}],
 "total": {"date": "", "vehicle": "All vehicles", "stops": 7, "done": 6, "failed": 1, "km": 76.5, "driving_min": 131,
           "on_road_min": 260, "stops_per_hour": 1.6, "on_time_pct": 100}}

Vehicles #

GET /api/v1/routes/vehicles #

The vehicles, in the order you keep them. active=true leaves out the inactive ones. source is own (made in Delivery Routing) or core (a vehicle from Rentals & Bookings › Staff & resources, changed there).

curl -s "$API/routes/vehicles" -H "Authorization: Bearer $KEY"
# 200
[{"id": 1, "key": "v1", "name": "Box truck 1", "colour": "#2563eb", "location_gid": "gid://shopify/Location/801",
  "units": 10, "kg": 1500, "m3": null, "features": [], "shift_from": "07:30", "shift_to": "16:00", "active": true, "source": "own"}]

GET /api/v1/routes/vehicles/{id} #

One vehicle. 404 rentals_rest_not_found otherwise.

POST /api/v1/routes/vehicles #

Adds a vehicle. Answers 201 with the vehicle.

Body fieldNotes
nameRequired.
colourA hex colour for the map, #2563eb.
location_idThe depot it starts from.
units, kg, m3What it can carry; leave one out for no limit.
featuresTags the planner matches with stops (for example lift).
shift_from, shift_toIts working hours, HH:MM.
activefalse keeps it out of planning.
curl -s -X POST "$API/routes/vehicles" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"name": "Van 3", "colour": "#7c3aed", "units": 8, "kg": 900, "shift_from": "08:00", "shift_to": "17:00"}'

Refusals: 400 rentals_rest_invalid (“Name the vehicle.”, data.field: "name"), 403 rentals_rest_vehicle_limit at your plan’s number of vehicles (Free 1, Regular 2, Pro 5, Ultimate no limit).

PATCH or PUT /api/v1/routes/vehicles/{id} #

Changes a vehicle; fields you don’t send keep their values. A vehicle from Rentals & Bookings answers 400 rentals_rest_managed_in_rentals.

DELETE /api/v1/routes/vehicles/{id} #

Deletes a vehicle: {"ok": true}. 400 rentals_rest_vehicle_in_use while it is on a route that isn’t finished.

Drivers, areas, depots, settings and slots #

Read only. Drivers, areas and settings are changed in the app.

GET /api/v1/routes/drivers #

The drivers: [{id, name, vehicle_key, active}]. Never an email, a phone number or a sign-in link.

GET /api/v1/routes/areas #

The delivery areas: [{id, name, public_name, kind, postcodes, radius_km, lat, lng, location_gid, days, window_list, day_cap, fee_hint, active, position}]. An area is a list of postcodes (ranges like 43201-43229 work), a radius around a point, or both; days lists the weekdays it is served (empty: every day).

GET /api/v1/routes/locations #

The depots, the Shopify locations the app has read: [{location_gid, name, address, zip, country_code, lat, lng, active, deleted}].

GET /api/v1/routes/settings #

The app’s settings as {"settings": {…}}, the same object the settings pages save. Map keys are never included.

GET /api/v1/routes/slots #

The time slots offered on a day: the store’s, or the delivery area’s when you give a postcode. Parameters: date (required), zip.

curl -s "$API/routes/slots?date=2026-11-20&zip=43215" -H "Authorization: Bearer $KEY"
# 200
{"date": "2026-11-20", "zip": "43215", "slots": [{"from": "08:00", "to": "12:00"}, {"from": "12:00", "to": "16:00"}, {"from": "16:00", "to": "19:00"}]}

Webhooks #

A webhook sends a signed JSON POST to your https address when something happens. A store can have up to 10.

TopicWhen
stop.status_changedA stop changes status, whoever changed it: an order, your team, the API or the driver. data.from and data.to are the statuses.
routes.sentA day’s routes are sent to the drivers.
plan.acceptedA plan is accepted as the day’s routes.

GET /api/v1/routes/webhooks #

Your webhooks: [{id, topic, url, active, created_at, last_status, last_at}]. last_status is the HTTP status of the last delivery. The secret is never shown again.

POST /api/v1/routes/webhooks #

Body: topic and url (https only, not a private network). Answers 201 with the webhook and its secret: store it now, it is shown only in this answer.

curl -s -X POST "$API/routes/webhooks" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"topic": "stop.status_changed", "url": "https://example.com/hooks/deliveries"}'
# 201
{"id": 1, "topic": "stop.status_changed", "url": "https://example.com/hooks/deliveries", "active": true,
 "created_at": "2026-11-11T01:22:13+00:00", "last_status": null, "last_at": null, "secret": "whsec_3f9a…"}

Refusals: 400 rentals_rest_url_not_allowed (“The address must start with https://.”, or an address on a private network), 400 rentals_rest_too_many_webhooks (“You can have 10 webhooks. Delete one first.”), 400 rentals_rest_invalid for an unknown topic (data.field: "topic").

DELETE /api/v1/routes/webhooks/{id} #

Deletes a webhook: {"ok": true}.

What a delivery looks like #

Every delivery is a POST with a JSON body and these headers:

HeaderValue
X-SIRoutes-TopicThe topic, stop.status_changed.
X-SIRoutes-DeliveryA unique id for this delivery (the same on retries).
X-SIRoutes-TimestampWhen it was sent, Unix seconds.
X-SIRoutes-Signaturesha256= and the hex HMAC-SHA256 of the raw body, keyed with the webhook’s secret.
User-AgentSI-Delivery-Routing/1
{"id": "9b1f0c3e-6a2d-4f57-9a3e-2c8d1e4b7f10", "topic": "stop.status_changed", "shop": "{your-store}.myshopify.com",
 "occurred_at": "2026-11-20T14:02:11+00:00",
 "data": {"stop_id": 10, "from": "on_the_way", "to": "done", "kind": "delivery", "source": "order", "date": "2026-11-20",
          "from_time": "09:00", "to_time": "09:30", "order_gid": "gid://shopify/Order/5550001", "order_name": "#1042",
          "orders": [{"gid": "gid://shopify/Order/5550001", "name": "#1042"}], "location_gid": "gid://shopify/Location/801", "zip": "43215", "route_id": 1, "vehicle_key": "v1"}}

routes.sent and plan.accepted carry {date, location_gid, routes: [{id, name, vehicle_key, driver, stop_ids}]} in data, and plan.accepted adds plan_id.

Check the signature before you trust a delivery: compute the HMAC of the raw body with your secret and compare it with the header in constant time. Answer with any 2xx within 10 seconds. A delivery that fails is tried again after 1, 5, 15 and 60 minutes; after 10 deliveries in a row that finally failed, the webhook is switched off (active: false).

# Python
import hmac, hashlib
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
ok = hmac.compare_digest(expected, request.headers["X-SIRoutes-Signature"])

Errors #

Errors have the shape of every endpoint of the API: {"code": "rentals_rest_<code>", "message": "…", "data": {"status": …}}. The codes you will meet here:

StatusCodeWhen
400invalid_paramA parameter is malformed (date must be a day, YYYY-MM-DD.); data.params names it.
400invalidA body field is missing or wrong; data.field names it.
400not_movable, not_deletable, rental_dayThe stop can’t change that way (see the stop endpoints).
400managed_in_rentals, vehicle_in_useThe vehicle is managed in Rentals & Bookings, or still on a route.
400url_not_allowed, too_many_webhooksWebhook address not https, or already 10 webhooks.
403forbiddenA read key tried to change something.
403plan_requiredThe feature needs a higher plan: data.feature, data.required_plan.
403vehicle_limitYour plan’s number of vehicles is reached.
403app_not_installedThe store doesn’t have SI Delivery Routing (data.app: "routes").
404not_foundNo such stop, route, vehicle, plan or webhook in this store.
409not_readyNothing to plan or send on that day, or the plan has no proposal.
409version_conflictThe day changed since it was planned: plan it again.
429rate_limitedToo many requests: wait for Retry-After seconds.