These endpoints live under https://your-store.com/wp-json/wc-sibooking/v1/ and need a WooCommerce REST API key with Read/Write permissions, as described in the REST API overview. They are the warehouse side of the plugin: what is due to go out today, what is still with customers, and the two movements that change it — sending equipment out and taking it back, serial code by serial code.
Everything the Rentals → Send and Rentals → Return screens do is here, which means you can drive a barcode scanner, a tablet on the loading bay or a warehouse management system against the same data your staff see in wp-admin.
How sending and returning work #
Each rental line on an order is a booking, and a booking carries three numbers: how many units were reserved, how many have gone out, and how many have come back. A booking appears on the send queue while units are still to go out, and on the return queue while units are still with the customer.
quantity 4 reserved on the order
quantity_sent 3 handed over so far -> 1 still to send
quantity_returned 1 back on the shelf -> 2 still out
On a product with serial tracking switched on, each movement also names the individual codes. Sending a serial marks it Not Available, returning it puts it back to Available, and both are written to the movement history with the date and the staff member who did it.
What is due to go out #
GET /send is the picking list: the bookings that still have units to hand over.
| Parameter | Value |
|---|---|
when | today, overdue, week (the next seven days), all (the default), or a single Y-m-d day. |
product_id | Only this product. |
order_id | Only this order. |
search | Order number or product name. |
include_serials | true adds the serial codes that may be moved on each row. Left out by default because it costs a query per row. |
page, per_page | Paging, up to 200 per page. The total is in the X-WP-Total header. |
curl -u "ck_your_key:cs_your_secret" \
"https://your-store.com/wp-json/wc-sibooking/v1/send?when=today&include_serials=true"
[
{
"booking_id": 812,
"product_id": 123,
"product_name": "Canon EOS R6 body",
"order_id": 4471,
"order_number": "4471",
"customer": "Dana Whitfield",
"quantity": 2,
"quantity_sent": 0,
"quantity_returned": 0,
"quantity_to_send": 2,
"start": "2026-10-03 09:00:00",
"end": "2026-10-06 17:00:00",
"due": "2026-10-03 09:00:00",
"overdue": false,
"tracks_serials": true,
"serials": [ "SN-0001", "SN-0004", "SN-0009" ]
}
]
dueis the date the row is measured against — the rental start on the send queue, the rental end on the return queue — andoverduesays whether that moment has passed.quantity_to_sendis what is left, not what was ordered. A booking that is half out stays on the list with the remainder.serialson the send queue is every free code for the product, which is what the operator may choose from. It is not a reservation of those particular units.tracks_serialsisfalsewhen the product does not use serial tracking, or uses it but has no codes on record yet. Those bookings move by quantity alone.
GET /send/counts returns just the size of each segment, for a dashboard tile:
{ "today": 6, "overdue": 2, "week": 19, "all": 34 }
Sending a rental out #
POST /send hands units over. Send one booking by naming it at the top level, or a whole van load with items.
| Field | Value |
|---|---|
booking_id | The booking to send. Required unless you are sending items. |
quantity | How many units. Defaults to the number of serials given, or 1. |
serials | The codes going out, as an array. Omit on a product without serial tracking. |
notes | Free text kept on the movement row — who picked it, what condition it left in. |
items | An array of { booking_id, quantity, serials, notes } objects to move together. |
notify | true emails the customer, exactly as the admin screen does. Default false. |
atomic | Batches only. true (the default) refuses the whole batch if any row is invalid. |
curl -u "ck_your_key:cs_your_secret" \
-H "Content-Type: application/json" \
-X POST "https://your-store.com/wp-json/wc-sibooking/v1/send" \
-d '{ "booking_id": 812, "serials": ["SN-0001", "SN-0004"], "notes": "Picked by Sam, both bodies checked" }'
{
"processed": 1,
"items": [
{
"booking_id": 812,
"product_id": 123,
"order_id": 4471,
"quantity": 2,
"serials": [ "SN-0001", "SN-0004" ],
"quantity_sent": 2,
"quantity_returned": 0,
"outstanding": 0
}
],
"failures": [],
"orders_notified": 0
}
outstanding is what is still to go out after the move, so 0 means the booking has left the send queue. Because two serials were named and no quantity was given, the quantity was taken as two.
The rules a move must satisfy #
- You cannot send more units than the booking has left, or take back more than are out. The refusal names the figure you had to work with.
- A serial must belong to the booking’s product and be
Availableto go out, and must actually be out on that booking to come back. A code from another product, or one already out, is refused. - Fewer serials than units is allowed — a partly serialised shipment, where two of the three cameras have labels. More serials than units is refused: a serial cannot leave the building without a unit to leave on.
- The same code twice in one request is refused. Scanning a label twice is the single most likely operator slip, and it would otherwise count as two units.
- Serial codes only have to be unique within a product. If the same code exists on two products, the move only ever touches the one the booking is for.
Moving several bookings at once #
{
"items": [
{ "booking_id": 812, "serials": ["SN-0001", "SN-0004"] },
{ "booking_id": 813, "quantity": 1 },
{ "booking_id": 817, "serials": ["TR-0033"], "notes": "Tripod leg taped" }
],
"notify": true
}
Every row is checked before anything is written. With atomic left at its default, one bad row means nothing at all is recorded and the response is a 409 listing what was wrong with which row — a half-applied dispatch is worse than a rejected one, because the operator cannot tell which half happened.
{
"code": "sibooking_rest_batch_refused",
"message": "1 of 3 item(s) cannot be processed; nothing was recorded.",
"data": {
"status": 409,
"failures": [
{
"index": 2,
"booking_id": 817,
"code": "sibooking_rest_serial_unavailable",
"message": "Serial(s) not available for this product: TR-0033",
"status": 409
}
]
}
}
Send "atomic": false instead and the valid rows are applied, with the rest reported in failures alongside the items that went through. Either way, a booking listed twice in one batch is refused: each row is judged against the state before the batch, so the pair could together send more than the booking has.
What is still out, and taking it back #
GET /return is the mirror image: bookings with units still in customers’ hands, taking the same when, product_id, order_id, search and include_serials parameters. when=overdue is the chase list — hires whose end date has passed with equipment still out. GET /return/counts gives the segment sizes.
Here include_serials means something sharper than on the send queue: it lists the codes actually out on that booking, which is exactly the set a return may draw from.
curl -u "ck_your_key:cs_your_secret" \
-H "Content-Type: application/json" \
-X POST "https://your-store.com/wp-json/wc-sibooking/v1/return" \
-d '{ "booking_id": 812, "serials": ["SN-0001"], "notes": "Lens cap missing, charged" }'
{
"processed": 1,
"items": [
{
"booking_id": 812,
"product_id": 123,
"order_id": 4471,
"quantity": 1,
"serials": [ "SN-0001" ],
"quantity_sent": 2,
"quantity_returned": 1,
"outstanding": 1
}
],
"failures": [],
"orders_notified": 0
}
outstanding is now what is still with the customer. notes is where damage and shortages belong: it is kept on the movement row, so the history shows the condition each unit came back in.
Barcode scanning #
An operator holding a scanner has a serial code, not a booking id. The two scan endpoints close that gap, and they take a deliberately different shape because the two directions are not equally certain.
Scanning something back in #
A serial that is out is out on exactly one booking, so POST /return/scan needs nothing but the code. Point the scanner at the label, book it in, move to the next box.
curl -u "ck_your_key:cs_your_secret" \
-H "Content-Type: application/json" \
-X POST "https://your-store.com/wp-json/wc-sibooking/v1/return/scan" \
-d '{ "code": "SN-0001" }'
{
"action": "returned",
"code": "SN-0001",
"booking_id": 812,
"product_id": 123,
"order_id": 4471,
"quantity_sent": 2,
"quantity_returned": 1,
"outstanding": 1
}
A code that is not out anywhere — never sent, or already booked in — answers 404 serial_not_out, which is the response a scanning app should turn into a beep and a message rather than a silent failure. A serial that has been out several times is matched against its most recent dispatch.
Scanning something out #
Sending is not unambiguous. A camera on the shelf could go out on any booking for that product, so POST /send/scan resolves the booking only when there is exactly one waiting. When there is a choice, it refuses and hands back the candidates for the operator to pick from.
POST /send/scan { "code": "SN-0001" }
{
"code": "sibooking_rest_ambiguous_booking",
"message": "3 bookings are waiting for this product; pass booking_id or order_id.",
"data": {
"status": 409,
"candidates": [
{ "booking_id": 812, "order_number": "4471", "customer": "Dana Whitfield",
"due": "2026-10-03 09:00:00", "overdue": false, "quantity_to_send": 2 },
{ "booking_id": 826, "order_number": "4488", "customer": "Priya Raman",
"due": "2026-10-04 09:00:00", "overdue": false, "quantity_to_send": 1 }
]
}
}
Candidates come back soonest first, so a scanning app can show them as a short list and repeat the call with the chosen booking_id. Passing order_id instead narrows the candidates to one order, which is usually all a picker needs: they are packing a named order anyway.
| Field | Value |
|---|---|
code | Required. The serial code on the label. |
booking_id | Send only. Which booking to put the unit on when several are open. |
order_id | Send only. Narrows the candidates to one order. |
product_id | Only needed when the same code exists on more than one product. |
notes | Kept on the movement row. |
notify | true emails the customer. Default false. |
Both scan routes move exactly one unit, which is what a scan means. For anything else use POST /send or POST /return.
Movement history #
GET /send/history and GET /return/history are the audit trail: every movement ever recorded, newest first, with the serials involved and the staff member who did it.
| Parameter | Value |
|---|---|
booking_id | One booking’s movements. |
product_id | One product’s movements. |
date_from, date_to | Y-m-d, inclusive of both days. |
search | Product name, order number or serial code. |
orderby | datetime (default), id, booking_id or quantity. |
order | desc (default) or asc. |
curl -u "ck_your_key:cs_your_secret" \
"https://your-store.com/wp-json/wc-sibooking/v1/return/history?date_from=2026-10-01&date_to=2026-10-31"
[
{
"id": 3391,
"type": "returned",
"datetime": "2026-10-06 16:12:04",
"quantity": 1,
"serials": [ "SN-0001" ],
"booking_id": 812,
"order_id": 4471,
"product_name": "Canon EOS R6 body",
"quantity_booked": 2,
"start": "2026-10-03 09:00:00",
"end": "2026-10-06 17:00:00",
"user_id": 7,
"user_name": "Sam Okafor"
}
]
Movements recorded in the same second — a batch, or a scanner firing twice — come back in a stable order, so paging through a long history never drops or repeats a row.
For a single booking there is also GET /bookings/{id}/history, which returns its sends and returns together, and GET /bookings/{id}/serials, which lists the codes currently out on it. Both are covered in the bookings and serials reference.
Emailing the customer #
The admin Send and Return screens always email the customer. The API does not, unless you ask it to with "notify": true. The default is silence on purpose: an integration that syncs warehouse state, replays a day’s movements or corrects a mistake would otherwise mail customers every time it ran.
When you do ask for it, notifications are grouped by order the way the admin bulk actions group them — one email per order, listing everything that moved — and orders_notified in the response says how many went out. Bookings with no order behind them (API-created reservations, rental-queue checkouts) are skipped, since there is nobody to write to. The emails themselves are the usual WooCommerce ones, so they respect whatever you have configured under WooCommerce → Settings → Emails.
Errors #
Errors follow the WordPress shape — code, message and a data object carrying the HTTP status — and every code is prefixed sibooking_rest_. The ones specific to these endpoints:
| Code | HTTP | Meaning |
|---|---|---|
nothing_to_send | 409 | Every unit on the booking has already gone out. |
nothing_to_return | 409 | Nothing is out on the booking. |
too_many | 409 | More units than are left; data.remaining or data.out gives the figure. |
serial_count | 400 | More serials than units. |
duplicate_serial | 400 | The same code appears twice in one request. |
serial_unavailable | 409 | A code is not free for this product; data.serials lists which. |
serial_not_out | 409 / 404 | A code is not out on that booking, or (on a scan) not out anywhere. |
serial_not_found | 404 | No serial with that code exists. |
ambiguous_serial | 409 | The code exists on more than one product; pass product_id. |
ambiguous_booking | 409 | Several bookings are waiting; data.candidates lists them. |
no_booking | 409 | Nothing is waiting to go out for that product. |
wrong_product | 409 | The serial belongs to a different product than the booking named. |
duplicate_booking | 409 | One booking appears more than once in a batch. |
batch_refused | 409 | An atomic batch had a bad row; data.failures says which. |
invalid_when | 400 | when is not today, overdue, week, all or a Y-m-d date. |
A move that names a single booking_id always answers with the specific reason it failed. Only a batch is wrapped in batch_refused.
A working scanner loop #
Putting it together, a returns desk needs remarkably little code. Fetch what is overdue, scan boxes as they come off the van, and let the codes find their own bookings.
const API = 'https://your-store.com/wp-json/wc-sibooking/v1';
const AUTH = 'Basic ' + btoa('ck_your_key:cs_your_secret');
// What should have come back already?
const overdue = await fetch(API + '/return?when=overdue&include_serials=true',
{ headers: { Authorization: AUTH } }).then(r => r.json());
// Book in whatever the scanner reads.
async function scanned(code) {
const res = await fetch(API + '/return/scan', {
method: 'POST',
headers: { Authorization: AUTH, 'Content-Type': 'application/json' },
body: JSON.stringify({ code })
});
const body = await res.json();
if (res.ok) return ok(code + ' booked in, ' + body.outstanding + ' still out');
if (body.code === 'sibooking_rest_serial_not_out') return warn(code + ' is not out on any rental');
return warn(body.message);
}
The same loop works on the loading bay with /send/scan, with one extra branch: when the answer is ambiguous_booking, show data.candidates and repeat the call with the booking_id the operator taps.
Trying it out #
The API tester that ships with the plugin lists every endpoint on this page under Send rentals out and Take rentals back, with a filled-in request body for each. It is the quickest way to see the shape of a real response from your own catalogue before writing any code — open it from Rentals → Tools in wp-admin.
Serials themselves — creating them, editing them, retiring damaged units — are covered in Bookings, Availability & Serials.
