View Categories

Launching soon on the Shopify App StoreJoin the waitlist

REST API reference: warehouse, stock and settings

30 min read

The REST API v1 of Rentals & Bookings, part 2: the Send & Return desk (check-outs, check-ins and scans), serial numbers, stock by location (with transfers between locations and location rules), rental kits, delivery & pickup (delivery areas, the postcode check and the day sheet), POS alerts, the rental settings spreadsheet and API keys. Part 1 covers bookings, availability, pricing, orders, balances and calendar sync: REST API reference: bookings and availability. Keys, limits, pagination and errors: API overview.

Every endpoint on this page needs the Rentals & Bookings app, except the API keys endpoints, which work with any of the apps. GET needs a read key; everything else needs a key with Allow changes (write). The examples use two shell variables:

API="https://YOUR-API-ADDRESS/api/v1"   # the REST address from Settings › API
KEY="sirb_your_secret_key"

Send & Return (check-outs and check-ins) #

These endpoints do what the Send & Return desk does, with the same rules. send checks units out to the customer; return checks them back in. In every path below, send and return are interchangeable.

  • The send list holds bookings with units still to go out, dated by the rental start. The return list holds bookings with units out, dated by the rental end. Blocked dates from an imported calendar never go out.
  • quantity defaults to the number of serials sent, else 1. It can’t be more than is still to go out (send) or still out (return).
  • A product that tracks serial numbers needs one serial number per unit at check-out (400 serials_required). A full check-in closes every serial number still out, without naming them.
  • A check-out or check-in fires the same events as the desk, so the store’s customer emails and Shopify Flow triggers follow its settings. notify: true asks the notification rules to tell the customer; the store’s settings decide. A cancelled booking can’t be sent or returned.

GET /api/v1/send #

The send list (or GET /api/v1/return, the return list). Paged.

ParameterNotes
whentoday, overdue (due before today), week (today and the next 7 days), all (default) or a day, 2026-12-04.
product_id, variant_id, order_idNarrow the list.
searchAn order name or number, a booking number, the customer, a title or a SKU.
location_idPicked up at this location.
include_serialstrue: add serials: the free serial numbers of the variant for a send, the ones out on the booking for a return.
curl -s "$API/send?when=today&include_serials=true" -H "Authorization: Bearer $KEY"
# 200, X-Total: 3, X-Total-Pages: 1
[
  {
    "booking_id": 42, "product_id": 88001, "variant_id": 44001, "product_name": "Party tent 6×12 m", "variant_title": null,
    "order_id": 5550001, "order_name": "#1042", "order_number": "#1042", "customer": "Jordan Lee",
    "quantity": 2, "quantity_sent": 0, "quantity_returned": 0,
    "start": "2026-12-04 00:00:00", "end": "2026-12-06 00:00:00", "due": "2026-12-04 00:00:00", "overdue": false,
    "status": "reserved", "tracks_serials": true, "serials": ["TENT-001", "TENT-003"], "quantity_to_send": 2
  }
]

A return list row has quantity_to_return instead of quantity_to_send. X-Total counts every match, not just the page.

GET /api/v1/send/counts #

How many bookings each list holds (or GET /api/v1/return/counts).

curl -s "$API/send/counts" -H "Authorization: Bearer $KEY"
# 200
{"today": 3, "overdue": 1, "week": 7, "all": 13}

POST /api/v1/send #

Checks units out (or POST /api/v1/return: checks them in): one booking, or a batch of up to 200.

Body fieldNotes
booking_idOne booking. With quantity, serials and notes.
itemsOr a batch: [{booking_id, quantity?, serials?, notes?}].
atomicBatches only. true (default): every item is checked first, and one bad item refuses the whole batch, with nothing recorded. false: the good items are recorded and the rest listed in failures.
notifyAsk the notification rules to tell the customer.
location_idWhere the check-out or check-in happened.
curl -s -X POST "$API/send" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"items": [{"booking_id": 42, "serials": ["TENT-001", "TENT-003"]}, {"booking_id": 57, "quantity": 1}]}'
# 200
{
  "processed": 2,
  "items": [
    {"booking_id": 42, "product_id": 88001, "variant_id": 44001, "order_id": 5550001, "order_name": "#1042", "quantity": 2,
     "serials": ["TENT-001", "TENT-003"], "quantity_sent": 2, "quantity_returned": 0, "status": "out", "outstanding": 0},
    {"booking_id": 57, "…": "…"}
  ],
  "failures": [],
  "orders_notified": 0
}

A refused all-or-nothing batch answers 409:

{"code": "rentals_rest_batch_refused", "message": "1 of 2 item(s) cannot be processed; nothing was recorded.",
 "data": {"failures": [{"code": "rentals_rest_too_many", "index": 1, "booking_id": 57, "message": "…", "status": 409}], "status": 409}}

