GET/api/v1/catalog-items
GET /api/catalog-items — newest/active first, filterable.
Parameters
| Name | In | Type | Required | Description |
|---|
active | query | — | No | `None` = all items (active + archived). |
category | query | — | No | |
group_name | query | — | No | |
search | query | — | No | Substring search over name/sku/category/group. |
Responses
Security: bearer
POST/api/v1/catalog-items
POST /api/catalog-items — create through the instant-first write path.
Request body
CreateCatalogItemRequest
Responses
| Status | Description |
|---|
200 | |
409 | SKU already exists or idempotency key reused |
422 | validation failure |
Security: bearer
GET/api/v1/catalog-items/{id}
GET /api/catalog-items/{id}.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
| Status | Description |
|---|
200 | |
404 | not visible in scope |
Security: bearer
PATCH/api/v1/catalog-items/{id}
PATCH /api/catalog-items/{id} — absent fields keep their value.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
UpdateCatalogItemRequest
Responses
| Status | Description |
|---|
200 | |
404 | not visible in scope |
409 | SKU already exists, version conflict, or idempotency key reused |
422 | validation failure |
Security: bearer
POST/api/v1/catalog-items/{id}/archive
POST /api/catalog-items/{id}/archive — flip `active` off (data, never
a delete). Already-archived items archive as a no-op replay of state.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
ArchiveCatalogItemRequest
Responses
| Status | Description |
|---|
200 | |
404 | not visible in scope |
409 | version conflict or idempotency key reused |
Security: bearer
GET/api/v1/client-loans
GET /api/client-loans.
Parameters
| Name | In | Type | Required | Description |
|---|
status | query | — | No | open | returned (absent = both). |
client_id | query | — | No | |
overdue_only | query | — | No | Only open loans past their expected return. |
limit | query | — | No | |
Responses
Security: bearer
POST/api/v1/client-loans
POST /api/client-loans — instant-first (IF1/IF2).
Request body
IssueLoanRequest
Responses
| Status | Description |
|---|
200 | |
404 | client / contract / device not visible in scope |
409 | device not in stock / already on an open loan / incomplete lease terms |
Security: bearer
GET/api/v1/client-loans/{id}
GET /api/client-loans/{id}.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
| Status | Description |
|---|
200 | |
404 | not visible in scope |
Security: bearer
POST/api/v1/client-loans/{id}/return
POST /api/client-loans/{id}/return — instant-first (IF1/IF2).
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
ReturnLoanRequest
Responses
| Status | Description |
|---|
200 | |
404 | not visible in scope |
409 | already returned / missing return location for stock devices |
Security: bearer
GET/api/v1/pipeline/deals
GET /api/pipeline/deals — newest first, filterable.
Parameters
| Name | In | Type | Required | Description |
|---|
client_id | query | — | No | |
stage_id | query | — | No | |
quote_id | query | — | No | |
status | query | — | No | Exact status: `open|won|lost`. |
owner_user_id | query | — | No | |
Responses
Security: bearer
POST/api/v1/pipeline/deals
POST /api/pipeline/deals — create an opportunity and record its
initial placement in the transition ledger (same transaction).
Request body
CreateDealRequest
Responses
| Status | Description |
|---|
200 | |
404 | client, quote, or stage not visible in scope |
409 | idempotency key reused |
422 | validation failure |
Security: bearer
GET/api/v1/pipeline/deals/{id}
GET /api/pipeline/deals/{id} — deal + transition ledger.
(Registered at the `get_one` name in the router.)
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
| Status | Description |
|---|
200 | |
404 | not visible in scope |
Security: bearer
PATCH/api/v1/pipeline/deals/{id}
PATCH /api/pipeline/deals/{id} — editable fields; the stage is not
touched here (that is the audited /stage leg).
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
UpdateDealRequest
Responses
| Status | Description |
|---|
200 | |
404 | deal, client, or quote not visible in scope |
409 | version conflict or idempotency key reused |
422 | validation failure |
Security: bearer
POST/api/v1/pipeline/deals/{id}/stage
POST /api/pipeline/deals/{id}/stage — one audited stage transition.
Guarded on the CURRENT stage (concurrent moves: first wins, loser gets
a typed 409); the ledger row commits in the same transaction.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
MoveStageRequest
Responses
| Status | Description |
|---|
200 | |
404 | deal or target stage not visible in scope |
409 | concurrent move or version conflict |
422 | validation failure |
Security: bearer
GET/api/v1/pipeline/stages
GET /api/pipeline/stages — ordered pipeline definition.
Parameters
| Name | In | Type | Required | Description |
|---|
include_inactive | query | — | No | `None` = active stages only (default); `true` includes inactive. |
Responses
Security: bearer
POST/api/v1/pipeline/stages
POST /api/pipeline/stages — create a stage (name unique per org).
Request body
CreateStageRequest
Responses
| Status | Description |
|---|
200 | |
409 | stage name already exists or idempotency key reused |
422 | validation failure |
Security: bearer
PATCH/api/v1/pipeline/stages/{id}
PATCH /api/pipeline/stages/{id}.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
UpdateStageRequest
Responses
| Status | Description |
|---|
200 | |
404 | stage not visible in scope |
409 | stage name already exists, version conflict, or idempotency key reused |
422 | validation failure |
Security: bearer
GET/api/v1/quotes
GET /api/quotes — newest first, client/status filters.
Parameters
| Name | In | Type | Required | Description |
|---|
client_id | query | — | No | |
status | query | — | No | Exact status: `draft|sent|accepted|declined|expired`. |
Responses
Security: bearer
POST/api/v1/quotes
POST /api/quotes — create a numbered draft. Lines snapshot catalog
context; roll-ups are computed server-side (never trusted from the
client).
Request body
CreateQuoteRequest
Responses
| Status | Description |
|---|
200 | |
404 | client or catalog item not visible in scope |
409 | idempotency key reused |
422 | validation failure |
Security: bearer
GET/api/v1/quotes/{id}
GET /api/quotes/{id} — the quote plus its lines.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
| Status | Description |
|---|
200 | |
404 | not visible in scope |
Security: bearer
PATCH/api/v1/quotes/{id}
PATCH /api/quotes/{id} — edit a DRAFT (header + optionally the whole
line list). Sent+ quotes are immutable: typed 409.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
UpdateQuoteRequest
Responses
| Status | Description |
|---|
200 | |
404 | quote or catalog item not visible in scope |
409 | quote is not a draft, version conflict, or idempotency key reused |
422 | validation failure |
Security: bearer
POST/api/v1/quotes/{id}/accept
POST /api/quotes/{id}/accept — sent → accepted (terminal).
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
QuoteLifecycleRequest
Responses
| Status | Description |
|---|
200 | |
404 | quote not visible in scope |
409 | illegal transition, version conflict, or idempotency key reused |
Security: bearer
POST/api/v1/quotes/{id}/convert-to-invoice
POST /api/quotes/{id}/convert-to-invoice — materialize an ACCEPTED
quote into a fixed-fee contract + DRAFT invoice through the existing
invoicing persistence (the seam's rationale is in the module docs).
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
ConvertQuoteRequest
Responses
| Status | Description |
|---|
200 | |
404 | quote not visible in scope |
409 | quote not accepted / outside validity / already converted, version conflict, or idempotency key reused |
422 | validation failure |
Security: bearer
POST/api/v1/quotes/{id}/decline
POST /api/quotes/{id}/decline — sent → declined. Terminal.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
QuoteLifecycleRequest
Responses
| Status | Description |
|---|
200 | |
404 | quote not visible in scope |
409 | illegal transition, version conflict, or idempotency key reused |
Security: bearer
POST/api/v1/quotes/{id}/expire
POST /api/quotes/{id}/expire — explicitly expire a quote PAST its
validity window (decline is the in-window exit).
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
QuoteLifecycleRequest
Responses
| Status | Description |
|---|
200 | |
404 | quote not visible in scope |
409 | still inside validity window, illegal transition, version conflict, or idempotency key reused |
Security: bearer
POST/api/v1/quotes/{id}/send
One lifecycle leg: shared shape for send/accept/decline (expire has a
POST /api/quotes/{id}/send — draft → sent. Locks the document.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
QuoteLifecycleRequest
Responses
| Status | Description |
|---|
200 | |
404 | quote not visible in scope |
409 | illegal transition, version conflict, or idempotency key reused |
Security: bearer
GET/api/v1/stock-items
GET /api/stock-items.
Parameters
| Name | In | Type | Required | Description |
|---|
active | query | — | No | `None` = all items (active + archived). |
search | query | — | No | |
limit | query | — | No | |
Responses
Security: bearer
POST/api/v1/stock-items
POST /api/stock-items — create through the instant-first write path.
Request body
CreateStockItemRequest
Responses
| Status | Description |
|---|
200 | |
409 | SKU already exists or idempotency key reused |
422 | validation failure |
Security: bearer
GET/api/v1/stock-items/{id}
GET /api/stock-items/{id}.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
| Status | Description |
|---|
200 | |
404 | not visible in scope |
Security: bearer
PATCH/api/v1/stock-items/{id}
PATCH /api/stock-items/{id} — absent fields keep their value.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
UpdateStockItemRequest
Responses
| Status | Description |
|---|
200 | |
404 | not visible in scope |
409 | SKU already exists, version conflict, or idempotency key reused |
422 | validation failure |
Security: bearer
POST/api/v1/stock-items/{id}/archive
POST /api/stock-items/{id}/archive — archival is a flag.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
ArchiveStockItemRequest
Responses
| Status | Description |
|---|
200 | |
404 | not visible in scope |
Security: bearer
GET/api/v1/stock-items/{id}/movements
GET /api/stock-items/{id}/movements — THE traceability ledger read.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
limit | query | — | No | |
Responses
Security: bearer
GET/api/v1/stock-items/{id}/serials
GET /api/stock-items/{id}/serials — serialized WHERE-IT-IS-NOW.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
location_id | query | — | No | |
status | query | — | No | |
limit | query | — | No | |
Responses
Security: bearer
GET/api/v1/stock-locations
GET /api/stock-locations.
Parameters
| Name | In | Type | Required | Description |
|---|
kind | query | — | No | |
active_only | query | — | No | |
Responses
Security: bearer
POST/api/v1/stock-locations
POST /api/stock-locations.
Request body
CreateStockLocationRequest
Responses
| Status | Description |
|---|
200 | |
409 | idempotency key reused or name conflicts |
422 | validation failure |
Security: bearer
GET/api/v1/stock-locations/{id}
GET /api/stock-locations/{id}.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
| Status | Description |
|---|
200 | |
404 | not visible in scope |
Security: bearer
PATCH/api/v1/stock-locations/{id}
PATCH /api/stock-locations/{id}.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
UpdateStockLocationRequest
Responses
| Status | Description |
|---|
200 | |
404 | not visible in scope |
422 | validation failure |
Security: bearer
GET/api/v1/stock-serials
GET /api/stock-serials — WHERE IS IT NOW, per serialized unit (tools
and loaned devices; each row carries its live holder or client).
Parameters
| Name | In | Type | Required | Description |
|---|
item_id | query | — | No | |
location_id | query | — | No | |
status | query | — | No | in_stock | out | checked_out | loaned. |
holder_user_id | query | — | No | |
tool_only | query | — | No | `true` = tools only; `false` = devices only; absent = both. |
overdue_only | query | — | No | Only units past their checkout due date or loan expected return. |
limit | query | — | No | |
Responses
Security: bearer
POST/api/v1/stock/adjust
POST /api/stock/adjust — the signed counter-fix (NO serialized items;
move the unit instead).
Request body
AdjustRequest
Responses
| Status | Description |
|---|
200 | |
409 | insufficient stock or serialized item |
422 | validation failure (delta must be non-zero) |
Security: bearer
POST/api/v1/stock/consume
POST /api/stock/consume — the OUT leg charging a ticket's job.
Request body
ConsumeRequest
Responses
| Status | Description |
|---|
200 | |
409 | insufficient stock or invalid movement |
422 | validation failure (qty_milli must be positive) |
Security: bearer
GET/api/v1/stock/consumption-velocity
GET /api/stock/consumption-velocity — the outbound-rate read model.
Parameters
| Name | In | Type | Required | Description |
|---|
window_days | query | — | No | Lookback window in days (default 30; outbound legs summed). |
item_id | query | — | No | |
limit | query | — | No | |
Responses
Security: bearer
POST/api/v1/stock/levels/thresholds
POST /api/stock/levels/thresholds — min/max/reorder per (item,
location); NULL clears. Creates the level row on first touch, without
moving stock.
Request body
ThresholdsRequest
Responses
Security: bearer
GET/api/v1/stock/on-hand
GET /api/stock/on-hand — the stock-on-hand read model.
Parameters
| Name | In | Type | Required | Description |
|---|
item_id | query | — | No | |
location_id | query | — | No | |
low_only | query | — | No | true → only rows at/below the reorder point. |
limit | query | — | No | |
Responses
Security: bearer
POST/api/v1/stock/receive
POST /api/stock/receive — a positive leg into ONE location. Serialized
items move one physical unit per ledger row (exactly one serial).
Request body
ReceiveRequest
Responses
| Status | Description |
|---|
200 | |
404 | item or location not visible in scope |
409 | idempotency key reused, item archived, or invalid movement |
422 | validation failure |
Security: bearer
POST/api/v1/stock/ship-out
POST /api/stock/ship-out — OUT to a client or an on-site job, with
optional carrier + tracking ref.
Request body
ShipOutRequest
Responses
| Status | Description |
|---|
200 | |
409 | insufficient stock or invalid movement |
422 | validation failure |
Security: bearer
POST/api/v1/stock/transfer
POST /api/stock/transfer — shrink one location, grow another.
Request body
TransferRequest
Responses
| Status | Description |
|---|
200 | |
409 | insufficient stock or invalid movement |
422 | validation failure |
Security: bearer
GET/api/v1/tickets/{id}/stock-movements
GET /api/tickets/{id}/stock-movements — the job's parts-used ledger
read: every movement anchored to this ticket, newest first. Ticket
visibility gates the read; cost fields stay `financial:read`-gated
(RB2), same as the per-item ledger read.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
limit | query | — | No | |
Responses
Security: bearer