View Categories

REST API: condition photos and meter readings

27 min read

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 #

NamespaceWhat lives thereNeeds
/wp-json/wc-sibooking/v1/Condition photos, reading sets and their fields, which sets apply to a product or serial, readings, rental usageRentals & Bookings Regular 1.0.72 or newer
  • wc-sibooking/v1 is a wc- 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 an X-WP-Nonce header inside wp-admin.
  • Send bodies as JSON (Content-Type: application/json). A photo upload can also be multipart/form-data.
  • Readings answer recorded_at in the store’s time zone with its offset (2026-11-10T09:00:00+11:00) and recorded_at_gmt in UTC (2026-11-09T22:00:00Z). A time you send with an offset or Z is 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 with sibooking_rest_; reading errors also name the field and the serial_code in data. A missing or invalid parameter answers 400 with rest_missing_callback_param or rest_invalid_param.
  • Lists take page and per_page (default 20, at most 200) and send X-WP-Total and X-WP-TotalPages headers.
  • Serial ids come from GET /wc-sibooking/v1/serials and booking ids from GET /wc-sibooking/v1/bookings (see Bookings, availability and serials endpoints). Deleting a serial that has readings or photos needs force=true there (409 sibooking_rest_serial_has_readings otherwise), and its readings and photos are deleted with it.
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 #

AbilityWho has itOpens
manage_woocommerceShop managers and administratorsEvery endpoint on this page

A request without it gets 401 (not signed in) or 403, both with sibooking_rest_forbidden. 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_photos_capability 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_rest_forbidden.

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_photo_max_bytes and sibooking_photo_max_pixels). 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.

StatusMeaning
pendingUploaded with pending: true, waiting to be named on a send or return. Still pending after 24 hours: removed by the daily clean-up
attachedOn the serial’s record
trashedIn the trash; can be restored for 30 days, then deleted permanently
deletedDeleted permanently: the files are gone, the record stays with who deleted it, when and why

A photo has:

FieldNotes
id, serial_id, product_idThe photo, the unit and its product
status, context, noteValues above; note is "" when there is none
booking_id, order_id, send_id, return_idThe rental it belongs to and the send or return it was taken at, or null
reading_id, field_idThe reading it proves and the photo field it was taken for, or null
width, height, bytesOf the stored master
keep_evidencetrue: never removed by the store’s retention setting (“delete photos older than N months”)
taken_at_gmt, taken_at_sourceWhen it was taken (UTC) and where that came from: exif (the camera) or device (the taken_at you sent); null when unknown
uploaded_at_gmtWhen it was uploaded (UTC)
taken_display, uploaded_displayThe same times in the store’s time zone and date format, for people
uploaded_by, uploaded_by_nameThe user who uploaded it
deleted_at_gmt, delete_reasonSet on a permanently deleted photo
thumb_url, master_urlOnly 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.

ParameterNotes
fileMultipart: the image file
data, filenameJSON: the image as base64 (a data:image/png;base64, prefix is allowed) and its file name (default photo.jpg)
contextcondition (default), damage, service, send, return or other. Any other value is stored as condition
noteFree text
booking_idLinks the photo to a booking and its order; ignored when the booking does not exist
reading_idLinks the photo to a reading
fieldThe key of a photo field that applies to the serial (for example photos)
taken_atWhen 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
pendingtrue: upload now and name the photo on a send or return later (below). Default false: attached at once
client_refYour 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.

ErrorWhen
404 sibooking_rest_not_foundNo serial with that id
400 sibooking_rest_photo_missingNeither file nor data was sent
415 sibooking_rest_photo_type_not_allowedNot a JPEG, PNG or WebP (or HEIC the server can’t read), an empty file, or data that is not base64
413 sibooking_rest_photo_too_largeOver 15 MB
413 sibooking_rest_photo_too_many_pixelsOver 40 megapixels
400 sibooking_rest_reading_not_applicablefield is not a photo field of this serial (data.field)
500 sibooking_rest_photo_processing_failedThe 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).

ParameterNotes
statusattached (default), pending, trashed, deleted or any
contextOne context
booking_id, send_id, return_idOnly photos linked to that booking, send or return
since, untilUploaded from / up to, ISO 8601 (no offset = store time). A value that cannot be read is ignored
page, per_pageDefault 1 and 20; per_page at most 200

Answers an array of photos. 404 sibooking_rest_not_found 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_rest_photo_not_found when there is no photo with that id.

GET /wc-sibooking/v1/photos/{id}/file #

The image itself, not JSON.