Refusals of one item: 409 nothing_to_send / nothing_to_return, 409 too_many (data.remaining for a send, data.out for a return), 409 not_sendable / not_returnable (a cancelled booking), 400 invalid_quantity, the serial number codes (400 serials_required, duplicate_serial, serial_count; 409 serial_not_found, serial_unavailable, serial_not_out) and 404 not_found. Batches: 400 no_items, 400 too_many_items (more than 200), duplicate_booking and invalid_booking inside failures.

POST /api/v1/bookings/{id}/send #

The same for one booking, by its id in the path (or POST /api/v1/bookings/{id}/return). Body: quantity (default 1), serials, notes, notify, location_id. Answers the booking.

curl -s -X POST "$API/bookings/42/return" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"quantity": 2, "notes": "Both back, one pole bent"}'
# 200: the booking, "status": "returned"

POST /api/v1/send/scan #

Checks one unit out from a scanned code (or POST /api/v1/return/scan: checks one in), as a barcode scanner app would.

Body fieldNotes
codeRequired: a serial number (case ignored), an order name (#1042 or 1042), a booking number or a SKU.
booking_id, order_id, variant_id, product_idPick the booking or product when the code matches several.
notes, notify, location_idAs for POST /send.

A serial number is looked up first. For a return, the unit comes back from the booking it is out on. For a send, it goes out on its variant’s open booking, with that serial number. Any other code moves one unit of the one booking it names.

curl -s -X POST "$API/send/scan" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{"code": "TENT-003"}'
# 200
{"action": "sent", "code": "TENT-003", "serials": ["TENT-003"], "booking_id": 42, "product_id": 88001, "variant_id": 44001,
 "order_id": 5550001, "order_name": "#1042", "quantity_sent": 1, "quantity_returned": 0, "status": "out", "outstanding": 1}

Refusals: 400 invalid_code (no code), 404 not_found (unknown code), 409 ambiguous_booking (data.candidates: pass booking_id or order_id), 409 ambiguous_serial (the code exists on several products: pass product_id or variant_id), 409 wrong_product, 409 serial_unavailable (data.serial_status), 409 no_booking (nothing waiting for this unit), 404 serial_not_out, 400 serials_required (a product that tracks serial numbers, scanned by order or SKU).

GET /api/v1/send/history #

Every check-out on record (or GET /api/v1/return/history: every check-in), newest first. Paged; ties are broken by id, so pages never repeat or skip a row.

ParameterNotes
booking_id, variant_id, product_idNarrow the list.
date_from, date_toDays, Y-m-d, store time.
searchA product, order, customer or SKU, or a serial number it moved.
orderby, orderdatetime (default), id, booking_id or quantity; desc (default) or asc.
curl -s "$API/send/history?date_from=2026-12-01&date_to=2026-12-07" -H "Authorization: Bearer $KEY"
# 200, X-Total: 12
[
  {"id": 1601, "type": "sent", "datetime": "2026-12-04 08:12:40", "quantity": 2, "serials": ["TENT-001", "TENT-003"],
   "booking_id": 42, "order_id": 5550001, "order_name": "#1042", "product_id": 88001, "variant_id": 44001,
   "product_name": "Party tent 6×12 m", "quantity_booked": 2, "start": "2026-12-04 00:00:00", "end": "2026-12-06 00:00:00",
   "user_id": null, "user_name": null, "notes": ""}
]

user_id is the staff member who used the desk; null for moves made through the API or Shopify Flow.

Serial numbers #

A serial number is one physical unit of a rent variant that tracks serial numbers. Codes are unique per variant, ignoring case. See Serial numbers.

A serial number answers {id, product_id, variant_id, code, status, cost, acquired, retired, notes, comments, booking_id, order_id, last_movement}. status is available, out, maintenance or retired. out is set by a check-out and cleared by a check-in, never by hand. booking_id and order_id are the booking it is out on now. comments repeats notes.

GET /api/v1/serials #

Paged. Parameters: product_id, variant_id, status, search (part of the code).

curl -s "$API/serials?variant_id=44001&status=available" -H "Authorization: Bearer $KEY"
# 200, X-Total: 3
[
  {"id": 17, "product_id": 88001, "variant_id": 44001, "code": "TENT-001", "status": "available", "cost": "4650.00",
   "acquired": "2024-04-12", "retired": null, "notes": "", "comments": "", "booking_id": null, "order_id": null,
   "last_movement": {"type": "returned", "datetime": "2026-11-20 16:40:02", "user_id": null, "booking_id": 31}}
]

POST /api/v1/serials #

Adds one serial number. Body: variant_id or product_id, code (required, up to 100 characters), status (available, the default, maintenance or retired), cost (a decimal), acquired (Y-m-d, default today), notes (comments works too). Answers 201 with the serial number; 409 duplicate_code when the variant has that code.

curl -s -X POST "$API/serials" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"variant_id": 44001, "code": "TENT-005", "cost": "4650.00", "acquired": "2026-11-01"}'
# 201: the serial number

POST /api/v1/serials/bulk #

Adds a run of codes, at most 500. Body: variant_id or product_id, and from + to (TENT-001, TENT-020) or range (TENT-001…TENT-020; .. or - work too), plus status, cost, acquired, notes for every code. Codes that exist already are skipped.

