View Categories

Launching soon on the Shopify App StoreJoin the waitlist

REST API reference: Maintenance

26 min read

Table of Contents

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:i in the store’s time zone; expected_back is 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’s opened_at and closed_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 page and per_page (default 20, at most 200); the totals are in the X-Total and X-Total-Pages headers. 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.

statusShown asGroupBlocks stockActions it allows
reportedReportedopennoconfirm, cancel
newNewopenyesstart, ready, wait_parts, send_vendor, write_off, cancel
in_progressIn progressopenyesready, wait_parts, send_vendor, write_off, cancel
waiting_partsWaiting for partswaitingyesresume, ready, send_vendor, write_off, cancel
at_vendorAt vendorwaitingyesback_from_vendor, ready, write_off, cancel
doneReady to rentclosednoreopen
written_offWritten offclosednonone
cancelledCancelledclosednoreopen

The ticket:

FieldNotes
id, number, referencereference is MT-<number>.
title, description, problemproblem is one of the store’s problem types (Settings › Maintenance), e.g. Damaged.
kindrepair, service or inspection.
sourceWhere 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_stockThe 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.
prioritylow, normal, high or urgent.
product_id, variant_id, product_title, variant_title, skuThe rent variant.
units, quantityOne line per serial ({unit_id, code, quantity: 1, hold_state}), or one quantity line without unit_id and code. quantity is the total.
location_idThe Shopify location, or null.
hold_modeblock (keeps the units out of stock) or none (a log only).
hold_start, expected_back, overdueWhen 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_nameThe technician.
booking_id, order_id, order_name, customer_idThe 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_idOur cost (every cost line), the lines billed to the customer, and the vendor card’s vendor.
opened_at, closed_at, outcomeOnce closed, outcome is fixed, no_fault, replaced, written_off or cancelled.
write_off{at, reduce_stock, units} once written off, else null.
actionsThe actions the status allows now.
created_at, updated_atISO 8601.
events, attachments, costsOne 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.

ParameterNotes
statusopen (default: not closed), overdue (open and past expected back), waiting (waiting for parts or at a vendor), closed or all.
searchAn MT number, title, product, SKU, order name or serial.
variant_id, product_idOne product.
serialTickets that hold this serial.
booking_idTickets linked to this booking.
ticket_statusOne status key, e.g. waiting_parts.
prioritylow, normal, high or urgent.
updated_sinceY-m-d or Y-m-d H:i: changed since then.
page, per_pagePaging.
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.

FieldNotes
variant_idRequired: a rent variant (the number or gid://shopify/ProductVariant/…). A kit can’t go to maintenance.
unitsThe serial numbers (or unit ids) of a product that tracks serials, e.g. ["PW-07"].
quantityUnits of a product without serials (when there are no units).
kindrepair (default), service or inspection.
prioritylow, normal (default), high or urgent.
titleDefault: the problem, else the kind. At most 200 characters.
description, problemproblem is one of the store’s problem types.
location_idA Shopify location.
hold_modeblock or none. Default: the store’s setting in Settings › Maintenance.
startY-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_backY-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_idThe rental it came back from: its order and customer come with it, and a damage charge needs it.
may_chargetrue: the customer may be charged for the damage (the security deposit is held until you decide).
assign_toA 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.

FieldNotes
actionRequired: confirm, start, ready, wait_parts, send_vendor, resume, back_from_vendor, write_off, cancel, reopen or move.
noteA note added to the timeline with the move.
outcomeWith ready: fixed (default), no_fault or replaced.
reduce_stockWith write_off: true (default) takes the units out of stock for good; false only closes the ticket.
confirmWith 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_idWith move: one of the store’s own open or waiting statuses (GET /maintenance/statuses).
ActionLeads to
confirmNew: a reported ticket is confirmed and its hold starts.
start, resume, back_from_vendorIn progress.
readyReady to rent: the ticket closes and the stock is free.
wait_partsWaiting for parts.
send_vendorAt vendor (the vendor card’s sent day starts as today).
write_offWritten off.
cancelCancelled.
reopenIn progress again, with a new hold from now.
moveThe 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.

FieldNotes
id, name, variant_id, active
interval, rentals, rental_days, hoursWhat makes it due: a calendar interval (30d, 2w, 6mo, 1y), a number of rentals, days on rent, meter hours.
due_soon_percentDue soon this share of the way before.
on_dueticket_block (open a service ticket that holds the stock), ticket (open one without a hold) or remind.
checklist_id, assignee_id, est_daysThe 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.

FieldNotes
untilYYYY-MM-DD, later than today.
rentalsSnooze for this many more rentals.
reasonAt most 200 characters.
cleartrue 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.

FieldNotes
checklist_idRequired.
variant_idThe rent variant (not needed with ticket_id).
unitThe serial (or unit id). Required for a product with serials.
momentcheckout, checkin, service or manual (default; service with ticket_id).
booking_idThe rental, if any.
ticket_idFilled during this ticket’s service.
answersRequired: {"<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:

FieldNotes
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, currencyOur cost (every line) and the lines billed to the customer.
labour_rateThe labour rate per hour from Settings › Maintenance, or null.
timer{started_at, by} while the labour timer runs, else null.
vendorThe 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}.

FieldNotes
kindRequired: part, labour, vendor or other.
part_idA part of the parts list: the description and unit cost come from it, and a counted part’s stock goes down by whole parts.
descriptionRequired for other, and for a part not on the list.
quantity, unit_costParts: the amount is quantity × unit cost (quantity 1 by default).
minutes or hours, rateLabour: 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).
amountVendor and other lines.
vendor_idA vendor line’s vendor (default: the ticket’s vendor card).
unit_idThe serial on the ticket it was for.
billableBill 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.

