Rentals
Create, update, and query rental quotes and orders through the rentals API.
Rentals API
CRUD and lifecycle endpoints for rentals — the event-sourced quote/order documents that drive the rental lifecycle.
Rentals use a two-axis model: a state (Enquiry, Quote, Order) and a per-state status (e.g. Quote/Reserved, Order/On Hire). Reads hit a read-optimised projection; every write is recorded as an event so full history is preserved.
Endpoints
| Method | URL | Description |
|---|---|---|
| GET | /api/v1/rentals |
List rentals |
| GET | /api/v1/rentals/{id} |
Show a rental |
| GET | /api/v1/rentals/{id}/assets |
List every line's asset assignments (flat) |
| GET | /api/v1/rentals/{id}/availability |
Per-line availability picture |
| GET | /api/v1/rentals/{id}/activity |
Scoped audit timeline |
| GET | /api/v1/rentals/{id}/available_actions |
Legal actions for the current state |
| POST | /api/v1/rentals |
Create a rental (as an Enquiry) |
| PUT | /api/v1/rentals/{id} |
Update a rental's header fields |
| DELETE | /api/v1/rentals/{id} |
Delete (soft-delete) a rental |
| POST | /api/v1/rentals/{id}/restore |
Restore a soft-deleted rental |
| POST | /api/v1/rentals/{id}/clone |
Clone a rental into a new Enquiry |
| POST | /api/v1/rentals/{id}/convert_to_quote |
Convert an Enquiry into a Quote |
| POST | /api/v1/rentals/{id}/convert_to_order |
Convert a Quote into an Order |
| POST | /api/v1/rentals/{id}/change_status |
Move to another status within the current state |
| POST | /api/v1/rentals/{id}/reinstate |
Reinstate a closed rental |
| POST | /api/v1/rentals/{id}/reopen |
Re-open a completed order |
| POST | /api/v1/rentals/{id}/revert_to_quote |
Revert an Order back to a Quote |
| POST | /api/v1/rentals/{id}/revert_to_enquiry |
Revert a Quote back to an Enquiry |
| POST | /api/v1/rentals/{id}/lock_locks |
Apply FX/tax locks (freeze rates) |
| POST | /api/v1/rentals/{id}/unlock_locks |
Release FX/tax locks for re-pricing |
| POST | /api/v1/rentals/{id}/involved_contacts |
Add a involved_contact |
| PATCH | /api/v1/rentals/{id}/involved_contacts/{involved_contact} |
Update a involved_contact's role or mute flag |
| DELETE | /api/v1/rentals/{id}/involved_contacts/{involved_contact} |
Remove a involved_contact |
| POST | /api/v1/rentals/{id}/items |
Add a line item |
| PATCH | /api/v1/rentals/{id}/items/{item} |
Update a line item |
| DELETE | /api/v1/rentals/{id}/items/{item} |
Remove a line item |
| PATCH | /api/v1/rentals/{id}/items/tree |
Restructure the line-item tree (reorder/nest) |
| PATCH | /api/v1/rentals/{id}/items/{item}/fulfilment |
Dispatch/return/adjust a bulk line |
| POST | /api/v1/rentals/{id}/quick_allocate |
Batch-allocate barcoded assets |
| POST | /api/v1/rentals/{id}/quick_prepare |
Batch-prepare allocated assets |
| POST | /api/v1/rentals/{id}/quick_book_out |
Batch-dispatch barcoded assets |
| POST | /api/v1/rentals/{id}/quick_check_in |
Batch-return barcoded assets |
| POST | /api/v1/rentals/{id}/costs |
Add an ad-hoc cost |
| PATCH | /api/v1/rentals/{id}/costs/{cost} |
Update a cost |
| DELETE | /api/v1/rentals/{id}/costs/{cost} |
Remove a cost |
| POST | /api/v1/rentals/{id}/deal_price |
Set a manual deal-total override |
| DELETE | /api/v1/rentals/{id}/deal_price |
Clear the deal-total override |
Authentication
Requires a Sanctum bearer token with rentals:read (GET) or rentals:write (POST/PUT/DELETE) ability, alongside the matching rentals.view / rentals.create / rentals.edit / rentals.delete permission.
Rental Field Reference
All rental responses include the following fields under the rental key.
Identity & Header
| Field | Type | Description |
|---|---|---|
id |
integer | Rental identifier |
title |
string | Rental title |
number |
string|null | Zero-padded rental number (e.g. "0000000042") |
reference |
string|null | External reference / PO number |
description |
string|null | Internal description |
external_description |
string|null | Customer-facing description |
account_id |
integer|null | Owning account (customer) id |
venue_id |
integer|null | Venue account id |
warehouse_id |
integer|null | Home warehouse id |
owned_by |
integer|null | Owner (staff account) id |
source_rental_id |
integer|null | Source rental id when this was cloned from another |
rating |
integer|null | Customer satisfaction rating (1–5) |
tag_list |
string[] | Tag labels attached to the rental |
custom_fields |
object | Custom field values as a flat key/value map ({} when none) |
State & Status
| Field | Type | Description |
|---|---|---|
state |
integer | compatibility state code: 0 Enquiry, 1 Quote, 2 Order |
state_label |
string | Human-readable state label (e.g. "Quote") |
state_name |
string | Alias of state_label (API compatibility) |
status |
integer | Per-state status integer (see the Two-Axis table below) |
status_label |
string | Human-readable status label (e.g. "Reserved") |
status_name |
string | Alias of status_label (API compatibility) |
availability_phase |
string | Demand phase implied by the current status (e.g. "reserved", "on_hire") |
Dates
| Field | Type | Description |
|---|---|---|
starts_at |
string|null | Hire start (ISO 8601 UTC) |
ends_at |
string|null | Hire end (ISO 8601 UTC) |
charge_starts_at |
string|null | Chargeable period start |
charge_ends_at |
string|null | Chargeable period end |
ordered_at |
string|null | Timestamp when the rental was converted to an order |
quote_invalid_at |
string|null | Date/time after which the quote expires |
prep_starts_at / prep_ends_at |
string|null | Warehouse preparation window |
load_starts_at / load_ends_at |
string|null | Loading window |
deliver_starts_at / deliver_ends_at |
string|null | Delivery window |
setup_starts_at / setup_ends_at |
string|null | Setup/rigging window |
show_starts_at / show_ends_at |
string|null | Show/event window |
takedown_starts_at / takedown_ends_at |
string|null | Takedown/de-rig window |
collect_starts_at / collect_ends_at |
string|null | Collection window |
unload_starts_at / unload_ends_at |
string|null | Unloading window |
deprep_starts_at / deprep_ends_at |
string|null | De-preparation window |
Fulfilment Flags
| Field | Type | Description |
|---|---|---|
use_chargeable_days |
boolean | Whether chargeable days override the duration calculation |
chargeable_days |
string|null | Override chargeable day count (decimal string) |
open_ended_rental |
boolean | Whether the hire has no fixed return date |
customer_collecting |
boolean | Customer collects from warehouse (affects delivery demand) |
customer_returning |
boolean | Customer returns to warehouse (affects return demand) |
invoiced |
boolean | Whether the rental has been invoiced |
has_shortage |
boolean | Whether any line item currently has a shortage |
Delivery & Collection Addresses
| Field | Type | Description |
|---|---|---|
delivery_address_id |
integer|null | Account address id for delivery |
collection_address_id |
integer|null | Account address id for collection |
delivery_instructions |
string|null | Free-text delivery instructions |
collection_instructions |
string|null | Free-text collection instructions |
Include delivery_address and collection_address via ?include=delivery_address,collection_address to embed the full address object (street, city, postcode, country, etc.).
Pricing & Totals
All money values are returned as decimal strings (e.g. "125.50").
| Field | Type | Description |
|---|---|---|
currency_code |
string|null | ISO 4217 currency code (e.g. "GBP") |
exchange_rate |
string | Exchange rate snapshot at creation or last unlock (decimal string) |
exchange_rate_locked |
boolean | Whether the exchange rate is locked |
tax_locked |
boolean | Whether the tax snapshot is locked |
pricing_locked |
boolean | Computed: true when either exchange_rate_locked or tax_locked is set |
prices_include_tax |
boolean | Whether prices are tax-inclusive |
charge_total |
string | Gross headline total (net when no deal override) |
deal_total |
string|null | Manual deal-total override when set |
rental_charge_total |
string | Total for rental lines |
sale_charge_total |
string | Total for sale lines |
service_charge_total |
string | Total for service, labour, and surcharge lines |
sub_rental_charge_total |
string | Total for sub-rental lines |
transit_charge_total |
string | Total for delivery costs |
loss_damage_charge_total |
string | Total for loss/damage costs |
charge_excluding_tax_total |
string | Net total (all lines and costs, tax excluded) |
tax_total |
string | Total tax |
charge_including_tax_total |
string | Gross total including tax |
Quote Version Summary
| Field | Type | Description |
|---|---|---|
active_version_id |
integer | Id of the currently active quote version |
version_count |
integer | Total number of versions |
has_alternatives |
boolean | Whether there is more than one non-superseded version |
Lazy Includes
These fields are null unless the relationship is eager-loaded via ?include=:
| Include key | Field | Description |
|---|---|---|
account |
account |
{id, name} reference for the owning account |
venue |
venue |
{id, name} reference for the venue account |
warehouse |
warehouse |
{id, name} reference for the warehouse |
owner |
owner |
{id, name} reference for the owning user/account |
delivery_address |
delivery_address |
Full address object |
collection_address |
collection_address |
Full address object |
items |
items |
Array of line item objects |
costs |
costs |
Array of cost objects |
versions |
versions |
Array of quote version objects |
involved_contacts |
involved_contacts |
Array of involved_contact objects |
Response Meta Block
Every show/write response includes a top-level meta block alongside rental:
{
"rental": { ... },
"meta": {
"can_edit": true,
"can_destroy": false
}
}
| Field | Type | Description |
|---|---|---|
can_edit |
boolean | Whether the authenticated user can update this rental (respects policy + closed state) |
can_destroy |
boolean | Whether the authenticated user can delete (soft-delete) this rental |
Two-Axis State Model
| State | state |
Statuses (status) |
|---|---|---|
| Enquiry | 0 |
0 Unconfirmed |
| Quote | 1 |
0 Provisional, 1 Reserved, 2 Lost, 3 Dead, 4 Postponed |
| Order | 2 |
0 Active, 1 Dispatched, 2 On Hire, 3 Returned, 4 Checked, 5 Complete, 6 Cancelled |
Each response includes both the raw compatibility integer codes (state, status) and human-readable labels (state_label, status_label), plus the availability_phase the current status places on the availability engine.
List Rentals
GET /api/v1/rentals
Returns rentals under the rentals key with a meta block (total, per_page, page).
Filters
Ransack-compatible q[field_predicate]=value parameters:
| Parameter | Description |
|---|---|
q[subject_cont]=stage |
title contains |
q[state_eq]=1 |
Filter by state (0 Enquiry, 1 Quote, 2 Order) |
q[status_eq]=0 |
Filter by per-state status |
q[account_id_eq]=42 |
Filter by account |
q[warehouse_id_eq]=3 |
Filter by warehouse |
q[reference_eq]=PO-123 |
Filter by reference |
q[invoiced_eq]=false |
Filter by invoiced flag |
q[starts_at_gteq]=2026-01-01 |
Starts on or after |
q[cf.field_name_eq]=value |
Filter by custom field |
Includes
Eager-load relationships with ?include=account,venue,warehouse,owner,items,items.assets,costs,versions,versions.items.
The rental's custom_fields object is populated from the customFieldValues relation, which is eager-loaded by default — custom_fields is always present ({} when none are set), with no need to request it explicitly.
Live filters
Apply a saved view with ?filter_id=42. The view's columns project a sparse response (only those fields plus id); custom-field columns (cf.*) filter the custom_fields object. Explicit q filters take priority over the view's filters. See Live filters API for details.
Sort
sort=created_at, sort=-starts_at, sort=title, sort=charge_total, sort=state, sort=status (prefix with - for descending).
Pagination
Offset-based: ?page=2&per_page=20 (max per_page 100).
Show Rental
GET /api/v1/rentals/{id}
Returns the rental under the rental key. Supports ?include=.
The rental body also exposes the version summary fields active_version_id, version_count, and has_alternatives, the tag_list array, and (when included) a versions collection — so the Show page can sideload the quote versions in one call with ?include=versions,versions.items.
Rental Assets
GET /api/v1/rentals/{id}/assets
A flat, paginated list of every per-asset assignment across all of the rental's line items (the rental_item_assets rows), returned under the assets key with a meta block. Use this for the Show page's assets tab without loading the full items payload. Each row matches the asset shape used elsewhere (id, rental_item_id, asset_id, status + status_label, lifecycle timestamps).
Requires rentals.view / rentals:read.
Filters
Ransack-compatible q[field_predicate]=value:
| Parameter | Description |
|---|---|
q[status_eq]=2 |
Filter by assignment status (0 Allocated … 5 Finalised) |
q[rental_item_id_eq]=10 |
Filter to a single line |
q[asset_id_eq]=55 |
Filter by assigned asset |
Sort
sort=status, sort=-dispatched_at, sort=allocated_at, sort=created_at (prefix with - for descending). Defaults to oldest-first.
Rental Availability
GET /api/v1/rentals/{id}/availability
Returns the per-line availability picture under the availability key: for each catalogue-item-backed line, how many units are free over the line's own window at its own warehouse with the line's own demand excluded, plus its shortage shortfall. Lines that reference no catalogue item (services, ad-hoc lines) are omitted. Computed live from demands (no snapshot dependency), so it always reflects current state.
Each entry: rental_item_id, catalogue_item_id, warehouse_id, requested_quantity, available_for_item, shortage_quantity, has_shortage, from, to.
Requires rentals.view / rentals:read.
Rental Activity
GET /api/v1/rentals/{id}/activity
A paginated, newest-first read of the audit trail (action_logs) scoped to this rental, returned under the activity key with a meta block. Saves the caller from knowing the underlying model FQCN that the global /api/v1/actions endpoint filters on. Each entry matches the action-log shape (action, user_id, user_name, old_values, new_values, ip_address, created_at).
Gated like the global action-log endpoint: requires action-log.view / action-log:read.
Create Rental
POST /api/v1/rentals
Creates the rental as a Enquiry / Unconfirmed. A zero-padded compatibility number (e.g. "0000000042") is auto-generated from a per-warehouse running sequence and returned in the response. Money is supplied as a decimal string or minor-unit integer and returned as a decimal string.
Identity & Header
| Field | Type | Required | Description |
|---|---|---|---|
title |
string | Yes | The rental title (max 255) |
account_id |
integer | No | Owning account — must be an Company account (not archived) |
venue_id |
integer | No | Venue account |
owned_by |
integer | No | Owner account |
warehouse_id |
integer | No | Warehouse |
reference |
string | No | External reference / PO number (max 255) |
description |
string | No | Internal description |
external_description |
string | No | Customer-facing description |
currency |
string | No | ISO-4217 currency code (default GBP) |
prices_include_tax |
boolean | No | Whether entered prices are tax-inclusive (set at create time only) |
charge_total |
numeric | No | Header charge total (int = minor units, decimal string/float = major units) |
rating |
integer | No | Sales priority rating 0–5 |
tag_list |
string[] | No | Tag labels |
custom_fields |
object | No | Custom field values as a flat key/value map |
Hire & Charge Dates
| Field | Type | Required | Description |
|---|---|---|---|
starts_at |
date | No | Hire start |
ends_at |
date | No | Hire end (on/after starts_at) |
charge_starts_at |
date | No | Chargeable period start |
charge_ends_at |
date | No | Chargeable period end (on/after charge_starts_at) |
Event Logistics Dates
All fields are optional ISO 8601 date strings. Each phase is a _starts_at / _ends_at pair.
| Field pair | Description |
|---|---|
prep_starts_at / prep_ends_at |
Warehouse preparation window |
load_starts_at / load_ends_at |
Loading window |
deliver_starts_at / deliver_ends_at |
Delivery window |
setup_starts_at / setup_ends_at |
Setup/rigging window |
show_starts_at / show_ends_at |
Show/event window |
takedown_starts_at / takedown_ends_at |
Takedown/de-rig window |
collect_starts_at / collect_ends_at |
Collection window |
unload_starts_at / unload_ends_at |
Unloading window |
deprep_starts_at / deprep_ends_at |
De-preparation window |
ordered_at |
Timestamp when the rental was converted to an order |
quote_invalid_at |
Date/time after which the quote expires |
Fulfilment Flags
| Field | Type | Required | Description |
|---|---|---|---|
use_chargeable_days |
boolean | No | Whether chargeable_days overrides the duration calculation (default false) |
chargeable_days |
numeric string | No | Override chargeable day count |
open_ended_rental |
boolean | No | Whether the hire has no fixed return date (default false) |
customer_collecting |
boolean | No | Customer collects from warehouse (default false) |
customer_returning |
boolean | No | Customer returns to warehouse (default false) |
Delivery & Collection
| Field | Type | Required | Description |
|---|---|---|---|
delivery_address_id |
integer | No | Account address id for delivery (must belong to account_id) |
collection_address_id |
integer | No | Account address id for collection (must belong to account_id) |
delivery_instructions |
string | No | Free-text delivery instructions |
collection_instructions |
string | No | Free-text collection instructions |
Returns 201 with the created rental under the rental key.
Update Rental
PUT /api/v1/rentals/{id}
Partial update of editable header fields. All fields are optional — omitted fields are left unchanged. A closed/terminal rental (Complete, Cancelled, Lost, Dead) cannot be edited and yields a 422.
For nullable clearable fields (venue_id, reference, description, external_description, delivery_instructions, collection_instructions, delivery_address_id, collection_address_id, chargeable_days, rating, tag_list) an explicit null clears the value; omitting the key entirely leaves it unchanged.
Identity & Header
| Field | Type | Description |
|---|---|---|
title |
string | Rental title (max 255) |
account_id |
integer|null | Owning account — must be an Company account |
venue_id |
integer|null | Venue account (null clears) |
owned_by |
integer|null | Owner account |
warehouse_id |
integer|null | Warehouse |
reference |
string|null | External reference / PO number (null clears) |
description |
string|null | Internal description (null clears) |
external_description |
string|null | Customer-facing description (null clears) |
rating |
integer|null | Sales priority rating 0–5 (null clears) |
tag_list |
string[]|null | Replacement tag list (null clears all tags) |
custom_fields |
object | Custom field values to update |
Hire & Charge Dates
| Field | Type | Description |
|---|---|---|
starts_at |
date|null | Hire start |
ends_at |
date|null | Hire end (on/after starts_at) |
charge_starts_at |
date|null | Chargeable period start |
charge_ends_at |
date|null | Chargeable period end |
Event Logistics Dates
All fields are optional ISO 8601 date strings. Providing a value sets or moves the date; omitting the key leaves it unchanged. Dates in this group are not clearable via null — use the Create path or contact the warehouse's team if a date must be removed.
| Field pair | Description |
|---|---|
prep_starts_at / prep_ends_at |
Warehouse preparation window |
load_starts_at / load_ends_at |
Loading window |
deliver_starts_at / deliver_ends_at |
Delivery window |
setup_starts_at / setup_ends_at |
Setup/rigging window |
show_starts_at / show_ends_at |
Show/event window |
takedown_starts_at / takedown_ends_at |
Takedown/de-rig window |
collect_starts_at / collect_ends_at |
Collection window |
unload_starts_at / unload_ends_at |
Unloading window |
deprep_starts_at / deprep_ends_at |
De-preparation window |
ordered_at |
Timestamp when converted to an order |
quote_invalid_at |
Quote expiry date/time |
Fulfilment Flags
| Field | Type | Description |
|---|---|---|
use_chargeable_days |
boolean | Toggle chargeable-days override |
chargeable_days |
numeric string|null | Override chargeable day count (null clears) |
open_ended_rental |
boolean | Toggle open-ended rental |
customer_collecting |
boolean | Toggle customer-collects flag |
customer_returning |
boolean | Toggle customer-returns flag |
invoiced |
boolean | Mark the rental as invoiced |
Delivery & Collection
| Field | Type | Description |
|---|---|---|
delivery_address_id |
integer|null | Account address id for delivery (null clears) |
collection_address_id |
integer|null | Account address id for collection (null clears) |
delivery_instructions |
string|null | Free-text delivery instructions (null clears) |
collection_instructions |
string|null | Free-text collection instructions (null clears) |
Delete Rental
DELETE /api/v1/rentals/{id}
Soft-deletes the rental. The deletion is recorded as an event so history is preserved; the row drops out of list and availability reads. Returns 204.
Convert to Quote
POST /api/v1/rentals/{id}/convert_to_quote
Transitions a Enquiry rental to a Quote / Provisional. Returns the updated rental under the rental key. An invalid transition (not currently an Enquiry) yields a 422.
Convert to Order
POST /api/v1/rentals/{id}/convert_to_order
Transitions a Quote rental to an Order / Active. Returns the updated rental. An invalid transition yields a 422.
Change Status
POST /api/v1/rentals/{id}/change_status
Moves the rental to a different status within its current state.
| Field | Type | Required | Description |
|---|---|---|---|
status |
integer | Yes | The per-state status integer (see the table above) |
Returns the updated rental under the rental key.
Error Cases
| Status | Condition |
|---|---|
| 401 | No valid Sanctum token |
| 403 | Token lacks rentals:read/rentals:write, or the user lacks the permission |
| 404 | Rental not found (or soft-deleted) |
| 422 | Validation failure, or an invalid state/status transition |
Available Actions
GET /api/v1/rentals/{id}/available_actions
Returns the set of legal lifecycle actions for the current state/status combination, resolving each action's feasibility given the current state, the actor's permissions, and system checks (shortage gate, lock state, dispatch state). The UI uses this to show/hide action buttons without needing to reproduce the guard logic.
Requires rentals.view / rentals:read.
Response
{
"available_actions": [
{
"action": "convert_to_order",
"label": "Convert to Order",
"available": true,
"disabled": false,
"reason": null,
"reason_code": null
},
{
"action": "revert_to_enquiry",
"label": "Revert to Enquiry",
"available": false,
"disabled": true,
"reason": "Only an open, provisional quotation can be reverted to a draft.",
"reason_code": "invalid_state"
}
]
}
Each entry has action (the endpoint slug), label, available (boolean — the actor has permission and the state allows it), disabled, reason (human-readable explanation when not available), and reason_code (machine-readable, e.g. invalid_state, nothing_to_unlock, dispatched, permission).
Restore Rental
POST /api/v1/rentals/{id}/restore
Restores a soft-deleted rental. The {id} must identify a soft-deleted record (use trashed=true on the list or the Delete/Restore flow from the UI). Returns the restored rental under the rental key (200 OK).
Requires rentals.delete / rentals:write.
Clone Rental
POST /api/v1/rentals/{id}/clone
Creates a copy of the rental as a new Enquiry, including its line items, costs, and custom fields. The clone gets a fresh zero-padded rental number and is otherwise independent of the source; its source_rental_id points back to the original. Returns the new rental under the rental key (201 Created).
Requires rentals.create / rentals:write.
| Field | Type | Required | Description |
|---|---|---|---|
title |
string | No | title for the clone (defaults to "Copy of [original title]") |
Reinstate
POST /api/v1/rentals/{id}/reinstate
Reinstates a Lost, Dead, Postponed, or Cancelled rental back to its default open status (Quote/Provisional or Order/Active). Returns the updated rental. A 422 is returned for any other state/status.
Requires rentals.edit / rentals:write.
Revert to Quote
POST /api/v1/rentals/{id}/revert_to_quote
Reverts an open Order (Active) back to a Quote / Provisional. The rental must not have any dispatched assets — if assets have been dispatched, the transition is rejected (422) until they are returned and all quantity demand is unwound.
Requires rentals.edit / rentals:write.
Revert to Enquiry
POST /api/v1/rentals/{id}/revert_to_enquiry
Reverts an open, provisional Quote back to a Enquiry / Unconfirmed. Only allowed while the quotation is in the Provisional (open) status — a Reserved or closed quotation cannot be reverted. Returns the updated rental.
Requires rentals.edit / rentals:write.
Reopen
POST /api/v1/rentals/{id}/reopen
Re-opens a Completed order back to Order / Active. Complete is the only terminal order status that can be re-opened (Cancelled orders use reinstate). Returns the updated rental.
Requires rentals.edit / rentals:write.
Unlock Locks
POST /api/v1/rentals/{id}/unlock_locks
Releases any FX rate lock and/or tax lock currently in place on an order. Once unlocked, the next write (line edit, cost add, deal-price change) will re-snapshot the current exchange rates and tax rules. Returns the updated rental.
Requires the rentals.unlock_rates permission and rentals:write ability. Returns 422 when neither lock is set.
Lock Rates
POST /api/v1/rentals/{id}/lock_locks
Applies FX and tax locks on a rental — snapshots the current exchange rate and freezes the tax treatment so later writes re-price the agreed net basis at the frozen rate rather than re-deriving FX/tax. This is the inverse of unlock_locks.
| Field | Type | Required | Description |
|---|---|---|---|
reason |
string | No | Free-text note recorded with the lock event |
Requires the rentals.unlock_rates permission and rentals:write ability. Returns 422 when locks are already active. Returns the updated rental.
Restructure Items Tree
PATCH /api/v1/rentals/{id}/items/tree
Reorders and/or re-nests the line-item tree in one operation. The body carries the full item set in display pre-order (top-to-bottom), each node carrying its target tree depth; the server rebuilds every item's materialised path from order + depth.
| Field | Type | Required | Description |
|---|---|---|---|
nodes |
array | Yes | Ordered [{id, depth}] — every item in final display pre-order, depth 1-based |
prune_orphans |
boolean | No | When true, items omitted from nodes are removed (used by the local-first editor sync). Default false |
Requires rentals.edit / rentals:write. Returns the updated rental.
Rental Involved contacts
Involved contacts are accounts (contacts or staff) attached to a rental in a named role — for example, a site contact, a crew chief, or an on-site supervisor. They are compatibility aliases (involved_contacts[] in the field set) and returned via ?include=involved_contacts.
List (via include)
GET /api/v1/rentals/{id}?include=involved_contacts
Involved contacts are returned in the involved_contacts array on the rental. Each entry:
| Field | Type | Description |
|---|---|---|
id |
integer | Involved contact record id |
rental_id |
integer | Parent rental id |
account_id |
integer | The account record attached |
role |
string|null | Named role (free text, e.g. "Site Contact") |
mute |
boolean | Whether notifications are suppressed for this involved_contact |
created_at / updated_at |
string | ISO 8601 UTC timestamps |
account |
object|null | {id, name} reference when include=involved_contacts.account is also requested |
Add Involved contact
POST /api/v1/rentals/{id}/involved_contacts
| Field | Type | Required | Description |
|---|---|---|---|
account_id |
integer | Yes | The account to attach (must exist) |
role |
string | No | Named role label |
mute |
boolean | No | Suppress notifications (default false) |
Returns the new involved_contact under the involved_contact key (201 Created).
Requires rentals.edit / rentals:write.
Update Involved contact
PATCH /api/v1/rentals/{id}/involved_contacts/{involved_contact}
| Field | Type | Description |
|---|---|---|
role |
string|null | New role (set null to clear) |
mute |
boolean | Updated notification-mute flag |
Returns the updated involved_contact under the involved_contact key. The {involved_contact} must belong to the rental (404 otherwise).
Remove Involved contact
DELETE /api/v1/rentals/{id}/involved_contacts/{involved_contact}
Removes the involved_contact. Returns 204 No Content. The {involved_contact} must belong to the rental.
Rental Items
Line items are the priced rows of a rental. Every write below flows through the
event-sourced lifecycle: the rate engine resolves the per-unit price and duration-aware
subtotal, the line discount is applied to the net (before tax), and the tax engine
computes line-level tax. The rental's totals (charge_excluding_tax_total,
tax_total, charge_including_tax_total, the per-type rental/sale/service
totals, and the headline charge_total) are recomputed and rolled up automatically.
Every item endpoint returns the parent rental (under the rental key)
with its refreshed totals — include ?include=items (or items.assets) when reading
to see the line rows themselves.
Optional lines (is_optional = true) still claim availability but are excluded from
all charge totals.
Each barcoded line also reports its bulk-line fulfilment progress via dispatched_quantity
and returned_quantity (decimal strings) — the quantities physically booked out and
checked back in — so the UI can show dispatch progress without loading the asset rows.
A quantity reduction is rejected (422) when the new quantity would drop below the
number of barcoded assets already allocated to the line, or below the line's
dispatched_quantity. Deallocate or return units first.
Add Line Item
POST /api/v1/rentals/{id}/items
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Display name of the line |
item_id |
integer | No | Catalogue item id (catalogue item) the line refers to |
item_type |
string | No | Polymorphic type (CatalogueItem / FQCN) — pairs with item_id |
description |
string | No | Line description |
quantity |
numeric | No | Quantity (default 1) |
transaction_type |
integer | No | 0 Rental, 1 Sale, 2 Service, 3 Sub-rental |
charge_period |
integer | No | 0 Hour, 1 Day, 2 Week, 3 Month, 4 Fixed |
starts_at / ends_at |
datetime | No | Per-item hire window (inherits the rental's dates when null) |
is_optional |
boolean | No | Exclude from charge totals (default false) |
unit_price |
money | No | Manual unit-price override (int = minor units, decimal string/float = major units against currency). Omit to price from the rate engine |
currency |
string | No | Currency scale for unit_price (default base currency) |
discount_percent |
numeric | No | Line discount percentage (applied before tax) |
sort_order |
integer | No | Display ordering |
notes |
string | No | Free-form notes |
custom_fields |
object | No | Inline line-item custom-field map |
Returns the rental with refreshed totals under the rental key (201 Created).
Update Line Item
PATCH /api/v1/rentals/{id}/items/{item}
Accepts any subset of the fields below; each provided field dispatches its own lifecycle event in turn (quantity → price → discount → dates → optional → substitution).
| Field | Type | Description |
|---|---|---|
quantity |
numeric | New quantity (resyncs availability demand) |
unit_price |
money | Manual unit-price override; send null to clear it and revert to rate pricing |
currency |
string | Currency scale for unit_price |
discount_percent |
numeric | Line discount percentage; send null to clear |
starts_at / ends_at |
datetime | Per-item hire window (resyncs availability demand) |
is_optional |
boolean | Toggle whether the line counts toward totals |
item_id / item_type |
integer / string | Substitute the catalogue reference (re-prices, resyncs demand) |
name |
string | New display name (with a substitution) |
Returns the rental with refreshed totals.
Remove Line Item
DELETE /api/v1/rentals/{id}/items/{item}
Releases the line's availability demand, removes the row, and rolls the totals back
down. Returns the rental with refreshed totals (200 OK).
Asset Allocation
Barcoded line items track which specific physical assets (assets) are committed to them. Each allocation pins one asset to a line, increments the stock level's allocated quantity, and transitions the line's availability demand from a single quantity-based demand to one asset-specific demand per allocated unit (the quantity-based demand shrinks by the number allocated). Deallocation reverses this.
Allocation is permitted while the rental is open (a Reserved quotation or an
Order). The asset must belong to the line's catalogue item, be barcoded, and be free for
the line's window — otherwise a 422 is returned. Allocations carry no body beyond
the asset id; the asset assignment is returned under the singular asset key.
Allocate Asset
POST /api/v1/rentals/{id}/items/{item}/assets
Body: asset_id (required). Fires AssetAllocated. Returns the new
assignment (201 Created).
Update Asset
PATCH /api/v1/rentals/{id}/items/{item}/assets/{asset}
Mutates an existing assignment. The action field (required) selects the
operation:
action |
Effect | Extra body |
|---|---|---|
prepare |
Allocated → Prepared (picked/packed) | — |
revert |
Prepared → Allocated | — |
set_container |
Nest the asset inside a kit/case | container_asset_id |
clear_container |
Remove from its flightcase | — |
substitute |
Swap the physical asset, preserving status | new_asset_id, optional reason |
dispatch |
Allocated/Prepared → Dispatched (order only) | optional dispatched_by, vehicle_id, notes, dispatched_at |
on_hire |
Dispatched → On Hire | — |
return |
Dispatched/On Hire → Checked In | optional received_by, return_warehouse_id, returned_at |
check |
Checked In → Finalised (condition assessment) | condition (0=Good, 1=Damaged, 2=Missing), optional checked_by, damage_notes, checked_at |
revert_status |
Step the asset back to an earlier status | revert_to, optional reason |
An invalid status transition (e.g. preparing an already-prepared asset, dispatching
an unallocated asset, or dispatching on a quote rather than an order) yields a 422.
Returns the updated assignment (200 OK).
The fulfilment actions auto-promote the parent rental's aggregate status — see Dispatch & Return.
Deallocate Asset
DELETE /api/v1/rentals/{id}/items/{item}/assets/{asset}
Optional body: reason. Fires AssetDeallocated — removes the assignment row,
decrements the asset's allocated quantity, and reverts the freed unit to a
quantity-based demand. Allowed only while the asset is Allocated or Prepared;
otherwise 422. Returns 204 No Content.
Quick Allocate (batch)
POST /api/v1/rentals/{id}/quick_allocate
Body: allocations — a non-empty array of {rental_item_id, asset_id}
pairs. Every allocation fires inside a single atomic commit, so a failure on any one
(asset unavailable, wrong catalogue item, or allocating beyond the line's quantity) rolls
back the whole batch. All line items must belong to the rental. Returns the
rental with its items + assets (200 OK).
Quick Prepare (batch)
POST /api/v1/rentals/{id}/quick_prepare
Body: asset_ids — a non-empty array of asset-assignment ids, plus optional
prepared_at. Every asset is prepared (Allocated → Prepared) inside a single atomic
commit, so a failure on any one (an asset not in the Allocated status) rolls back the
whole batch. All assets must belong to the rental. Returns the rental with
its items + assets (200 OK).
Dispatch & Return
Once a rental is an order, its line items move through the fulfilment
cycle — out of the warehouse on dispatch, back on return, then condition-checked.
Barcoded lines track this per physical asset (via the asset actions above);
bulk (non-barcoded) lines track it as aggregate quantities on the line.
Aggregate auto-promotion
Every dispatch/return/check re-derives the rental's order sub-status from the
state of all its items (the "lowest common denominator") and, when it changes, fires
a distinct RentalStatusPromoted event — a real, audited status change with its
own timestamp, not a silent column write:
| Condition across all items | Order status |
|---|---|
| Nothing dispatched yet | Active |
| Some allocated assets still undispatched | Dispatched |
| Everything out, nothing returned | On Hire |
| Some returned but not yet checked | Returned |
| Everything dispatched is checked | Checked |
A dispatch before the line's planned start pulls that asset's availability demand start back to the actual dispatch time; a return moves the demand end to the actual return time (with turnaround applied off the real return) and releases availability immediately on scan — there is no finalisation gate.
Bulk-line fulfilment
PATCH /api/v1/rentals/{id}/items/{item}/fulfilment
Dispatch, return, or adjust a non-barcoded line. The action field (required)
selects the operation:
action |
Effect | Extra body |
|---|---|---|
dispatch |
Record a (partial) dispatch (order only) | quantity, optional dispatched_by |
return |
Record a (partial) return | quantity, optional received_by, condition |
adjust |
Change the requested quantity mid-cycle | new_quantity, optional reason |
Partial dispatch/return is first-class: 60 m of a 100 m cable line can go out now and
40 m later. Over-dispatch (beyond the requested quantity), over-return (beyond what is
out), or adjusting below the dispatched quantity each yield a 422. The line's
effective demand is quantity - returned_quantity. Returns the updated line (200 OK).
Quick Book-Out (batch)
POST /api/v1/rentals/{id}/quick_book_out
Body: asset_ids — a non-empty array of asset-assignment ids, plus optional
dispatched_by, vehicle_id, dispatched_at. Every asset is dispatched inside a
single atomic commit (a failure on any one rolls back the batch), and the order's
aggregate status promotes once consistently. All assets must belong to the
rental. Returns the rental with its items + assets (200 OK).
Quick Check-In (batch)
POST /api/v1/rentals/{id}/quick_check_in
Body: asset_ids (required array), optional received_by, return_warehouse_id,
returned_at, and finalise (boolean). Each asset is returned in one atomic commit;
when finalise is true each return is immediately condition-checked (Good), clearing
the check-in queue in a single pass. Returns the rental with its items + assets
(200 OK).
Rental Costs
Costs are ad-hoc charges that sit alongside the priced line items — delivery, crew
labour, surcharges, insurance, loss/damage recovery, etc. Unlike line items they are
not priced by the rate engine: each carries its own amount (per unit). Costs are
taxed exactly like line items (inclusive/exclusive, line-level rounding) and rolled into
the rental totals.
Each cost's net is routed into an cost category bucket by cost_type:
cost_type |
Value | Total bucket |
|---|---|---|
| Delivery | 0 |
transit_charge_total |
| Labour | 1 |
service_charge_total |
| Surcharge | 2 |
service_charge_total |
| Insurance | 3 |
service_charge_total |
| Loss / Damage | 4 |
loss_damage_charge_total |
| Miscellaneous | 5 |
service_charge_total |
Regardless of type, every non-optional cost also feeds charge_excluding_tax_total,
tax_total, charge_including_tax_total, and the headline charge_total. Optional
costs (is_optional = true) are excluded from all totals. Every cost endpoint returns
the parent rental (under the rental key) with its refreshed totals;
include ?include=costs when reading to see the cost rows.
Add Cost
POST /api/v1/rentals/{id}/costs
| Field | Type | Required | Description |
|---|---|---|---|
description |
string | Yes | Cost description |
cost_type |
integer | No | 0 Delivery, 1 Labour, 2 Surcharge, 3 Insurance, 4 Loss/Damage, 5 Misc (default 5) |
transaction_type |
integer | No | 0 Rental, 1 Sale, 2 Service, 3 Sub-rental (default 2 Service) |
amount |
money | No | Per-unit charge (int = minor units, decimal string/float = major units against currency) |
currency |
string | No | Currency scale for amount (default base currency) |
quantity |
numeric | No | Quantity (default 1) |
is_optional |
boolean | No | Exclude from charge totals (default false) |
sort_order |
integer | No | Display ordering |
notes |
string | No | Free-form notes |
Returns the rental with refreshed totals under the rental key (201 Created).
Update Cost
PATCH /api/v1/rentals/{id}/costs/{cost}
Accepts any subset of the fields below; omitted fields are left untouched.
| Field | Type | Description |
|---|---|---|
description |
string | New description |
cost_type |
integer | New cost type |
transaction_type |
integer | New transaction type |
amount |
money | New per-unit charge |
currency |
string | Currency scale for amount |
quantity |
numeric | New quantity |
is_optional |
boolean | Toggle whether the cost counts toward totals |
sort_order |
integer | Display ordering |
notes |
string | Free-form notes |
Returns the rental with refreshed totals.
Remove Cost
DELETE /api/v1/rentals/{id}/costs/{cost}
Removes the cost row and rolls the totals back down. Returns the rental with
refreshed totals (200 OK).
Set Deal Price
POST /api/v1/rentals/{id}/deal_price
| Field | Type | Required | Description |
|---|---|---|---|
deal_total |
money | Yes | Manual deal total (int = minor units, decimal string/float = major units) |
currency |
string | No | Currency scale for deal_total |
Replaces the engine-computed headline charge_total with the manual override. The
per-type and tax totals continue to reflect the line items. Returns the rental.
Rejected (422) on a locked order — like a line price override or discount, setting a
deal price changes the effective charge and is blocked while the order's FX/tax locks are
in place. Release the locks (POST /api/v1/rentals/{id}/unlock_locks) first.
Clear Deal Price
DELETE /api/v1/rentals/{id}/deal_price
Clears the manual override, reverting charge_total to the engine-computed gross total.
Returns the rental. Also rejected (422) on a locked order.
Quote Versions
A quotation can carry multiple versions — sequential revisions (each new
revision supersedes its parent) and parallel alternatives (concurrent options the
customer chooses between). Exactly one version is active at any time: the
rental's totals, line-item scope, and availability demand all follow the active
version. Versions are a sub-resource of a rental — there is no top-level
versions endpoint. They reuse the rentals:read / rentals:write
abilities and the rentals.view / rentals.edit permissions.
A version's status is one of 0 Draft, 1 Sent, 2 Accepted, 3 Declined,
4 Superseded. Its version_type is 0 Revision or 1 Alternative. Money totals
are NET (tax-exclusive) decimal strings.
List Versions
GET /api/v1/rentals/{id}/versions
Returns the rental's versions (oldest first) in a versions collection. Add
?include=items to embed each version's line items.
Show Version
GET /api/v1/rentals/{id}/versions/{version}
Returns a single version. Add ?include=items to embed its line items.
Create Version
POST /api/v1/rentals/{id}/versions
| Field | Type | Required | Description |
|---|---|---|---|
version_type |
int | No | 0 Revision (default), 1 Alternative |
label |
string | No | Display label for the version |
source_version_id |
int | No | Version whose items are cloned (defaults to the active version) |
notes |
string | No | Free-text notes |
Clones the source version's line items into the new version, which becomes active. A
revision supersedes its parent; an alternative coexists. Valid only while the
rental is a Quote and within the rentals.max_versions (20) and
rentals.max_alternatives (5) caps. Returns the new version (201 Created).
Activate Version
POST /api/v1/rentals/{id}/versions/{version}/activate
Makes the version active. The rental's totals and item scope switch to it, and availability demand swaps from the previously-active version's items to this one's.
Send / Accept / Decline Version
POST /api/v1/rentals/{id}/versions/{version}/send
POST /api/v1/rentals/{id}/versions/{version}/accept
POST /api/v1/rentals/{id}/versions/{version}/decline
Advance the version through its customer workflow. send (Draft → Sent) stamps
sent_at; accept (Draft/Sent → Accepted) stamps accepted_at; decline
(Draft/Sent → Declined) stamps declined_at. An accepted version takes priority
when the quotation is converted to an order (an accepted version is confirmed over the
active one, and every other version is superseded).
Rename Version
PATCH /api/v1/rentals/{id}/versions/{version}
| Field | Type | Required | Description |
|---|---|---|---|
label |
string|null | Yes | New label (null clears it) |
Diff Versions
GET /api/v1/rentals/{id}/versions/{from}/diff/{to}
Returns a diff of the item-level content delta between two versions of the same
rental: added (in target, not source), removed (in source, not target),
changed (quantity / unit price / discount differ), and net_change (signed total
delta). Lines are matched by catalogue item. Both versions must belong to the rental.
Delete Version
DELETE /api/v1/rentals/{id}/versions/{version}
Removes a version and its line items (204 No Content). The active version and the
only remaining version cannot be deleted, and deletion is allowed only while the
rental is a Quote.
Error Cases
| Status | Condition |
|---|---|
| 401 | No valid Sanctum token |
| 403 | Token lacks rentals:write, or the user lacks the rentals.edit permission |
| 404 | Rental not found, or the line item / cost / version does not belong to the rental |
| 422 | Validation failure, a write against a closed/terminal rental, a version cap breach, or an invalid version transition (e.g. sending an already-sent version, deleting the active or only version) |