curl -s -X POST "$API/serials/bulk" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"variant_id": 44001, "range": "TENT-001..TENT-020"}'
# 201
{"created": [{"id": 18, "code": "TENT-006", "…": "…"}], "skipped": ["TENT-001", "TENT-002", "TENT-003", "TENT-004", "TENT-005"]}

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

One serial number. 404 not_found otherwise.

PUT or PATCH /api/v1/serials/{id} #

Changes a serial number; only the fields you send change: code, status, cost, acquired, notes, and variant_id to move it to another rent variant (refused while it is out). Answers the serial number.

DELETE /api/v1/serials/{id} #

Deletes a serial number: {"deleted": true, "previous": {…}}. Refused with 409 serial_out while it is out and 409 serial_has_history once it has been rented: retire it instead ("status": "retired").

GET /api/v1/serials/{id}/history #

Every check-out and check-in of this unit: [{id, type, datetime, user_id, booking_id, order_id, order_name, quantity, notes}].

GET /api/v1/serials/labels #

A printable sheet of labels, as a PDF (Content-Type: application/pdf). Parameters: ids (comma-separated serial ids) or product_id (every serial number of the product that isn’t retired), format (qr, the default, c39e for Code 39 or c128 for Code 128) and names (true prints the product name under each code). At most 500 labels.

curl -s "$API/serials/labels?product_id=88001&format=qr&names=true" -H "Authorization: Bearer $KEY" -o labels.pdf

GET /api/v1/serials/usage #

How much each serial number was used over a window: days owned, days out, idle days, use rate (utilisation, a percentage), hires and revenue. Parameters: days (30, 90 or 365 days ending today; default 30) or start (Y-m-d) with days (up to 366), product_id, sort (code, product, days_out, days_idle, utilisation, hires, revenue, revenue_per_day …) and dir (asc or desc).

curl -s "$API/serials/usage?days=90&product_id=88001&sort=utilisation&dir=desc" -H "Authorization: Bearer $KEY"
# 200
{
  "window": {"start": "2026-09-02", "end": "2026-11-30"},
  "rows": [{"serial_id": 17, "variant_id": 44001, "product_id": 88001, "code": "TENT-001", "product": "Party tent 6×12 m",
            "status": "available", "acquired": "2024-04-12", "days_owned": 90, "days_out": 54, "days_idle": 36,
            "utilisation": 60, "hires": 11, "revenue": 5400, "revenue_per_day": 60, "unassigned": false}],
  "totals": {"days_out": 140, "days_owned": 360, "revenue": 17215, "serials": 4, "idle": 1,
             "unassigned_revenue": 1600, "unassigned_days": 6, "over_days": 0}
}

Locations and stock #

A store with two or more locations that rent lets customers choose where they pick up, and each product can keep its own stock at each location. See Rental locations and stock by location. location_id (Shopify’s location id) also works on availability, pricing, bookings, check-outs and check-ins; an unknown location answers 404 location_not_found.

GET /api/v1/locations #

The store’s Shopify locations and whether rentals are picked up there. rules are the location’s own rules (see below); null values use the store’s settings.

curl -s "$API/locations" -H "Authorization: Bearer $KEY"
# 200
[
  {"id": 66001, "name": "Main warehouse", "shopify_name": "Main warehouse", "public_name": null,
   "address": {"address1": "1 Example Street", "address2": "", "city": "Springfield", "province": "Oregon", "zip": "97477",
               "country": "United States", "country_code": "US", "phone": ""},
   "area": "Springfield, Oregon, US", "active": true, "rentals_enabled": true, "is_default": true,
   "accepts_returns": true, "pickup_enabled": false, "instructions": null,
   "rules": {"hours": null, "closed_days": [], "turnaround": {"before": null, "after": null}, "lead_time": null, "price": null}}
]

PUT /api/v1/locations/{locationId}/rules #

Sets a location’s own rules, which beat the store’s settings for rentals picked up there. Keys you leave out keep their value; null clears one, so the store’s setting applies again. Answers the location, with its rules.

Body fieldNotes
hours{open, close} as HH:mm (the close after the open), or null.
closed_days[{from, to, repeats, applies_to, reason}]: Y-m-d dates; repeats is none, weekly, monthly or yearly; applies_to is both, start, end or stays (the night itself is closed).
turnaround{before, after}, each {amount, unit} (hours or days) or null.
lead_time{amount, unit}: how far ahead a rental must be booked here.
priceA price rule for rentals picked up here: {mode, percent, amount, daily, hourly}. mode percent raises or lowers the price (percent: 10 is 10% more, -10 is 10% less; above -100), add adds amount, replace uses a price per daily / hourly. A variant’s own rule at the location (see the stock below) beats it.
curl -s -X PUT "$API/locations/66002/rules" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"hours": {"open": "09:00", "close": "17:00"},
       "closed_days": [{"from": "2026-12-25", "to": "2026-12-26", "repeats": "yearly", "applies_to": "both", "reason": "Christmas"}],
       "turnaround": {"before": null, "after": {"amount": 1, "unit": "days"}},
       "price": {"mode": "percent", "percent": 10}}'