ParameterNotes
sizemaster (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="photo-912-master.jpg" and Cache-Control: private, max-age=86400. 404 sibooking_rest_photo_not_found 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:

FieldNotes
note"" clears it
contextOne of the six contexts
booking_id, reading_idRe-link it; 0 removes the link. order_id does not change
keep_evidencetrue keeps it out of the retention clean-up

Answers the photo. 400 sibooking_rest_invalid_context for an unknown context, 404 sibooking_rest_photo_not_found for an unknown id.

DELETE /wc-sibooking/v1/photos/{id} #

Moves a photo to the trash, or deletes it permanently.

ParameterNotes
forcetrue deletes permanently: the files go now and the record stays as deleted. Default false: to the trash
reasonRequired with force=true, up to 255 characters

Answers the photo with its new status (trashed or deleted).

ErrorWhen
404 sibooking_rest_photo_not_foundUnknown id, or already deleted permanently
400 sibooking_rest_reason_requiredforce=true without a reason
403 sibooking_rest_forbiddenSee 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_rest_photo_not_found 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.

TypeHolds
meterA counter that only goes up: hours, miles, cycles. Usage is measured between readings
numberAny number within an optional min / max: battery %, a count
choiceOne of two or more options: fuel E, ¼, ½, ¾, F
yesnoYes or no
textUp to 500 characters
photoPhotos, uploaded with the photo endpoints; never typed

A field definition, as you send it when creating a set or a field:

FieldNotes
labelRequired
keyLowercase 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
typeOne of the types above; default meter
unitMeter and number only, up to 20 characters (mi, km, h, %, L and so on)
decimalsMeter and number only, 0 to 3; default 0
captureWhere 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
requiredAn array of send and return, only where it is captured. Or required_send and required_return
min, maxMeter and number only
rollover_atMeter only: the value where the counter wraps back to zero (10000 on a 4-digit hour meter). Above zero
max_per_dayMeter only: usage per day above this gives a warning when a reading is recorded (it is still recorded)
optionsChoice 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_photosPhoto only, 0 to 20: how many photos a required photo field needs
hintHelp text under the field, up to 255 characters
sort_orderPosition in the set; new fields go last
statusactive (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).

ParameterNotes
statusactive (default), archived or any
include_fieldsDefault 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.

ParameterNotes
nameRequired unless template is given
slugMade from the name when left out (a taken slug then gets -2, -3); a taken slug you send answers 409
descriptionFree text
fieldsArray of field definitions
templateA 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.

ErrorWhen
400 sibooking_rest_invalid_setNo name
409 sibooking_rest_duplicate_slugThe slug you sent is taken
400 sibooking_rest_invalid_fieldA 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_rest_duplicate_field_keyTwo fields share a key
404 sibooking_rest_unknown_templateNo 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_rest_not_found 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.

ErrorWhen
400 sibooking_rest_invalid_setAn empty name or slug
409 sibooking_rest_duplicate_slugThe slug is taken
409 sibooking_rest_field_key_conflictMaking it active again would give some serial two fields with the same key
404 sibooking_rest_not_foundUnknown 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_rest_has_readings (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.

ErrorWhen
400 sibooking_rest_invalid_fieldThe definition is wrong
409 sibooking_rest_duplicate_field_keyThe set already has that key
409 sibooking_rest_field_key_conflictAnother set that applies to some of the same serials already has that key (data.sets)
404 sibooking_rest_not_foundUnknown 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.

ErrorWhen
404 sibooking_rest_unknown_reading_fieldThe set has no field with that key (data.field)
409 sibooking_rest_field_has_readingsThe field has readings and the change would alter its type, unit or key, or lower its decimals (data.readings)
400 sibooking_rest_invalid_fieldThe changed definition is wrong
409 sibooking_rest_duplicate_field_key, 409 sibooking_rest_field_key_conflictA 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_rest_has_readings 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_rest_field_key_conflict 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_rest_invalid_product 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_rest_invalid_product, 404 sibooking_rest_not_found (a set id that does not exist) or 409 sibooking_rest_field_key_conflict.

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_rest_not_found 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.

ErrorWhen
400 sibooking_rest_invalid_assignmentThe same set is in both lists
404 sibooking_rest_not_foundUnknown serial or set
409 sibooking_rest_field_key_conflictTwo 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:

FieldNotes
id, serial_id, product_idThe reading, the unit and its product
field, field_id, unitThe field’s key, id and unit
valueIn the field’s type: a number (meter, number), true/false (yesno), the option key (choice) or text
deltaMeter only: usage since the previous valid reading; null for the first reading of a meter
kindSee below
contextsend, return (recorded on a move), manual, inspection or service
booking_id, order_id, send_id, return_idThe rental and the move it belongs to, or null
recorded_at, recorded_at_gmtWhen the value was true (store time with offset, and UTC)
created_at_gmtWhen it was entered (UTC)
user_id, sourceWho recorded it and from where: admin (the screens), rest or scan
client_refThe client_ref of the request with the row’s position added (yard-app-7781:0)
noteThe note, or the reason on a correction
supersedes_idOn a correction: the reading it replaces
voided, voided_at_gmt, voided_by, void_reasonSet when the reading was voided or corrected
old_finalOnly on a meter_swap: the old meter’s final reading
KindMeaning
baselineThe first reading of a meter: the starting point, no usage
readingAn ordinary value
rolloverThe meter wrapped past its rollover_at; delta counts across the wrap
meter_swapA 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
correctionReplaces the reading in supersedes_id
skippedA 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.

ParameterNotes
valuesRequired. 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
contextmanual (default), inspection or service
recorded_atWhen 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
noteStored on each reading, unless the value object has its own note
booking_idLinks the readings to a booking and its order; ignored when the booking does not exist
client_refYour 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:

TypeSend
meter, numberA JSON number, or a string like "1234.5" (dot decimal, no thousands separator), with at most the field’s decimals and within min / max
choiceThe option key, or its label (case does not matter): "half" or "½"
yesnotrue/false, 1/0, or "yes"/"no" and the other common spellings
textUp to 500 characters

A meter never goes backwards on its own. A value lower than the last reading answers 409 sibooking_rest_reading_decreased (with data.last_value and data.last_recorded_at) unless you say why with event:

EventSendNotes
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_rest_reading_out_of_sequence). 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_reading", "field": "odometer", "message": "+3,478 mi in 1 day(s) is above the 1,500 mi a day this field expects. Check the digits.", "per_day": 3478, "serial_code": "VAN-07"}.

ErrorWhen
404 sibooking_rest_not_foundNo serial with that id
400 sibooking_rest_unknown_reading_fieldNo field anywhere has that key
400 sibooking_rest_reading_not_applicableThe field exists but does not apply to this serial
400 sibooking_rest_invalid_readingA 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_rest_reading_out_of_rangeOutside min / max, a negative meter, or a roll-over value at or above rollover_at
409 sibooking_rest_reading_decreasedA 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_rest_reading_out_of_sequenceA back-dated value that does not fit between its neighbors (data.next_value), or an event that is not the latest reading
400 sibooking_rest_no_readingsNothing to record (empty values, or only photo fields)
400 sibooking_rest_invalid_recorded_atrecorded_at can’t be read
400 sibooking_rest_recorded_in_futurerecorded_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).

