SIGNALS Documentation
API Reference

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 defaultcustom_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)