View Categories

Sending & Returning Rentals, and Serial Codes

11 min read

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.

ParameterValue
whentoday, overdue, week (the next seven days), all (the default), or a single Y-m-d day.
product_idOnly this product.
order_idOnly this order.
searchOrder number or product name.
include_serialstrue adds the serial codes that may be moved on each row. Left out by default because it costs a query per row.
page, per_pagePaging, 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" ]
  }
]
  • due is the date the row is measured against — the rental start on the send queue, the rental end on the return queue — and overdue says whether that moment has passed.
  • quantity_to_send is what is left, not what was ordered. A booking that is half out stays on the list with the remainder.
  • serials on 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_serials is false when 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.

FieldValue
booking_idThe booking to send. Required unless you are sending items.
quantityHow many units. Defaults to the number of serials given, or 1.
serialsThe codes going out, as an array. Omit on a product without serial tracking.
notesFree text kept on the movement row — who picked it, what condition it left in.
itemsAn array of { booking_id, quantity, serials, notes } objects to move together.
notifytrue emails the customer, exactly as the admin screen does. Default false.
atomicBatches 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 Available to 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.

FieldValue
codeRequired. The serial code on the label.
booking_idSend only. Which booking to put the unit on when several are open.
order_idSend only. Narrows the candidates to one order.
product_idOnly needed when the same code exists on more than one product.
notesKept on the movement row.
notifytrue 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.

ParameterValue
booking_idOne booking’s movements.
product_idOne product’s movements.
date_from, date_toY-m-d, inclusive of both days.
searchProduct name, order number or serial code.
orderbydatetime (default), id, booking_id or quantity.
orderdesc (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:

CodeHTTPMeaning
nothing_to_send409Every unit on the booking has already gone out.
nothing_to_return409Nothing is out on the booking.
too_many409More units than are left; data.remaining or data.out gives the figure.
serial_count400More serials than units.
duplicate_serial400The same code appears twice in one request.
serial_unavailable409A code is not free for this product; data.serials lists which.
serial_not_out409 / 404A code is not out on that booking, or (on a scan) not out anywhere.
serial_not_found404No serial with that code exists.
ambiguous_serial409The code exists on more than one product; pass product_id.
ambiguous_booking409Several bookings are waiting; data.candidates lists them.
no_booking409Nothing is waiting to go out for that product.
wrong_product409The serial belongs to a different product than the booking named.
duplicate_booking409One booking appears more than once in a batch.
batch_refused409An atomic batch had a bad row; data.failures says which.
invalid_when400when 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.