ParameterNotes
fieldA 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
contextsend, return, manual, inspection, service or import
booking_idReadings linked to that booking
since, untilRecorded 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_voidedDefault false; true adds voided and corrected readings
page, per_pageDefault 1 and 20; per_page at most 200

Answers an array of readings. 404 sibooking_rest_not_found for an unknown serial, 400 sibooking_rest_invalid_recorded_at 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_rest_not_found 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.

FieldNotes
valueRequired: the right value, in the same forms as when recording
reasonRequired, up to 255 characters

Answers the new reading (kind correction, supersedes_id = the original).

ErrorWhen
404 sibooking_rest_not_foundUnknown reading, or its field was deleted
400 sibooking_rest_reason_requiredreason is empty
409 sibooking_rest_reading_voidedThe reading was already corrected or voided: correct the newest one
400 sibooking_rest_invalid_readingA meter_swap reading (void it and record it again instead), or no value
Value errorsAs when recording: invalid_reading, reading_out_of_range, reading_decreased, reading_out_of_sequence
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_rest_not_found, 400 sibooking_rest_reason_required or 409 sibooking_rest_reading_voided.

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}
}
FieldNotes
serialsBy serial code, then by field key
usageMeters 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
basissend: measured from the send reading (exact). previous_reading: no send reading, so the return reading’s own delta (an estimate). null: unknown
send, returnThe values recorded at the send and the return of this booking, or null
totalsUsage per field over all the booking’s serials (a unit swapped during the rental adds both)

404 sibooking_rest_not_found 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/v1/bookings/{id}/send and /return, and the scans POST /wc-sibooking/v1/send/scan and /return/scan. They take three more fields:

FieldNotes
readingsBy serial code, then field key: {"VAN-07": {"odometer": 48522, "fuel": "half"}}. On a scan, by field key only. Values as in POST /serials/{id}/readings; a required field that can’t be read takes {"skip": "Gauge broken"}
photosPhoto 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_photo_max_per_move)
strict_readingstrue refuses the move when a required field is missing (400 sibooking_rest_reading_required, or sibooking_rest_photo_required 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_photo_uploaded, sibooking_reading_recorded, sibooking_reading_corrected, sibooking_reading_voided and sibooking_readings_assignments_changed. Filters: sibooking_photos_capability, sibooking_readings_capability, sibooking_photo_store (where photos are stored), sibooking_photo_max_bytes, sibooking_photo_max_pixels and sibooking_photo_max_per_move.