View Categories

Launching soon on the Shopify App StoreJoin the waitlist

REST API reference: warehouse, stock and settings

15 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, rental kits, delivery & pickup, 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.

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}
]

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}, {"location_id": 66002, "units": 1, "activated": true}]}]}

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}]}]; for shared, units per variant, or leave it out to keep the total), force. 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}]}]}'

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, post or let customers pick up at set times. See Delivery & pickup. Methods are pickup, delivery and shipped (posted); return methods are in_store, collect and post. Every booking carries its handover.

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.

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": null, "left": null, "full": false}]

GET /api/v1/settings/delivery #

The store’s delivery & pickup settings: the same object the app’s settings page saves. methods lists the methods on offer by their short keys (p pick-up, d delivery, s posted), and each method has its own block under that key: labels, lead time, cut-off, days, time slots (windowList), transit times, return methods and checkout rules.

curl -s "$API/settings/delivery" -H "Authorization: Bearer $KEY"
# 200 (trimmed)
{
  "v": 1, "enabled": true, "methods": ["p", "d"], "default": "p", "holidays": [],
  "d": {"label": "", "lead": {"amount": 1, "unit": "days"}, "cutoff": "14:00", "days": ["mon", "tue", "wed", "thu", "fri", "sat"],
        "windows": "list", "windowList": [{"days": ["mon", "tue", "wed", "thu", "fri", "sat"], "from": "08:00", "to": "10:00", "capacity": null}],
        "transitOut": {"amount": 0, "unit": "days"}, "transitBack": {"amount": 0, "unit": "days"}, "returns": ["d"], "…": "…"},
  "p": {"…": "…"}, "s": {"…": "…"}
}

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. 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 posted 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"}'

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.

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

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.