FieldNotes
enabledDamage charges are on in Settings › Maintenance.
may_charge, decidedCustomer may be charged; what was decided (charge, no_charge or null).
can_charge, whyWhether a charge can be created now, and why not: off, no_rental (no rental on the ticket) or open (a charge is open).
order_nameThe rental’s order.
amount, billable, currencyWhat a new charge starts with: the billable costs, or 0.00 when the rental bought the damage waiver.
waiverThe 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.

FieldNotes
amountRequired: above 0, store currency, before tax.
note_customerShown to the customer (email, invoice).
reason_staffFor staff only.
routereview (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).
methodWith counter: cash (default), terminal, bank or other.
toWith 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).

ParameterNotes
from, toDays, YYYY-MM-DD, store time. Default: this month up to today. At most 366 days are read (an earlier from is moved up).
variant_idOne rent variant.
location_idOne location.
kindrepair, 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:

ReportRows
costPer 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.
downtimePer unit (unit_id, serial): days out of stock for maintenance, tickets, failures, mttr (mean days to repair), rentals and between (rentals between failures).
utilisationPer product: stock, stock_days, blocked and blocked_pct (days held for maintenance), rented and rented_pct, and shortage (days it fell short of bookings).
compliancePer service schedule (schedule_id, schedule): done, on_time, late, overdue and percent on time.
problemsPer 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_run defaults to true: every row is checked the same way and nothing is saved. Send false to 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_id or sku) and name; a new schedule is imported paused unless the file has an active column.
  • 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:

StatusCodeWhen
400rentals_rest_invalid_paramA field is missing or wrong (data.field or data.params says which).
400rentals_rest_invalid_productOpening a ticket for a variant that isn’t a rent variant, or a kit.
400rentals_rest_unit_not_found, rentals_rest_unit_retiredA serial that isn’t a unit of the product, or is retired.
400rentals_rest_too_many_lines, rentals_rest_too_manyA ticket’s 200 cost lines, or the 2,000 parts or vendors.
400rentals_rest_no_rentalA damage charge for a ticket with no rental.
400rentals_rest_reason_requiredWaiving a charge without a reason.
403rentals_rest_forbidden, rentals_rest_plan_requiredA read key, or the Free plan, on a change.
404rentals_rest_not_foundNo such ticket, booking, cost line, unit row, part, vendor, charge or report.
409rentals_rest_maintenance_offOpening a ticket while maintenance is turned off.
409rentals_rest_unit_on_ticketA serial is on an open ticket already (data.ticketId, data.number).
409rentals_rest_confirm_backdateA start in the past without confirm_backdate: true (data.start, data.now).
409rentals_rest_not_allowedAn action the ticket’s status doesn’t allow, or a move to a status it can’t go to.
409rentals_rest_confirm_write_offA write-off would leave bookings short (data.conflicts): send confirm: true.
409rentals_rest_ticket_closedA change a closed ticket can’t take: reopen it first.
409rentals_rest_timer_running, rentals_rest_timer_stoppedStarting a running timer, stopping a stopped one.
409rentals_rest_charges_off, rentals_rest_charge_openDamage charges are off, or the ticket has an open charge.
409rentals_rest_not_reviewable, rentals_rest_paid, rentals_rest_not_waivableCollecting or waiving a charge in the wrong state.