# 200: the location, with "rules" as saved

Refusals: 400 invalid (data.field names the rule, such as rules.hours or rules.price.percent), 404 not_found.

GET /api/v1/products/{productId}/stock #

A product’s rental stock: one shared stock (mode: "shared", picked up at the default location) or units at each location (mode: "by_location").

curl -s "$API/products/88001/stock" -H "Authorization: Bearer $KEY"
# 200
{"product_id": 88001, "mode": "by_location",
 "variants": [{"variant_id": 44001, "title": "Default Title", "total": 4,
               "locations": [{"location_id": 66001, "units": 3, "activated": true, "min": null, "max": null, "price": null},
                             {"location_id": 66002, "units": 1, "activated": true, "min": 1, "max": 4, "price": null}]}],
 "one_way": {"returns": "same", "locations": [], "policy": "strict", "fee": null, "routes": []}}

Per location, min and max are the targets the transfer suggestions use (null = no target) and price is the variant’s own price rule there (see PUT /locations/{locationId}/rules for its shape; null = the location’s rule). one_way says where customers may return the product: returns is same (back where they picked it up, the default), any (any location that takes returns) or list (the listed locations); policy is strict (the pick-up location must keep enough units for its later bookings) or pooled (all locations together cover the rental and the store moves units); fee is a one-way fee and routes are fees for a route, [{from, to, amount}] (a missing from or to means any location).

PUT /api/v1/products/{productId}/stock #

Sets a product’s rental stock. Body: mode (shared or by_location), variants ([{variant_id, units?, locations: [{location_id, units, min?, max?, price?}]}]; for shared, units per variant, or leave it out to keep the total), one_way ({returns, locations, policy, fee, routes} as above; 400 invalid with the field when a value is wrong), force. min and max set the targets (max not below min) and price the variant’s own price rule at that location. A location you leave out gets 0. When future bookings would no longer fit, the answer is 409 stock_conflict with data.bookings ({id, orderName, start, end, quantity, locationId, short}); send force: true to save anyway. Answers the stock with conflicts.

curl -s -X PUT "$API/products/88001/stock" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"mode": "by_location", "variants": [{"variant_id": 44001, "locations": [{"location_id": 66001, "units": 2}, {"location_id": 66002, "units": 2}]}]}'

GET /api/v1/stock/board #

The stock by location board: for each rent variant that keeps stock at each location, the units based at every location, how many are free today, out with customers, and on the way. Optional product_id narrows it to one product. Variants of kits and products with one shared stock are not listed.

curl -s "$API/stock/board?product_id=88001" -H "Authorization: Bearer $KEY"
# 200 (trimmed)
{
  "locations": [{"id": 66001, "name": "Main warehouse", "rentals": true, "accepts_returns": true},
                {"id": 66002, "name": "Harbour depot", "rentals": true, "accepts_returns": true}],
  "rows": [{"product_id": 88001, "variant_id": 44001, "title": "Party tent 6×12 m", "variant_title": null, "total": 4, "one_way": false,
            "locations": [{"location_id": 66001, "units": 3, "free": 2, "out": 1, "in_transit": 0, "min": null, "max": null, "below_min": false, "above_max": false},
                          {"location_id": 66002, "units": 1, "free": 1, "out": 0, "in_transit": 0, "min": 2, "max": null, "below_min": true, "above_max": false}]}],
  "truncated": false
}

GET /api/v1/transfers #

Transfers of units between locations, newest first. Paged (page, per_page). Parameters: status (open, the default: planned and in transit; or planned, in_transit, arrived, cancelled), location_id (from or to this location) and inbound=1 (with location_id: only transfers to it). A transfer counts at once: the origin loses the units from the ship time and the destination gains them at the arrival.

curl -s "$API/transfers?status=open" -H "Authorization: Bearer $KEY"
# 200, X-Total: 1
[
  {
    "id": 9, "status": "planned", "late": false,
    "from": {"id": 66001, "name": "Main warehouse"}, "to": {"id": 66002, "name": "Harbour depot"},
    "ship_at": "2026-12-02T09:00", "arrive_at": "2026-12-03T09:00", "shipped_at": null, "received_at": null, "cancelled_at": null,
    "note": null, "forced_reason": null, "created_by": "api:sirb_1a2b", "created_at": "2026-11-30T14:05:11+00:00",
    "items": [{"variant_id": 44001, "product_id": 88001, "title": "Party tent 6×12 m", "quantity": 1, "dispatched": 0, "received": 0,
               "in_transit": 0, "serials": [], "received_serials": []}]
  }
]

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

One transfer, as in the list. 404 not_found when the store has no such transfer. status is planned, in_transit (dispatched, not all received), arrived or cancelled; late is true for a transfer in transit past its arrive_at.

POST /api/v1/transfers #

