- Tickets
- GET /api/v1/maintenance/tickets
- GET /api/v1/maintenance/tickets/{id}
- POST /api/v1/maintenance/tickets
- PUT or PATCH /api/v1/maintenance/tickets/{id}
- POST /api/v1/maintenance/tickets/{id}/actions
- POST /api/v1/maintenance/tickets/{id}/comments
- POST /api/v1/maintenance/tickets/{id}/assign
- GET /api/v1/maintenance/statuses
- GET /api/v1/maintenance/technicians
- Service schedules and checklists
- Costs and the vendor card
- Damage charges
- Parts and vendors
- Reports and the dashboard
- CSV export and import
- Errors
The maintenance endpoints of the Rentals & Bookings REST API v1: open and work maintenance tickets for rental units (repairs, services and inspections) and the stock they keep out, assign technicians, read service schedules and fill checklists, record costs, keep the parts and vendors lists, charge customers for damage, and read the maintenance reports. Keys, limits, pagination and the error format are in the API overview. The rest of the core REST API is in part 1, REST API reference: bookings and availability, and part 2, REST API reference: warehouse, stock and settings. The same operations are in GraphQL: GraphQL API reference: Maintenance.
Every endpoint on this page needs the Rentals & Bookings app. GET needs a read key; everything else needs a key with Allow changes (write), and a read key gets 403 rentals_rest_forbidden. The Free plan’s API reads only: a change answers 403 rentals_rest_plan_required with data.plan: "free" and data.required_plan: "regular". Maintenance is on every plan; while it is turned off in Settings › Maintenance, opening a ticket answers 409 rentals_rest_maintenance_off (tickets already open keep working). The examples use two shell variables:
API="https://YOUR-API-ADDRESS/api/v1" # the REST address from Settings › API
KEY="sirb_your_secret_key"
Ids in the examples are made up: product 88001, variant 44001 (a pressure washer with serial numbers), order 5550001 (#1042), booking 42, ticket 17 (MT-17), technician 3.
- Times are
Y-m-d H:iin the store’s time zone;expected_backis a day (Y-m-d, back by the end of it) or a moment. Timestamps ending in_at(created_at,updated_at,paid_at) are ISO 8601, except a ticket’sopened_atandclosed_at, which are store time like the other times. - Money is a decimal string in the store currency (
"45.00"). - Request bodies are JSON with snake_case names. Only the fields you send change.
- The ticket list is paged with
pageandper_page(default 20, at most 200); the totals are in theX-TotalandX-Total-Pagesheaders. The other lists come whole. - A ticket never carries the customer’s name or email: only the order and customer ids.
Tickets #
A ticket is one repair, service or inspection of one rent variant: a few serial numbers, or a quantity of a product without serials. While its status blocks stock and its hold mode is block, the units are out of stock from hold_start until expected_back (or until the ticket closes when there is no expected back), so they can’t be booked.
Every store starts with these statuses; a store can add its own in the open and waiting groups (they have no key, only a status_id). The ticket’s actions lists what its status allows now.
status | Shown as | Group | Blocks stock | Actions it allows |
|---|---|---|---|---|
reported | Reported | open | no | confirm, cancel |
new | New | open | yes | start, ready, wait_parts, send_vendor, write_off, cancel |
in_progress | In progress | open | yes | ready, wait_parts, send_vendor, write_off, cancel |
waiting_parts | Waiting for parts | waiting | yes | resume, ready, send_vendor, write_off, cancel |
at_vendor | At vendor | waiting | yes | back_from_vendor, ready, write_off, cancel |
done | Ready to rent | closed | no | reopen |
written_off | Written off | closed | no | none |
cancelled | Cancelled | closed | no | reopen |
The ticket:
| Field | Notes |
|---|---|
id, number, reference | reference is MT-<number>. |
title, description, problem | problem is one of the store’s problem types (Settings › Maintenance), e.g. Damaged. |
kind | repair, service or inspection. |
source | Where it came from: manual, checkin, checkout, inspection, customer, schedule, api, import, flow or pos. Tickets opened through this API are api. |
status, status_label, status_id, status_category, blocks_stock | The status key (null for a status the store added), its name, its id, its group (open, waiting, closed) and whether it keeps the units out of stock. |
priority | low, normal, high or urgent. |
product_id, variant_id, product_title, variant_title, sku | The rent variant. |
units, quantity | One line per serial ({unit_id, code, quantity: 1, hold_state}), or one quantity line without unit_id and code. quantity is the total. |
location_id | The Shopify location, or null. |
hold_mode | block (keeps the units out of stock) or none (a log only). |
hold_start, expected_back, overdue | When the hold starts; when the units are expected back (null: until done); past expected back and still open. |
hold | {state, covers_now, start, end}: state is none, planned, held, open_ended, rolling_overdue, released, closed or log_only. |
assignee_id, assignee_name | The technician. |
booking_id, order_id, order_name, customer_id | The rental it came back from, when linked. |
may_charge, charge_decided | “Customer may be charged”, and what was decided: charge, no_charge or null. |
cost_total, billable_total, vendor_id | Our cost (every cost line), the lines billed to the customer, and the vendor card’s vendor. |
opened_at, closed_at, outcome | Once closed, outcome is fixed, no_fault, replaced, written_off or cancelled. |
write_off | {at, reduce_stock, units} once written off, else null. |
actions | The actions the status allows now. |
created_at, updated_at | ISO 8601. |
events, attachments, costs | One ticket only (GET /maintenance/tickets/{id} and the answers to changes): the timeline, oldest first ({id, type, text, actor, customer_visible, at}), the photos and files (metadata only: {id, name, mime, size, image, at}; the files open in the app) and the costs. |
GET /api/v1/maintenance/tickets #
The tickets of a list, paged. Open lists come most urgent first (priority, then expected back); closed and all come last updated first. The list leaves out events, attachments and costs.
| Parameter | Notes |
|---|---|
status | open (default: not closed), overdue (open and past expected back), waiting (waiting for parts or at a vendor), closed or all. |
search | An MT number, title, product, SKU, order name or serial. |
variant_id, product_id | One product. |
serial | Tickets that hold this serial. |
booking_id | Tickets linked to this booking. |
ticket_status | One status key, e.g. waiting_parts. |
priority | low, normal, high or urgent. |
updated_since | Y-m-d or Y-m-d H:i: changed since then. |
page, per_page | Paging. |
curl -s "$API/maintenance/tickets?status=overdue&per_page=50" -H "Authorization: Bearer $KEY"
# 200, X-Total: 1, X-Total-Pages: 1
[{"id": 17, "number": 17, "reference": "MT-17", "title": "Trigger stuck", "kind": "repair", "source": "checkin",
"status": "waiting_parts", "status_label": "Waiting for parts", "status_id": 104, "status_category": "waiting", "blocks_stock": true,
"priority": "high", "problem": "Damaged", "description": "Trigger sticks halfway; hose is fine.",
"product_id": 88001, "variant_id": 44001, "product_title": "Pressure washer", "variant_title": "Rent", "sku": "PW-RENT",
"units": [{"unit_id": 9007, "code": "PW-07", "quantity": 1, "hold_state": "held"}], "quantity": 1,
"location_id": 801, "hold_mode": "block", "hold_start": "2026-11-09 16:40", "expected_back": "2026-11-12", "overdue": true,
"hold": {"state": "rolling_overdue", "covers_now": true, "start": "2026-11-09 16:40", "end": null},
"assignee_id": 3, "assignee_name": "Sam Rivera", "booking_id": 42, "order_id": 5550001, "order_name": "#1042",
"customer_id": 7001234, "may_charge": true, "charge_decided": null, "cost_total": "86.50", "billable_total": "86.50",
"vendor_id": null, "opened_at": "2026-11-09 16:40", "closed_at": null, "outcome": null, "write_off": null,
"actions": ["resume", "ready", "send_vendor", "write_off", "cancel"],
"created_at": "2026-11-09T21:40:12+00:00", "updated_at": "2026-11-13T15:02:40+00:00"}]
GET /api/v1/maintenance/tickets/{id} #
One ticket with events, attachments and costs. 404 rentals_rest_not_found when the store has no such ticket.
curl -s "$API/maintenance/tickets/17" -H "Authorization: Bearer $KEY"
# 200: the ticket as in the list, plus
"events": [{"id": 301, "type": "opened", "text": "Ticket opened (Check-in)", "actor": "user:12", "customer_visible": false, "at": "2026-11-09T21:40:12+00:00"},
{"id": 305, "type": "status", "text": "In progress → Waiting for parts", "actor": "api:sirb_xxxxxxxx", "customer_visible": false, "at": "2026-11-10T14:05:31+00:00"}],
"attachments": [{"id": 51, "name": "trigger.jpg", "mime": "image/jpeg", "size": 184220, "image": true, "at": "2026-11-09T21:41:02+00:00"}],
"costs": {"lines": [...], "total": "86.50", "billable": "86.50", "currency": "USD", "labour_rate": "45.00", "timer": null, "vendor": {...}}
actor says who did it: user:<staff id>, api:<key prefix> for a change through this API, system and so on.
POST /api/v1/maintenance/tickets #
Open a ticket (source api). Answers 201 with the ticket. It starts as New; with hold mode block its units are out of stock from start.
| Field | Notes |
|---|---|
variant_id | Required: a rent variant (the number or gid://shopify/ProductVariant/…). A kit can’t go to maintenance. |
units | The serial numbers (or unit ids) of a product that tracks serials, e.g. ["PW-07"]. |
quantity | Units of a product without serials (when there are no units). |
kind | repair (default), service or inspection. |
priority | low, normal (default), high or urgent. |
title | Default: the problem, else the kind. At most 200 characters. |
description, problem | problem is one of the store’s problem types. |
location_id | A Shopify location. |
hold_mode | block or none. Default: the store’s setting in Settings › Maintenance. |
start | Y-m-d H:i (default now). A start in the past answers 409 rentals_rest_confirm_backdate (with data.start and data.now) unless you send confirm_backdate: true: the stock is then held from that moment. |
expected_back | Y-m-d or Y-m-d H:i; null means until done. Default: the store’s usual repair time, if set. It must be after the start. |
booking_id | The rental it came back from: its order and customer come with it, and a damage charge needs it. |
may_charge | true: the customer may be charged for the damage (the security deposit is held until you decide). |
assign_to | A technician id from GET /maintenance/technicians; they get an email. |
curl -s -X POST "$API/maintenance/tickets" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"variant_id": 44001, "units": ["PW-07"], "priority": "high", "problem": "Damaged", "title": "Trigger stuck",
"booking_id": 42, "may_charge": true, "expected_back": "2026-11-12", "assign_to": 3}'
# 201: the ticket ("source": "api", "status": "new")
# the same serial again while MT-17 is open:
# 409 {"code": "rentals_rest_unit_on_ticket", "message": "Serial PW-07 is already on MT-17.",
# "data": {"field": "units", "ticketId": 17, "number": 17, "status": 409}}
Also refused: 400 rentals_rest_invalid_product (not a rent variant), 400 rentals_rest_unit_not_found (a serial that isn’t a unit of this product), 400 rentals_rest_unit_retired, 400 rentals_rest_invalid_param (no units or quantity, a bad date, expected back before the start), 404 rentals_rest_not_found (no such booking) and 409 rentals_rest_maintenance_off.
PUT or PATCH /api/v1/maintenance/tickets/{id} #
Change the details and the hold. Fields: title, description, kind, priority, start, expected_back, hold_mode, location_id, confirm_backdate (moving the start into the past) and may_charge. On a closed ticket only title, description, priority and may_charge change; the others answer 409 rentals_rest_ticket_closed (“This ticket is closed. Reopen it first.”).
curl -s -X PATCH "$API/maintenance/tickets/17" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"expected_back": "2026-11-15", "priority": "urgent"}'
# 200: the ticket
POST /api/v1/maintenance/tickets/{id}/actions #
Move the ticket on, like the buttons on the ticket page. An action the status doesn’t allow answers 409 rentals_rest_not_allowed.
| Field | Notes |
|---|---|
action | Required: confirm, start, ready, wait_parts, send_vendor, resume, back_from_vendor, write_off, cancel, reopen or move. |
note | A note added to the timeline with the move. |
outcome | With ready: fixed (default), no_fault or replaced. |
reduce_stock | With write_off: true (default) takes the units out of stock for good; false only closes the ticket. |
confirm | With write_off: go ahead although bookings would be left short. Without it such a write-off answers 409 rentals_rest_confirm_write_off with the bookings in data.conflicts. |
status_id | With move: one of the store’s own open or waiting statuses (GET /maintenance/statuses). |
| Action | Leads to |
|---|---|
confirm | New: a reported ticket is confirmed and its hold starts. |
start, resume, back_from_vendor | In progress. |
ready | Ready to rent: the ticket closes and the stock is free. |
wait_parts | Waiting for parts. |
send_vendor | At vendor (the vendor card’s sent day starts as today). |
write_off | Written off. |
cancel | Cancelled. |
reopen | In progress again, with a new hold from now. |
move | The store’s own status status_id. |
curl -s -X POST "$API/maintenance/tickets/17/actions" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"action": "ready", "outcome": "replaced", "note": "New trigger fitted and tested."}'
# 200: the ticket ("status": "done", "status_category": "closed", "outcome": "replaced", "actions": ["reopen"])
POST /api/v1/maintenance/tickets/{id}/comments #
Add a comment to the timeline: {"text": "Ordered a trigger, due Friday."} (at most 5,000 characters). Answers with the ticket.
POST /api/v1/maintenance/tickets/{id}/assign #
Assign a technician: {"technician_id": 3}; they get an email. {"technician_id": null} unassigns. technician_id is required. Answers with the ticket.
GET /api/v1/maintenance/statuses #
The store’s statuses in their order: [{id, key, label, category, blocks_stock, tone}]. key is null for a status the store added; tone is how the app colours it (attention, info, warning, success, neutral).
curl -s "$API/maintenance/statuses" -H "Authorization: Bearer $KEY"
[{"id": 101, "key": "reported", "label": "Reported", "category": "open", "blocks_stock": false, "tone": "attention"},
{"id": 102, "key": "new", "label": "New", "category": "open", "blocks_stock": true, "tone": "info"},
...
{"id": 108, "key": "cancelled", "label": "Cancelled", "category": "closed", "blocks_stock": false, "tone": "neutral"}]
GET /api/v1/maintenance/technicians #
Who tickets can be assigned to, the least busy first: [{"id": 3, "name": "Sam Rivera", "open_tickets": 2}]. Never an email address.
Service schedules and checklists #
A service schedule makes a rent variant’s units due for a service every interval, number of rentals, days on rent or meter hours, whichever comes first. Schedules and checklists are set up in the app; the API reads them, snoozes a unit’s service and records inspections.
GET /api/v1/maintenance/schedules #
The schedules with each unit’s state. variant_id limits them to one rent variant.
| Field | Notes |
|---|---|
id, name, variant_id, active | |
interval, rentals, rental_days, hours | What makes it due: a calendar interval (30d, 2w, 6mo, 1y), a number of rentals, days on rent, meter hours. |
due_soon_percent | Due soon this share of the way before. |
on_due | ticket_block (open a service ticket that holds the stock), ticket (open one without a hold) or remind. |
checklist_id, assignee_id, est_days | The checklist filled during the service, who it is assigned to, and how many days a service takes. |
units | [{id, schedule_id, unit_id, code, state, trigger, next_due, last_done_at, snooze_until, snooze_until_rentals, snooze_reason, ticket_id}]: one row per serial (one row for a product without serials). state is ok, due_soon, due, overdue or snoozed; trigger what makes it due first; ticket_id the open service ticket. |
curl -s "$API/maintenance/schedules?variant_id=44001" -H "Authorization: Bearer $KEY"
[{"id": 4, "name": "Pump service", "variant_id": 44001, "interval": "6mo", "rentals": 25, "rental_days": null, "hours": null,
"due_soon_percent": "10", "on_due": "ticket_block", "active": true, "checklist_id": 2, "assignee_id": null, "est_days": 1,
"units": [{"id": 61, "schedule_id": 4, "unit_id": 9007, "code": "PW-07", "state": "due_soon", "trigger": "rentals",
"next_due": "2026-11-20", "last_done_at": "2026-06-02", "snooze_until": null, "snooze_until_rentals": null,
"snooze_reason": null, "ticket_id": null}]}]
POST /api/v1/maintenance/schedules/units/{id}/snooze #
Snooze one unit’s service. {id} is the unit row’s id from the schedules.
| Field | Notes |
|---|---|
until | YYYY-MM-DD, later than today. |
rentals | Snooze for this many more rentals. |
reason | At most 200 characters. |
clear | true ends the snooze. |
Send until, rentals or both (else 400 rentals_rest_invalid_param). Answers with the unit row.
GET /api/v1/maintenance/checklists #
The checklists: [{id, name, moments, variant_ids, active, items}]. With variant_id, only the active ones filled for that product; add moment (checkout, checkin, service or manual) for those filled then. An item is {key, label, type, required, options, fail_values, on_fail, unit}: type is pass_fail, choice, number, meter (hours, which feed the service schedules), text or photo; on_fail is ticket (a failed answer opens a ticket) or record.
POST /api/v1/maintenance/inspections #
Fill a checklist for a unit. A failed item set to open a ticket opens one (source inspection, or checkout at check-out) or joins the unit’s open ticket; while maintenance is turned off, no ticket is opened.
| Field | Notes |
|---|---|
checklist_id | Required. |
variant_id | The rent variant (not needed with ticket_id). |
unit | The serial (or unit id). Required for a product with serials. |
moment | checkout, checkin, service or manual (default; service with ticket_id). |
booking_id | The rental, if any. |
ticket_id | Filled during this ticket’s service. |
answers | Required: {"<item key>": value} or {"<item key>": {"value": …, "skipped": true, "reason": "…"}}. A pass/fail item takes pass or fail. |
curl -s -X POST "$API/maintenance/inspections" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"checklist_id": 2, "variant_id": 44001, "unit": "PW-07", "moment": "checkin", "booking_id": 42,
"answers": {"hose": "pass", "trigger": "fail", "hours": 212}}'
# 200
{"inspection": {"id": 88, "checklist_id": 2, "checklist": "Return check", "variant_id": 44001, "unit_id": 9007, "unit": "PW-07",
"booking_id": 42, "ticket_id": 18, "moment": "checkin", "failed": true, "meter": "212",
"answers": [...], "at": "2026-11-13 10:15", "by": "api:sirb_xxxxxxxx"},
"ticket_id": 18, "ticket_opened": true, "failed": ["Trigger"]}
failed lists the labels of the failed items.
Costs and the vendor card #
A ticket’s costs: parts, labour, a vendor’s invoice or anything else, in the store currency. “Bill to customer” (billable) starts ticked when the ticket says Customer may be charged; the billable lines are what a damage charge starts from. Lines can be added to a closed ticket (invoices come in later), at most 200 per ticket.
The costs, as every endpoint of this section answers them:
| Field | Notes |
|---|---|
lines | [{id, kind, description, part_id, part, vendor_id, vendor, unit_id, quantity, unit_cost, minutes, rate, amount, billable, stock_taken, invoice_id, date}]. kind is part, labour, vendor or other; stock_taken is the parts taken from a counted part’s stock; invoice_id a ticket file; date YYYY-MM-DD. |
total, billable, currency | Our cost (every line) and the lines billed to the customer. |
labour_rate | The labour rate per hour from Settings › Maintenance, or null. |
timer | {started_at, by} while the labour timer runs, else null. |
vendor | The vendor card: {id, name, ref, sent, due, quote, late}; late is true when due back has passed and the ticket still waits. |
GET /api/v1/maintenance/tickets/{id}/costs #
The ticket’s costs.
POST /api/v1/maintenance/tickets/{id}/costs #
Add a line. Answers 201 with {line_id, costs}.
| Field | Notes |
|---|---|
kind | Required: part, labour, vendor or other. |
part_id | A part of the parts list: the description and unit cost come from it, and a counted part’s stock goes down by whole parts. |
description | Required for other, and for a part not on the list. |
quantity, unit_cost | Parts: the amount is quantity × unit cost (quantity 1 by default). |
minutes or hours, rate | Labour: the amount is the time × the hourly rate. Without rate, the labour rate from Settings › Maintenance (400 rentals_rest_invalid_param when none is set). |
amount | Vendor and other lines. |
vendor_id | A vendor line’s vendor (default: the ticket’s vendor card). |
unit_id | The serial on the ticket it was for. |
billable | Bill to customer (default: the ticket’s may_charge). |
A vendor’s invoice file is added in the app.
curl -s -X POST "$API/maintenance/tickets/17/costs" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"kind": "labour", "minutes": 50}'
# 201
{"line_id": 140, "costs": {"lines": [
{"id": 139, "kind": "part", "description": "Trigger assembly", "part_id": 12, "part": "Trigger assembly", "vendor_id": 5,
"vendor": "Northside Repairs", "unit_id": null, "quantity": "1", "unit_cost": "49.00", "minutes": null, "rate": null,
"amount": "49.00", "billable": true, "stock_taken": 1, "invoice_id": null, "date": "2026-11-12"},
{"id": 140, "kind": "labour", "description": "Labour", "part_id": null, "part": null, "vendor_id": null, "vendor": null,
"unit_id": null, "quantity": "0.833", "unit_cost": "45.00", "minutes": 50, "rate": "45.00", "amount": "37.50",
"billable": true, "stock_taken": 0, "invoice_id": null, "date": "2026-11-13"}],
"total": "86.50", "billable": "86.50", "currency": "USD", "labour_rate": "45.00", "timer": null,
"vendor": {"id": null, "name": null, "ref": null, "sent": null, "due": null, "quote": null, "late": false}}}
PUT or PATCH /api/v1/maintenance/tickets/{id}/costs/{line_id} #
Change a line with the same fields; only those sent change. Answers with {line_id, costs}. 404 rentals_rest_not_found when the ticket has no such line.
DELETE /api/v1/maintenance/tickets/{id}/costs/{line_id} #
Remove a line (a counted part goes back in stock). Answers with the costs.
POST /api/v1/maintenance/tickets/{id}/timer #
The labour timer, one per ticket: {"action": "start"} or {"action": "stop"}. Stop adds a labour line of the whole minutes it ran; closing the ticket stops it too. Answers with the costs. Refused: 409 rentals_rest_timer_running, 409 rentals_rest_timer_stopped, and 409 rentals_rest_ticket_closed when starting it on a closed ticket.
PUT or PATCH /api/v1/maintenance/tickets/{id}/vendor #
The vendor card: vendor_id, vendor_ref (their RMA or job number), vendor_sent and vendor_due (YYYY-MM-DD), vendor_quote. Only the fields sent change; null clears one. Answers with the costs.
Damage charges #
When Settings › Maintenance has damage charges on, a ticket linked to a rental can charge the customer for the damage. A damage charge (DC-<order number>-<n>) goes on the rental’s order and is collected the way late fees are: the payment link, the counter or the security deposit. One open charge per ticket. Nothing is charged until you decide: a charge is saved To review, or collected at once by the route you pick.
A charge has the fields of a late fee (see REST API reference: Late fees): (id, number, order_id, order_name, customer_id, status, amount, currency, amount_shop, shop_currency, calculated, adjusted, tax, route, method, paid_via, paid_amount, counter_method, paid_at, refunded, note_customer, reason_staff, waive_reason, last_error, reviewed_at, created_at, lines, booking_ids and more) plus ticket_id. status is to_review, charging, pay_link_sent, needs_confirmation, failed, paid, waived, cancelled, refunded or part_refunded.
GET /api/v1/maintenance/tickets/{id}/charge #
The ticket’s damage charge state.
| Field | Notes |
|---|---|
enabled | Damage charges are on in Settings › Maintenance. |
may_charge, decided | Customer may be charged; what was decided (charge, no_charge or null). |
can_charge, why | Whether a charge can be created now, and why not: off, no_rental (no rental on the ticket) or open (a charge is open). |
order_name | The rental’s order. |
amount, billable, currency | What a new charge starts with: the billable costs, or 0.00 when the rental bought the damage waiver. |
waiver | The damage waiver the rental bought (Custom Fields app): {bought, state, amount, label} (state accepted, required or declined), or null. |
charges | [{id, number, status, amount, currency, route, paid_via, paid_at, note_customer, created_at}], newest first. |
POST /api/v1/maintenance/tickets/{id}/charge #
Create a damage charge. Answers 201 with the charge plus collect_error.
| Field | Notes |
|---|---|
amount | Required: above 0, store currency, before tax. |
note_customer | Shown to the customer (email, invoice). |
reason_staff | For staff only. |
route | review (default: only create it, To review), auto (the store’s ways to collect, in their order), link (the payment link), counter or deposit (kept from the order’s security deposit). |
method | With counter: cash (default), terminal, bank or other. |
to | With link: the email to send it to (default: the customer’s). |
When collecting fails, the charge stays to_review and collect_error is {code, message}; otherwise it is null. Refused: 409 rentals_rest_charges_off, 409 rentals_rest_charge_open, 400 rentals_rest_no_rental (the ticket has no rental), 400 rentals_rest_invalid_param (amount, route).
curl -s -X POST "$API/maintenance/tickets/17/charge" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"amount": "86.50", "note_customer": "Replacement trigger and labour", "route": "link"}'
# 201: the charge ("number": "DC-1042-1", "status": "pay_link_sent", "ticket_id": 17, ...) and "collect_error": null
POST /api/v1/maintenance/tickets/{id}/no-charge #
Decide not to charge: {"reason": "Normal wear"} (optional). The ticket records charge_decided: no_charge and the security deposit no longer waits. Answers with the ticket. 409 rentals_rest_charge_open while a charge is open: waive it first.
GET /api/v1/maintenance/charges/{id} #
One damage charge, with payment_url for a write key. A late fee’s id is 404 here, and a damage charge’s id is 404 among late fees.
POST /api/v1/maintenance/charges/{id}/collect #
Collect a charge waiting for review, as Accept does for a late fee: route auto (default), link, counter or deposit, with method and to as above. Answers with the charge. 409 rentals_rest_not_reviewable when it isn’t To review; 400 rentals_rest_no_deposit when no held deposit covers it.
POST /api/v1/maintenance/charges/{id}/waive #
Waive a charge: {"reason": "Goodwill"}. The reason is required (400 rentals_rest_reason_required). 409 rentals_rest_paid for a paid charge, 409 rentals_rest_not_waivable when it can’t be waived any more. Answers with the charge.
Parts and vendors #
The parts list and the vendors (repair shops, suppliers) used on cost lines. Each list keeps at most 2,000 entries.
GET /api/v1/maintenance/parts #
[{id, name, sku, unit_cost, vendor_id, vendor, stock, low, active}], by name. search matches the name or SKU; include_hidden=true adds hidden parts. stock is null when the part isn’t counted; low is true when a counted part has none left.
curl -s "$API/maintenance/parts?search=trigger" -H "Authorization: Bearer $KEY"
[{"id": 12, "name": "Trigger assembly", "sku": "TRG-200", "unit_cost": "49.00", "vendor_id": 5, "vendor": "Northside Repairs",
"stock": 3, "low": false, "active": true}]
POST /api/v1/maintenance/parts #
Create a part: name (required), sku, unit_cost, vendor_id, stock (null: not counted), active. Answers 201 with the part.
PUT or PATCH /api/v1/maintenance/parts/{id} #
Change a part with the same fields. Answers with the part.
DELETE /api/v1/maintenance/parts/{id} #
Remove a part: {"hidden": false, "deleted": true}. A part a cost line used is hidden instead ("hidden": true).
GET /api/v1/maintenance/vendors #
[{id, name, contact, email, phone, notes, active}], by name; include_hidden=true adds hidden vendors.
POST /api/v1/maintenance/vendors #
Create a vendor: name (required), contact, email, phone, notes, active. Answers 201 with the vendor.
PUT or PATCH /api/v1/maintenance/vendors/{id} #
Change a vendor with the same fields. Answers with the vendor.
DELETE /api/v1/maintenance/vendors/{id} #
Remove a vendor: {"hidden": false, "deleted": true}, or hidden when a ticket, part or cost line uses it.
Reports and the dashboard #
GET /api/v1/maintenance/reports/{type} #
One of the five reports of the Reports page. {type} is cost, downtime, utilisation, compliance or problems (another answers 404 rentals_rest_not_found).
| Parameter | Notes |
|---|---|
from, to | Days, YYYY-MM-DD, store time. Default: this month up to today. At most 366 days are read (an earlier from is moved up). |
variant_id | One rent variant. |
location_id | One location. |
kind | repair, service or inspection. |
The answer is {type, from, to, variant_id, location_id, kind, currency, rows, totals, top}. Each report fills its own row fields; money is a string with two decimals:
| Report | Rows |
|---|---|
cost | Per product (variant_id, product): part, labour, vendor, other, total, billable, tickets, revenue (its rental revenue) and percent (cost as a share of revenue), with serials per unit. |
downtime | Per unit (unit_id, serial): days out of stock for maintenance, tickets, failures, mttr (mean days to repair), rentals and between (rentals between failures). |
utilisation | Per product: stock, stock_days, blocked and blocked_pct (days held for maintenance), rented and rented_pct, and shortage (days it fell short of bookings). |
compliance | Per service schedule (schedule_id, schedule): done, on_time, late, overdue and percent on time. |
problems | Per product and problem type: tickets and share; top is the problem types overall. |
curl -s "$API/maintenance/reports/cost?from=2026-10-01&to=2026-10-31" -H "Authorization: Bearer $KEY"
{"type": "cost", "from": "2026-10-01", "to": "2026-10-31", "variant_id": null, "location_id": null, "kind": null, "currency": "USD",
"rows": [{"variant_id": 44001, "product": "Pressure washer", "part": "49.00", "labour": "37.50", "vendor": "0.00", "other": "0.00",
"total": "86.50", "billable": "86.50", "tickets": 1, "revenue": "1260.00", "percent": 6.9, "serials": [...]}],
"totals": {"part": "49.00", "labour": "37.50", "vendor": "0.00", "other": "0.00", "total": "86.50", "billable": "86.50",
"tickets": 1, "revenue": "1260.00", "percent": 6.9},
"top": null}
GET /api/v1/maintenance/dashboard #
The Maintenance board’s top row: {down, back_today, overdue, open, waiting, services_due_soon, services_due, services_overdue, downtime_days, downtime_percent, cost, currency}. down is the units out of stock for maintenance now; downtime_days, downtime_percent and cost are this month’s.
curl -s "$API/maintenance/dashboard" -H "Authorization: Bearer $KEY"
{"down": 2, "back_today": 1, "overdue": 1, "open": 4, "waiting": 1, "services_due_soon": 3, "services_due": 1,
"services_overdue": 0, "downtime_days": 6.5, "downtime_percent": 1.2, "cost": "312.40", "currency": "USD"}
CSV export and import #
The parts list and the service schedules as CSV, the same files as Maintenance › Parts and › Services in the app.
GET /api/v1/maintenance/export/{kind}.csv #
{kind} is parts or schedules. The CSV the import reads: parts have the columns name, sku, unit_cost, vendor, stock, active; schedules variant_id, sku, product, name, every, rentals, rental_days, hours, due_soon_percent, on_due, active, est_days.
curl -s "$API/maintenance/export/parts.csv" -H "Authorization: Bearer $KEY" -o parts.csv
POST /api/v1/maintenance/import/{kind} #
Import parts or schedules: a JSON body {"csv": "<the file's text>", "dry_run": false}, or a multipart upload with file (at most 2 MB) and dry_run. At most 2,000 rows.
dry_rundefaults totrue: every row is checked the same way and nothing is saved. Sendfalseto save.- Parts match an existing part by SKU, or by name when the row has no SKU; a vendor the list doesn’t have is added. An empty unit cost is 0 and an empty stock means not counted. Schedules match by product (
variant_idorsku) and name; a new schedule is imported paused unless the file has anactivecolumn. - A column in the file sets the value and an empty cell clears it; a column the file leaves out keeps the value.
- Bad rows are refused one by one with their line number; good rows are saved.
curl -s -X POST "$API/maintenance/import/parts" -H "Authorization: Bearer $KEY" -F "[email protected]" -F "dry_run=true"
{"kind": "parts", "dry_run": true, "summary": {"rows": 3, "created": 1, "updated": 1, "unchanged": 0, "refused": 1},
"rows": [{"line": 2, "action": "created", "name": "Trigger assembly", "errors": []},
{"line": 3, "action": "updated", "name": "Hose 10 m", "errors": []},
{"line": 4, "action": "refused", "name": "", "errors": ["Enter the part's name."]}],
"unknown_columns": []}
A file that can’t be read, has no rows or lacks the needed columns answers 400 rentals_rest_invalid_param.
Errors #
Errors have the shape described in the API overview: {"code": "rentals_rest_<code>", "message": "…", "data": {"status": …}}, with data.field naming the field when one is wrong. The maintenance codes:
| Status | Code | When |
|---|---|---|
| 400 | rentals_rest_invalid_param | A field is missing or wrong (data.field or data.params says which). |
| 400 | rentals_rest_invalid_product | Opening a ticket for a variant that isn’t a rent variant, or a kit. |
| 400 | rentals_rest_unit_not_found, rentals_rest_unit_retired | A serial that isn’t a unit of the product, or is retired. |
| 400 | rentals_rest_too_many_lines, rentals_rest_too_many | A ticket’s 200 cost lines, or the 2,000 parts or vendors. |
| 400 | rentals_rest_no_rental | A damage charge for a ticket with no rental. |
| 400 | rentals_rest_reason_required | Waiving a charge without a reason. |
| 403 | rentals_rest_forbidden, rentals_rest_plan_required | A read key, or the Free plan, on a change. |
| 404 | rentals_rest_not_found | No such ticket, booking, cost line, unit row, part, vendor, charge or report. |
| 409 | rentals_rest_maintenance_off | Opening a ticket while maintenance is turned off. |
| 409 | rentals_rest_unit_on_ticket | A serial is on an open ticket already (data.ticketId, data.number). |
| 409 | rentals_rest_confirm_backdate | A start in the past without confirm_backdate: true (data.start, data.now). |
| 409 | rentals_rest_not_allowed | An action the ticket’s status doesn’t allow, or a move to a status it can’t go to. |
| 409 | rentals_rest_confirm_write_off | A write-off would leave bookings short (data.conflicts): send confirm: true. |
| 409 | rentals_rest_ticket_closed | A change a closed ticket can’t take: reopen it first. |
| 409 | rentals_rest_timer_running, rentals_rest_timer_stopped | Starting a running timer, stopping a stopped one. |
| 409 | rentals_rest_charges_off, rentals_rest_charge_open | Damage charges are off, or the ticket has an open charge. |
| 409 | rentals_rest_not_reviewable, rentals_rest_paid, rentals_rest_not_waivable | Collecting or waiving a charge in the wrong state. |
