- Base URL and sign-in
- Who may do what
- Condition photos
- POST /wc-sibooking/v1/serials/{id}/photos
- GET /wc-sibooking/v1/serials/{id}/photos
- GET /wc-sibooking/v1/bookings/{id}/photos
- GET /wc-sibooking/v1/photos/{id}
- GET /wc-sibooking/v1/photos/{id}/file
- PATCH /wc-sibooking/v1/photos/{id}
- DELETE /wc-sibooking/v1/photos/{id}
- POST /wc-sibooking/v1/photos/{id}/restore
- Reading sets
- GET /wc-sibooking/v1/reading-templates
- GET /wc-sibooking/v1/reading-sets
- POST /wc-sibooking/v1/reading-sets
- GET /wc-sibooking/v1/reading-sets/{id}
- PATCH /wc-sibooking/v1/reading-sets/{id}
- DELETE /wc-sibooking/v1/reading-sets/{id}
- POST /wc-sibooking/v1/reading-sets/{id}/fields
- PATCH /wc-sibooking/v1/reading-sets/{id}/fields/{key}
- DELETE /wc-sibooking/v1/reading-sets/{id}/fields/{key}
- Which sets apply to a serial
- Readings
- Readings and photos on a send or return
- Hooks
The REST API for condition photos and meter readings: the photos staff take of a unit, the reading sets that say what to record for it (hour meters, odometers, fuel, checks), the readings themselves and the usage of each rental. They give an outside system the same data and the same rules as the Serials settings, the serial page and the Send and Return screens, so a yard app, a telematics feed or a damage-claims tool can record and read readings and photos. The setup and the conventions shared by every endpoint (keys, errors, pagination) are in REST API overview and authentication.
Base URL and sign-in #
| Namespace | What lives there | Needs |
|---|---|---|
/wp-json/ | Condition photos, reading sets and their fields, which sets apply to a product or serial, readings, rental usage | Rentals & Bookings Regular 1.0.72 or newer |
wc-sibooking/v1is awc-namespace, so it takes a WooCommerce REST API key (WooCommerce › Settings › Advanced › REST API) sent as HTTP Basic, like the other rental endpoints. Give the key Read/Write permission to change anything. A WordPress Application Password (Users › Profile › Application Passwords) works too, as does the logged-in cookie with anX-WP-Nonceheader inside wp-admin.- Send bodies as JSON (
Content-Type: application/json). A photo upload can also bemultipart/form-data. - Readings answer
recorded_atin the store’s time zone with its offset (2026-11-10T09:00:00+11:00) andrecorded_at_gmtin UTC (2026-11-09T22:00:00Z). A time you send with an offset orZis used as given; a time without one is store time. Photos answer UTC times (taken_at_gmt,uploaded_at_gmt) plus the same times formatted for people. - Errors use the WordPress shape
{"code", "message", "data": {"status"}}. The codes on this page start withsibooking_rest_; reading errors also name thefieldand theserial_codeindata. A missing or invalid parameter answers400withrest_ormissing_ callback_ param rest_invalid_param. - Lists take
pageandper_page(default 20, at most 200) and sendX-WP-TotalandX-WP-TotalPagesheaders. - Serial ids come from
GET /wc-sibooking/and booking ids fromv1/ serials GET /wc-sibooking/(see Bookings, availability and serials endpoints). Deleting a serial that has readings or photos needsv1/ bookings force=truethere (409 sibooking_otherwise), and its readings and photos are deleted with it.rest_ serial_ has_ readings
STORE="https://example.com/wp-json"
curl -u "ck_your_key:cs_your_secret" "$STORE/wc-sibooking/v1/serials/123/readings/current"
Who may do what #
| Ability | Who has it | Opens |
|---|---|---|
manage_woocommerce | Shop managers and administrators | Every endpoint on this page |
A request without it gets 401 (not signed in) or 403, both with sibooking_. Two photo actions check again in the model: deleting a photo permanently needs the photos capability (manage_woocommerce unless a site changes it with the sibooking_ filter), and moving a photo to the trash or restoring it needs that capability or being the person who uploaded it. Otherwise these answer 403 sibooking_.
Condition photos #
A condition photo is a picture of one serial (one unit) with a note and a context: condition, damage, service, send, return or other. It can be linked to a booking (and so to its order), to the send or return it was taken at, to a reading (the dashboard that proves an odometer value) and to a photo field of a reading set. Photos are private: they are stored outside the Media Library, customers never see them, and they reach a client only through these endpoints or a signed link.
Every upload is checked from its bytes, not its file name. JPEG, PNG and WebP are accepted, and HEIC/HEIF where the server’s image library can read it; at most 15 MB and 40 megapixels (filters sibooking_ and sibooking_). The store keeps a master (long edge at most 2048 px, JPEG at 85%) and a 320 px thumbnail, both turned upright, so every photo is served as JPEG. The uploaded file itself is kept only when “Also keep the full-resolution file” is ticked in the photo settings (Rentals › Settings › Serials). The date a camera wrote into a JPEG’s EXIF becomes taken_at_gmt.
| Status | Meaning |
|---|---|
pending | Uploaded with pending: true, waiting to be named on a send or return. Still pending after 24 hours: removed by the daily clean-up |
attached | On the serial’s record |
trashed | In the trash; can be restored for 30 days, then deleted permanently |
deleted | Deleted permanently: the files are gone, the record stays with who deleted it, when and why |
A photo has:
| Field | Notes |
|---|---|
id, serial_id, product_id | The photo, the unit and its product |
status, context, note | Values above; note is "" when there is none |
booking_id, order_id, send_id, return_id | The rental it belongs to and the send or return it was taken at, or null |
reading_id, field_id | The reading it proves and the photo field it was taken for, or null |
width, height, bytes | Of the stored master |
keep_evidence | true: never removed by the store’s retention setting (“delete photos older than N months”) |
taken_at_gmt, taken_at_source | When it was taken (UTC) and where that came from: exif (the camera) or device (the taken_at you sent); null when unknown |
uploaded_at_gmt | When it was uploaded (UTC) |
taken_display, uploaded_display | The same times in the store’s time zone and date format, for people |
uploaded_by, uploaded_by_name | The user who uploaded it |
deleted_at_gmt, delete_reason | Set on a permanently deleted photo |
thumb_url, master_url | Only while pending or attached. Signed links for the user who made the request, good for a day or so. They open only in a browser that is signed in to WordPress as that user; an API client gets 403 from them and should use GET /photos/{id}/file instead |
POST /wc-sibooking/v1/serials/{id}/photos #
Uploads a photo of a serial. Send the file as multipart field file, or send JSON with the bytes in data.
| Parameter | Notes |
|---|---|
file | Multipart: the image file |
data, filename | JSON: the image as base64 (a data:image/ prefix is allowed) and its file name (default photo.jpg) |
context | condition (default), damage, service, send, return or other. Any other value is stored as condition |
note | Free text |
booking_id | Links the photo to a booking and its order; ignored when the booking does not exist |
reading_id | Links the photo to a reading |
field | The key of a photo field that applies to the serial (for example photos) |
taken_at | When it was taken, ISO 8601 (no offset = store time). Ignored when it cannot be read or is in the future, and when the JPEG has its own EXIF date |
pending | true: upload now and name the photo on a send or return later (below). Default false: attached at once |
client_ref | Your own id for the upload, up to 64 characters. Sending the same client_ref for the same serial again answers the first photo (200 instead of 201) instead of storing a second one, so a retry after a dropped connection is safe |
Answers 201 with the photo.
| Error | When |
|---|---|
404 sibooking_ | No serial with that id |
400 sibooking_ | Neither file nor data was sent |
415 sibooking_ | Not a JPEG, PNG or WebP (or HEIC the server can’t read), an empty file, or data that is not base64 |
413 sibooking_ | Over 15 MB |
413 sibooking_ | Over 40 megapixels |
400 sibooking_ | field is not a photo field of this serial (data.field) |
500 sibooking_ | The server could not read or resize the image |
curl -X POST -u "ck_your_key:cs_your_secret" \
-F "[email protected]" \
-F "context=damage" \
-F "note=Scratch on the rear door" \
-F "booking_id=456" \
"$STORE/wc-sibooking/v1/serials/123/photos"
curl -X POST -u "ck_your_key:cs_your_secret" \
-H "Content-Type: application/json" \
-d '{"filename": "dashboard.jpg", "data": "/9j/4AAQSkZJRgABAQ...", "field": "photos", "pending": true, "client_ref": "yard-app-7781"}' \
"$STORE/wc-sibooking/v1/serials/123/photos"
{
"id": 912,
"serial_id": 123,
"product_id": 789,
"status": "attached",
"context": "damage",
"note": "Scratch on the rear door",
"booking_id": 456,
"order_id": 1201,
"send_id": null,
"return_id": null,
"reading_id": null,
"field_id": null,
"width": 2048,
"height": 1536,
"bytes": 412883,
"keep_evidence": false,
"taken_at_gmt": "2026-11-09T22:15:00Z",
"taken_at_source": "exif",
"uploaded_at_gmt": "2026-11-11T02:21:26Z",
"taken_display": "November 10, 2026 9:15 am",
"uploaded_display": "November 11, 2026 1:21 pm",
"uploaded_by": 14,
"uploaded_by_name": "Sarah Miller",
"deleted_at_gmt": null,
"delete_reason": null,
"thumb_url": "https://example.com/?sib_photo=912&size=thumb&e=...&t=...",
"master_url": "https://example.com/?sib_photo=912&size=master&e=...&t=..."
}
GET /wc-sibooking/v1/serials/{id}/photos #
A serial’s photos, newest first (by when taken, or when uploaded if that is unknown).
| Parameter | Notes |
|---|---|
status | attached (default), pending, trashed, deleted or any |
context | One context |
booking_id, send_id, return_id | Only photos linked to that booking, send or return |
since, until | Uploaded from / up to, ISO 8601 (no offset = store time). A value that cannot be read is ignored |
page, per_page | Default 1 and 20; per_page at most 200 |
Answers an array of photos. 404 sibooking_ when the serial does not exist.
GET /wc-sibooking/v1/bookings/{id}/photos #
The attached photos linked to a booking, from every serial on it. Takes page and per_page. A booking without photos (or an unknown id) answers an empty array.
GET /wc-sibooking/v1/photos/{id} #
One photo, whatever its status. 404 sibooking_ when there is no photo with that id.
GET /wc-sibooking/v1/photos/{id}/file #
The image itself, not JSON.
| Parameter | Notes |
|---|---|
size | master (default), thumb or original |
Answers the bytes with Content-Type: image/jpeg (an original keeps its own type: image/png, image/webp, image/heic or image/heif), Content-Disposition: inline; filename= and Cache-Control: private, max-age=86400. 404 sibooking_ when the photo is trashed or deleted, when size=original and the original was not kept, or when the file is missing from storage.
curl -u "ck_your_key:cs_your_secret" -o photo-912.jpg \
"$STORE/wc-sibooking/v1/photos/912/file?size=master"
PATCH /wc-sibooking/v1/photos/{id} #
Changes a photo (PUT works the same). Send any of:
| Field | Notes |
|---|---|
note | "" clears it |
context | One of the six contexts |
booking_id, reading_id | Re-link it; 0 removes the link. order_id does not change |
keep_evidence | true keeps it out of the retention clean-up |
Answers the photo. 400 sibooking_ for an unknown context, 404 sibooking_ for an unknown id.
DELETE /wc-sibooking/v1/photos/{id} #
Moves a photo to the trash, or deletes it permanently.
| Parameter | Notes |
|---|---|
force | true deletes permanently: the files go now and the record stays as deleted. Default false: to the trash |
reason | Required with force=true, up to 255 characters |
Answers the photo with its new status (trashed or deleted).
| Error | When |
|---|---|
404 sibooking_ | Unknown id, or already deleted permanently |
400 sibooking_ | force=true without a reason |
403 sibooking_ | See Who may do what |
curl -X DELETE -u "ck_your_key:cs_your_secret" \
"$STORE/wc-sibooking/v1/photos/912?force=true&reason=Wrong%20unit"
POST /wc-sibooking/v1/photos/{id}/restore #
Takes a photo out of the trash; it comes back as attached. Answers the photo. 404 sibooking_ when the photo is not in the trash.
Reading sets #
Readings are what staff record about a unit: an hour meter, an odometer, a fuel level, a yes/no check, a short note, photos. On screen they are Serial custom fields (Rentals › Settings › Serials, see Serial custom fields and field sets). The fields come in reading sets (“field sets” on screen): named groups such as Vehicle meter (odometer, fuel, interior clean, photos) that apply to every serial of the products they are given to. Each field says whether it is asked for at send, at return or both, and whether it is required there. Any field can also be recorded on its own, for an inspection or a service.
| Type | Holds |
|---|---|
meter | A counter that only goes up: hours, miles, cycles. Usage is measured between readings |
number | Any number within an optional min / max: battery %, a count |
choice | One of two or more options: fuel E, ¼, ½, ¾, F |
yesno | Yes or no |
text | Up to 500 characters |
photo | Photos, uploaded with the photo endpoints; never typed |
A field definition, as you send it when creating a set or a field:
| Field | Notes |
|---|---|
label | Required |
key | Lowercase a-z, 0-9 and _, up to 60 characters, unique within the set; other characters become _. Made from the label when left out (Oil level OK becomes oil_level_ok). Readings are sent and returned by key |
type | One of the types above; default meter |
unit | Meter and number only, up to 20 characters (mi, km, h, %, L and so on) |
decimals | Meter and number only, 0 to 3; default 0 |
capture | Where it is asked for: an array of send and return. Default ["return"]. capture_send and capture_return (true/false) do the same one side at a time |
required | An array of send and return, only where it is captured. Or required_send and required_return |
min, max | Meter and number only |
rollover_at | Meter only: the value where the counter wraps back to zero (10000 on a 4-digit hour meter). Above zero |
max_per_day | Meter only: usage per day above this gives a warning when a reading is recorded (it is still recorded) |
options | Choice only, at least two: ["Empty", "Half", "Full"] or [{"key": "half", "label": "½"}]. Labels up to 60 characters; keys are made from labels when left out |
min_photos | Photo only, 0 to 20: how many photos a required photo field needs |
hint | Help text under the field, up to 255 characters |
sort_order | Position in the set; new fields go last |
status | active (default) or archived (kept in the history, no longer asked for) |
A field in a response has id, set_id, key, label, type, unit, decimals, capture and required (arrays), min, max, rollover_at, max_per_day (numbers or null), hint, sort_order and status, plus options on a choice field and min_photos on a photo field. Inside a set from GET, POST or PATCH /reading-sets/{id} each field also has readings, how many readings it has (voided ones included).
Once a field has readings (or a photo field has photos), its type, unit and key can no longer change and decimals can only go up, because the history would no longer mean the same thing. It can’t be deleted either; archive it instead. The same goes for a set: a set with readings can only be archived.
GET /wc-sibooking/v1/reading-templates #
The ready-made sets a new set can start from, each with slug, name, industry, description and fields (as above, with id and set_id 0). The slugs in 1.0.89: vehicle-meter, e-bike, engine-hours, marine-engine, generator, camera-body, projector, drone, flight-battery, condition-check, concentrator, copier-meter, rv and hours-and-wear.
GET /wc-sibooking/v1/reading-sets #
Every set (no paging), each with id, slug, name, description, status, sort_order, fields and products (the ids of the products that have it as a default set).
| Parameter | Notes |
|---|---|
status | active (default), archived or any |
include_fields | Default true; false leaves fields out. Archived fields are included |
POST /wc-sibooking/v1/reading-sets #
Creates a set, with its fields or from a template. Every field is checked before anything is saved, so a refused request creates nothing.
| Parameter | Notes |
|---|---|
name | Required unless template is given |
slug | Made from the name when left out (a taken slug then gets -2, -3); a taken slug you send answers 409 |
description | Free text |
fields | Array of field definitions |
template | A slug from GET /reading-templates: the set gets the template’s fields. name, slug and description override the template’s |
Answers 201 with the set: its fields (each with readings) and products.
| Error | When |
|---|---|
400 sibooking_ | No name |
409 sibooking_ | The slug you sent is taken |
400 sibooking_ | A field definition is wrong: no label, an unknown type, a choice with fewer than two options, min above max, a limit that is not a number, a rollover_at of zero or less (data.field names it) |
409 sibooking_ | Two fields share a key |
404 sibooking_ | No template with that slug |
curl -X POST -u "ck_your_key:cs_your_secret" \
-H "Content-Type: application/json" \
-d '{"name": "Generator", "fields": [{"key": "engine_hours", "label": "Engine hours", "type": "meter", "unit": "h", "decimals": 1, "capture": ["send", "return"], "required": ["return"], "rollover_at": 10000, "max_per_day": 24}, {"label": "Oil level OK", "type": "yesno"}, {"key": "fuel_level", "label": "Fuel level", "type": "choice", "options": ["Empty", "Half", "Full"], "capture": ["send", "return"]}]}' \
"$STORE/wc-sibooking/v1/reading-sets"
curl -X POST -u "ck_your_key:cs_your_secret" \
-H "Content-Type: application/json" \
-d '{"template": "vehicle-meter", "name": "Vans"}' \
"$STORE/wc-sibooking/v1/reading-sets"
GET /wc-sibooking/v1/reading-sets/{id} #
One set with all its fields, archived ones included, each with readings, and products. 404 sibooking_ for an unknown id.
PATCH /wc-sibooking/v1/reading-sets/{id} #
Changes a set (PUT works the same): any of name, slug, description and status (active or archived). An archived set no longer applies to any serial; its product and serial assignments are kept and come back when it is made active again. Answers the set.
| Error | When |
|---|---|
400 sibooking_ | An empty name or slug |
409 sibooking_ | The slug is taken |
409 sibooking_ | Making it active again would give some serial two fields with the same key |
404 sibooking_ | Unknown id |
DELETE /wc-sibooking/v1/reading-sets/{id} #
Deletes a set that was never used, with its fields and assignments. Answers {"deleted": true, "previous": {set}}. 409 sibooking_ (with data.readings) when any of its fields has readings or photos: archive it instead.
POST /wc-sibooking/v1/reading-sets/{id}/fields #
Adds a field: the body is one field definition. Answers 201 with the field.
| Error | When |
|---|---|
400 sibooking_ | The definition is wrong |
409 sibooking_ | The set already has that key |
409 sibooking_ | Another set that applies to some of the same serials already has that key (data.sets) |
404 sibooking_ | Unknown set |
curl -X POST -u "ck_your_key:cs_your_secret" \
-H "Content-Type: application/json" \
-d '{"key": "damage_notes", "label": "Damage notes", "type": "text", "capture": ["return"], "hint": "Scratches, dents, missing parts"}' \
"$STORE/wc-sibooking/v1/reading-sets/3/fields"
PATCH /wc-sibooking/v1/reading-sets/{id}/fields/{key} #
Changes a field (PUT works the same). Send any keys of the definition; what you leave out keeps its value. {"status": "archived"} archives it. Answers the field.
| Error | When |
|---|---|
404 sibooking_ | The set has no field with that key (data.field) |
409 sibooking_ | The field has readings and the change would alter its type, unit or key, or lower its decimals (data.readings) |
400 sibooking_ | The changed definition is wrong |
409 sibooking_, 409 sibooking_ | A new key that is taken, as above |
DELETE /wc-sibooking/v1/reading-sets/{id}/fields/{key} #
Deletes a field that has never been used. Answers {"deleted": true, "previous": {field}}. 409 sibooking_ when it has readings or photos: archive it instead.
Which sets apply to a serial #
A serial carries the sets of its product (for a variation, the parent product’s sets too), plus the sets added for that serial alone, minus the sets turned off for it. Only active sets count. Two sets that both have a field with the same key can never apply to the same serial, because readings are sent by key: such a change answers 409 sibooking_ with the key in data.field and the two set ids in data.sets.
GET /wc-sibooking/v1/products/{id}/reading-sets #
A product’s default sets and what each of its serials (and its variations’ serials) ends up with.
{"product_id": 789, "set_ids": [3],
"serials": [{"serial_id": 123, "effective": [3, 5], "excluded": []},
{"serial_id": 124, "effective": [], "excluded": [3]}]}
set_ids lists the active sets given to this product itself. 404 sibooking_ for an unknown id.
PUT /wc-sibooking/v1/products/{id}/reading-sets #
Replaces the product’s default sets: set_ids (required), an array of set ids; [] removes them all. New serials of the product get these sets too. Answers the same as GET. 404 sibooking_, 404 sibooking_ (a set id that does not exist) or 409 sibooking_.
curl -X PUT -u "ck_your_key:cs_your_secret" \
-H "Content-Type: application/json" \
-d '{"set_ids": [3]}' \
"$STORE/wc-sibooking/v1/products/789/reading-sets"
GET /wc-sibooking/v1/serials/{id}/reading-sets #
The sets that apply to one serial and where each comes from.
{"serial_id": 123,
"effective": [{"id": 3, "name": "Vehicle meter", "source": "product"},
{"id": 5, "name": "Generator", "source": "serial"}],
"product": [3], "include": [5], "exclude": []}
source is product or serial (added for this serial). product lists the product’s sets, include and exclude the serial’s own changes. 404 sibooking_ for an unknown serial.
PUT /wc-sibooking/v1/serials/{id}/reading-sets #
Replaces the serial’s own changes: include (sets added for this serial) and exclude (product sets turned off for it), both arrays of set ids, default []. So {} takes the serial back to its product’s sets. Answers the same as GET.
| Error | When |
|---|---|
400 sibooking_ | The same set is in both lists |
404 sibooking_ | Unknown serial or set |
409 sibooking_ | Two sets would share a field key on this serial |
Readings #
The readings of a serial are a log that is only ever added to. A correction adds a new reading that replaces the old one, and voiding marks a reading as void with who did it and why; both stay in the history, because a reading can be the evidence in a damage or overage dispute. For a meter, every reading carries delta, the usage since the previous valid reading, and the serial’s lifetime usage is the sum of the deltas.
A reading has:
| Field | Notes |
|---|---|
id, serial_id, product_id | The reading, the unit and its product |
field, field_id, unit | The field’s key, id and unit |
value | In the field’s type: a number (meter, number), true/false (yesno), the option key (choice) or text |
delta | Meter only: usage since the previous valid reading; null for the first reading of a meter |
kind | See below |
context | send, return (recorded on a move), manual, inspection or service |
booking_id, order_id, send_id, return_id | The rental and the move it belongs to, or null |
recorded_at, recorded_at_gmt | When the value was true (store time with offset, and UTC) |
created_at_gmt | When it was entered (UTC) |
user_id, source | Who recorded it and from where: admin (the screens), rest or scan |
client_ref | The client_ref of the request with the row’s position added (yard-app-7781:0) |
note | The note, or the reason on a correction |
supersedes_id | On a correction: the reading it replaces |
voided, voided_at_gmt, voided_by, void_reason | Set when the reading was voided or corrected |
old_final | Only on a meter_swap: the old meter’s final reading |
| Kind | Meaning |
|---|---|
baseline | The first reading of a meter: the starting point, no usage |
reading | An ordinary value |
rollover | The meter wrapped past its rollover_at; delta counts across the wrap |
meter_swap | A replaced meter: value is the new meter’s starting value, old_final the old one’s last value. Followed by a reading on the new meter |
correction | Replaces the reading in supersedes_id |
skipped | A required field skipped at a send or return, with the reason in note |
POST /wc-sibooking/v1/serials/{id}/readings #
Records readings outside a rental move: a yard inspection, a service, a telematics update.
| Parameter | Notes |
|---|---|
values | Required. An object by field key: {"odometer": 48522, "fuel": "half"}. A value can also be an object {value, event, old_final, new_start, note} (below). Photo fields are skipped: upload photos instead |
context | manual (default), inspection or service |
recorded_at | When the values were read, ISO 8601 (no offset = store time). Default now; at most 5 minutes in the future. An earlier time records a back-dated reading |
note | Stored on each reading, unless the value object has its own note |
booking_id | Links the readings to a booking and its order; ignored when the booking does not exist |
client_ref | Your own id for the request, up to 56 characters. Sending it again for the same serial answers the readings of the first request (200, replayed: true) instead of recording them twice |
How values are read:
| Type | Send |
|---|---|
meter, number | A JSON number, or a string like "1234.5" (dot decimal, no thousands separator), with at most the field’s decimals and within min / max |
choice | The option key, or its label (case does not matter): "half" or "½" |
yesno | true/false, 1/0, or "yes"/"no" and the other common spellings |
text | Up to 500 characters |
A meter never goes backwards on its own. A value lower than the last reading answers 409 sibooking_ (with data.last_value and data.last_recorded_at) unless you say why with event:
| Event | Send | Notes |
|---|---|---|
rollover | {"value": 12.5, "event": "rollover"} | The counter wrapped. The field needs a rollover_at, and the value must be below it and below the last reading |
meter_swap | {"value": 3, "event": "meter_swap", "old_final": 1250, "new_start": 0} | The meter was replaced. new_start is the new meter’s starting value (default 0), old_final the old meter’s last value (optional; when given, the usage up to it is counted) |
Either event is accepted only as the latest reading. A back-dated reading must fit between the readings before and after it (400 sibooking_). Usage above the field’s max_per_day is recorded, with an implausible_reading warning.
Answers 201:
{
"serial_id": 123,
"readings": {
"odometer": {
"id": 5012, "serial_id": 123, "product_id": 789,
"field": "odometer", "field_id": 41, "value": 48522, "unit": "mi",
"delta": 312, "kind": "reading", "context": "inspection",
"booking_id": null, "order_id": null, "send_id": null, "return_id": null,
"recorded_at": "2026-11-10T09:00:00+11:00",
"recorded_at_gmt": "2026-11-09T22:00:00Z",
"created_at_gmt": "2026-11-11T02:19:50Z",
"user_id": 14, "source": "rest", "client_ref": "yard-app-7781:0",
"note": "Yard check", "supersedes_id": null,
"voided": false, "voided_at_gmt": null, "voided_by": null, "void_reason": null
},
"fuel": { "...": "..." }
},
"warnings": [],
"replayed": false
}
A warning looks like {"code": "implausible_.
| Error | When |
|---|---|
404 sibooking_ | No serial with that id |
400 sibooking_ | No field anywhere has that key |
400 sibooking_ | The field exists but does not apply to this serial |
400 sibooking_ | A value of the wrong form: not a number, too many decimals, not one of the options, not yes/no, text over 500 characters, an unknown event, a roll-over on a field without rollover_at |
400 sibooking_ | Outside min / max, a negative meter, or a roll-over value at or above rollover_at |
409 sibooking_ | A meter lower than its last reading without an event. With meter_swap, a value below new_start or an old_final below the last reading answers the same code with 400 |
400 sibooking_ | A back-dated value that does not fit between its neighbors (data.next_value), or an event that is not the latest reading |
400 sibooking_ | Nothing to record (empty values, or only photo fields) |
400 sibooking_ | recorded_at can’t be read |
400 sibooking_ | recorded_at is in the future |
curl -X POST -u "ck_your_key:cs_your_secret" \
-H "Content-Type: application/json" \
-d '{"values": {"odometer": 48522, "fuel": "half", "interior_clean": true}, "context": "inspection", "recorded_at": "2026-11-10T09:00:00", "note": "Yard check", "client_ref": "yard-app-7781"}' \
"$STORE/wc-sibooking/v1/serials/123/readings"
curl -X POST -u "ck_your_key:cs_your_secret" \
-H "Content-Type: application/json" \
-d '{"values": {"engine_hours": {"value": 3, "event": "meter_swap", "old_final": 1250, "new_start": 0}}, "context": "service", "note": "Hour meter replaced"}' \
"$STORE/wc-sibooking/v1/serials/123/readings"
GET /wc-sibooking/v1/serials/{id}/readings #
A serial’s log, newest first (by recorded_at).
| Parameter | Notes |
|---|---|
field | A field key. Also finds readings recorded under a set that no longer applies to the serial; a key that no field has answers an empty list |
context | send, return, manual, inspection, service or import |
booking_id | Readings linked to that booking |
since, until | Recorded from / up to, ISO 8601 or Y-m-d (a date alone means the start of that day for since, the end of it for until); no offset = store time |
include_voided | Default false; true adds voided and corrected readings |
page, per_page | Default 1 and 20; per_page at most 200 |
Answers an array of readings. 404 sibooking_ for an unknown serial, 400 sibooking_ when since or until can’t be read.
GET /wc-sibooking/v1/serials/{id}/readings/current #
The latest valid value of every field the serial carries (photo fields left out), in set and field order:
[
{"field": "odometer", "label": "Odometer", "type": "meter", "unit": "mi",
"value": 52000, "lifetime": 3790, "reading_id": 5016,
"recorded_at": "2026-11-11T13:20:14+11:00", "recorded_at_gmt": "2026-11-11T02:20:14Z"},
{"field": "fuel", "label": "Fuel", "type": "choice", "unit": "",
"value": "half", "lifetime": null, "reading_id": 5013, "...": "..."},
{"field": "fuel_level", "label": "Fuel level", "type": "choice", "unit": "",
"value": null, "lifetime": null, "reading_id": null,
"recorded_at": null, "recorded_at_gmt": null}
]
lifetime is a meter’s total usage over all its readings. A field with nothing recorded yet has null values. 404 sibooking_ for an unknown serial.
PATCH /wc-sibooking/v1/readings/{id} #
Corrects a reading (PUT works the same): a new correction reading takes its place, with the same context, booking, move and recorded time, and the original is voided with your reason. Photos linked to the original move to the correction.
| Field | Notes |
|---|---|
value | Required: the right value, in the same forms as when recording |
reason | Required, up to 255 characters |
Answers the new reading (kind correction, supersedes_id = the original).
| Error | When |
|---|---|
404 sibooking_ | Unknown reading, or its field was deleted |
400 sibooking_ | reason is empty |
409 sibooking_ | The reading was already corrected or voided: correct the newest one |
400 sibooking_ | A meter_swap reading (void it and record it again instead), or no value |
| Value errors | As when recording: invalid_reading, reading_out_of_range, reading_decreased, reading_ |
curl -X PATCH -u "ck_your_key:cs_your_secret" \
-H "Content-Type: application/json" \
-d '{"value": 49100, "reason": "Typo: the dashboard showed 49,100"}' \
"$STORE/wc-sibooking/v1/readings/5016"
DELETE /wc-sibooking/v1/readings/{id} #
Voids a reading: it stays in the log with voided: true, and a meter’s next reading is measured from the reading before it. reason is required (in the query string or the JSON body), up to 255 characters. Answers {"voided": true, "previous": {reading}} with the voided reading. 404 sibooking_, 400 sibooking_ or 409 sibooking_.
curl -X DELETE -u "ck_your_key:cs_your_secret" \
"$STORE/wc-sibooking/v1/readings/5013?reason=Entered%20on%20the%20wrong%20unit"
GET /wc-sibooking/v1/bookings/{id}/usage #
How much each meter moved during one rental, and what non-meter fields read at send and at return, for every serial with readings on the booking.
{
"booking_id": 456,
"order_id": 1201,
"serials": {
"VAN-07": {
"odometer": {"field": "odometer", "label": "Odometer", "type": "meter", "unit": "mi",
"usage": 312, "basis": "send", "send": 48210, "return": 48522},
"fuel": {"field": "fuel", "label": "Fuel", "type": "choice", "unit": "",
"usage": null, "basis": null, "send": "f", "return": "half"}
}
},
"totals": {"odometer": 312}
}
| Field | Notes |
|---|---|
serials | By serial code, then by field key |
usage | Meters only: the usage after the send reading up to the return reading (or the latest reading while the unit is still out), so an inspection, a roll-over or a replaced meter during the rental are counted and driving between rentals is not. null when it can’t be measured |
basis | send: measured from the send reading (exact). previous_reading: no send reading, so the return reading’s own delta (an estimate). null: unknown |
send, return | The values recorded at the send and the return of this booking, or null |
totals | Usage per field over all the booking’s serials (a unit swapped during the rental adds both) |
404 sibooking_ for an unknown booking.
Readings and photos on a send or return #
The move endpoints in Sending and returning rentals, and serial codes record readings and attach photos as part of the move: POST /wc-sibooking/v1/send and /return (at the top level or in each items[] row), POST /wc-sibooking/ and /return, and the scans POST /wc-sibooking/ and /return/scan. They take three more fields:
| Field | Notes |
|---|---|
readings | By serial code, then field key: {"VAN-07": {"odometer": 48522, "fuel": "half"}}. On a scan, by field key only. Values as in POST /serials/; a required field that can’t be read takes {"skip": "Gauge broken"} |
photos | Photo ids by serial code: {"VAN-07": [912, 913]}; on a scan, a plain list. Each photo must have been uploaded for that serial with pending: true and not used yet. An entry can also be {"id": 912, "field": "photos", "note": "...", "reading": "odometer"} to tag it to a photo field, add a note or link it to that field’s reading in the same move. At most 20 per serial per move (filter sibooking_) |
strict_readings | true refuses the move when a required field is missing (400 sibooking_, or sibooking_ when only photos are missing). Default false: each missing field is a reading_missing warning and the move goes through |
The readings are recorded with context send or return and linked to the booking, its order and the move, which is what GET /bookings/{id}/usage measures. A refused reading refuses the whole move. When the request had readings or photos, or there are warnings, the answer adds readings (by serial code and field key: reading_id, value, unit, and on a return usage and basis for a meter, out for a choice), photos (the attached ids by serial code) and warnings. GET /send and GET /return take include_readings=true to add readings_context to each row (on a return, the fields of each serial out with their last values; on a send, the product’s fields under *), and a scan with dry_run: true answers the fields for that serial without moving anything.
Hooks #
The endpoints use the same code as the screens, so these actions fire for API changes too: sibooking_, sibooking_, sibooking_, sibooking_ and sibooking_. Filters: sibooking_, sibooking_, sibooking_photo_store (where photos are stored), sibooking_, sibooking_ and sibooking_.