Plans a transfer. Body: from_location_id, to_location_id (two different locations), items ([{variant_id, quantity, serials?}]; each variant once, at most one item per variant; serials are the serial numbers that travel), and optionally ship_at and arrive_at (dates or Y-m-d H:i; the arrival not before the departure) and note. The products must keep stock at each location. Answers 201 with the transfer. When the origin would be short for its bookings, the answer is 409 transfer_leaves_short (data.items: each item’s deficit); send force: true with a reason to plan it anyway (400 reason_required without a reason).

curl -s -X POST "$API/transfers" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"from_location_id": 66001, "to_location_id": 66002, "ship_at": "2026-12-02", "arrive_at": "2026-12-03",
       "items": [{"variant_id": 44001, "quantity": 1}]}'
# 201: the transfer, "status": "planned"

POST /api/v1/transfers/{id}/dispatch #

The units leave the origin now: they must be free there (409 not_on_hand, data.free). Without a body, every planned item goes. To send only some, send items ([{variant_id, quantity?, serials?}]); the items you leave out are dropped from the transfer. Serial numbers scanned out travel with it. The status becomes in_transit. Refusals: 409 transfer_closed (already dispatched or closed), 400 bad_quantity, 409 serial_not_found.

POST /api/v1/transfers/{id}/receive #

Receives all or part of a dispatched transfer: the destination’s stock grows now. Without a body, everything still on the way arrives. Send items ([{variant_id, quantity?, serials?}]) or serials (the serial numbers scanned in) to receive part; the transfer stays in_transit until everything is in, then becomes arrived. Refusals: 409 transfer_closed (not dispatched yet, or closed), 400 bad_quantity, 409 serial_not_on_transfer.

POST /api/v1/transfers/{id}/cancel #

Cancels a transfer: a planned one stops counting; a dispatched one is turned back (what is still on the way goes back to the origin). 409 transfer_closed when it is already closed.

GET /api/v1/transfers/suggestions #

Transfers that would bring a location below its min back up, taking from locations above their max (the largest surplus first). Nothing moves by itself: create the transfer if you agree. Each suggestion is {variant_id, product_id, title, from, to, quantity, reason} with from and to as location ids and reason below_minimum.

curl -s "$API/transfers/suggestions" -H "Authorization: Bearer $KEY"
# 200
[{"variant_id": 44001, "product_id": 88001, "title": "Party tent 6×12 m", "from": 66001, "to": 66002, "quantity": 1, "reason": "below_minimum"}]

Rental kits #

A kit is a product rented as a set of items, each with its own stock. See Rental kits. A kit answers:

{"id": 3, "product_id": 88100, "variant_id": 44100, "title": "Wedding tent package", "source": "app", "pricing": "own",
 "items_discount_percent": null, "use_item_rules": true,
 "items": [{"product_id": 88001, "variant_id": 44001, "title": "Party tent 6×12 m", "variant_title": null, "quantity": 1},
           {"product_id": 88002, "variant_id": 44002, "title": "Folding chair", "variant_title": null, "quantity": 60}]}

source is app, or shopify_bundles for a Shopify Bundles product run as a kit (change its items in Shopify). pricing is own (the kit’s own rates) or items_sum (the items’ rental prices added up, less items_discount_percent). use_item_rules: the items’ closed days and length rules apply to the kit too.

GET /api/v1/kits #

The kits, oldest first. Paged.

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

One kit.

POST /api/v1/kits #

Makes a kit: a new Shopify product (active, but on no sales channel yet: publish it in Shopify) rented as its items. Body: title (required), items ([{variant_id, quantity}], 1 to 30 items, quantities 1 to 2,000, rental items only), pricing (own, the default, or items_sum), items_discount_percent (0 to 99.99, with items_sum), use_item_rules (default true), rental_type (date, datetime, datefixed or datetimefixed; default the first item’s). Answers 201 with the kit.

curl -s -X POST "$API/kits" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"title": "Wedding tent package", "items": [{"variant_id": 44001, "quantity": 1}, {"variant_id": 44002, "quantity": 60}], "pricing": "items_sum", "items_discount_percent": "10"}'

PUT or PATCH /api/v1/kits/{id} #

Changes a kit’s title, items, pricing, items_discount_percent or use_item_rules. Existing bookings keep the items they were booked with.

DELETE /api/v1/kits/{id} #

Removes a kit and archives its Shopify product: {"deleted": true, "id": 3}. 409 has_bookings while a booking of the kit isn’t over.

Refusals: 400 invalid (data.field: title, items, items.0, pricing …), 404 not_found, 502 shopify_error.

Delivery & pickup #

For stores that deliver, transport or let customers pick up at set times. See Delivery & pickup. Methods are pickup, delivery and shipped (transport); return methods are in_store, collect and post. Every booking carries its handover. A store can also set up delivery areas (by postcode or radius), check a postcode, and print the day’s run sheet: these are below.

GET /api/v1/handover/windows #

The time slots of one hand-over day. Parameters: variant_id or product_id, method, date (required, Y-m-d), location_id, back (true for the return day’s slots), return_method, and for delivery postcode or zone_id (the delivery area’s own slots; with several locations and no location_id, the area’s location is used). Once the store has delivery areas, a delivery needs a postcode inside one: 400 postcode_required or 400 outside_area (data.field: "postcode").

curl -s "$API/handover/windows?variant_id=44001&method=delivery&date=2026-12-04" -H "Authorization: Bearer $KEY"
# 200
[{"id": "08:00-10:00", "from": "08:00", "to": "10:00", "label": "8:00 AM – 10:00 AM", "capacity": 4, "left": 3, "full": false}]

capacity is how many stops a slot takes at a location on a day (null = no limit), left how many are still free and full true when none is. Booking into a full slot is refused with 409 window_full (data.field window or back_window).

GET /api/v1/settings/delivery #

The store’s delivery & pickup settings: enabled, methods (the methods on offer, by name: pickup, delivery, shipped), default_method, holidays (Y-m-d days nothing is handed over) and one block per method under its name. Each block has: label, start_label, end_label, lead_time ({amount, unit}), cutoff (HH:mm), days, window_mode (none, hours or list), windows (the time slots: [{days, from, to, capacity, fee}]), back_window_mode (same or none), transit_out, transit_back, count_days (calendar or business), receive_days, blackouts ([{from, to, reason}]), return_methods (in_store, collect, post), late_after and checkout ({delivery_types, match_store, allow_pickup_points, hide_shipping_named, rename_with_window, when_missing}). Three more: fee_line (delivery only: charge the delivery fee as its own cart line), customer_changes (pick-up and delivery: {enabled, hours}, may customers change the time slot until hours before it) and post_back_to (transport only: the location a transported rental goes back to). A slot’s fee is a surcharge for that slot.

curl -s "$API/settings/delivery" -H "Authorization: Bearer $KEY"
# 200 (trimmed)
{
  "enabled": true, "methods": ["pickup", "delivery"], "default_method": "pickup", "holidays": [],
  "delivery": {"label": "", "lead_time": {"amount": 1, "unit": "days"}, "cutoff": "14:00", "days": ["mon", "tue", "wed", "thu", "fri", "sat"],
               "window_mode": "list",
               "windows": [{"days": ["mon", "tue", "wed", "thu", "fri", "sat"], "from": "08:00", "to": "10:00", "capacity": 4}],
               "transit_out": {"amount": 0, "unit": "days"}, "transit_back": {"amount": 0, "unit": "days"},
               "return_methods": ["collect"], "fee_line": false, "customer_changes": {"enabled": false, "hours": 24}, "…": "…"},
  "pickup": {"…": "…"}, "shipped": {"…": "…"}
}

PUT /api/v1/settings/delivery #

Changes the settings: what you send is merged over the current settings, then checked as the settings page checks it (400 invalid with data.field). Read them first and send back only what you change; use the same readable names as above (400 invalid_param with data.field naming the key, such as delivery.lead, for one that does not exist). Answers the settings.

PATCH /api/v1/bookings/{id}/handover #

Changes a booking’s hand-over. Body: method, return_method, window, back_window (time slots such as 08:00-10:00), return_tracking (a transported return’s tracking number or link), force (keep the change even when the new transit days are booked; otherwise 409 not_available). Answers the booking.

curl -s -X PATCH "$API/bookings/42/handover" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"method": "delivery", "window": "08:00-10:00"}'

GET /api/v1/delivery/check #

Can the store deliver to a postcode, from which area and location, and when can a delivered rental first start? Parameters: postcode (required), product_id or variant_id (optional: adds earliest_start) and location_id (optional: only that location’s areas). Without delivery areas every postcode is fine. The same check is open to storefronts with a publishable key (storefront API).

curl -s "$API/delivery/check?postcode=2000&variant_id=44001" -H "Authorization: Bearer $KEY"
# 200
{"ok": true, "postcode": "2000", "zone": {"id": 7, "name": "Sydney CBD", "fee_hint": "15.00", "min_order_hint": null, "location_id": 66001},
 "location_id": 66001, "code": null, "message": "", "earliest_start": "2026-12-03"}

When it is not ok, code is postcode_required (no usable postcode) or outside_area, with a message to show shoppers, and zone is null.

GET /api/v1/delivery-zones #

The store’s delivery areas, in order. Once a store has at least one active area, a delivery booked online needs a postcode inside one (postcode_required / outside_area); staff and the API may book any address. An area can set its own delivery days, time slots and lead times, which replace the delivery method’s for it. A postcode match beats a radius match, the first area in order wins, and among radius matches the nearest centre.

FieldNotes
id, name, public_name, display_nameThe area and its name (the public name is what shoppers see; display_name is the one used).
location_id, location_nameThe location it is delivered from (null = the default location).
postcodesA list of patterns: an exact postcode (2000, SW1A1AA), a prefix (20*, SW1*) or a numeric range (2000-2099, both ends the same length). Compared in upper case without spaces; a US ZIP+4 by its first five digits. At most 500.
radius_km, center_lat, center_lngA radius around a point instead of (or as well as) postcodes.
fee_hint, min_order_hintShown to shoppers only: Shopify’s own delivery rates charge the fee.
days, window_listThe area’s own delivery days (mon … sun) and time slots ([{days, from, to, capacity, fee}]); null = the delivery method’s.
deliver_before, collect_afterDays before the rental starts that it is delivered, and days after it ends that it is collected (0 to 30); null = the method’s.
active, positionSwitched on, and its place in the order.
curl -s "$API/delivery-zones" -H "Authorization: Bearer $KEY"
# 200
[{"id": 7, "name": "Sydney CBD", "public_name": null, "display_name": "Sydney CBD", "location_id": 66001, "location_name": "Main warehouse",
  "postcodes": ["2000", "2001", "20*"], "radius_km": null, "center_lat": null, "center_lng": null, "fee_hint": "15.00", "min_order_hint": null,
  "days": null, "window_list": null, "deliver_before": null, "collect_after": null, "active": true, "position": 1}]

POST /api/v1/delivery-zones #

Adds an area. Body: the fields above, in snake_case or camelCase (windows works for window_list). name is required (up to 120 characters); an area needs either postcodes or a radius_km with its centre (center_lat and center_lng). postcodes may also be one text with a pattern per line or comma. Answers 201 with the area. Refusals: 400 invalid with data.field (such as name, postcodes, center_lat, window_list.0.to).

curl -s -X POST "$API/delivery-zones" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"name": "Sydney CBD", "postcodes": ["2000", "20*"], "location_id": 66001, "fee_hint": "15.00",
       "days": ["mon", "tue", "wed", "thu", "fri"], "windows": [{"from": "08:00", "to": "10:00", "capacity": 4}]}'
# 201: the area

PUT /api/v1/delivery-zones/{id} #

Changes an area: the fields you send change, the others keep their value (null clears days, window_list, deliver_before or collect_after). Answers the area. 404 not_found for an unknown area.

DELETE /api/v1/delivery-zones/{id} #

Removes an area. Bookings made for it keep their postcode. Answers {"deleted": true, "id": 7}; 404 not_found for an unknown area.

GET /api/v1/schedule #

One day’s run sheet: what goes out and comes back, grouped for the people doing it. Parameters: date (Y-m-d, default today in the store’s time zone), location_id (one location) and method (pickup, delivery or shipped: only that way’s sections).

FieldNotes
day, today, location_id, location_nameThe day, the store’s today, and the location asked for.
sectionsdeliveries, collections (delivered rentals to collect), pickups, returns (in store), to_post (parcels to transport) and due_back (transported returns due; today’s also lists the late ones). Each is a list of stops.
routeThe keys of the delivery and collection stops in route order: the saved order, then time slot, then postcode.
map_linksMap directions for the route, in legs of 9 stops.
counts, emptyThe number of stops per section, and true when there are none.

A stop is one order: {key, order_id, order_name, customer_name, email, phone, address, address_text, map_url, postcode, zone, window, window_label, at, items, booking_ids, note, status, late, tracking}. items is [{booking_id, title, variant_title, quantity}]; status is due or done; late flags an overdue transported return.

curl -s "$API/schedule?date=2026-12-04&location_id=66001" -H "Authorization: Bearer $KEY"
# 200 (trimmed)
{
  "day": "2026-12-04", "today": "2026-12-01", "location_id": 66001, "location_name": "Main warehouse",
  "sections": {
    "deliveries": [{"key": "deliveries:o5550001", "order_id": 5550001, "order_name": "#1042", "customer_name": "Jordan Lee",
                    "address_text": "1 Example Street, Sydney NSW 2000", "postcode": "2000", "zone": "Sydney CBD",
                    "window": "08:00-10:00", "window_label": "8:00 AM – 10:00 AM",
                    "items": [{"booking_id": 42, "title": "Party tent 6×12 m", "variant_title": null, "quantity": 1}],
                    "status": "due", "late": false, "tracking": null, "…": "…"}],
    "collections": [], "pickups": [], "returns": [], "to_post": [], "due_back": []
  },
  "route": ["deliveries:o5550001"], "map_links": ["https://www.google.com/maps/dir/…"],
  "counts": {"deliveries": 1, "collections": 0, "pickups": 0, "returns": 0, "to_post": 0, "due_back": 0}, "empty": false
}

400 invalid_param when date is not a day.

POS alerts #

A rent item rung up at Shopify POS without the Rentals desk has no dates and no booking. It raises an alert until the merchant books the dates or dismisses it. An alert is {id, kind, status, order_id, order_name, line_item_id, product_id, variant_id, title, variant_title, quantity, unit_price, currency, customer_id, customer_name, booking_id, note, text, created_at, resolved_at}; status is open, booked (a booking is attached to the order line) or dismissed.

GET /api/v1/pos/alerts #

Lists alerts, newest first. Paged (page, per_page). status: open (the default), booked, dismissed or all; anything else is 400 invalid_param.

curl -s "$API/pos/alerts" -H "Authorization: Bearer $KEY"
# 200, X-Total: 1
[{"id": 3, "kind": "no_dates", "status": "open", "order_id": 5550009, "order_name": "#1012", "line_item_id": 13050, "product_id": 88001,
  "variant_id": 44001, "title": "Party tent 6×12 m", "variant_title": null, "quantity": 1, "unit_price": "450.00", "currency": "USD",
  "customer_id": null, "customer_name": null, "booking_id": null, "note": null,
  "text": "Sold without rental dates: Party tent 6×12 m on #1012. Book the dates or refund it.", "created_at": "2026-12-01T10:15:00+00:00", "resolved_at": null}]

POST /api/v1/pos/alerts/{id}/resolve #

Closes an alert. With booking_id (a booking you made some other way, for the same product), the alert is booked and the booking’s order line is attached when it has none; without it, the alert is dismissed with the note (up to 1,000 characters). Answers the alert. Refusals: 404 not_found, 409 alert_closed (it is not open), 409 already_booked (that order line already has a booking), 400 invalid (the booking is for another product).

Rental settings spreadsheet #

Every rent variant’s settings as a CSV file, to change many products at once in a spreadsheet.

GET /api/v1/rental-settings/export #

A CSV file (text/csv), one row per rent variant, with the columns product_id, variant_id, sku, handle, title, variant_title, enabled, rental_type, mode, units, allow_overbooking, sell_too, pricing_type, daily_price, hourly_price, fixed_price, price_table, price_period, custom_price_text, turnaround, buffer_before, min_length, max_length, padding, max_future_days, stay_type, check_in_time, check_out_time.

curl -s "$API/rental-settings/export" -H "Authorization: Bearer $KEY" -o rental-settings.csv

The last three are for stays by the night: stay_type is days, days_free (Days: can go out again on the return day), nights or default (the store’s Booked by); check_in_time and check_out_time are the times guests see, as HH:mm (such as 15:00), or default / none for the store’s setting. The export writes default where the product follows the store. On import, an empty cell leaves the setting as it is and a wrong value is reported against its line and column.

POST /api/v1/rental-settings/import #

Imports the file. Send the CSV as the body with Content-Type: text/csv, or as JSON {"csv": "…"}. dry_run is true by default: the answer reports what would change and nothing is written. Read the report, then send the same file with dry_run=false to apply the products without errors.

Rows are matched by variant id, product id, SKU, then handle. An empty cell leaves that setting as it is. Settings belong to the product, so the first row of a product wins. At most 2,000 rows or 2 MB; a file that isn’t UTF-8 is read as Windows-1252 (Excel).

curl -s -X POST "$API/rental-settings/import?dry_run=true" -H "Authorization: Bearer $KEY" \
  -H "Content-Type: text/csv" --data-binary @rental-settings.csv
# 200 (trimmed)
{
  "dry_run": true,
  "summary": {"rows": 14, "products": 12, "update": 1, "make_rentable": 0, "unchanged": 11, "invalid": 0, "unmatched": 0},
  "products": [{"product_id": 88001, "title": "Party tent 6×12 m", "matched_by": "variant_id", "lines": [2], "action": "update",
                "changes": {"dailyPrice": {"from": "450.00", "to": "475.00"}}, "errors": [], "warnings": []}],
  "errors": [], "unknown_columns": []
}

400 invalid (data.field: "file") for an empty file.

API keys #

The store’s secret keys, the same ones listed under Settings › API. These endpoints work with a key of any of the apps.

GET /api/v1/api-keys #

Active keys newest first, then the 20 most recently revoked. Never a secret.

curl -s "$API/api-keys" -H "Authorization: Bearer $KEY"
# 200
[{"id": 3, "name": "Warehouse scanner", "prefix": "sirb_3f9a2c1d", "last_four": "Q2xa", "scopes": ["read", "write"],
  "created_at": "2026-11-02T09:12:44+00:00", "last_used_at": "2026-11-30T08:01:19+00:00", "revoked_at": null}]

POST /api/v1/api-keys #

Makes a key. Body: name (up to 100 characters), scopes (["read"], the default, or ["read", "write"]). The answer holds the full key in secret, this once only. A key can’t make a key with more access than its own (403 forbidden), and a read key can’t make keys at all. 400 too_many_keys at 20 active keys.

curl -s -X POST "$API/api-keys" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"name": "Accounting export", "scopes": ["read"]}'
# 201
{"key": {"id": 4, "name": "Accounting export", "prefix": "sirb_7b1e04aa", "last_four": "k9Tz", "scopes": ["read"], "…": "…"},
 "secret": "sirb_7b1e04aa_…"}

DELETE /api/v1/api-keys/{id} #

Revokes a key at once: {"key": {…, "revoked_at": "…"}}. Revoking twice is harmless, and a key may revoke itself (the last step of a rotation). 404 not_found for a key of another store.