{"openapi":"3.0.3","info":{"title":"CloudERM API","version":"1.0.0","description":"Built from the YAML under docs/openapi/ and the @swagger blocks still in routes/** and lotus/routes/**, verified one route file at a time (story DOZ-1688). Raw document: /docs/openapi.json. Every route needs a bearer token (Authorize) unless marked otherwise."},"servers":[{"url":"http://localhost:5000/v1","description":"local (PORT defaults to 5000)"},{"url":"https://api-dev.clouderm.com/v1","description":"dev (api-dev deployment, DOZ-7637)"},{"url":"https://api-stage.clouderm.com/v1","description":"staging"},{"url":"https://api.clouderm.com/v1","description":"production"}],"security":[{"bearerAuth":[]}],"paths":{"/lotus/invoices/search":{"post":{"summary":"Search Invoices","description":"Paginated invoice list across the order types the caller may view. Needs at least one of RENTAL_INVOICE_VIEW, SALE_INVOICE_VIEW or WORK_ORDER_INVOICE_VIEW (403 with none); rows are limited to the types the caller holds, and an order_type filter outside them returns an empty page. A customer-portal session only sees its own customer's invoices - customer_id is forced to the session's customer. Never listed - invoices of imported orders, and work-order invoices while the org has work orders disabled. The body is optional; without it every permitted invoice is listed, 25 per page.\n","tags":["Invoices"],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/invoice_search_body"}}}},"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"$ref":"#/components/schemas/invoice_search_result"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"description":"Missing, invalid or expired token, or a session whose roles hold no permissions at all (a missing invoice permission is a 403)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller holds none of the three invoice view permissions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/invoices/unbilled/search":{"post":{"summary":"Search Unbilled Orders","description":"What has not been billed yet, per open order. Lists every committed, non-deleted, non-imported RENTAL, WORK_ORDER and SALE order that is not yet closed, with its projected contract value (what the credit-limit checks price, not accrued to date: a reservation counts its full value, and a rental stops accruing at its expected return date, so an overdue rental still out stops there), what has been invoiced against it, and the difference (unbilled). Invoiced is net of card fees, counts DRAFT invoices (draft_invoiced_total says how much of it is still draft), ignores voided invoices and is reduced by credit notes. Unbilled is raw - negative means over-invoiced. By default only orders with something left to bill are listed (plus any order whose projection failed); include_settled lists the rest. totals cover every computed order, before that filter and before paging: unbilled_total sums the positive unbilled figures and over_invoiced_total the magnitudes of the negative ones. A work order's parent-order parts stay on the open work order. An order whose projection failed is returned with projection_error true and null projected_total and unbilled - never as 0 - and is counted in orders_with_errors. Read-only. At most 500 candidate orders are computed per request; more is a 400 asking to narrow by customer or order type. Needs at least one of RENTAL_INVOICE_VIEW, SALE_INVOICE_VIEW or WORK_ORDER_INVOICE_VIEW; rows are limited to the types the caller holds, an order_type filter outside them returns an empty page, and work orders are left out while the org has work orders disabled. A customer-portal session only sees its own customer's orders. The body is optional; without it, orders with something to bill are listed, largest unbilled first, 25 per page.\n","tags":["Invoices"],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/unbilled_search"}}}},"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"$ref":"#/components/schemas/unbilled_search_result"}}}},"400":{"description":"Validation failed (an unknown key, a malformed uuid, an order_by column outside unbilled, projected_total, invoiced_total and order_number, perPage above 100), or more than 500 open orders matched - \"Too many open orders to compute at once (over 500). Narrow the search by customer or order type.\"\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, invalid or expired token, or a session whose roles hold no permissions at all","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller holds permissions, but none of the three invoice view permissions. An MCP session is also answered 403 (never 401) for a missing permission.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/invoices/receipt/{id}":{"get":{"summary":"Get An Invoice Receipt","description":"The rendered invoice. format=pdf returns the PDF the customer is emailed, as a download named invoice-<invoice_number>.pdf; any other value (default html) returns the HTML rendering. Requires the view permission of the invoice's own order type (RENTAL_INVOICE_VIEW, SALE_INVOICE_VIEW or WORK_ORDER_INVOICE_VIEW), else 403.\n","tags":["Invoices"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}},{"in":"query","name":"format","required":false,"schema":{"type":"string","example":"pdf"},"description":"pdf for the PDF; anything else, or nothing, for HTML."}],"responses":{"200":{"description":"The invoice document","content":{"application/pdf":{"schema":{"type":"string","format":"binary","description":"Returned for format=pdf (Content-Disposition attachment)."}},"text/html":{"schema":{"type":"string","description":"Returned for any other format."}}}},"400":{"description":"id is not a valid uuid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, invalid or expired token, or a session whose roles hold no permissions at all (a missing invoice permission is a 403)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller lacks the view permission of this invoice's order type","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such invoice in the caller's org (or customer, for a customer-portal session), or it is deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/invoices/approve":{"post":{"summary":"Approve An Invoice","description":"Approves a DRAFT invoice. The customer's balance is applied to it first, from the deposits its balance_application_setting allows (ORDER_ONLY - this order's deposits plus refund credit; ORDER_AND_FLOATING - this order's deposits plus any deposit tied to no order; ALL - every deposit), and an early payment discount the customer qualifies for may be granted first (it is not part of balance_applied). The invoice therefore ends UNPAID, PARTIALLY_PAID or PAID. A customer set up for auto-payment then has a card charge started for what is left, plus the org's card processing fee and the sales tax on it, as on pay/cc. A failed auto-charge does not undo the approval - it is reported in charge_error with a 200. After the approval the invoice is emailed to the customer, unless the org's invoice delivery setting for this kind of invoice is MANUAL or this version of it was already emailed. Requires the approve permission of the invoice's order type (RENTAL_INVOICE_APPROVE, SALE_INVOICE_APPROVE or WORK_ORDER_INVOICE_APPROVE), else 403.\n","tags":["Invoices"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/invoice_approve_body"}}}},"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"$ref":"#/components/schemas/invoice_approve_result"}}}},"400":{"description":"Validation failed (including a missing body), or the invoice is not DRAFT","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, invalid or expired token, or a session whose roles hold no permissions at all (a missing invoice permission is a 403)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller lacks the approve permission of this invoice's order type","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such invoice in the caller's org (or customer, for a customer-portal session), or it is deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/invoices/pay/cc":{"post":{"summary":"Pay An Invoice By Credit Card","description":"Starts a Stripe card charge on an UNPAID or PARTIALLY_PAID invoice, optionally applying customer balance first (use_balance). Partial payments are allowed and so is paying more than the balance - the excess stays as customer balance. When the org has a card processing fee (its FEES.CC_PROCESSING_FEE percentage), the card is charged amount plus that fee plus the order's sales tax on the fee; once the payment is confirmed the fee is added to the invoice as a CC_PROCESSING_FEE charge (charges.ccFee) and its tax as tax charges (charges.adjustment), and the invoice total grows by the same amount. A payment that settles the invoice in full while the customer's early payment discount window is open is granted the discount first. The 200 only means the charge was started; the payment is confirmed or failed later by the Stripe webhook, which on success also does what pay/externally does after a payment (notification, opening a fully paid reservation rental, a sale's on-payment work). With amount 0 and use_balance above 0 no card is charged and payment_id / payment_intent_id are null. Requires the pay-by-card permission of the invoice's order type (RENTAL_INVOICE_PAY_CC, SALE_INVOICE_PAY_CC or WORK_ORDER_INVOICE_PAY_CC), else 403.\n","tags":["Invoices"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/invoice_pay_cc_body"}}}},"responses":{"200":{"description":"Card charge started, or customer balance applied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/invoice_pay_cc_result"}}}},"400":{"description":"Validation failed (including a missing body); or the invoice is deleted, not UNPAID / PARTIALLY_PAID, a credit invoice, or has nothing left to charge (fully paid, or card charges already in flight cover the balance); or use_balance exceeds the customer's balance, or the part of it their balance_application_setting allows; or Stripe is not configured for the org, or the customer is not set up with Stripe; or the payment method is not attached to the invoice's customer or is not a card; or Stripe refused the charge (e.g. card declined - the error carries Stripe's message)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, invalid or expired token, or a session whose roles hold no permissions at all (a missing invoice permission is a 403)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller lacks the pay-by-card permission of this invoice's order type","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such invoice in the caller's org (or customer, for a customer-portal session), or the customer or payment method was not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/invoices/pay/externally":{"post":{"summary":"Record An External Payment On An Invoice","description":"Records a payment received outside the card flow (CASH, CHECK, ACH, WIRE, OTHER) on an UNPAID or PARTIALLY_PAID invoice, optionally applying customer balance first (use_balance). Takes effect immediately; partial payments are allowed. A payment that settles the invoice in full while the customer's early payment discount window is open - with the payment, the balance or both - is granted the discount first. CASH and CHECK land in a cash drawer - cash_drawer_id, or the org's default drawer. Afterwards the customer and the org are sent the payment-received notification. When the payment leaves the invoice PAID, a RESERVATION rental with no other unpaid invoice is opened (OPEN_CONTRACT, with its open-contract emails), and a sale gets the checklists, inspections and work orders its org creates on payment. Requires the external-payment permission of the invoice's order type (RENTAL_INVOICE_PAY_EXTERNALLY, SALE_INVOICE_PAY_EXTERNALLY or WORK_ORDER_INVOICE_PAY_EXTERNALLY), else 403.\n","tags":["Invoices"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/invoice_pay_externally_body"}}}},"responses":{"200":{"description":"Payment recorded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/invoice_pay_externally_result"}}}},"400":{"description":"Validation failed (including a missing body, or cash_drawer_id sent with payment_type ACH, WIRE or OTHER); or the invoice is deleted, not UNPAID / PARTIALLY_PAID, a credit invoice, or has nothing left to record (fully paid, or card charges already in flight cover the balance); or use_balance exceeds the customer's balance, or the part of it their balance_application_setting allows\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, invalid or expired token, or a session whose roles hold no permissions at all (a missing invoice permission is a 403)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller lacks the external-payment permission of this invoice's order type","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such invoice in the caller's org (or customer, for a customer-portal session), or the customer or cash drawer was not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/invoices/email":{"post":{"summary":"Email An Invoice","description":"Emails the invoice, following the org's new_invoices_email setting - the body is the HTML invoice when its html flag is on, otherwise the line \"Please see the attached invoice for details.\" (sent even when no PDF is attached), and the PDF is attached when its pdf flag is on. receiver_type customer sends to the customer's contacts that opted in to invoice emails (or only contact_ids), self to the caller, custom to custom_emails. A draft can be emailed too. With at least one recipient, a customer send while customer emails are disabled is skipped and answers 200 with status skipped_customer_emails_disabled; with no recipient it is a 400. status sent means the email was handed to the mailer - it is also reported when email sending is switched off for the environment or the mail provider failed. Requires the view permission of the invoice's order type (RENTAL_INVOICE_VIEW, SALE_INVOICE_VIEW or WORK_ORDER_INVOICE_VIEW), else 403.\n","tags":["Invoices"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/invoice_email_body"}}}},"responses":{"200":{"description":"Email sent, or skipped because customer emails are disabled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/invoice_email_result"}}}},"400":{"description":"Validation failed (including a missing body, custom without custom_emails, or contact_ids with self / custom); a contact id is not this customer's, not subscribed to invoice emails or has no email address; the caller has no email address (self); or there is no recipient to send to\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, invalid or expired token, or a session whose roles hold no permissions at all (a missing invoice permission is a 403)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller lacks the view permission of this invoice's order type","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such invoice in the caller's org (or customer, for a customer-portal session), or it is deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/invoices/{id}":{"get":{"summary":"Get An Invoice By Id","description":"The invoice with its order, customer, site, issuing org, billed charges (grouped for display) and applied payments. Requires the view permission of the invoice's order type (RENTAL_INVOICE_VIEW, SALE_INVOICE_VIEW or WORK_ORDER_INVOICE_VIEW), else 403.\n","tags":["Invoices"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"$ref":"#/components/schemas/invoice_detail"}}}},"400":{"description":"id is not a valid uuid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, invalid or expired token, or a session whose roles hold no permissions at all (a missing invoice permission is a 403)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller lacks the view permission of this invoice's order type","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such invoice in the caller's org (or customer, for a customer-portal session), or it is deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"summary":"Delete (Void) An Invoice","description":"Soft-deletes the invoice - it is then listed as VOIDED. Refused while any successful payment points at the invoice - a payment applied to it, or the refund credit a credit note creates, so a credit note cannot be voided. The body is optional. Requires the delete permission of the invoice's order type (RENTAL_INVOICE_DELETE, SALE_INVOICE_DELETE or WORK_ORDER_INVOICE_DELETE), else 403.\n","tags":["Invoices"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/invoice_delete_body"}}}},"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","example":"Invoice deleted"}}}}}},"400":{"description":"Validation failed, id is not a valid uuid, the invoice is already deleted, or a successful payment points at it (always the case for a credit note)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, invalid or expired token, or a session whose roles hold no permissions at all (a missing invoice permission is a 403)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller lacks the delete permission of this invoice's order type","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such invoice in the caller's org (or customer, for a customer-portal session)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/platform-settings":{"get":{"summary":"The org's settings page, layout groups plus effective settings","description":"Every signed-in user may call it (no permission check; PLATFORM_GENERAL_VIEW is only forwarded): the server applies the gates itself. Groups and settings tagged with modules the org has not enabled are absent; a group carrying its own view permission the user lacks is absent together with its settings; `editable` reflects the per-setting edit permission; `value` is the org's effective value, the stored override or else the org's default. An absent key means \"not applicable to this org or user\" (or switched off for this org by its own row), never \"unset\". PLATFORM_GENERAL_VIEW gates only the settings page in the client, nothing on the server.\n","tags":["Platform Settings"],"responses":{"200":{"description":"The page for the current org and user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/platform_settings_page"}}}},"401":{"description":"Not signed in, or a session whose roles hold no permission at all","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"An AI-connector (MCP) session, the route is not on the connector allowlist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"put":{"summary":"Save settings as a list of diffs","description":"Requires PLATFORM_GENERAL_UPDATE, and a key may carry its own edit permission (403 without it). `value: null` resets the key to the org's default; a value equal to the org's default is stored the same way (NULL, so `is_overridden` turns false). Every value is validated against its definition: data type, allowed values, referenced entity. Unchanged values are ignored; the batch is one transaction (any error rolls it all back) and, only when at least one key changed, one journal entry, with a financial audit row when a financial key changed. A save where nothing changed writes nothing and still answers 200. A key the org has switched off with its own disabled row is hidden by the GET but still accepted here. Side effects of a change: turning CUSTOMER.DEFAULT_PAY_ON_OPEN from false to true sets `pay_on_open` on every unvalidated customer of the org (same transaction); changing ASSET.ENABLE_DEPRECIATION or ASSET.ENABLE_CAPITALIZATION starts, after the commit and without waiting, a revalidation of every asset of the org that ends with a system notification to the org. Answers with the refreshed page, the same shape as the GET.\n","tags":["Platform Settings"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/platform_settings_update"}}}},"responses":{"200":{"description":"The refreshed page after the save","content":{"application/json":{"schema":{"$ref":"#/components/schemas/platform_settings_page"}}}},"400":{"description":"Body shape (no body or no `settings` ('\"settings\" is required'), an empty list, more than 200 diffs, an unknown key in the envelope or in a diff, a diff without `value`, a malformed key), an unknown or disabled setting (\"Unknown setting: KEY\"), or a value the definition rejects (\"Invalid value for KEY: ...\"). These 400s carry no `code`.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The user lacks the edit permission of one of the keys (\"You are not allowed to change KEY\"), a key the org's modules do not reach (\"Setting KEY is not available to this organization's modules\"), or an AI-connector (MCP) session (the route is not on the connector allowlist)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Organization not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/lotus/purchase-orders/{id}/allocations":{"post":{"summary":"Allocate N units of a purchase order line to a work order","description":"DOZ-7310. The inverse of allocating parts from the work-order screen: adds the PO line to the work order as a part (or CUSTOM) line backed by an allocation, with the same checks the work-order save applies. The PO must be released or partially_received (DOZ-7308) with capacity left on the line (ordered minus everything already allocated or received into stock), and the work order must not be DONE. Asset lines (item_type asset - the line's item_id is the asset itself) cannot be allocated. If the work order already holds a live, unreceived allocation of the same PO line, the quantities are MERGED into that line rather than creating a second one: only its quantity (and its price, when unit_price is sent) changes; its bill_to, capitalized and name are kept. Writes an \"allocated\" (or \"allocation_updated\") audit row on the PO. When the org disables adding items to work orders, only CUSTOM lines can be allocated. Requires BOTH PURCHASE_ORDERS_UPDATE and WORK_ORDER_UPDATE; PURCHASE_ORDERS_UPDATE_RELEASED is not needed.\n","tags":["Purchase Orders"],"parameters":[{"$ref":"#/components/parameters/po_allocation_po_id"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["purchase_order_item_id","order_id","quantity"],"description":"Unknown keys are dropped silently.","properties":{"purchase_order_item_id":{"type":"string","format":"uuid","description":"The purchase_order_items row id. Required for ALL line types."},"order_id":{"type":"string","format":"uuid","description":"The target work order (lotus_orders.id)."},"quantity":{"type":"number","exclusiveMinimum":true,"minimum":0},"bill_to":{"type":"string","enum":["PARENT_ORDER","CUSTOMER","ASSET"],"default":"ASSET","description":"A pricing input, not just a billing label - it selects the customer vs asset markup shelf. CUSTOMER needs a customer on the work order; PARENT_ORDER needs both a parent order and a customer. On a merge the existing line keeps its own bill_to, but the value sent is still checked against the work order.\n"},"capitalized":{"type":"boolean","description":"Omit to inherit the PO line's flag."},"unit_price":{"type":"number","description":"CUSTOM lines only; ignored for part lines, and required when a CUSTOM line is allocated to the work order for the first time. The BASE price, before the Custom Items markup shelf, rounded to 2 decimals - the stored unit_price is this plus the shelf markup, and the base is kept in item_details.base_unit_price (DOZ-7737). Omitted on a merge into an existing line = keep its current price.\n"},"name":{"type":"string","nullable":true,"description":"The work-order line's name; null or omitted takes the PO line's name ('' is stored as sent).\n"}},"example":{"purchase_order_item_id":"00000000-0000-4000-8000-000000000000","order_id":"00000000-0000-4000-8000-000000000000","quantity":1}}}}},"responses":{"201":{"description":"Allocation created (or merged into the work order's existing one)","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","example":"Allocation created"},"data":{"allOf":[{"$ref":"#/components/schemas/po_allocation_result"},{"type":"object","properties":{"line_item_id":{"type":"string","format":"uuid","description":"The work-order line written"},"asset_mismatch":{"$ref":"#/components/schemas/po_allocation_asset_mismatch"}}}]}}}}}},"400":{"description":"The body or a path id failed validation (\"Validation error: ...\"), or the line is not allocatable - an asset line, a line already closed for receiving, a PO that is not released or partially_received (\"Purchase order(<id>) is not in released status.\"), not enough capacity on the line, a 'supplies' part, a fractional quantity of a part that is not supplies (\"Part (<name>) is a '<type>' type part and must have a whole number quantity.\"), a work order that is DONE, bill_to's prerequisite missing on the work order, a new CUSTOM allocation without unit_price, or a part line while the org disables adding items to work orders.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/po_allocation_ai_connector_forbidden"},"404":{"description":"The PO line is not a live line of this purchase order in the caller's org (\"Purchase order line not found on this purchase order.\"); the purchase order itself is voided (\"One or more purchase orders were not found: <id>\"); the line's catalog part is deleted (\"One or more parts were not found in the inventory: <id>\"); the work order does not exist (\"Work order not found.\"); or the caller's org is gone (\"Organization not found.\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The work order's existing allocation of this line has already been received (\"... Adjust it from the work order instead.\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/purchase-orders/{id}/allocations/{allocationId}":{"patch":{"summary":"Change an allocation's quantity","description":"Sets the allocation, and the work-order line it backs, to the new quantity, under the same capacity and state checks as allocating. Refused once anything has been received against the allocation - remove the line from the work order instead, so inventory is reconciled there. Writes an \"allocation_updated\" audit row. Requires BOTH PURCHASE_ORDERS_UPDATE and WORK_ORDER_UPDATE.\n","tags":["Purchase Orders"],"parameters":[{"$ref":"#/components/parameters/po_allocation_po_id"},{"$ref":"#/components/parameters/po_allocation_id"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["quantity"],"description":"Unknown keys are dropped silently.","properties":{"quantity":{"type":"number","exclusiveMinimum":true,"minimum":0,"description":"The allocation's new total, not an increment"}},"example":{"quantity":1}}}}},"responses":{"200":{"description":"Allocation updated","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","example":"Allocation updated"},"data":{"allOf":[{"$ref":"#/components/schemas/po_allocation_result"},{"type":"object","properties":{"line_item_id":{"type":"string","format":"uuid"},"asset_mismatch":{"$ref":"#/components/schemas/po_allocation_asset_mismatch"}}}]}}}}}},"400":{"description":"The body or a path id failed validation; the allocation is a stock receipt (\"This is a stock receipt, not a work-order allocation, and cannot be changed.\"); it has been received (\"Quantity for parts allocated via a PO and received cannot be updated. Remove and add back from stock.\"); or the new quantity fails the allocation checks (capacity, PO state, work order DONE, line closed for receiving, a whole number for a part that is not supplies), or the work order no longer meets the line's stored bill_to (lost its customer or parent order).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/po_allocation_ai_connector_forbidden"},"404":{"description":"The caller's org is gone (\"Organization not found.\"); no live allocation with this id on this purchase order in the caller's org (\"Allocation not found.\"); its purchase order line, work-order line or work order is gone (\"Purchase order line not found on this purchase order.\", \"Allocated work order line not found.\", \"Work order not found.\"); the purchase order is voided (\"One or more purchase orders were not found: <id>\"); or the line's catalog part is deleted (\"One or more parts were not found in the inventory: <id>\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"summary":"Remove an allocation","description":"Voids the allocation AND soft-deletes the work-order line it backs, leaving inventory untouched. Refused once anything has been received. Writes an \"allocation_removed\" audit row. Requires BOTH PURCHASE_ORDERS_UPDATE and WORK_ORDER_UPDATE.\n","tags":["Purchase Orders"],"parameters":[{"$ref":"#/components/parameters/po_allocation_po_id"},{"$ref":"#/components/parameters/po_allocation_id"}],"responses":{"200":{"description":"Allocation removed","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","example":"Allocation removed"},"data":{"$ref":"#/components/schemas/po_allocation_result"}}}}}},"400":{"description":"A path id is not a uuid; the allocation is a stock receipt; it has been received (\"This allocation has already been received. Remove the line from the work order instead, so the received quantity is returned to inventory.\"); or its work order is DONE.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/po_allocation_ai_connector_forbidden"},"404":{"$ref":"#/components/responses/po_allocation_not_found"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/purchase-orders/{id}/invoices":{"get":{"summary":"List the vendor invoices recorded against a purchase order","description":"Powers the PO's Invoices tab. Returns every live invoice with its lines and attachments, oldest invoice_date first, the per-PO-line invoicing position (ordered, received, already invoiced, still invoiceable, still billable ahead of delivery) and the reconciliation summary that drives the summary strip and the close banner. Each stored line carries a `current` block: where its quantity stands against today's receipts, which can differ from the columns frozen when the invoice was saved. DOZ-7848 - each line also carries price_approval_status and quantity_approval_status (APPROVED, PENDING, or null when that kind is not a variance on the line), with price_resolved_by(_username) / quantity_resolved_by(_username) naming who approved each kind; approval_status is the line as a whole. The reconciliation's pending_approval is split into price and quantity counters. Works on a purchase order in any state. Requires PURCHASE_ORDERS_READ.\n","tags":["Purchase Orders"],"parameters":[{"$ref":"#/components/parameters/po_invoice_po_id"}],"responses":{"200":{"description":"Vendor invoices fetched","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","example":"Vendor invoices fetched"},"data":{"type":"object","properties":{"invoices":{"type":"array","items":{"$ref":"#/components/schemas/po_vendor_invoice"}},"positions":{"type":"array","description":"One per live PO line","items":{"$ref":"#/components/schemas/po_invoice_line_position"}},"reconciliation":{"$ref":"#/components/schemas/po_reconciliation"}}}}}}}},"400":{"$ref":"#/components/responses/po_invoice_bad_params"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/po_invoice_ai_connector_forbidden"},"404":{"$ref":"#/components/responses/po_invoice_po_not_found"},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"summary":"Record a vendor invoice against a purchase order","description":"Matches each submitted line against what has been RECEIVED and not yet invoiced, and each header charge against the PO's own freight, sales tax and miscellaneous amounts. A quantity billed ahead of delivery is accepted as awaiting receipt while the line can still receive and the quantity is within what was ordered. A price below the purchase order's is in our favor (in_our_favor) and is not a variance, so it needs no approval (DOZ-7786). A price above the purchase order's is a PRICE variance; a quantity no delivery can cover (past what was ordered, or on a line closed for receiving) is a QUANTITY variance. Each is saved pending approval unless the caller accepts it, and each kind has its own approver (DOZ-7848) - a price the purchase order's purchaser or a holder of PURCHASE_ORDERS_INVOICE_VARIANCE_APPROVE, a quantity only a holder of PURCHASE_ORDERS_INVOICE_QUANTITY_VARIANCE_APPROVE. Accept one kind with price_variance_resolution or quantity_variance_resolution, or every kind on the line with variance_resolution; accepting a kind without its authority is a 403. A pending invoice reads status PENDING_APPROVAL and the purchase order cannot be closed until the variance is approved (the approve endpoint), the line is corrected, or the invoice is removed. An ACCEPTED sent for a kind the line does not carry is ignored. Charges are matched with a percentage tolerance from the org setting and never block the save. The vendor is taken from the purchase order; a vendor key in the body is dropped. Recomputes the PO's invoice_state and writes an \"invoice_recorded\" audit row. Requires PURCHASE_ORDERS_RECONCILE; the two INVOICE_*_VARIANCE_APPROVE permissions are read only to judge an acceptance.\n","tags":["Purchase Orders"],"parameters":[{"$ref":"#/components/parameters/po_invoice_po_id"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/po_vendor_invoice_write"}}}},"responses":{"201":{"description":"Vendor invoice recorded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/po_vendor_invoice_write_result"}}}},"400":{"$ref":"#/components/responses/po_invoice_write_refused"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/po_invoice_acceptance_forbidden"},"404":{"$ref":"#/components/responses/po_invoice_po_not_found"},"409":{"description":"The invoice_number is already recorded against this purchase order (\"Invoice '<number>' has already been recorded against this purchase order\"). Saves to one purchase order are serialized on its row, so the check sees every live invoice.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/purchase-orders/{id}/invoices/match_preview":{"post":{"summary":"Dry-run the match for an in-progress vendor invoice","description":"Writes nothing. Returns the same line and charge matching the save would apply, so the variance panel can update as the user types, and deliberately does NOT reject unresolved variances - surfacing them is the point. Unlike the save, it keeps lines with quantity_invoiced 0: they come back in lines[], and a duplicated or unknown po_item_id on one is a 400 here that the save, which drops them first, would not raise. Pass invoice_id when editing an existing invoice so its own lines are excluded from the already-invoiced totals. Every key is optional; an empty body previews nothing and still returns every position. Requires PURCHASE_ORDERS_READ.\n","tags":["Purchase Orders"],"parameters":[{"$ref":"#/components/parameters/po_invoice_po_id"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","description":"Unknown keys are dropped silently.","properties":{"invoice_id":{"type":"string","format":"uuid","nullable":true,"description":"The invoice being edited, if any"},"freight":{"type":"number","minimum":0,"maximum":99999999.99,"nullable":true,"default":0},"sales_tax":{"type":"number","minimum":0,"maximum":99999999.99,"nullable":true,"default":0},"miscellaneous":{"type":"number","minimum":0,"maximum":99999999.99,"nullable":true,"default":0},"lines":{"type":"array","items":{"type":"object","required":["po_item_id","quantity_invoiced","invoice_unit_price"],"properties":{"po_item_id":{"type":"string","format":"uuid"},"quantity_invoiced":{"type":"number","minimum":0,"maximum":99999999.99,"description":"Rounded to 2 decimals"},"invoice_unit_price":{"type":"number","minimum":0,"maximum":99999999.99,"description":"Rounded to 4 decimals"}}}}},"example":{"freight":0,"lines":[]}}}}},"responses":{"200":{"description":"Vendor invoice match preview","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","example":"Vendor invoice match preview"},"data":{"$ref":"#/components/schemas/po_invoice_match_preview"}}}}}},"400":{"description":"The body failed validation (\"Validation error: ...\"), the purchase order cannot take an invoice (new, or closed), or a line is not on the purchase order or is sent twice.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/po_invoice_ai_connector_forbidden"},"404":{"$ref":"#/components/responses/po_invoice_po_not_found"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/purchase-orders/{id}/invoices/{invoiceId}":{"patch":{"summary":"Replace a recorded vendor invoice","description":"A full replace, not a partial update - send the whole invoice, with the same body and rules as recording one. The invoice's own existing lines are excluded from the already-invoiced totals while it is re-matched, so re-saving an unchanged invoice does not read as a duplicate bill of itself. A line whose quantity and unit price are unchanged keeps the acceptance and original approver of each variance kind it still carries, whatever is echoed. Writes an \"invoice_updated\" audit row. Requires PURCHASE_ORDERS_RECONCILE; the two INVOICE_*_VARIANCE_APPROVE permissions are read only to judge an acceptance.\n","tags":["Purchase Orders"],"parameters":[{"$ref":"#/components/parameters/po_invoice_po_id"},{"$ref":"#/components/parameters/po_invoice_id"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/po_vendor_invoice_write"}}}},"responses":{"200":{"description":"Vendor invoice updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/po_vendor_invoice_write_result"}}}},"400":{"$ref":"#/components/responses/po_invoice_write_refused"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/po_invoice_acceptance_forbidden"},"404":{"$ref":"#/components/responses/po_invoice_not_found"},"409":{"description":"Another live invoice on this purchase order already has the invoice_number (\"Invoice '<number>' has already been recorded against this purchase order\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"summary":"Remove a recorded vendor invoice","description":"Soft-deletes the invoice and its lines, frees the invoice number for re-entry, recomputes the purchase order's invoice_state and writes an \"invoice_removed\" audit row. Refused on a new or closed purchase order. Requires PURCHASE_ORDERS_RECONCILE.\n","tags":["Purchase Orders"],"parameters":[{"$ref":"#/components/parameters/po_invoice_po_id"},{"$ref":"#/components/parameters/po_invoice_id"}],"responses":{"200":{"description":"Vendor invoice removed","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","example":"Vendor invoice removed"},"data":{"type":"object","properties":{"invoice_state":{"type":"string","enum":["not_invoiced","partially_invoiced","invoiced"]},"reconciliation":{"$ref":"#/components/schemas/po_reconciliation"}}}}}}}},"400":{"description":"A path id is not a uuid, or the purchase order cannot take invoice changes (\"Release the purchase order before recording a vendor invoice against it\", or \"A vendor invoice can't be recorded against a 'closed' purchase order\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/po_invoice_ai_connector_forbidden"},"404":{"$ref":"#/components/responses/po_invoice_not_found"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/purchase-orders/{id}/invoices/{invoiceId}/approve":{"post":{"summary":"Approve the pending variances on a vendor invoice","description":"DOZ-7758. Accepts every variance on the invoice still awaiting approval that the caller may approve, stamping the caller as that kind's approver. Price variances - the purchase order's purchaser or a holder of PURCHASE_ORDERS_INVOICE_VARIANCE_APPROVE; quantity variances - a holder of PURCHASE_ORDERS_INVOICE_QUANTITY_VARIANCE_APPROVE (DOZ-7848). Kinds the caller may not approve stay pending; approved lists what this call signed and still_pending says whether anything is left. The optional lines array sets the reason on individual lines, keyed by the stored invoice line id: a listed line's reason is replaced by the variance_reason sent, and a listed line without one has its reason cleared. Lines not listed keep theirs. To reject a variance, correct or remove the invoice instead. Writes an \"invoice_variance_approved\" audit row. Requires PURCHASE_ORDERS_READ; the authority comes from the purchaser rule and the two forwarded INVOICE_*_VARIANCE_APPROVE permissions.\n","tags":["Purchase Orders"],"parameters":[{"$ref":"#/components/parameters/po_invoice_po_id"},{"$ref":"#/components/parameters/po_invoice_id"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","description":"Unknown keys are dropped silently.","properties":{"lines":{"type":"array","description":"Ids must be unique.","items":{"type":"object","required":["id"],"properties":{"id":{"type":"string","format":"uuid","description":"A stored invoice line id (lines[].id of the invoice), not a po_item_id."},"variance_reason":{"type":"string","nullable":true,"description":"Replaces the line's stored reason; omitted, null or '' clears it."}}}}},"example":{"lines":[]}}}}},"responses":{"200":{"description":"Vendor invoice variances approved. The message says when some still await another approver (\"Vendor invoice variances approved; some still await another approver\").\n","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"},"data":{"allOf":[{"$ref":"#/components/schemas/po_vendor_invoice_and_reconciliation"},{"type":"object","properties":{"approved":{"type":"array","description":"The lines this call signed, and which kinds on each","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"kinds":{"type":"array","items":{"type":"string","enum":["PRICE","QUANTITY"]}}}}},"still_pending":{"type":"boolean","description":"A variance kind on this invoice still awaits another approver"}}}]}}}}}},"400":{"description":"A path id is not a uuid, the body failed validation, or lines[] lists a line this call approves nothing on, with or without a reason (\"Line <id> has no variance awaiting your approval on this vendor invoice\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The caller may approve none of the variance kinds pending on this invoice. details.unapproved_variances names each pending line as { id (the invoice line), po_item_id, unapproved_kinds } - a different shape from the 403 of recording or updating an invoice. An AI-connector (MCP) session gets this status before any of this (\"Not available to AI connectors\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"$ref":"#/components/responses/po_invoice_not_found"},"409":{"description":"The invoice has no variances awaiting approval (\"This vendor invoice has no variances awaiting approval\", code CONFLICT) - the normal answer on a closed purchase order, whose close stamps what it hardens; approve has no state gate. The same invoice update or delete answers 400 there.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/rentals/search":{"post":{"summary":"Search Rental Orders","description":"Paginated rental orders of the caller's org, each with its full details (the GET /lotus/rentals/{id} shape without ancestry and without the per-asset date and hour-meter fields). Never listed - imported orders, and QUOTEs whose valid_until has passed or is empty. deleted true lists only the voided orders, never both. With search.format \"csv\" the whole result is streamed as a CSV download (rentals-report-MM-DD-YYYY.csv) instead, without pagination. A customer-portal session only sees its own customer's orders (search.customer is forced). Requires RENTAL_VIEW.\n","tags":["Rentals"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/rental_search_body"}}}},"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"$ref":"#/components/schemas/rental_search_result"}},"text/csv":{"schema":{"type":"string","description":"For search.format \"csv\". Columns - Order Number, Continuation Of, Order Status, Customer PO, Customer Name, Customer Number, Site Name, Assignee, Billing Type, Signed Status, Day Rate Enabled, Asset Numbers, Item Names, Item Statuses, Start Date, End Date, Created At. Start Date is the earliest expected dispatch and End Date the latest expected return across the lines.\n"}}}},"400":{"description":"Validation failed - including a missing body or paginate (\"Paginate object is required\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/rentals/{id}":{"get":{"summary":"Get A Rental Order","description":"One rental order with everything the order screen shows - customer, site, contact, assignee, parent chain, fees and taxes, freight tickets, invoices, signed document, line items with their dispatch, inspections and current asset, and the projected totals. Voided (deleted) orders are returned too, with their voided invoices and removed line items. Read only. Requires RENTAL_VIEW.\n","tags":["Rentals"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"The rental order id."}],"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"$ref":"#/components/schemas/rental_order_detail"}}}},"400":{"description":"id is not a valid uuid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No rental order with this id in the caller's org (\"Rental order not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"summary":"Void A Rental Order","description":"Voids (soft-deletes) a QUOTE, RESERVATION or OPEN_CONTRACT. The order, its line items, dispatches, inspections and freight tickets are marked deleted and every invoice of the order becomes VOIDED. For an OPEN_CONTRACT the dispatched assets are also released, and the payments already applied to its invoices go back to the customer's balance as credit (no card refund); system-granted early payment discounts on the order are voided, and the order's deposits are freed for any future order. QUOTE and RESERVATION skip that step, so a payment already applied to a PARTIALLY_PAID reservation invoice stays applied to the voided invoice. One ORDER_VOIDED audit row is written. Refused for CLOSED_CONTRACT and COMPLETED_CONTRACT, for a RESERVATION with a PAID invoice, and for an OPEN_CONTRACT with a card payment still in progress. The body is optional. Requires RENTAL_DELETE.\n","tags":["Rentals"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"The rental order id."}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/rental_delete_body"}}}},"responses":{"200":{"description":"The order was voided. The body is a bare JSON string.","content":{"application/json":{"schema":{"type":"string","example":"Rental order deleted successfully"}}}},"400":{"description":"Validation failed or id is not a valid uuid; the order is CLOSED_CONTRACT or COMPLETED_CONTRACT; a reservation whose invoice is PAID; an open contract with a payment in progress\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No live rental order with this id in the caller's org (\"Order not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/rentals/receipt/{id}":{"get":{"summary":"Get A Rental Order Document","description":"The rental contract document. format=pdf returns the PDF (the same document the customer is emailed) as a download named \"Rental Contract - <order_number>.pdf\"; any other value (default html) returns the HTML rendering as a download named receipt-<order_number>.html. Voided orders render too. Read only. Requires RENTAL_VIEW.\n","tags":["Rentals"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"The rental order id."},{"in":"query","name":"format","required":false,"schema":{"type":"string","example":"pdf"},"description":"pdf for the PDF; anything else, or nothing, for HTML."}],"responses":{"200":{"description":"The rental contract document","content":{"application/pdf":{"schema":{"type":"string","format":"binary","description":"Returned for format=pdf (Content-Disposition attachment)."}},"text/html":{"schema":{"type":"string","description":"Returned for any other format (Content-Disposition attachment)."}}}},"400":{"description":"id is not a valid uuid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No rental order with this id in the caller's org","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/rentals/{id}/credit-check":{"get":{"summary":"Check A Rental Order Against The Customer's Credit Limit","description":"The customer's credit position if this order is committed - the pre-check the web runs before converting a quote to a reservation (or closing a quote as BACKDATED_CLOSED_CONTRACT). For a QUOTE the order's projected total is added to the exposure; for any other status it is already counted. Nothing is changed (an over-limit result is logged). Requires RENTAL_VIEW.\n","tags":["Rentals"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"The rental order id."}],"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"$ref":"#/components/schemas/rental_credit_check_result"}}}},"400":{"description":"id is not a valid uuid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No live rental order with this id in the caller's org (\"Order not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/rentals/charges_summary":{"post":{"summary":"Preview Rental Order Charges","description":"Prices a rental order that is being built, without saving anything - line item totals, the org's taxes and fees (rates resolved from the site, then the customer, then the org address), discounts and quoted freight. customer_id is required, so credit_check (the customer's credit position including this preview's total) always comes back. A tax or fee flag left out is filled the way a create fills it - the four tax flags are off when the site is tax exempt, otherwise each flag is the customer's own; a flag that is sent is priced as sent (nothing is saved, so no fee permission is checked). Requires RENTAL_CREATE or RENTAL_UPDATE.\n","tags":["Rentals"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/rental_charges_summary_body"}}}},"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"$ref":"#/components/schemas/rental_charges_summary_result"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"The customer is not in the caller's org (\"Customer not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/rentals/check-overlaps":{"post":{"summary":"Check Assets For Booking Overlaps","description":"For each asset and window asked about, the other live rental orders of the org that hold the asset in an overlapping window (dates compared by day). Every live line of a live rental order matches, whatever the order status (a COMPLETED_CONTRACT too); only expired QUOTEs are skipped. A line's window is its dispatch's actual dates (dispatched_date to off_rent_date), else its expected dates. A settled dispatch (OFF_RENT_INSPECTION, RELEASED, REMOVED or REPLACED) still bounds the window by its real off-rent date, so an asset returned early is free from that date on whatever the contract's status, and a settled dispatch that never went out holds nothing (DOZ-8032; before it, such a line matched on its expected window until the rental was originally due back). The asset a line holds is its dispatch's asset when it has one, so after a mid-contract replacement the replacement asset is the one held and the swapped-out asset is free from the swap. Only QUOTE, RESERVATION and OPEN/CLOSED contract matches set a flag; a COMPLETED_CONTRACT match sets none but is still listed in matched_orders. order_id leaves out the order being edited. Every asset asked about gets one entry; an asset without a match has all flags false and out_of_service false (whatever the asset's real state). Read only. Requires RENTAL_CREATE, RENTAL_UPDATE or RENTAL_READ.\n","tags":["Rentals"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/rental_check_overlaps_body"}}}},"responses":{"200":{"description":"One entry per asset asked about","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/rental_overlap"}}}}},"400":{"description":"Validation failed (including a missing body). Any failure while checking - a database error included - is also answered 400, with its message.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/rentals/asset_history":{"post":{"summary":"Get An Asset's Order History","description":"Every rental and sale order of the org this asset has been on, newest first. A rental gives one row per dispatch (so a replacement asset's trip and two trips on one order are separate rows), with its inspections; a sale gives one row per order. Rental quotes are not listed (a rental row needs a dispatch, which starts at RESERVATION); sale quotes are. Voided orders are left out; imported ones are included. search.query matches the order number or customer name; search.order_type narrows to RENTAL or SALE before paginating. Read only. Needs a signed-in session but no permission - the route has no permission guard, and rows are not limited to a customer-portal session's own customer.\n","tags":["Rentals"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/rental_asset_history_body"}}}},"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"$ref":"#/components/schemas/rental_asset_history_result"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/rentals/create":{"post":{"summary":"Create A Rental Order","description":"Creates a rental order. It is always saved as a QUOTE first - order number, line items (each asset logged as QUOTED), taxes, fees and discounts (rates resolved from the site, then the customer, then the org; the source is recorded in metadata), and freight tickets - in one transaction. Three freight tickets (DELIVERY, PICKUP, ADDITIONAL) are always stored, at $0 for any fee left out. With status RESERVATION it is then converted as PUT /lotus/rentals/convert does (dispatch and inspection records per asset, credit-limit gate). If that conversion fails for any reason (a 400 such as a non-compliant customer or an out-of-service asset, or the credit gate's 403/409) the QUOTE stays saved, with its line items, and the error carries details {order_id, order_number, saved_as - \"QUOTE\"}; retry with PUT /lotus/rentals/convert on that id, not another create - a client that re-POSTs the create after the error makes a duplicate quote. Afterwards the org's contacts (and Slack, when set up) and the customer's contacts are emailed the order, each as the org's notification settings allow; only the customer's copy can carry the PDF, and only when the org's customer email settings for that status turn it on. A journal row is written per email. data in the response is the order row as first saved, so its status reads QUOTE even after a successful fast-forward. Requires RENTAL_CREATE. A tax or fee flag left out of the body is filled from the customer's own flag, and the four tax flags are off when the site is tax exempt. Without UPDATE_FEES_ON_RENTAL_ORDERS a flag that is sent with a different value from that is refused with 401. TRANSACTION_FLAT_CASH_DISCOUNT is needed for flat_discount_value above 0, RENTAL_BACKDATED for dates in the past, FF_TO_RESERVATION for status RESERVATION, and CREDIT_LIMIT_OVERRIDE to pass a credit-limit BLOCK.\n","tags":["Rentals"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/rental_create_body"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/rental_create_result"}}}},"400":{"description":"Validation failed (including a missing body); the assignee is not a sales user; the site does not belong to the customer; an asset is not rentable or would be rented at $0.00 while zero-cost rentals are off; a line asks for more units than its asset owns; the same asset twice; a circular or not-allowed attachment; a CUSTOM line with a parent_item_id; more than one free (100% discount) attachment on one asset; a fully backdated order asked as RESERVATION; e-signature required before a reservation; the conversion's own 400s (non-compliant customer, asset out of service) - for these the QUOTE is already saved and the error carries details {order_id, order_number, saved_as \"QUOTE\"}, so retry with PUT /lotus/rentals/convert, not another create\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, invalid or expired token, or the caller lacks RENTAL_CREATE; or lacks the permission for a feature the body uses - a tax or fee flag that differs from the customer and site default (UPDATE_FEES_ON_RENTAL_ORDERS), a flat discount, backdated dates, status RESERVATION\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The customer is banned; the discounts exceed the caller's role limit; the customer is over a BLOCK credit limit (status RESERVATION, without CREDIT_LIMIT_OVERRIDE)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The customer, site, contact or an asset (\"Asset with id … not found\") is not in the caller's org","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"status RESERVATION and the customer is over their credit limit - resend the conversion with acknowledge_credit_warning true (the QUOTE is already saved)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/rentals/update":{"put":{"summary":"Update A Quote Or Reservation","description":"Replaces the editable content of a QUOTE or RESERVATION - customer, contact, site, assignee, dates, notes, line items, taxes, fees, discounts and freight. The status cannot change here. Line items sent with their id are updated, new ones added, missing ones removed together with their dispatches and inspections. Every save stores the body as sent - valid_until left out is cleared (a QUOTE then drops out of search), a RESERVATION needs site_id again (else 404), and billing_type is re-read from the customer's pay_on_open. On a RESERVATION, a newly added asset gets a dispatch and inspection records (which notify the org), and the credit-limit gate runs. On a RESERVATION that already has an invoice (ADVANCE billing, invoiced on the way to OPEN_CONTRACT), an edit that changes the charges needs RENTAL_INVOICE_IMPACTING_EDIT (else 403) and re-bills through the invoice sequence; an edit that leaves the charges as they are (notes, assignee, contact) saves without it. Requires RENTAL_UPDATE; UPDATE_CUSTOMER_ON_QUOTE_RESERVATION to change the customer, UPDATE_FEES_ON_RENTAL_ORDERS to change a tax or fee flag (an omitted flag keeps the order's current value - on when the order has that adjustment - unless the edit moves the order to another customer or site, when it is filled as on create: off for a tax-exempt site's taxes, else the customer's flag; a flag sent with a different value is a 401 without it - a 0% tax or fee is never stored, so it counts as off and sending true for one is a change), TRANSACTION_FLAT_CASH_DISCOUNT for a flat discount, RENTAL_BACKDATED for past dates, CREDIT_LIMIT_OVERRIDE to pass a credit BLOCK.\n","tags":["Rentals"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/rental_update_body"}}}},"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"$ref":"#/components/schemas/rental_message_result"},"example":{"message":"Order Updated","metadata":null}}}},"400":{"description":"Validation failed (including a missing body); the order is not a QUOTE or RESERVATION; the edit would make a reservation fully backdated; a line item of another order; the assignee is not a sales user; the site does not belong to the customer; a line asks for more units than its asset owns; the same asset twice, a circular or not-allowed attachment, a CUSTOM line with a parent_item_id, or more than one free attachment on one asset\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, invalid or expired token, or the caller lacks RENTAL_UPDATE; or lacks the permission to change the customer, a tax or fee flag, give a flat discount or set past dates\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The edit changes the charges of a reservation that already has an invoice and the caller lacks RENTAL_INVOICE_IMPACTING_EDIT (\"You do not have permission to make changes that alter an already-generated invoice\"); the discounts exceed the caller's role limit; the customer is over a BLOCK credit limit\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No live rental order with this id in the caller's org (a voided one included); the customer, site, contact or an asset is not found - including a RESERVATION saved without site_id (\"Site doesn't exist\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"A reservation edit puts the customer over their credit limit (a WARN, or a BLOCK the caller may pass with CREDIT_LIMIT_OVERRIDE) - resend with acknowledge_credit_warning true\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/rentals/update-contract":{"put":{"summary":"Update An Open Or Closed Contract","description":"Edits an OPEN_CONTRACT or CLOSED_CONTRACT. Takes the same body as PUT /lotus/rentals/update, but only assignee_id, customer_po, the notes, the line items, the tax/fee/discount fields and the freight fields are used. customer_id is required but ignored; contact_id, site_id and valid_until are accepted and ignored. A line item whose dispatch is OFF_RENT, OFF_RENT_INSPECTION, RELEASED, REMOVED or REPLACED cannot be removed or changed. Removing any other line item marks its dispatch REMOVED, as if the asset was never on the contract - no charge on ON_ACCOUNT, a credit through the invoice sequence on ADVANCE; its off-rent inspection becomes PENDING (due in 3 days), and an asset already dispatched gets an off_rent_date. To bill the time it was out, off-rent it with PUT /lotus/rentals/dispatch/status instead. A new asset on an open contract gets a dispatch, inspection records (which notify the org) and an overlap check; a closed contract takes no new assets - only fees, custom lines and adjustments. Billing - ADVANCE orders and every closed order are re-billed through the invoice sequence: the latest invoice is changed in place when it is DRAFT or UNPAID with nothing paid, otherwise a delta invoice or credit note is made; it is emailed as the org's delivery setting allows. When the org setting INVOICE.APPROVAL.RENTAL_INVOICES is on (default) and the invoice itself is approved (not DRAFT or VOIDED), the customer's deposits and credit are applied first, then a customer on auto-payment with a saved card is charged what is left plus the card fee and sales tax on the fee; a card Stripe refuses does not stop the edit - the invoice stays UNPAID (as on OFF_RENT), data.auto_charge says DECLINED and the message ends \"; the automatic card charge was declined, please collect payment manually\". A DRAFT is settled when it is approved. An ON_ACCOUNT open contract is billed by the regular billing run and at close instead. A tax or fee the order already carries keeps the rate it was priced at - only one being turned on takes today's rate - so a save that changes nothing else bills nothing after a rate change. Requires RENTAL_UPDATE; a closed order also needs RENTAL_EDIT_CLOSED_ORDER, or RENTAL_EDIT_CLOSED_ORDER_INVOICE_SENT plus acknowledge_invoice_sent once its invoice was sent (any approved invoice of the order that is not VOIDED); RENTAL_INVOICE_IMPACTING_EDIT for an edit that changes billing on an ADVANCE open contract or a closed contract (never on an ON_ACCOUNT open contract, which is billed later); UPDATE_FEES_ON_RENTAL_ORDERS to change a tax or fee flag (an omitted flag keeps the order's current value; a flag sent with a different value is a 401 without it - a 0% tax or fee is never stored, so it counts as off and sending true for one is a change); TRANSACTION_FLAT_CASH_DISCOUNT for a flat discount, and CREDIT_LIMIT_OVERRIDE to pass a credit BLOCK.\n","tags":["Rentals"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/rental_update_contract_body"}}}},"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"$ref":"#/components/schemas/rental_update_contract_result"}}}},"400":{"description":"Validation failed (including a missing body); the order is not OPEN_CONTRACT or CLOSED_CONTRACT (a sale or work order id included); a line item already returned, removed or replaced is removed or changed; the quantity of an existing ASSET line changes; a new asset on a closed contract; an overlap with another open contract; the assignee is not a sales user. New asset lines also get the asset checks: the same asset twice (\"Duplicate asset found in rental order line items\"), an asset still onboarding or approved for sale, not a valid asset for the order, no unit in inventory or fewer units than the line's quantity, no rental rates set, a circular or not-allowed attachment (\"Invalid attachment <-> parent asset pair.\"), a CUSTOM line with a parent_item_id, more than one free attachment on one asset\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, invalid or expired token, or the caller lacks RENTAL_UPDATE; or changes a tax or fee flag without UPDATE_FEES_ON_RENTAL_ORDERS, or gives a flat discount without TRANSACTION_FLAT_CASH_DISCOUNT","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"A closed order without RENTAL_EDIT_CLOSED_ORDER (or, once its invoice was sent, RENTAL_EDIT_CLOSED_ORDER_INVOICE_SENT); a billing change without RENTAL_INVOICE_IMPACTING_EDIT; a BLOCK credit limit\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No rental order with this id in the caller's org, or a new asset line's asset (\"Asset with id … not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The closed order's invoice was already sent - resend with acknowledge_invoice_sent true; or the edit puts the customer over their credit limit (a WARN, or a BLOCK the caller may pass with CREDIT_LIMIT_OVERRIDE) - resend with acknowledge_credit_warning true\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/rentals/update-open":{"put":{"summary":"Update An Open Or Closed Contract (Deprecated Path)","deprecated":true,"description":"The original path of PUT /lotus/rentals/update-contract - same body, permissions, behaviour and responses. Kept working for web builds already shipped; use update-contract.\n","tags":["Rentals"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/rental_update_contract_body"}}}},"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"$ref":"#/components/schemas/rental_update_contract_result"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"See PUT /lotus/rentals/update-contract","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/rentals/{orderId}/dates":{"put":{"summary":"Edit Rental Dates","description":"Changes the dates of an OPEN_CONTRACT or CLOSED_CONTRACT - the expected dispatch and return dates, and the actual dispatched and off-rent dates. Top-level dates apply to every asset; items[] sets them per line item and wins over the top level. dispatched_date can only be set on an asset already dispatched and off_rent_date on one already off rent. Moving a dispatched_date into the future makes the dispatch IN_TRANSIT, and into the past DISPATCHED. backdated_status is recomputed when a date is more than 15 minutes in the past. Re-billing - an ADVANCE open contract is re-billed when a dispatched, off-rent or expected return date changes; an ON_ACCOUNT open contract never (it is billed at close); a closed contract only when a dispatched or off-rent date changes. The latest invoice is changed in place when it is DRAFT or UNPAID with nothing paid, otherwise a delta invoice or credit note is made; it is emailed as the org's delivery setting allows. When the org setting INVOICE.APPROVAL.RENTAL_INVOICES is on (default) and the invoice itself is approved (not DRAFT or VOIDED), the customer's deposits and credit are applied first, then a customer on auto-payment with a saved card is charged what is left plus the card fee and sales tax on the fee; a card Stripe refuses does not stop the edit - the invoice stays UNPAID, auto_charge says DECLINED and the message ends \"; the automatic card charge was declined, please collect payment manually\". A DRAFT is settled when it is approved. The credit-limit gate runs first. Requires RENTAL_UPDATE; RENTAL_BACKDATED to set a date in the past, RENTAL_DISPATCH_DATE_UPDATE / RENTAL_OFF_RENT_DATE_UPDATE to change a dispatched / off-rent date already set, RENTAL_INVOICE_IMPACTING_EDIT whenever the edit re-bills (by the rules above), CREDIT_LIMIT_OVERRIDE to pass a credit BLOCK.\n","tags":["Rentals"],"parameters":[{"in":"path","name":"orderId","required":true,"schema":{"type":"string","format":"uuid"},"description":"The rental order id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/rental_dates_body"}}}},"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"$ref":"#/components/schemas/rental_dates_result"}}}},"400":{"description":"Validation failed (including a missing body or no date at all); the order is not OPEN_CONTRACT or CLOSED_CONTRACT; a line item or dispatch of another order; nothing to edit; dispatched_date / off_rent_date on an asset not yet dispatched / off rent; dispatched_date after off_rent_date\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"A past date without RENTAL_BACKDATED; changing a dispatched / off-rent date already set without its permission; a billing change without RENTAL_INVOICE_IMPACTING_EDIT; a BLOCK credit limit\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No rental order with this id in the caller's org (\"Rental order not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The edit puts the customer over their credit limit (a WARN, or a BLOCK the caller may pass with CREDIT_LIMIT_OVERRIDE) - resend with acknowledge_credit_warning true","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/rentals/convert":{"put":{"summary":"Convert A Rental Order To The Next Status","description":"Moves the order one step - QUOTE to RESERVATION or (fully backdated quotes only) BACKDATED_CLOSED_CONTRACT, RESERVATION to OPEN_CONTRACT or OPEN_CONTRACT_SKIP_PAYMENT, OPEN_CONTRACT to CLOSED_CONTRACT, CLOSED_CONTRACT to COMPLETED_CONTRACT. When the org requires e-signatures for that step the order must be SIGNED, and non-compliant customers are refused where the org does not allow them.\nRESERVATION - credit-limit gate; a dispatch (DISPATCH_INSPECTION) per asset with a PENDING dispatch inspection (which notifies the org) and a HIDDEN off-rent inspection; asset lines move to DISPATCH_INSPECTION; valid_until cleared. The org's contacts (and Slack) and the customer's contacts are emailed the reservation as the notification settings allow; only the customer's copy can carry the PDF, when the org's customer email settings turn it on.\nOPEN_CONTRACT - no dispatch inspection may still be PENDING (a failed one does not block), and no PENDING work order may be on the order's assets. ON_ACCOUNT orders get no invoice here. On ADVANCE billing the whole order is invoiced first (emailed as the org's delivery setting allows) and must be paid before it opens - a customer on auto-payment with a saved card is charged the invoice total plus the org's card fee (FEES.CC_PROCESSING_FEE %, default 3) and sales tax on that fee, even when the invoice is still DRAFT, and the order opens when the payment succeeds; without auto-payment or a saved card it stays RESERVATION. Those cases are a 200 - the message says which. A card Stripe refuses straight away answers 400 with Stripe's message (only the failed payment attempt is saved). An order whose total is 0 gets no invoice and opens at once. OPEN_CONTRACT_SKIP_PAYMENT opens without waiting for payment. On ADVANCE billing the invoice is still created and emailed, and when it is approved (the default) the customer's deposits and credit are applied and an auto-payment customer with a saved card is charged the rest; a decline does not stop the open. On opening, every dispatch gets its dispatched_date (the one sent, else the one already set, else now - a backdated order falls back to the expected dispatch date) and becomes DISPATCHED, or IN_TRANSIT while that date is in the future (a job running every 5 minutes moves it to DISPATCHED); assets are marked dispatched or in_transit, and the org and customer are emailed.\nCLOSED_CONTRACT - refused while a work order on the contract is open. Dispatches go OFF_RENT (off_rent_date, default now), off-rent inspections become PENDING (due in 3 days), the order is billed through the invoice sequence and the org and customer are emailed. When the org setting INVOICE.APPROVAL.RENTAL_INVOICES (INVOICE.APPROVAL.RENTAL_CONTINUATION_INVOICES for a renewal) is on (default) and the invoice itself is approved (not DRAFT or VOIDED), the customer's deposits and credit are applied first, then an auto-payment customer with a saved card is charged what is left in this call (plus the card fee and its tax); a card Stripe refuses does not stop the close - the invoice stays UNPAID, auto_charge says DECLINED and the message ends \"; the automatic card charge was declined, please collect payment manually\". A DRAFT is settled when it is approved. BACKDATED_CLOSED_CONTRACT closes a fully backdated QUOTE directly, with the credit-limit gate - one dispatch per asset is created and released at once (no inspections) and the whole window is billed and charged the same way.\nCOMPLETED_CONTRACT - refused (400, the first reason) until every invoice is paid or voided (an order with no invoice at all qualifies), every off-rent inspection is done and no work order is open; dispatches are RELEASED and the org and customer are emailed. With the org setting RENTAL.AUTO_COMPLETE_CLOSED_CONTRACTS on (default off), an hourly job does this by itself for every contract closed at least RENTAL.AUTO_COMPLETE_CLOSED_CONTRACTS_DAYS ago (default 0) that nothing blocks.\nRequires RENTAL_STATUS_UPDATE; ALLOW_UNPAID_PAY_ON_OPEN_PROMOTION for OPEN_CONTRACT_SKIP_PAYMENT, RENTAL_BACKDATED for past dates (for any date sent on BACKDATED_CLOSED_CONTRACT), CREDIT_LIMIT_OVERRIDE to pass a credit BLOCK.\n","tags":["Rentals"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/rental_convert_body"}}}},"responses":{"200":{"description":"Request Successful - read message to know whether the order actually moved (ADVANCE OPEN_CONTRACT)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/rental_convert_result"}}}},"400":{"description":"Validation failed (including a missing body); the step is not allowed from the current status or backdated state; not signed while the org requires it; a non-compliant customer; no site, or the site was deleted; an asset out of service or not rentable; an asset overlapping another open contract at open; a dispatch inspection still PENDING; a PENDING work order on an asset; a card Stripe refuses at open; a close while a work order on the contract is open; dispatched_date given on a close of an open contract; an off_rent_date before a dispatched_date; BACKDATED_CLOSED_CONTRACT with a future date or an asset line without both expected dates; COMPLETED_CONTRACT while an invoice, off-rent inspection or work order is still open\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The customer is banned (RESERVATION, OPEN_CONTRACT, OPEN_CONTRACT_SKIP_PAYMENT and BACKDATED_CLOSED_CONTRACT only); OPEN_CONTRACT_SKIP_PAYMENT without ALLOW_UNPAID_PAY_ON_OPEN_PROMOTION; a past date (on BACKDATED_CLOSED_CONTRACT, any date) without RENTAL_BACKDATED; a BLOCK credit limit\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No live rental order with this id in the caller's org","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"RESERVATION or BACKDATED_CLOSED_CONTRACT puts the customer over their credit limit - resend with acknowledge_credit_warning true; or the order changed while it was being closed or completed (\"Rental order changed while it was being closed\" / \"... completed\", e.g. the auto-complete job completed it first) - reload and retry\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/rentals/dispatch/status":{"put":{"summary":"Update A Dispatch Status","description":"Moves one asset's dispatch. Allowed moves - to DISPATCHED from DISPATCH_INSPECTION (the order must be OPEN_CONTRACT and the dispatch inspection passed) or from OFF_RENT (this only clears off_rent_date - the off-rent inspection, the OFF_RENT billing (the ADVANCE re-bill and its charge, the ON_ACCOUNT charge rows) and the off-rent email are not reversed); to OFF_RENT from DISPATCHED or OFF_RENT_INSPECTION; to OFF_RENT_INSPECTION from OFF_RENT; to RELEASED from OFF_RENT_INSPECTION once every inspection of the dispatch is done. Any other move is a 409 - an IN_TRANSIT dispatch cannot be moved here at all. timestamp (default now) may be past or future; DISPATCHED with a future timestamp is stored as IN_TRANSIT (results[].to_status shows what was stored). OFF_RENT makes the dispatch's off-rent inspection PENDING (due in 3 days) and emails the customer an off-rent notice. Billing on OFF_RENT - ON_ACCOUNT gets no invoice, only charge rows the regular billing run picks up; ADVANCE is re-billed through the invoice sequence, and when that invoice is approved the customer's deposits and credit are applied and an auto-payment customer with a saved card is charged the rest (a decline does not stop the off-rent; auto_charge says DECLINED and the message gains the decline notice). When no other dispatch of an OPEN_CONTRACT is still DISPATCHED (IN_TRANSIT ones do not count) and no work order on it is open, the contract is closed in the same call as PUT /lotus/rentals/convert CLOSED_CONTRACT does, card charge included - its close invoice is not emailed automatically, and if that close fails the off-rent is rolled back too. RELEASED completes a CLOSED_CONTRACT, when nothing blocks completion, as COMPLETED_CONTRACT does - except that with RENTAL.AUTO_COMPLETE_CLOSED_CONTRACTS on, a contract closed less than RENTAL.AUTO_COMPLETE_CLOSED_CONTRACTS_DAYS ago stays CLOSED_CONTRACT and the hourly job completes it later. auto_closed and auto_completed say whether that happened, and auto_charge what settling the OFF_RENT re-bill or the close invoice did. Completing a dispatch's off-rent inspection (inspection routes) already releases it, after which RELEASED here is a 409. OFF_RENT_INSPECTION is record-keeping only and is the one status that takes several dispatches at once (dispatch_ids, up to 50, all on one order); dispatches that cannot move are skipped and listed in results, and the call fails only when none moved. Requires RENTAL_DISPATCH_STATUS_UPDATE.\n","tags":["Rentals"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/rental_dispatch_status_body"}}}},"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"$ref":"#/components/schemas/rental_dispatch_status_result"}}}},"400":{"description":"Validation failed (including a missing body, both or neither of dispatch_id and dispatch_ids, or several ids for a status other than OFF_RENT_INSPECTION); the dispatch inspection has not passed; the order is not open; a batch across orders; an OFF_RENT whose automatic close fails - another dispatch is IN_TRANSIT with a dispatched_date still ahead\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No such dispatch in the caller's org (\"Dispatch not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The dispatch cannot move from its current status to this one; RELEASED while an inspection of the dispatch is not done\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/rentals/dispatch/replace-asset":{"put":{"summary":"Replace The Asset On A Dispatch","description":"Swaps the physical asset of an open contract's dispatch. The old dispatch is marked REPLACED and a new one is created for the new asset in DISPATCH_INSPECTION, keeping the old dispatched_date, with new inspection records (which notify the org); the two are linked. The line item keeps naming the asset originally booked. Billing does not change. The old dispatch gets an off_rent_date when it had been dispatched; if it was DISPATCHED its off-rent inspection becomes PENDING (due in 3 days); otherwise (IN_TRANSIT or DISPATCH_INSPECTION) its inspections are removed. The new dispatch still has to be moved to DISPATCHED with PUT /lotus/rentals/dispatch/status once its inspection passes. Requires RENTAL_OPEN_CONTRACT_ASSET_REPLACE.\n","tags":["Rentals"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/rental_replace_asset_body"}}}},"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"$ref":"#/components/schemas/rental_replace_asset_result"}}}},"400":{"description":"Validation failed (including a missing body); the order is not OPEN_CONTRACT; the dispatch is not DISPATCHED, IN_TRANSIT or DISPATCH_INSPECTION; not an ASSET line item; the same asset; the new asset owns fewer units than the line rents; the new asset overlaps another open contract\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"The dispatch, its order or the new asset is not in the caller's org","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/rentals/email":{"post":{"summary":"Email A Rental Order","description":"Emails the rental order document. receiver_type customer sends it to every contact of the customer, whatever their notification flags (only contact_id when given), in the HTML and/or PDF form the org's customer email settings choose; self sends it to the caller; custom to the custom_email addresses. A journal row records the recipients. Requires RENTAL_READ.\n","tags":["Rentals"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/rental_email_body"}}}},"responses":{"200":{"description":"Sent. The body is a bare JSON string.","content":{"application/json":{"schema":{"type":"string","example":"Email sent successfully"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"receiver_type customer while customer emails are switched off for this deployment or by the org's send-external-emails setting","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The order, or contact_id among the customer's contacts, is not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"receiver_type customer and the customer has no contact to send this to","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/rentals/signnow/invite":{"post":{"summary":"Send A Rental Order For E-Signature","description":"Sends the rental contract to signer_email for signing through SignNow. The first invite uploads the contract PDF to SignNow and stores its document id on the order; later invites reuse it. The order's signed_status becomes SENT_TO_SIGNER; SignNow's completion webhook later marks it SIGNED and stores the signed PDF. Requires RENTAL_CREATE or RENTAL_UPDATE, and e-signatures enabled for the org.\n","tags":["Rentals"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/rental_signnow_body"}}}},"responses":{"200":{"description":"Sent. The body is a bare JSON string.","content":{"application/json":{"schema":{"type":"string","example":"e-signature invite sent to signer successfully"}}}},"400":{"description":"Validation failed (including a missing body); e-signatures are not enabled for the org; the document has no signer role for the customer","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No rental order with this id in the caller's org","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/sales/search":{"post":{"summary":"Search Sales Orders","description":"Paginated sales orders of the caller's org, each with its full details (the GET /lotus/sales/{id} shape without ancestry, assignee_details and can_close). Never listed - imported orders, and (unless deleted is true) QUOTEs whose valid_until has passed. deleted true lists only the voided orders, expired quotes included, never both. The date range applies only when both date_from and date_to are sent. A customer-portal session only sees its own customer's orders (search.customer is forced). JSON only. Requires SALE_VIEW.\n","tags":["Sales"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sale_search_body"}}}},"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"$ref":"#/components/schemas/sale_search_result"}}}},"400":{"description":"Validation failed (including a missing body); date_to without date_from; an order_by column outside orderByColumnNamesList\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/sales/{id}":{"get":{"summary":"Get A Sales Order","description":"One sales order with everything the order screen shows - sale details (quote approval, signing), customer, contact, assignee, parent chain, taxes and discounts, freight tickets, invoices, signed document, line items with their sale inspection, sale task, current asset and SPP warranty state, the projected totals, and for a live OPEN_SALES_ORDER what still blocks closing it. Voided (deleted) orders are returned too, with their voided invoices and removed line items. Read only. Requires SALE_READ.\n","tags":["Sales"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"The sales order id."}],"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"$ref":"#/components/schemas/sale_order_detail"}}}},"400":{"description":"id is not a valid uuid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No sales order with this id in the caller's org (\"Sales order not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"summary":"Delete Or Void A Sales Order","description":"A QUOTE is deleted - the order, its line items and freight tickets are marked deleted and its assets in \"selling\" go back to \"onboarded\". An OPEN_SALES_ORDER is voided - the same, plus every invoice of the order becomes VOIDED (paid ones included), the payments applied to them go back to the customer's balance as credit (no card refund), system-granted early payment discounts are voided, the order's deposits are freed for any future order, the assets are released (\"selling\" or \"sale_approved\" back to \"onboarded\") and the sale inspections, unfinished sale work orders and sale checklists are removed. One ORDER_VOIDED audit row is written. The order keeps its status (deleted marks it voided). Refused for CLOSED_SALES_ORDER and for an open order with a card payment still in progress. No email is sent and pending SignNow invites are not cancelled. The body is optional. Requires SALE_DELETE.\n","tags":["Sales"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"The sales order id."}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sale_delete_body"}}}},"responses":{"200":{"description":"Done. The body is a bare JSON string - \"Sales order deleted successfully\" for a QUOTE, \"Sales order voided successfully\" for an OPEN_SALES_ORDER.\n","content":{"application/json":{"schema":{"type":"string","example":"Sales order deleted successfully"}}}},"400":{"description":"Validation failed or id is not a valid uuid; the order is CLOSED_SALES_ORDER; an open order with a payment in progress\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No live sales order with this id in the caller's org (\"Order not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/sales/receipt/{id}":{"get":{"summary":"Get A Sales Order Document","description":"The sales order document. format=pdf returns the PDF (the same document the customer is emailed) as a download named \"Sales Order - <order_number>.pdf\"; any other value (default html) returns the HTML rendering as a download named receipt-<order_number>.html. Voided orders render too. Read only. Requires SALE_READ.\n","tags":["Sales"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"The sales order id."},{"in":"query","name":"format","required":false,"schema":{"type":"string","default":"html"},"description":"pdf returns the PDF; any other value (or none) returns the HTML.","example":"pdf"}],"responses":{"200":{"description":"The document","content":{"text/html":{"schema":{"type":"string"}},"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"id is not a valid uuid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No sales order with this id in the caller's org (\"Sales order not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/sales/{id}/credit-check":{"get":{"summary":"Check A Sales Order Against The Customer's Credit Limit","description":"The customer's credit position if this order is committed - the pre-check the web runs before converting a quote to an open sales order. For a QUOTE the order's projected total is added to the exposure; for any other status it is already counted. A sale without a customer answers decision OK with reason no_customer. Read only. Requires SALE_READ.\n","tags":["Sales"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"The sales order id."}],"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"$ref":"#/components/schemas/sale_credit_check_result"}}}},"400":{"description":"id is not a valid uuid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No live sales order with this id in the caller's org (\"Order not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/sales/charges_summary":{"post":{"summary":"Preview Sales Order Charges","description":"Prices a sales order that is being built, without saving anything - line item totals, the four sales taxes (rates resolved from the customer, then the org address), discounts and quoted freight (taxes apply to freight, discounts do not). Prices are taken from the body as sent. Sales carry no environmental fee or damage waiver. With customer_id it also returns the customer's credit position including this preview's total. Requires SALE_CREATE or SALE_UPDATE.\n","tags":["Sales"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sale_charges_summary_body"}}}},"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"$ref":"#/components/schemas/sale_charges_summary_result"}}}},"400":{"description":"Validation failed (including a missing body)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The customer is banned (\"Customer account is locked: <reason>\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The customer is not in the caller's org (\"Customer not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/sales/sales_analysis/{id}":{"post":{"summary":"Get A Sales Order Profit Analysis","description":"The deal profit-and-loss the Sale Analysis tab shows - revenue, cost, gross profit and margin for the whole order and per item (assets, attachments, custom fees and freight), where the revenue goes, and profitability notes. Built from the saved order, depreciation and commission settings; soldDate (default now) is the projected sale date used for the months-in-inventory and book-value figures. Values with no data source yet are null with status \"missing\". Read only. The body is optional. Requires SALE_CREATE or SALE_UPDATE.\n","tags":["Sales"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"The sales order id."}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sale_analysis_body"}}}},"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"$ref":"#/components/schemas/sale_analysis_result"}}}},"400":{"description":"Validation failed (soldDate is not a date) or id is not a valid uuid. An order with no main asset is not an error since DOZ-7943: it answers 200 with FEE and FREIGHT items only, and an order with no lines answers 200 with an empty items list and a zeroed deal.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No live sales order with this id in the caller's org (\"Sales order not found\") - voided orders included","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/sales/create":{"post":{"summary":"Create A Sales Quote","description":"Creates a sales order as a QUOTE (quote_status PENDING, order number S-YYYYMMDD-NNN) with its line items, taxes and discounts, freight tickets and the SPP warranty decision of each main asset (a main asset left without warranty_eligibility is saved as QUALIFIES when warranty_options are sent, otherwise DOES_NOT_QUALIFY). A customer is optional on a quote. assignee_id defaults to the customer's sales person. The assets are logged as QUOTED but their status does not change. No invoice, charge or email. The quote is approved later with PUT /lotus/sales/update. Requires SALE_CREATE; any of the four *_sales_tax_status keys needs UPDATE_FEES_ON_SALE_ORDERS, flat_discount_value above 0 needs TRANSACTION_FLAT_CASH_DISCOUNT, and more than one main asset needs ALLOW_MULTIPLE_ASSET_ON_SALES_ORDER.\n","tags":["Sales"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sale_create_body"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/sale_create_result"}}}},"400":{"description":"Validation failed (including a missing body); a contact without a customer; a customer discount above the customer's own; an asset not found as sellable (no unit, still being onboarded, sale_approved (\"is already on an open sales order\"), already sold). Create does not catch an asset on another open sale in general: with the default \"on payment\" sale settings such an asset stays onboarded, so the real double-sell check runs only at convert to OPEN_SALES_ORDER or when an open-order edit adds the asset; a duplicate or wrongly paired attachment; several main assets without the permission; a WARRANTY line whose asset_ref is not on the order; a commission rate above the org default; an assignee who cannot own orders, or who is not found in the org (\"assignee does not exist\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No SALE_CREATE, or a tax flag without UPDATE_FEES_ON_SALE_ORDERS (\"Not permitted to update fees & taxes\"), or a flat discount without TRANSACTION_FLAT_CASH_DISCOUNT\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The customer is banned; a discount above the caller's role limit (\"Not allowed to give discount greater than 'X%'\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The customer, contact or an asset is not in the caller's org","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/sales/update":{"put":{"summary":"Update A Sales Order","description":"Saves the whole order from this body. items is the full list (lines left out are removed) and every ASSET line is priced again (customer_price, else the asset's sell price). The four tax flags, the discounts and the freight fields are rebuilt from this body - leaving them out removes them - and the tax rates are looked up again. tax_exemption_expiration_date is cleared when left out. Only customer_id, contact_id, assignee_id, customer_po, the notes, sales_quote_status, sales_quote_rejection_reason, commission_rate and, on each ASSET line, warranty_recorded_outcome and warranty_selected_option_id keep their stored value when left out.\n\nOn a QUOTE it also approves or rejects the quote (sales_quote_status). Approving needs a customer and does not change the order status or the assets - converting is a separate PUT /lotus/sales/convert. Approving while a QUALIFIES asset has no recorded warranty choice is refused (409 WARRANTY_UNRESOLVED) until it is resent with acknowledge_warranty_warning true.\n\nWarranty - warranty_options are the tiers offered on the asset's authorization form and cannot change once that form has been sent. warranty_recorded_outcome records the customer's choice (only after the form is SIGNED); OPTION_SELECTED adds or updates a taxable WARRANTY line \"SPP — N months / H hrs\" priced at the option's retail_price, REFUSED or null removes it. warranty_void throws the asset's warranty away (tiers, form, sold line) while the asset stays.\n\nOn an OPEN_SALES_ORDER (needs SALE_UPDATE_OPEN; sales_quote_status is ignored) the edit is also billed. The order's charges are computed again; when they are the same as the charges it is billed for now (a save that only changes notes, the assignee and the like), nothing is billed or charged and billing_message is \"Open sales order updated\". Otherwise the latest invoice is rewritten in place when it is DRAFT or UNPAID with nothing paid, else a new invoice for the difference is created. A result below zero (the order shrank below what was already billed) becomes a credit invoice that is absorbed straight away (marked PAID): any early-payment-discount clawback is taken off first, the rest pays down this order's own unpaid invoices newest first, and only what is left goes to the customer's balance. A new invoice is emailed as the org's delivery setting allows; a changed one is not re-sent. When INVOICE.APPROVAL.SALE_INVOICES is on (default) and the customer has auto_payment, a default card and a Stripe id, that card is charged the invoice's whole open balance (less card payments still in flight), not just the difference, plus the org's card fee (FEES.CC_PROCESSING_FEE %, default 3) and sales tax on that fee at the order's tax rates as this edit saves them. Once that card payment settles, the card fee and the tax on it are added to the invoice as charges, so the invoice total grows past the order total. A failed charge does not undo the edit, and the invoice stays UNPAID with nothing paid, so the next save that changes the charges rewrites it again and retries the card for the whole open balance. billing_message says \"An invoice was created\" when a new invoice was created and \"The invoice was updated\" when the existing invoice was rewritten in place. The credit limit is checked when the edit raises the customer's exposure. Removed assets are released and their sale inspections, unfinished sale work orders and checklists removed. When assets are added, the org's sale checklists, inspections and work orders are provisioned as the sale settings say (now, with provision_subwork_now) for every asset line on the order that does not have them yet, not only the added ones. Each sale inspection and work order created notifies the org (email and Slack, per its notification settings) once the save has committed, so an edit refused by the credit-limit check (403/409) sends none. A WARRANTY line whose asset_ref is not an asset on the order is silently dropped (not a 400); on an open order it is then credited by the re-bill. data.billing_message says what happened to billing and payment.\n\nBoth - when an asset whose warranty form was sent leaves the order or its warranty is voided, the SignNow invite is cancelled (unless already signed) and the signer is emailed (the signer email the form went to, else the order's contact, else the customer's first contact with an email). Forms the customer declined, that expired or whose invite was deleted get no email. Requires SALE_UPDATE; the same fee, flat-discount and multiple-asset permissions as create.\n","tags":["Sales"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sale_update_body"}}}},"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"$ref":"#/components/schemas/sale_update_result"}}}},"400":{"description":"Validation failed (including a missing body or an uppercase sales_quote_status); approving without a customer; a line item of another order; a warranty option id that is not a live option of that same line (\"warranty option <id> does not belong to this line item\"); an open order left without line items; warranty options changed after the form was sent; a recorded choice on an unsigned form, a non-qualifying asset or an unknown option; a sold SPP warranty line left out while its asset stays (send it back, or set the asset's warranty_recorded_outcome to REFUSED); an asset already on another open sales order (when an open-order edit adds it); the create errors, except the WARRANTY asset_ref one (the line is dropped instead)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No SALE_UPDATE, or the fee / flat-discount permission as on create","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"An OPEN_SALES_ORDER without SALE_UPDATE_OPEN; the customer is banned; a discount above the caller's role limit; a BLOCK credit limit without CREDIT_LIMIT_OVERRIDE\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The order (\"Sales quote does not exist\"), customer, contact or an asset is not in the caller's org","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The order is CLOSED_SALES_ORDER (\"Can not edit a closed sales order\"); approving with unresolved warranties (details.code WARRANTY_UNRESOLVED, with details.unresolved listing the assets - resend with acknowledge_warranty_warning true); an open-order edit puts the customer over their credit limit (resend with acknowledge_credit_warning true)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/sales/convert":{"put":{"summary":"Convert A Sales Order To The Next Status","description":"Moves the order one step - QUOTE to OPEN_SALES_ORDER, or OPEN_SALES_ORDER to CLOSED_SALES_ORDER.\n\nOPEN_SALES_ORDER - the quote must be APPROVED, and SIGNED when the org requires e-signatures for sales. Banned customers are refused and the credit limit is checked. The assets must not be on another open sales order. The order and its lines become OPEN_SALES_ORDER. Sale checklists (assets onboarded to selling), inspections and work orders are created when the org's sale settings say \"on open\" (default \"on payment\"), or now with provision_subwork_now unless that type is set to \"never\" (work orders also need a default task level); without sale checklists the assets go straight to sale_approved. Each sale inspection and work order created notifies the org (email and Slack, per its notification settings) once the conversion has committed.\n\nOn open the whole order is invoiced (UNPAID, or DRAFT when INVOICE.APPROVAL.SALE_INVOICES is off; a $0 order gets no invoice) and the new invoice is emailed as the org's delivery setting allows. When the invoice is UNPAID and the customer has auto_payment, a default card and a Stripe id, that card is charged the invoice total plus the org's card fee (FEES.CC_PROCESSING_FEE %, default 3) and sales tax on that fee at the order's tax rates; the charge settles later by webhook. Once that card payment settles, the card fee and the tax on it are added to the invoice as charges, so the invoice total grows past the order total. A DRAFT invoice is not charged. A charge Stripe refuses does not undo the conversion. message says what happened to the invoice and payment.\n\nCLOSED_SALES_ORDER - refused (400, with the reasons) until every invoice is paid, every asset is sale_approved and every sale inspection and sale work order is done. The lines are stamped sold with a profit snapshot (sale_snapshot), and the assets become sold. No invoice, charge or email.\n\nRequires SALE_STATUS_UPDATE; CREDIT_LIMIT_OVERRIDE to pass a credit BLOCK.\n","tags":["Sales"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sale_convert_body"}}}},"responses":{"200":{"description":"Request Successful - message says what happened to the invoice and payment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/sale_convert_result"}}}},"400":{"description":"Validation failed (including a missing body); the step is not allowed from the current status; the quote is not approved; not signed while the org requires it; an asset already on another open sales order; closing while something still blocks it (\"Can't close this sales order: <reason>; <reason>\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The customer is banned; a BLOCK credit limit without CREDIT_LIMIT_OVERRIDE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No live sales order with this id in the caller's org (\"Order doesn't exist\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The conversion puts the customer over their credit limit - resend with acknowledge_credit_warning true; the order changed status meanwhile\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/sales/email":{"post":{"summary":"Email A Sales Order","description":"Emails the sales order document (subject \"Sale - <Status> Email\"). receiver_type customer sends it to the customer's contacts (only contact_id when given), in the HTML and/or PDF form the org's customer email settings choose for the order's status; self sends the HTML to the caller; custom sends the HTML to the custom_email addresses. A journal row records the recipients. Requires SALE_READ.\n","tags":["Sales"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sale_email_body"}}}},"responses":{"200":{"description":"Sent. The body is a bare JSON string.","content":{"application/json":{"schema":{"type":"string","example":"Email sent successfully"}}}},"400":{"description":"Validation failed (including a missing body); receiver_type customer on a sale without a customer","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"receiver_type customer while customer emails are switched off for this deployment or org","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The order (\"Sales order not found\"), or contact_id among the customer's contacts, is not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"receiver_type customer and the customer has no contacts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/sales/signnow/invite":{"post":{"summary":"Send A Sales Order For E-Signature","description":"Sends the sales order to signer_email for signing through SignNow (SignNow emails \"Please sign the sales order\"; the invite expires after 30 days). The first invite uploads the order PDF to SignNow and stores its document id on the order; later invites reuse it. sale_details.signed_status becomes SENT_TO_SIGNER; SignNow's webhook later sets SIGNED and stores the signed PDF (or SIGNER_DECLINED, INVITE_EXPIRED, ...). A journal row is written. Requires SALE_CREATE or SALE_UPDATE, and e-signatures enabled for the org.\n","tags":["Sales"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sale_signnow_body"}}}},"responses":{"200":{"description":"Sent. The body is a bare JSON string.","content":{"application/json":{"schema":{"type":"string","example":"e-signature invite sent to signer successfully"}}}},"400":{"description":"Validation failed (including a missing body); e-signatures are not enabled for the org; the document has no signer role for the customer","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No live sales order with this id in the caller's org (\"Sales order not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/sales/warranty/signnow/invite":{"post":{"summary":"Send SPP Warranty Forms For E-Signature","description":"Sends one SPP (Specialty Protection Plan) warranty authorization form per main asset of the order to signer_email through SignNow - assets marked QUALIFIES get their warranty options to choose from (or REFUSED), DOES_NOT_QUALIFY assets an acknowledgement to sign. line_item_ids limits it to those assets. Each asset is handled on its own - an asset that already has a SignNow document is skipped (ALREADY_SENT; also when an earlier send failed after the upload - void the warranty on PUT /lotus/sales/update to send a fresh form), a QUALIFIES asset without options is skipped (SKIPPED_NO_OPTIONS), and one that fails (FAILED, with the reason) does not stop the others. A 200 means the batch ran, not that every form went out - read sent and results. For each form sent - the PDF is uploaded to SignNow, SignNow emails the signer (expires after 30 days), the asset's warranty_signed_status becomes SENT_TO_SIGNER and a journal row is written. SignNow's webhook later sets SIGNED and stores the signed form. Requires SALE_CREATE or SALE_UPDATE, and e-signatures enabled for the org.\n","tags":["Sales"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sale_warranty_invite_body"}}}},"responses":{"200":{"description":"The batch ran - one result per asset","content":{"application/json":{"schema":{"$ref":"#/components/schemas/sale_warranty_invite_result"}}}},"400":{"description":"Validation failed (including a missing body); e-signatures are not enabled for the org; no asset of the order (or of line_item_ids) carries a warranty decision\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No sales order with this id in the caller's org (\"Sales order not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/sales/warranty/signed-selection/{line_item_id}":{"get":{"summary":"Read The Customer's Choice On A Signed SPP Form","description":"Which warranty option the customer ticked on this asset's signed SPP form, read live from SignNow. It reports and never saves - the choice is recorded with PUT /lotus/sales/update (warranty_recorded_outcome, warranty_selected_option_id), and matches_recorded says whether that is already done. Every answer is a 200 with a status - READ (outcome and selected option filled in), NOT_APPLICABLE (the line has no warranty decision), NO_CHOICE_ON_FORM (the asset does not qualify), NOT_SENT, NOT_SIGNED, or UNREADABLE (signed, but no recognisable choice). SignNow is called only for a SIGNED form. Requires SALE_READ.\n","tags":["Sales"],"parameters":[{"in":"path","name":"line_item_id","required":true,"schema":{"type":"string","format":"uuid"},"description":"The asset's line item id."}],"responses":{"200":{"description":"Request Successful - read status","content":{"application/json":{"schema":{"$ref":"#/components/schemas/sale_signed_selection_result"}}}},"400":{"description":"line_item_id is not a valid uuid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No live sales order line item with this id in the caller's org (\"Sales order line item not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"},"502":{"description":"SignNow could not be reached to read the signed form - worth retrying (details.code SIGNNOW_READ_FAILED)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/lotus/search":{"get":{"summary":"Search every entity the caller may see","description":"The query is `[<token>::]<text>`. Without a token every bucket runs; a token runs only its bucket: `a` assets, `p` parts (and, with `include=purchase_orders`, the purchase orders carrying them or matching by PO number, vendor name or vendor number), `c` customers, `s` sites, `o` orders, `i` invoices, `t` work orders and inspections, `r` reports, `st` settings. The text is split on whitespace and a row matches only when every word appears in it, case-insensitively. `%` and `_` act as LIKE wildcards, except in the parts and purchase_orders buckets, where they are literal as on the Parts page.\n\nEvery bucket permission is forwarded, so the route admits any signed-in user whose roles hold at least one permission of any kind; each bucket then runs only for its own, and a user holding none of them gets every bucket empty: assets `ASSET_VIEW`; parts `PARTS_VIEW`; purchase_orders `PURCHASE_ORDERS_VIEW`, only with `include=purchase_orders` and never for a customer-portal user; customers and sites `CUSTOMER_VIEW`; orders `RENTAL_VIEW` / `SALE_VIEW`; invoices `RENTAL_INVOICE_VIEW` / `SALE_INVOICE_VIEW` / `WORK_ORDER_INVOICE_VIEW`; tasks `WORK_ORDER_VIEW`; reports `REPORT_VIEW`; settings `PLATFORM_GENERAL_VIEW`. Every bucket key is always present: a bucket that did not run, or found nothing, is `{ data: [], length: 0 }`. A customer-portal session is scoped to its own customer in the customers, sites, orders, invoices and tasks buckets. With work orders switched off for the org, the tasks bucket holds only inspections and the invoices bucket no work-order invoices.\n","tags":["Search"],"parameters":[{"in":"query","name":"query","required":true,"description":"`[<token>::]<text>`, trimmed, 1 to 200 characters. A text that is empty after the token (e.g. `o::`) returns every bucket empty.\n","schema":{"type":"string","minLength":1,"maxLength":200},"example":"p::hydraulic filter"},{"in":"query","name":"include","required":false,"description":"Buckets to run on top of the default ones; repeat the parameter for several. `purchase_orders` runs the purchase-orders bucket (DOZ-7866). Without it that key is always `{ data: [], length: 0 }`, so a client that does not render it never receives rows. An unknown value is a 400.\n","schema":{"type":"array","items":{"type":"string","enum":["purchase_orders"]}},"style":"form","explode":true,"example":["purchase_orders"]}],"responses":{"200":{"description":"One `{ data, length }` bucket per entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/generic_search_result"}}}},"400":{"description":"`query` missing, empty, only spaces or longer than 200 characters, or an `include` value other than `purchase_orders` (messages start with `Validation error: `); or an unknown token (`Invalid search token`)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Not signed in, or a session whose roles hold no permission at all (holding only permissions outside the bucket list is a 200 with every bucket empty)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"An AI-connector (MCP) session, the route is not on the connector allowlist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/inspections/create":{"post":{"summary":"Create Stand-Alone Inspections","description":"Creates an inspection for each asset. With independent_inspection false the asset gets a rental pair - a DISPATCH inspection (PENDING, due on due_date) and an OFF_RENT inspection kept HIDDEN until the dispatch inspection completes, when it becomes PENDING due in three days - and with true a single INDEPENDENT inspection (PENDING, due on due_date). The form items come from the inspection form templates of the asset's category (the INDEPENDENT or the RENTAL scope); an asset with an hour meter starts with an unverified empty reading. Asset ids that are not the org's are skipped; when none is, 404. An asset that already has an INDEPENDENT inspection that is not DONE refuses the whole request with details.blocking_assets listing those inspections. A customer may be attached only when the org's work-order settings allow customer assignment. For every asset the org's inspection contacts are notified by Slack and email (\"A new Dispatch Inspection is created against (#<asset number>)\", or Independent Inspection), with a journal row. Requires INSPECTION_CREATE.\n","tags":["Inspections"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/inspection_create_body"}}}},"responses":{"200":{"description":"Created (the status is 200, not 201)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/inspection_create_result"}}}},"400":{"description":"Validation failed (including a missing body; code VALIDATION); an asset with an open INDEPENDENT inspection (\"Some incomplete inspections exist for the selected assets. Please complete those before creating new stand alone inspections\", with details.blocking_assets); a form template that cannot be resolved for the asset's category; a customer sent while the org has customer assignment switched off (\"Assigning a customer to an inspection is disabled for this organization.\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, invalid or expired token, or the caller lacks INSPECTION_CREATE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"None of the asset ids is an asset of the caller's org (\"No asset found against provided asset ids\"); organization not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/inspections/quick-pass":{"post":{"summary":"Quick-Pass Inspections","description":"Completes the given inspections as passed without answering their items. Every inspection that is not already DONE becomes DONE, passed, quick_pass true, assigned to the caller, with the note as its internal notes and the completion time stamped; each unanswered item gets a neutral passing answer (TEXT and DROPDOWN \"quick_passed\", NUMBER 0, PASS_FAIL and YES_NO true, YES_NO_AND_GAUGE no reading, CHECK_AND_GAUGE checked with 0, DAMAGE_AND_PHOTO no damage, GAUGE_AND_PHOTO 0), and an asset with an hour meter has its current reading recorded as verified. Then the same cascade as completing an inspection by hand - a stand-alone DISPATCH inspection's hidden OFF_RENT pair becomes PENDING due in three days; an OFF_RENT inspection releases its dispatch when it is OFF_RENT or OFF_RENT_INSPECTION (RELEASED, off-rent date kept or set now, state logged, the line item RELEASED), clears the asset's rental status once its dispatch is settled, and, when every dispatch of the rental has a DONE off-rent inspection, drops \"Not all assets have gone through an off_rent inspection\" from the rental's can_complete reasons; the asset's latest inspection date is stamped; one inspection journal row records what the pass changed (DOZ-7836) and a platform event is published per inspection. Inspections already DONE, deleted or not the org's are skipped silently. Requires QUICK_PASS_INSPECTIONS.\n","tags":["Inspections"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/inspection_quick_pass_body"}}}},"responses":{"200":{"description":"Quick-passed (also when nothing needed completing)","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","example":"Inspections have successfully been quick-passed"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"description":"Missing, invalid or expired token, or the caller lacks QUICK_PASS_INSPECTIONS","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"The caller's user row is missing (\"User details not found for snapshot\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/inspections/anonymous/link/token/{inspection_id}":{"get":{"summary":"Mint An Anonymous Link Token For An Inspection","description":"Mints a signed token that lets anyone read one inspection through GET /lotus/inspections/anonymous/link/data/{access_token} without signing in. The token lasts the org's \"inspection link duration\" setting (hours; one hour by default) and is stored with its expiry. Requires WORK_ORDER_READ.\n","tags":["Inspections"],"parameters":[{"name":"inspection_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The token, as a JSON string","content":{"application/json":{"schema":{"type":"string","example":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpbnNwZWN0aW9uX2lkIjoiLi4uIn0.signature"}}}},"400":{"description":"The id is not a uuid (\"Invalid Inspection ID\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, invalid or expired token, or the caller lacks WORK_ORDER_READ","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"No inspection with that id in the caller's org (\"Inspection record not found\"), or it is deleted (\"Inspection is deleted\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/inspections/anonymous/link/data/{access_token}":{"get":{"summary":"Read An Inspection Through An Anonymous Link","description":"Public - no bearer token. Resolves a token minted by GET /lotus/inspections/anonymous/link/token/{inspection_id} to its inspection. The token must verify and its stored link must not have expired. The inspection comes back as GET /lotus/inspections/{id} returns it to a non-admin (internal_notes null, no assignment scoping, no analysis); a HIDDEN or deleted inspection is a 404.\n","tags":["Inspections"],"security":[],"parameters":[{"name":"access_token","in":"path","required":true,"description":"The token string exactly as minted","schema":{"type":"string"}}],"responses":{"200":{"description":"The inspection and the link's expiry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/inspection_anonymous_data"}}}},"400":{"description":"Empty token (\"Invalid access token\"), or one that does not verify or has expired (\"Link has expired or is malformed, Please request a new link from the owner\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No unexpired link for the token (\"No link against this access token was found, Please request a new link from the owner\"); the inspection is HIDDEN or missing (\"Inspection record not found\") or deleted (\"Inspection is deleted\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/inspections/email":{"post":{"summary":"Email An Inspection Report","description":"Emails the inspection report - the asset, dates, hour meter and every item with its answer and photos inline - with the subject \"<type> Email (#<asset number>)\", e.g. \"Dispatch Inspection Email (#A-102)\", to the caller's own address (receiver_type self) or to the addresses in custom_email (custom). Internal notes are included. A journal row (email_delivery_inspections) records the recipients. HIDDEN inspections cannot be emailed. Requires WORK_ORDER_READ.\n","tags":["Inspections"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/inspection_email_body"}}}},"responses":{"200":{"description":"Sent","content":{"application/json":{"schema":{"type":"string","example":"Inspection email successfully sent"}}}},"400":{"description":"Validation failed - a missing body, an unknown receiver_type, custom without custom_email, or an entry that is not an email","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, invalid or expired token, or the caller lacks WORK_ORDER_READ","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"No such inspection in the caller's org, or it is HIDDEN (\"Inspection record not found\") or deleted (\"Inspection is deleted\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/inspections/update":{"put":{"summary":"Update Or Complete An Inspection","description":"Saves an inspection in place - assignment, access, priority, due date, status (PENDING or READY_FOR_INSPECTION), notes, customer, hour meter and item answers - and completes it with done true. Item answers are accepted only while the inspection is READY_FOR_INSPECTION or PARTIALLY_INSPECTED; answering any item (or verifying the hour meter without a status change) moves it to PARTIALLY_INSPECTED. Each answer must fit its item type (see inspection_item_value); a media answer keeps the photos already uploaded through POST /media/inspection/inspection_item and takes only the fields sent here. A failing answer - PASS_FAIL false, a CHECK_AND_GAUGE not checked or with an amount added, a DAMAGE_AND_PHOTO marked repair or replace, a GAUGE_AND_PHOTO with a reading above 0, or an hour-meter reading the org flags as unreasonable - fails the inspection when it completes. To complete (done true) the inspection must be READY_FOR_INSPECTION or PARTIALLY_INSPECTED, every required item answered, an asset with an hour meter needs a reading (verified true) not below the asset's current reading, rounded to one decimal, and, unless the caller holds INSPECTION_NOT_REQUIRE_PHOTO, an hour-meter photo already uploaded through POST /media/inspection/hour_meter_image and at least one photo (or lazy_upload true) on every required photo item (with that permission a required photo item may also stay unanswered). On completion the status is DONE and passed true or false. A failed inspection has the failing items appended to its notes under \"(From Mechanic)\" and a work order created for the asset, at the org's default work-order level, with the failing items as its notes - a child of the rental, with its customer and site, for an OFF_RENT inspection; a SALE work order linked to the sale for a SALE inspection; stand-alone for a DISPATCH or INDEPENDENT inspection. When an open work order is already linked to this inspection (or, for a SALE inspection, to the same sale and asset) its notes are appended instead. asset_out_of_service is applied only on a failed completion. notes is written on every save: omit it and the stored external notes are cleared. Then: a stand-alone DISPATCH inspection's hidden OFF_RENT pair becomes PENDING due in three days; an OFF_RENT inspection releases its dispatch when it is OFF_RENT or OFF_RENT_INSPECTION, clears the asset's rental status once the dispatch is settled and, once every dispatch of the rental has a DONE off-rent inspection, drops that reason from the rental's can_complete; the asset's hour meter and latest inspection date are updated, which may schedule a maintenance work order; one inspection journal row records what changed (DOZ-7836), a platform event is published and the org's pass or fail completion email sent. set_for_siblings copies the assignee (with all_assignees_access), due_date, status or priority to the not-DONE inspections of the same type on the rental's other dispatches. A changed assignee gets a push notification. A customer may be set only on a stand-alone inspection and only when the org allows customer assignment. Requires INSPECTION_UPDATE.\n","tags":["Inspections"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/inspection_update_body"}}}},"responses":{"200":{"description":"The inspection as saved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/inspection_update_result"}}}},"400":{"description":"Validation failed (an unknown key, a bad value, or a missing body); the inspection is already DONE; items sent while it is not ready (\"Inspection record is not yet ready for inspection\"); \"all assignees\" access while the org assigns inspections to one assignee; an hour meter on an asset without one; a missing hour-meter reading or photo on completion; a reading below the asset's (\"Faulty hour meter value\"); a customer on a dispatch-linked inspection, or while the org has customer assignment switched off; an assignee without ALLOW_ASSIGNEE_OWNERSHIP (\"Work Orders can only be assigned to mechanics (Missing ALLOW_ASSIGNEE_OWNERSHIP permission).\"); a required photo item without a photo (\"A photo is required for item (...) before this inspection can be completed\"); an answer that does not fit its item (\"Incorrect inspection item data for item (...) | ...\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, invalid or expired token, or the caller lacks INSPECTION_UPDATE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"No such inspection in the caller's org, or it is deleted (\"Inspection record doesn't exist\"); the assignee (\"Assignee not found.\"), the organization or the caller's user row not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"done true with a required item still unanswered (\"Can't mark an incomplete inspection as done\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/inspections/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"get":{"summary":"Get An Inspection","description":"One inspection with its asset, dispatch, order and customer, its form items, the paired dispatch inspection (on an off-rent inspection), the reasonable-hours check and, for an org with the Inspection Analysis add-on, the latest analysis. Without ACCESS_ALL_INSPECTIONS the caller sees only inspections that are READY_FOR_INSPECTION or PARTIALLY_INSPECTED and open to all assignees, or, with ALLOW_ASSIGNEE_OWNERSHIP, assigned to the caller; everything else is a 404. HIDDEN inspections are never returned. internal_notes is null unless the caller holds ACCESS_ALL_INSPECTIONS. created_at, asset_out_date and asset_in_date are formatted in the org's timezone (\"Tue, Oct 14, 2026 1:05 PM\"). Requires WORK_ORDER_READ.\n","tags":["Inspections"],"responses":{"200":{"description":"The inspection","content":{"application/json":{"schema":{"$ref":"#/components/schemas/inspection_detail"}}}},"400":{"description":"The id is not a uuid (\"Invalid Inspection ID\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, invalid or expired token, or the caller lacks WORK_ORDER_READ","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"No such inspection in the caller's org, HIDDEN, or outside the caller's access (\"Inspection record not found\"); deleted (\"Inspection is deleted\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"summary":"Delete A Stand-Alone Inspection","description":"Soft-deletes an inspection that is not linked to a dispatch, together with its paired dispatch or off-rent inspection, recording who deleted it, when and why. Dispatch-linked inspections cannot be deleted. Requires INSPECTION_DELETE.\n","tags":["Inspections"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/inspection_delete_body"}}}},"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/inspection_delete_result"}}}},"400":{"description":"The id is not a uuid (\"Invalid Inspection ID\"); no reason (a missing body counts); the inspection is linked to a dispatch (\"You can only delete stand-alone inspections\"); already deleted (\"Inspection is already deleted\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, invalid or expired token, or the caller lacks INSPECTION_DELETE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"No such inspection in the caller's org (\"Inspection record not found\"); the caller's user row is missing (\"User not found.\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/work-orders/create":{"post":{"summary":"Create A Work Order","description":"Creates a work order (number WO-YYYYMMDD-NNN) in PENDING for one asset, with its lines - parts and custom charges, each billed to the CUSTOMER (the work order's invoice), the ASSET (an internal cost of the asset) or the PARENT_ORDER (the parent rental contract's invoice). Stock parts are taken out of inventory now (supplies excepted); parts on a purchase order are reserved against it (parts_allocation_status ALLOCATED). The four sales-tax rates come from the ZIP lookup (the site's ZIP, then the customer's, then the org's address); the org's work-order tax rates are used only when no ZIP matches. The fees (damage waiver, environmental fee) always use the org's work-order rates. Which taxes apply is limited to what the parent rental, an exempt site or the customer allows. With a parent rental the customer comes from it, its site is used unless site_id is sent, and the rental cannot complete until the work order is DONE. An off-site work order with a parent rental also adds a service note to the customer's notes (\"Off-Site Task: <order number>\" with external_notes, the rental and the asset). After saving, the org's work-order recipients are emailed (and Slack notified) \"New Work Order For #<asset number>\". No invoice or charge. Requires WORK_ORDER_CREATE; refused when the org has work orders, customer assignment or item lines switched off.\n","tags":["Work Orders"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/work_order_create_body"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/work_order_create_result"}}}},"400":{"description":"Validation failed (including a missing body); PARENT_ORDER lines without a parent; customer assignment or item lines switched off for the org; all_assignees_access true when the org assigns work orders to one assignee only; lines billed to a customer without one; not enough stock; a fraction quantity on a part that is not supplies; a supplies part on a purchase order; a purchase order not released or without enough quantity; a CUSTOM line on a purchase order without purchase_order_item_id; a purchase-order freight or misc line put on a purchase order itself, or on a work order without that purchase order's items; an assignee who is not a mechanic; a site not of the customer\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Work orders are switched off for the org; a discount above the caller's role limit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The asset, level, assignee, customer, site, contact, a part, a purchase order or the parent rental (or the asset or dispatch on it) is not found; a line's item is not on its purchase order (\"Item(...) does not exist on purchase order(...)\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/work-orders/update/{id}":{"put":{"summary":"Update A Work Order","description":"Saves the whole work order - a full replace. items is the full list (lines left out are removed, their PO reservations failed); omitted booleans become false and the discount 0. Stock follows the change (removed or reduced lines go back to inventory, added ones come out). The status is not changed here (use POST /lotus/work-orders/convert/{id}). A DONE work order cannot be edited; WORK_COMPLETED still can. customer_id and site_id are saved as sent (null when left out): the parent rental's customer, and its site unless site_id is sent, are taken only when parent (or parent_id) is sent again on this save. When the org has customer assignment switched off, a missing customer_id, site_id, contact_id or customer_po keeps its stored value instead. Taxes are re-resolved the same way as on create (ZIP lookup, org work-order rates only as the fallback; fees at the org's rates). When parent is sent on an off-site work order, every save adds another service note to the customer's notes (as on create). The parent rental's \"can complete\" is refreshed. No invoice, charge or notification. One work_order_update journal row records what changed, none when nothing did (DOZ-7836). Requires WORK_ORDER_UPDATE.\n","tags":["Work Orders"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"The work order id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/work_order_update_body"}}}},"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"$ref":"#/components/schemas/work_order_id_result"}}}},"400":{"description":"id is not a valid uuid (\"Invalid Task ID\"); validation failed (including a missing body); the work order is DONE; a different parent; a line of another order; changing a received PO line's quantity; the create errors\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"A discount above the caller's role limit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No live work order with this id (\"Work order not found.\"), or a referenced record as on create","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/work-orders/convert/{id}":{"post":{"summary":"Change A Work Order's Status","description":"Moves the work order to any status except the one it is in; DONE is final. asset_out_of_service is required when leaving PENDING, going back to PENDING and going to DONE, and sets the asset's out-of-service flag (DONE always puts the asset back in service). WORK_COMPLETED and DONE are refused unless parts_allocation_status is null or RECEIVED (purchase-order parts still allocated, partly received or failed block them). IN_PROGRESS stamps the start (first time), WORK_COMPLETED the completion date. Leaving PENDING for READY, IN_PROGRESS or PAUSED sends the assignee a push notification.\n\nDONE bills the work order - CUSTOMER lines plus their taxes and fees become an invoice (UNPAID when INVOICE.APPROVAL.WORK_ORDER_INVOICES is true, the default; DRAFT otherwise), or are added to the order's latest invoice when it is DRAFT or UNPAID with nothing paid; PARENT_ORDER lines are billed on the parent rental's invoice when the parent is OPEN_CONTRACT or CLOSED_CONTRACT, and switched to CUSTOMER otherwise (parent missing or deleted, QUOTE, RESERVATION or COMPLETED); ASSET lines are recorded as a cost of the asset. A MAINTENANCE work order also resets the asset's maintenance baseline. message says what happened. A CUSTOMER total of exactly 0 creates no invoice. A negative CUSTOMER total (credit lines) creates a credit invoice that is absorbed straight away: it is marked PAID, the credit pays down the order's own open invoices (newest first), and only the rest goes to the customer's balance.\n\nCard charge on DONE - when INVOICE.APPROVAL.WORK_ORDER_INVOICES is true, a customer on auto-payment with a default card and a Stripe id is charged the invoice's open balance (less card payments still in flight), plus FEES.CC_PROCESSING_FEE % (default 3) and sales tax on that fee at the order's tax rates. With the setting off nothing is charged. PARENT_ORDER lines charge the parent rental's customer the same way when INVOICE.APPROVAL.RENTAL_INVOICES is on (default true). A card Stripe refuses does not undo the DONE move - the failed attempt is saved and the invoice stays open (for the work order's customer, message says to collect payment manually).\n\nEmails on DONE - a new work-order invoice follows INVOICE.DELIVERY.WORK_ORDER_INVOICES (default ON_APPROVAL - sent only once approved); a new parent-rental invoice follows INVOICE.DELIVERY.RENTAL_INVOICES (default IMMEDIATE - a DRAFT is sent too). An existing invoice that grew is not sent again.\n\nOne work_order_update journal row records what changed (DOZ-7836) and the parent rental's \"can complete\" is refreshed on every move. Requires WORK_ORDER_STATUS_UPDATE.\n","tags":["Work Orders"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"The work order id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/work_order_convert_body"}}}},"responses":{"200":{"description":"Request Successful - message says what happened to billing on DONE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/work_order_convert_result"}}}},"400":{"description":"id is not a valid uuid; validation failed (including a missing body); already in that status; asset_out_of_service missing where required; open or failed part reservations; leaving DONE\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No live work order with this id (\"Task doesn't exist\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/work-orders/search":{"post":{"summary":"Search Work Orders And Inspections","description":"The service list - work orders, inspections and NADC checklists together, told apart by record_type (an org with work orders switched off gets the inspections and checklists only). Callers without ACCESS_ALL_WORK_ORDERS see only work orders that are READY, IN_PROGRESS or PAUSED and open to all assignees or assigned to them (with ALLOW_ASSIGNEE_OWNERSHIP); callers without ACCESS_ALL_INSPECTIONS see only inspections that are READY_FOR_INSPECTION or PARTIALLY_INSPECTED and open to all or assigned to them (with ALLOW_ASSIGNEE_OWNERSHIP), and only checklists that are READY or IN_PROGRESS and assigned to them. internal_notes is null without the matching ACCESS_ALL permission. Never listed - imported work orders, hidden inspections, and DONE rows unless status includes DONE; dispatch inspections due more than the caller's \"show tasks within\" days (default 3) are left out unless skip_days_limit. deleted true lists only deleted rows. paginate is optional - left out, every match is returned. search.format \"csv\" streams every match as a CSV download (work-orders-report-MM-DD-YYYY.csv). Requires WORK_ORDER_VIEW. On the AI-connector (MCP) allowlist since DOZ-8321, for the connector's search_work_orders and get_work_order tools.\n","tags":["Work Orders"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/work_order_search_body"}}}},"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"$ref":"#/components/schemas/work_order_search_result"}},"text/csv":{"schema":{"type":"string","description":"For search.format \"csv\". Columns - Order Number, Priority, Type, Recurring Work Order, Maintenance Trigger, Status, Parts Status (Awaiting Parts, All Parts Received or blank), Start Date, Off Site WO, Work Time (Minutes), Due By, Assignee, Asset Name, Asset Manufacturer, Asset Model, Customer Name, Customer Number, Site Name, Due Amount, Notes.\n"}}}},"400":{"description":"Validation failed (including a missing body or search); date_to before date_from; an order_by column outside orderByColumnNamesList","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"An AI-connector (MCP) session that lacks WORK_ORDER_VIEW (the route is on the connector allowlist)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/work-orders/{id}":{"get":{"summary":"Get A Work Order","description":"One work order with its task details, customer, site, contact, assignee, level, asset, parent chain, lines (with their purchase-order reservation), taxes and fees, invoices, time logged per person, the inspection it came from and the projected totals by who pays. purchase_order_costs (purchase order and vendor bill figures) is filled only for callers with PURCHASE_ORDERS_READ. Deleted work orders are not returned. Callers without ACCESS_ALL_WORK_ORDERS only get work orders that are READY, IN_PROGRESS or PAUSED and open to all assignees (or, with ALLOW_ASSIGNEE_OWNERSHIP, assigned to them), and internal_notes is null for them; a customer-portal session only gets its customer's. Read only. Requires WORK_ORDER_READ. On the AI-connector (MCP) allowlist since DOZ-8321, for the connector's get_work_order tool.\n","tags":["Work Orders"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"The work order id."}],"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"$ref":"#/components/schemas/work_order_detail"}}}},"400":{"description":"id is not a valid uuid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"An AI-connector (MCP) session that lacks WORK_ORDER_READ (the route is on the connector allowlist)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found, deleted or not visible to the caller (\"Task not found or you do not have permission to view it\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"summary":"Delete A Work Order","description":"Deletes (soft) a PENDING or READY work order with a reason - the work order and its lines are marked deleted, stock parts and received purchase-order parts go back to inventory (when the same part is on several lines, only the last line's quantity is returned; known issue), the purchase-order reservations fail, the asset is put back in service and the parent rental's \"can complete\" is refreshed. Invoices, charges and notes are not touched. A work_order_update DELETE journal row keeps the work order as it was (DOZ-7836). Requires WORK_ORDER_DELETE.\n","tags":["Work Orders"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"The work order id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/work_order_delete_body"}}}},"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/work_order_delete_result"}}}},"400":{"description":"id is not a valid uuid; reason missing; already deleted; the status is IN_PROGRESS, PAUSED, WORK_COMPLETED or DONE (\"Task status '...' can't be deleted\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No work order with this id (\"Work order not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/work-orders/receipt/{id}":{"get":{"summary":"Get A Work Order Document","description":"The customer's work order document. format=pdf returns the PDF as a download named task-<order_number>.pdf; any other value (default html) the HTML as task-<order_number>.html. Lines billed to the asset are left out of the totals. The same visibility rules as GET /lotus/work-orders/{id}. Read only. Requires WORK_ORDER_READ.\n","tags":["Work Orders"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"The work order id."},{"in":"query","name":"format","required":false,"schema":{"type":"string","default":"html"},"description":"pdf (any case) returns the PDF; any other value returns the HTML."}],"responses":{"200":{"description":"The document","content":{"text/html":{"schema":{"type":"string"}},"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"id is not a valid uuid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Not found or not visible to the caller","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/work-orders/mechanic/receipt/{id}":{"get":{"summary":"Get A Work Order Mechanic Sheet","description":"The mechanic's copy of the work order - the job, asset and lines without prices. format=pdf returns the PDF (task-<order_number>.pdf), anything else the HTML (task-<order_number>.html). The same visibility rules as GET /lotus/work-orders/{id}. Read only. Requires WORK_ORDER_READ.\n","tags":["Work Orders"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"The work order id."},{"in":"query","name":"format","required":false,"schema":{"type":"string","default":"html"},"description":"pdf (any case) returns the PDF; any other value returns the HTML."}],"responses":{"200":{"description":"The document","content":{"text/html":{"schema":{"type":"string"}},"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"id is not a valid uuid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Not found or not visible to the caller","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/work-orders/charges_summary":{"post":{"summary":"Preview Work Order Charges","description":"Prices a work order that is being built, without saving anything - each line's markup or discount and total, the subtotal split by who pays (customer, asset, parent rental), and the taxes, fees and discount on the CUSTOMER lines - taxes on the taxable CUSTOMER lines only, fees and the discount on the full CUSTOMER subtotal. Tax rates come from the ZIP lookup (site, then customer, then org address), with the org's work-order rates only as the fallback; fees use the org's work-order rates. Parts are priced from inventory (the body's price only when the part is not found); a line_item_id prices a saved line at its saved price. With parent_id only the parent rental's taxes apply, its customer is used, and its site unless site_id is sent. No credit check. Requires WORK_ORDER_CREATE or WORK_ORDER_UPDATE.\n","tags":["Work Orders"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/work_order_charges_summary_body"}}}},"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"$ref":"#/components/schemas/work_order_charges_summary_result"}}}},"400":{"description":"Validation failed (including a missing body)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"The parent rental is not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/work-orders/email":{"post":{"summary":"Email A Work Order","description":"Emails the work order document as HTML (no PDF), subject \"Task - <STATUS> Email\" with the status in capitals and underscores as spaces (for example \"Task - WORK COMPLETED Email\"). receiver_type customer sends it to the customer's contacts (only contact_id when given), self to the caller, custom to the custom_email addresses. A journal row records the recipients. Requires WORK_ORDER_READ.\n","tags":["Work Orders"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/work_order_email_body"}}}},"responses":{"200":{"description":"Sent. The body is a bare JSON string.","content":{"application/json":{"schema":{"type":"string","example":"Email sent successfully"}}}},"400":{"description":"Validation failed (including a missing body); receiver_type customer on a work order without a customer","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"receiver_type customer while customer emails are switched off for this deployment or org","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The work order, its customer, or contact_id among the customer's contacts is not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"receiver_type customer and the customer has no contacts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/work-orders/types":{"get":{"summary":"List Work Order Types","description":"The fixed list of work order types (task_type). Requires WORK_ORDER_VIEW.","tags":["Work Orders"],"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/work_order_type"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/work-orders/levels":{"get":{"summary":"List Work Order Levels","description":"The org's work order levels (job categories with an expected duration in days), by weight. Requires WORK_ORDER_LEVEL_VIEW.","tags":["Work Orders"],"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/work_order_level"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/work-orders/levels/update":{"post":{"summary":"Update A Work Order Level","description":"Changes the fields sent on one existing level (levels are not created here). Requires WORK_ORDER_LEVEL_UPDATE.","tags":["Work Orders"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/work_order_level_update_body"}}}},"responses":{"200":{"description":"The updated level","content":{"application/json":{"schema":{"$ref":"#/components/schemas/work_order_level"}}}},"400":{"description":"Validation failed (including a missing body, or only id with nothing to change); a weight or duration that is not a whole number","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No level with this id in the caller's org (\"Work order level not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Another level of the org already has this name","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/work-orders/notes":{"post":{"summary":"Add A Work Order Note","description":"Adds a note to a work order - also the time sheet - minutes_logged and log_type (WORK, or a commute leg on an off-site work order). Photos are attached after the note exists, with POST /v1/media/orders/note. status_update also moves the work order to that status exactly as POST /lotus/work-orders/convert/{id} does - DONE included, with its billing; PENDING and DONE need WORK_ORDER_STATUS_UPDATE. If that move is refused the note is still saved. Requires WORK_ORDER_NOTES_CREATE.\n","tags":["Work Orders"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/work_order_note_create_body"}}}},"responses":{"200":{"description":"The saved note","content":{"application/json":{"schema":{"$ref":"#/components/schemas/work_order_note"}}}},"400":{"description":"Validation failed (including a missing body, or a non-empty images list); a commute log on an on-site work order; the status change is refused (the convert 400s)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"status_update PENDING or DONE without WORK_ORDER_STATUS_UPDATE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No live work order with this order_id (\"Work order not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/work-orders/notes/search":{"post":{"summary":"Search A Work Order's Notes","description":"A work order's notes, newest first, with who wrote them, and the work order's total WORK minutes (all notes, whatever the page or filter). Requires WORK_ORDER_NOTES_VIEW.\n","tags":["Work Orders"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/work_order_note_search_body"}}}},"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"$ref":"#/components/schemas/work_order_note_search_result"}}}},"400":{"description":"Validation failed (including a missing body or paginate - \"Paginate object is required\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No live work order with this id (\"Work order not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/lotus/work-orders/notes/{id}":{"put":{"summary":"Update A Work Order Note","description":"Changes the fields sent on a note. images may only reorder or remove photos already on it (removed files stay in storage). The status change a note made is not affected. Someone else's note needs UPDATE_ALL_WORK_ORDER_NOTES. Requires WORK_ORDER_NOTES_UPDATE.\n","tags":["Work Orders"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"The note id (note_id)."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/work_order_note_update_body"}}}},"responses":{"200":{"description":"The updated note","content":{"application/json":{"schema":{"$ref":"#/components/schemas/work_order_note"}}}},"400":{"description":"id is not a valid uuid (\"Invalid Note ID\"); validation failed (including a missing body); a commute log on an on-site work order; an image not already on the note\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Someone else's note without UPDATE_ALL_WORK_ORDER_NOTES","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No live note with this id (\"Task note not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"summary":"Delete A Work Order Note","description":"Deletes (soft) a note; its photos stay in storage and a status change it made is not undone. Someone else's note needs DELETE_ALL_WORK_ORDER_NOTES. Requires WORK_ORDER_NOTES_DELETE.\n","tags":["Work Orders"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"The note id (note_id)."}],"responses":{"200":{"description":"Deleted. The body is a bare JSON string.","content":{"application/json":{"schema":{"type":"string","example":"Note deleted"}}}},"400":{"description":"id is not a valid uuid (\"Invalid Note ID\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Someone else's note without DELETE_ALL_WORK_ORDER_NOTES","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No live note with this id (\"Task note not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/assets/checklists":{"post":{"summary":"Update An Asset Checklist Record","description":"Saves the answers of one checklist attached to an asset and, with valid true, validates it. The item tree may be nested (items[].items). Checkbox items are answered in value, text and select items in value_text; the column that does not apply is cleared. Requires ASSET_UPDATE; a section with a sign_off_permission can additionally only be worked by a holder of that permission (403).\n","tags":["Assets"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/asset_checklist"}}}},"responses":{"200":{"description":"The checklist record as stored, items rebuilt into a tree","content":{"application/json":{"schema":{"$ref":"#/components/schemas/asset_checklist"}}}},"400":{"description":"Validation failed, or a select answer is not one of the item's options","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The caller lacks the section's sign_off_permission","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such checklist on the asset in the caller's org","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"valid true was sent while a required item is unanswered","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/assets/checklists/finalize":{"post":{"summary":"Finalize An Asset's Buy Or Sale Checklists","description":"Once every checklist of the given type on the asset is valid, moves the asset's checklist_status to onboarded (Buy) or sale_approved (Sale). For an asset on a Lotus sale order only a selling asset is promoted. Requires ASSET_UPDATE, and the caller must also hold CHECKLIST_BUY_FINAL_SIGN_OFF or CHECKLIST_SELL_FINAL_SIGN_OFF for the type (403).\n","tags":["Assets"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["asset_id","type"],"properties":{"asset_id":{"type":"string","format":"uuid"},"type":{"type":"string","enum":["Buy","Sale"]}}}}}},"responses":{"200":{"description":"Finalized","content":{"application/json":{"schema":{"type":"string","example":"Asset checklists approved/finalized"}}}},"400":{"description":"Validation failed, the asset is already onboarded (Buy), or a checklist is still unsolved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The caller lacks the final sign-off permission for the type","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The asset does not exist in the caller's org, or has no checklists of the type","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/assets/depreciation_history":{"post":{"summary":"Retrieve asset depreciation history","description":"Fetches the depreciation schedule entries for a specific asset depreciation schedule ID. Supports server-side pagination for JSON responses and direct file download for CSV exports. Dates are adjusted to the organization's timezone. Requires ASSET_READ, and refused with a 403 while depreciation is disabled for the org.","tags":["Assets"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["search"],"properties":{"paginate":{"type":"object","description":"Pagination details. Optional, but if provided, both currentPage and perPage are required.","required":["currentPage","perPage"],"properties":{"currentPage":{"type":"integer","example":1,"description":"The page number to retrieve."},"perPage":{"type":"integer","example":25,"description":"Number of items per page."}}},"search":{"type":"object","description":"Search parameters used to filter the history.","required":["asset_depreciation_schedule_id"],"properties":{"asset_depreciation_schedule_id":{"type":"string","format":"uuid","example":"a3bb189e-8bf9-3888-9912-ace4e6543002","description":"The UUID of the specific depreciation schedule to query."},"format":{"type":"string","enum":["json","csv"],"default":"json","description":"The desired output format. 'csv' triggers a file download stream."}}}}}}}},"responses":{"200":{"description":"Successful retrieval of history.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"sequence":{"type":"integer","description":"The order number of the depreciation entry."},"entry_date":{"type":"string","example":"12-31-2023","description":"The date of the entry formatted as MM-DD-YYYY (Organization Timezone)."},"entry_amount":{"type":"number","format":"float","description":"The amount depreciated in this entry."},"accumulated_depreciation":{"type":"number","format":"float","description":"Total depreciation up to this point."},"ending_book_value":{"type":"number","format":"float","description":"The remaining book value after this entry."}}}},"pagination":{"description":"Standard pagination. Without paginate every entry is returned and perPage, from, to, lastPage, prevPage and nextPage are null.","allOf":[{"$ref":"#/components/schemas/pagination"}]}}}},"text/csv":{"schema":{"type":"string","format":"binary","description":"Returned for search.format csv - a CSV file stream with headers \"Sequence, Entry Date, Entry Amount, Accumulated Depreciation, Ending Book Value\"."}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Depreciation is disabled for the org","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/assets":{"post":{"summary":"Create An Asset","description":"Requires ASSET_CREATE. Attaching depreciation_schedules also needs SCHEDULE_ASSET_ASSIGNMENT and depreciation enabled for the org. Unknown keys, including the readOnly columns of asset, are refused with 400.\n","tags":["Assets"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/asset_create"}}}},"responses":{"200":{"description":"The created asset, read back as GET /assets/{id} returns it","content":{"application/json":{"schema":{"$ref":"#/components/schemas/asset_detail"}}}},"400":{"description":"Creation failed, e.g. the asset number is already taken","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"depreciation_schedules sent without SCHEDULE_ASSET_ASSIGNMENT, or with depreciation disabled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/assets/sales_analysis/{id}":{"get":{"summary":"Get asset's sales analysis data","description":"Returns the raw assets row. Requires SALE_CREATE or SALE_UPDATE. The web app's sales analysis now calls POST /v1/lotus/sales/sales_analysis/{id} instead.","tags":["Assets"],"parameters":[{"in":"path","name":"id","required":true,"description":"Asset id","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/asset"}}}},"400":{"description":"Asset not found in the caller's org, or a malformed id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/assets/dispatched":{"post":{"summary":"Get All Dispatched Assets","description":"One row per active Lotus dispatch (lotus_dispatches), soonest return first by default. A customer-portal session only sees its own customer's orders. Requires ASSET_VIEW_DISPATCHED.","tags":["Assets"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["search","paginate"],"properties":{"search":{"type":"object","properties":{"status":{"type":"array","description":"Order statuses to include","items":{"type":"string","enum":["quote","reservation","open_contract","closed_contract","completed_contract"]}},"state":{"type":"array","description":"Dispatch states to include. quoted is accepted but matches nothing - a quote has no dispatch rows yet.","items":{"type":"string","enum":["quoted","dispatch_inspection","dispatched","off_rent","off_rent_inspection","released"]}},"due":{"type":"boolean","nullable":true,"description":"true for past-due returns only, false for not-yet-due only"},"order_number":{"type":"string"},"company_name":{"type":"string"},"asset_number":{"type":"string"},"asset_name":{"type":"string"},"order_by":{"type":"array","items":{"type":"object","properties":{"column":{"type":"string","description":"Unknown columns are ignored","enum":["dispatches.return_at","dispatches.dispatch_at","dispatches.dispatched_date","dispatches.off_rent_date","dispatches.released_date","orders.order_number","orders.status","customers.company_name"]},"order":{"type":"string","enum":["asc","desc","ASC","DESC"]},"nulls":{"type":"string","enum":["first","last","FIRST","LAST"]}}}}}},"paginate":{"$ref":"#/components/schemas/paginate"}}}}}},"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"dispatch_id":{"type":"string","format":"uuid"},"status":{"type":"string","description":"Lotus dispatch status (UPPERCASE)"},"asset_id":{"type":"string","format":"uuid"},"asset_name":{"type":"string"},"asset_number":{"type":"string"},"order_id":{"type":"string","format":"uuid"},"order_number":{"type":"string"},"order_status":{"type":"string"},"order_contact_id":{"type":"string","format":"uuid","nullable":true},"order_total":{"type":"number","nullable":true,"description":"Projected rental total, computed per order"},"customer_details":{"type":"object","nullable":true},"is_quote":{"type":"boolean"},"is_backdated":{"type":"boolean"},"can_complete_order":{"type":"boolean","nullable":true},"dispatch_at":{"type":"string","format":"date-time","description":"Line item's expected dispatch date"},"return_at":{"type":"string","format":"date-time","description":"Line item's expected return date"},"dispatched_date":{"type":"string","format":"date-time","nullable":true},"off_rent_date":{"type":"string","format":"date-time","nullable":true},"released_date":{"type":"string","format":"date-time","nullable":true},"sold_date":{"type":"string","nullable":true,"description":"Always null (legacy field)"},"data":{"type":"object","nullable":true,"description":"Always null (legacy field)"},"dispatch_inspection_id":{"type":"string","format":"uuid","nullable":true},"off_rent_inspection_id":{"type":"string","format":"uuid","nullable":true},"dispatch_inspection_status":{"type":"string","nullable":true},"off_rent_inspection_status":{"type":"string","nullable":true},"dispatch_inspection_passed":{"type":"boolean","nullable":true},"off_rent_inspection_passed":{"type":"boolean","nullable":true},"dispatch_inspection_quick_pass":{"type":"boolean","nullable":true},"off_rent_inspection_quick_pass":{"type":"boolean","nullable":true},"dispatch_inspection_assigned":{"type":"boolean"},"off_rent_inspection_assigned":{"type":"boolean"},"status_history":{"type":"array","items":{"type":"object","properties":{"status":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"metadata":{"type":"object"}}}},"invoice_details":{"type":"array","description":"The order's invoices, oldest first. Empty when none.","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"invoice_number":{"type":"string"},"approved":{"type":"boolean"},"total_amount":{"type":"number"},"amount_paid":{"type":"number"},"balance_due":{"type":"number"},"payment_status":{"type":"string","description":"The invoice's status"},"created_at":{"type":"string","format":"date-time"}}}}}}},"pagination":{"$ref":"#/components/schemas/pagination"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/assets/next_asset_number":{"get":{"summary":"Get next available asset number","description":"Suggests the next number for every stored numbering pattern matching the input. Requires ASSET_CREATE.","tags":["Assets"],"parameters":[{"in":"query","name":"pattern","schema":{"type":"string"},"description":"Text to match stored patterns against (case-insensitive substring, e.g. 'CAT-'). Empty matches every pattern."}],"responses":{"200":{"description":"Next available number","content":{"application/json":{"schema":{"type":"object","properties":{"asset_number":{"type":"string","description":"The first suggestion, or an empty string when no pattern matched"},"exact_match":{"type":"boolean","description":"True when an asset in the org already has exactly the input as its number"},"suggestions":{"type":"array","items":{"type":"object","properties":{"pattern":{"type":"string","nullable":true,"example":"CAT-####"},"suggestion":{"type":"string","example":"CAT-0042"},"type":{"type":"string","enum":["pattern","legacy"]}}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/assets/search":{"post":{"summary":"Search Assets","description":"Requires ASSET_VIEW.","tags":["Assets"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/asset_search_body"}}}},"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"$ref":"#/components/schemas/asset_search_result"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/search/assets":{"post":{"summary":"Search Assets (legacy alias)","description":"Alias of POST /assets/search - same handler, body and response. The web app no longer calls it. Requires ASSET_VIEW.","deprecated":true,"tags":["Assets"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/asset_search_body"}}}},"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"$ref":"#/components/schemas/asset_search_result"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/assets/toggle/out_of_service":{"post":{"summary":"Mark An Asset Out Of Service Or Back In Service","description":"Sets assets.out_of_service and writes one asset UPDATE journal row (none when the status is unchanged; DOZ-7836). With task_id, the work order's asset_out_of_service flag is synced too. With out_of_service and create_task both true, a SERVICE work order is created afterwards - if that creation fails it is only logged, and the toggle still answers 200. Requires ASSET_UPDATE.\n","tags":["Assets"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["asset_id","out_of_service"],"properties":{"asset_id":{"type":"string","format":"uuid"},"out_of_service":{"type":"boolean"},"task_id":{"type":"string","format":"uuid","description":"A Lotus work order on this asset to keep in sync"},"create_task":{"type":"boolean"},"task_notes":{"type":"string","nullable":true,"description":"Required when create_task is true; becomes the new work order's internal notes"}}}}}},"responses":{"200":{"description":"Toggled","content":{"application/json":{"schema":{"type":"string","example":"Asset marked as out of service successfully."}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"The asset is not in the caller's org, or task_id is not a work order on it","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/assets/suggestions":{"post":{"summary":"Fetch Asset Filters Suggestions","description":"Distinct values of one assets column matching a substring, for filter autocompletes. search_for and extra_fields accept only the columns listed below; any other name is a 400. Requires ASSET_SUGGESTION_READ.\n","tags":["Assets"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["search","paginate"],"properties":{"search":{"type":"object","required":["search_string","search_for"],"properties":{"search_string":{"type":"string","description":"Case-insensitive substring; empty matches everything"},"search_for":{"type":"string","enum":["asset_number","name","manufacturer"],"description":"The assets column to suggest values of","example":"manufacturer"},"extra_fields":{"description":"Extra assets columns to return with each suggestion - one name, a comma-separated list, or an array. Each must be asset_number, name, manufacturer or checklist_status.\n","oneOf":[{"type":"string","example":"checklist_status"},{"type":"array","items":{"type":"string","enum":["asset_number","name","manufacturer","checklist_status"]}}]},"checklist_status":{"type":"array","items":{"type":"string"}},"categories":{"type":"array","items":{"type":"string","format":"uuid"}},"task":{"type":"object","nullable":true,"description":"Only suggest from assets on a work order or inspection in one of these statuses","required":["status"],"properties":{"status":{"type":"array","items":{"type":"string","enum":["PENDING","READY","IN_PROGRESS","PAUSED","READY_FOR_INSPECTION","PARTIALLY_INSPECTED","WORK_COMPLETED","DONE"]}}}}}},"paginate":{"$ref":"#/components/schemas/paginate"}}}}}},"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","description":"The search_for column, any extra_fields, and id"}},"pagination":{"$ref":"#/components/schemas/pagination"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/assets/liability":{"post":{"summary":"Fetch Asset Liability Records","description":"The org's assets_liability (loan) rows. Requires ASSET_VIEW.","tags":["Assets"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["paginate"],"properties":{"paginate":{"$ref":"#/components/schemas/paginate"}}}}}},"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"asset":{"type":"string","format":"uuid"},"asset_number":{"type":"string"},"year_make_model":{"type":"string"},"account_number":{"type":"string"},"loan_date":{"type":"string","format":"date-time"},"loan_amount":{"type":"number"},"org":{"type":"string","format":"uuid"},"interest_rate":{"type":"number"},"current_principal_balance":{"type":"number"},"accrued_interest":{"type":"number"},"current_payoff":{"type":"number"},"per_diem":{"type":"number"},"paid_off_date":{"type":"string","format":"date-time","nullable":true}}}},"pagination":{"$ref":"#/components/schemas/pagination"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/assets/details_template":{"post":{"summary":"Save The Asset Details (Details Tab) Of An Asset","description":"Upserts the asset's one asset_details_form_templates row (keyed on asset), stamped with the caller's org. Each template.items[].item_type must be an item type name; an unknown item_type, an old integer included (DOZ-7740), is refused. In the same transaction, a PENDING New Asset Detail Checklist parked on the asset re-derives its Asset Details answers from the saved blob (DOZ-7717). Requires ASSET_UPDATE.\n","tags":["Assets"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/asset_details_template"}}}},"responses":{"200":{"description":"Saved","content":{"application/json":{"schema":{"type":"string","example":"updated successfully"}}}},"400":{"description":"Validation failed (unknown keys included), or an item has an unknown item_type","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"The asset does not exist in the caller's org","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/assets/my_pending_assets":{"post":{"summary":"Get All Assets With Pending Checklists That User Can Solve","description":"Live assets that are onboarding, selling or sale_approved and carry at least one unvalidated checklist section the caller may sign off - by the section's sign_off_permission, or by its canonical type/name pair where that column is unset. Requires ASSET_VIEW.\n","tags":["Assets"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["paginate"],"properties":{"paginate":{"$ref":"#/components/schemas/paginate"}}}}}},"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/asset"}],"properties":{"category_details":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"category_id":{"type":"string","format":"uuid"},"super_category_id":{"type":"string","format":"uuid"},"add_on":{"type":"boolean"},"child_attachments":{"type":"array","items":{"type":"string","format":"uuid"}},"parent_categories":{"type":"array","items":{"type":"string","format":"uuid"}}}},"sales_order_details":{"type":"object","nullable":true,"description":"The newest Lotus sale order the asset is on","properties":{"order_number":{"type":"string"},"status":{"type":"string"},"customer_name":{"type":"string"},"customer_number":{"type":"string"}}},"checklists":{"type":"array","description":"The asset's matching sections","items":{"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string"},"valid":{"type":"boolean"}}}}}}},"pagination":{"$ref":"#/components/schemas/pagination"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/assets/checklists/{id}":{"get":{"summary":"Get An Asset's Checklists","description":"Every checklist attached to the asset in the caller's org, each with its items rebuilt into a tree; an asset of another org reads as having none. Requires ASSET_READ.","tags":["Assets"],"parameters":[{"in":"path","name":"id","required":true,"description":"Asset id","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"type","description":"Only checklists of this type","schema":{"type":"string","example":"Sale"}}],"responses":{"200":{"description":"Request Successful. An empty array when the asset has none.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/asset_checklist"}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/assets/restore/{id}":{"post":{"summary":"Restore a deleted asset","description":"Un-deletes the asset, writes one asset RESTORE journal row (deleted true to false, DOZ-7836) and restores its category use count. Depreciation schedules and entries deleted with it are not restored. Requires ASSET_RESTORE.","tags":["Assets"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"string","example":"Asset restored successfully"}}}},"400":{"description":"Malformed id, or no deleted asset with this id in the caller's org","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/assets/{id}":{"post":{"summary":"Update An Asset","description":"Partial update, except that category must always be sent (see asset_update). Requires ASSET_UPDATE. Sending asset_number at all needs ASSET_NUMBER_UPDATE and attaching depreciation_schedules needs SCHEDULE_ASSET_ASSIGNMENT with depreciation enabled for the org - both answer 403. The gates inside the update answer 400: purchase details or the lock (ASSET_PURCHASE_DETAILS_UPDATE), a changed in-service date (ASSET_IN_SERVICE_DATE_UPDATE), sale-only to rental (ASSET_SALE_ONLY_TO_RENTAL), hour_meter on a dispatched asset, a lower latest_maintenance_hour_meter, and any field the org's field settings make required. Writes one asset UPDATE journal row of the columns that changed, none when nothing did (DOZ-7836).\n","tags":["Assets"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/asset_update"}}}},"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"description":"The asset read back after the update, as GET /assets/{id} returns it","allOf":[{"$ref":"#/components/schemas/asset_detail"}]},"message":{"type":"string","example":"Asset Updated"}}}}}},"400":{"description":"Validation failed, category missing, a permission-gated change refused (purchase details, in-service date, sale-only to rental), or the update failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"asset_number sent without ASSET_NUMBER_UPDATE, depreciation_schedules sent without SCHEDULE_ASSET_ASSIGNMENT, or depreciation is disabled for the org","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}},"get":{"summary":"Get An Asset By Id","description":"Requires ASSET_READ. A deleted asset is returned only to holders of ASSET_RESTORE.","tags":["Assets"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"$ref":"#/components/schemas/asset_detail"}}}},"400":{"description":"No asset with this id in the caller's org, or a malformed id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"summary":"Delete An Asset By Id","description":"Soft-deletes the asset and writes one asset DELETE journal row (deleted false to true, the asset as snapshot.before; DOZ-7836). Rejected while the asset is still attached to any open rental, sale, work order, inspection or New Asset Detail Checklist - the same rule the can_delete/deletion_blockers fields advertise on the search and get-by-id payloads. Requires ASSET_DELETE.","tags":["Assets"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"type":"string","example":"deleted success"}}}},"400":{"description":"Asset is still attached to an open artifact, or does not exist","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ErrorResponse"}],"properties":{"details":{"type":"object","nullable":true,"description":"Present only for the attached-artifact rejection. Carries the same deletion_blockers object and delete_blocked_reasons array the read payloads return, so the FE can link straight to whatever is holding the asset.","properties":{"deletion_blockers":{"type":"object","description":"Same shape as extra_asset_information.deletion_blockers - keyed by rental / sale / work_order / inspection / nadc, each an array of the open artifacts holding the asset."},"delete_blocked_reasons":{"type":"array","items":{"type":"string"}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/assets/unavailability_dates/{id}":{"get":{"summary":"Get Unavailability Date Ranges For An Asset","description":"The expected dispatch/return window of every live Lotus dispatch holding the asset (RELEASED and REPLACED excluded). Requires ASSET_READ.","tags":["Assets"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"dispatch_at":{"type":"string","format":"date-time"},"return_at":{"type":"string","format":"date-time"},"status":{"type":"string","description":"Lotus dispatch status (UPPERCASE)"}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/assets/{id}/details_template":{"get":{"summary":"Get The Asset Details (Details Tab) Of An Asset","description":"The asset's saved Asset Details row, scoped to the caller's org. Requires ASSET_READ.","tags":["Assets"],"parameters":[{"in":"path","name":"id","required":true,"description":"Asset id","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The saved row. When the asset has none (or is not in the caller's org) the body is EMPTY - no JSON, not null.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/asset_details_template"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/otp":{"post":{"security":[],"summary":"Send a login OTP to a user's phone","description":"Anonymous only: a request carrying a valid, live login JWT is refused with 403. Any other Authorization header (expired, logged out, malformed, an OAuth token) is ignored and the request goes on.\nFinds the user by `phone` (exact match; not deleted, not a service credential, in an org that is not deleted) and sends a 6-digit code by SMS through Twilio Verify, then records a 3-minute resend window in users_otp. Nothing is sent, and the same 200 comes back, when the user has a saved override pin or the org has allow_fake_login, so the caller cannot tell the cases apart. Keys other than `phone` are ignored.\n","tags":["Auth"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["phone"],"properties":{"phone":{"type":"string","pattern":"^\\+[1-9]\\d{1,14}$","description":"The user's phone in E.164 form.","example":"+14155552671"}}}}}},"responses":{"200":{"description":"OTP sent (or not needed, see above)","content":{"application/json":{"schema":{"type":"string","example":"An OTP for CloudERM app login has been sent to your phone number"}}}},"400":{"description":"No body, `phone` missing or not in E.164 form (\"Incorrect phone number. Please send phone number in E.164 format i.e +14155552671\"); no user with that phone (\"No user found with this phone number\"); or a code was sent less than 3 minutes ago (\"Previous OTP has not yet expired. Please wait another 42 seconds\"). None carries a `code`.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The request carries a valid login JWT (\"Bad Request! You are already signed in\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/otp/login":{"post":{"security":[],"summary":"Log in with the OTP sent to a phone","description":"Anonymous only, like POST /otp (a valid login JWT gets 403).\nThe code is checked in this order: the user's saved override pin when one is set (`otp` is compared as a trimmed string, and the pin outranks allow_fake_login, so 000000 stops working for that user); otherwise, when the org has allow_fake_login, `otp` must be exactly the string \"000000\" (a number 0 fails); otherwise Twilio Verify checks it. On success the users_otp row is deleted and a session is opened exactly as POST /login does (one jwts row). Keys other than `otp` and `phone` are ignored.\n","tags":["Auth"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["otp","phone"],"properties":{"otp":{"oneOf":[{"type":"string"},{"type":"number"}],"description":"The SMS code or override pin. Must be a number or a numeric string; it is compared as sent (send it as a string - fake login matches only the string \"000000\", and a number loses leading zeros).\n","example":"123456"},"phone":{"type":"string","pattern":"^\\+[1-9]\\d{1,14}$","description":"The phone the code was sent to, in E.164 form.","example":"+14155552671"}}}}}},"responses":{"200":{"description":"Signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/auth_session"}}}},"400":{"description":"No body, `phone` missing or not E.164 (\"Incorrect phone number. Please send phone number in E.164 format i.e +14155552671\", no `code`); `otp` missing or not a number ('\"value\" is required', '\"value\" must be a number'; code VALIDATION); no code was requested and the user has neither a pin nor fake login (\"No OTP was requested against this user, Please request an OTP\"); a wrong code (\"Invalid OTP\"); the session could not be opened (\"This organization is no longer active\", \"Unable to start a session, please try again\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The request carries a valid login JWT (\"Bad Request! You are already signed in\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No user with that phone, or the user's organization is deleted (\"No user found against this phone number\"). The 400 \"This organization is no longer active\" is only reached when the organization is deleted between that lookup and the session.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Twilio threw on the check, which it does for an expired, already used or over-attempted verification (masked in production). An expired code is therefore a 500 today, not \"Invalid OTP\".\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/login":{"post":{"security":[],"summary":"Log in with username and password","description":"`username` is matched exactly (case-sensitive) against users that are not deleted and are not service credentials. The password is checked against the stored hash; the password policy is NOT applied at login, so passwords set before it still work. Opens a session: one jwts row holding the returned token. Unknown keys are refused ('\"x\" is not allowed').\n","tags":["Auth"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["username","password"],"properties":{"username":{"type":"string","format":"email","example":"jane.doe@example.com"},"password":{"type":"string","example":"Password123!"}}}}}},"responses":{"200":{"description":"Signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/auth_session"}}}},"400":{"description":"None carries a `code`. Joi: no body or a missing field ('\"username\" is required', '\"password\" is required'), '\"username\" must be a valid email', an unknown key. Then \"User jane.doe@example.com not found\", \"Incorrect password\", or the session could not be opened (\"This organization is no longer active\" - 400 here, 403 on the password routes; \"Unable to start a session, please try again\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/logout":{"post":{"summary":"Log out","description":"Ends the session of the token in the Authorization header: its jwts row is marked invalid, so the token answers 401 \"JWT does not exist\" from then on. Takes no body (one sent is ignored). Other tokens of the same user stay live. A service API credential is refused (it has no session).\n","tags":["Auth"],"responses":{"200":{"description":"Logged out","content":{"application/json":{"schema":{"type":"string","example":"Logout Successful"}}}},"400":{"description":"The token could not be invalidated, or the lookup after invalidating it failed (a user row that no longer exists - a hard delete; a soft-deleted user still logs out with 200 - and the token is already dead by then) - both \"Unable to logout\"; or both signing headers and a Bearer token were sent (\"Conflicting authentication credentials\"). No `code`.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/AuthSessionUnauthorized"},"403":{"description":"An AI-connector (MCP) token (\"Not available to AI connectors\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/login/refresh":{"post":{"summary":"Swap the current login token for a new one","description":"Invalidates the token in the Authorization header and returns a new session built exactly as POST /login builds it (fresh roles, permissions, preferences and org details). The old token is invalidated BEFORE the new one is minted, so a refresh that fails after that point leaves the caller with no live token. Takes no body. A service API credential is refused.\n","tags":["Auth"],"responses":{"200":{"description":"The new session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/auth_session"}}}},"400":{"description":"No `code`. The session could not be rebuilt (\"This organization is no longer active\", \"Unable to start a session, please try again\"; a user deleted since the token was issued answers \"Cannot read properties of undefined (reading 'id')\"); or both signing headers and a Bearer token were sent (\"Conflicting authentication credentials\"). A token already used for a refresh is refused by the 401 below; two refreshes sent at once with the same token both succeed, each with its own new session.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/AuthSessionUnauthorized"},"403":{"description":"An AI-connector (MCP) token (\"Not available to AI connectors\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/password/forgot":{"post":{"security":[],"summary":"Email a password reset link","description":"Looks the user up by `email` = username (exact, case-sensitive; not deleted) and emails a link to `<CLIENT_URL>/password/new/<user_id>/<token>`. The token is a JWT signed with the user's current password hash and valid for 30 minutes; nothing is stored, and setting any new password voids every outstanding link. The answer says whether the account exists (404). With ENABLE_EMAILS off no mail is sent and the answer is still 200. Writes an EMAIL_SENT journal row (component email_delivery_auth). Unknown keys are refused ('\"x\" is not allowed').\n","tags":["Auth"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email","description":"The account's username.","example":"jane.doe@example.com"}}}}}},"responses":{"200":{"description":"Reset email sent","content":{"application/json":{"schema":{"type":"string","example":"Password reset email is sent to your email address"}}}},"400":{"description":"Joi, no `code`: no body or no email ('\"email\" is required'), '\"email\" must be a valid email', an unknown key\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The user's org is deleted (\"This organization is no longer active\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such user (\"User jane.doe@example.com not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/password/reset":{"post":{"security":[],"summary":"Set a new password from a reset link","description":"`user_id` and `token` come from the emailed link (POST /password/forgot) or the new-account setup link. The org check runs first, then the token is verified against the user's current password hash. On success, in one transaction, the password is replaced and every AI-assistant (OAuth) connection of the user is revoked (reason PASSWORD_RESET). Login JWTs already issued stay live. Unknown keys are refused ('\"x\" is not allowed').\n","tags":["Auth"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["user_id","token","password"],"properties":{"user_id":{"type":"string","format":"uuid","example":"3f2c1a9e-8b7d-4c6e-9f0a-1b2c3d4e5f60"},"token":{"type":"string","description":"The token from the link.","example":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJJRCI6IjNmMmMxYTllIn0.c2lnbmF0dXJl"},"password":{"type":"string","minLength":8,"maxLength":128,"pattern":"^(?=.*[A-Z])(?=.*\\d)(?=.*[^a-zA-Z\\d\\s]).{8,}$","description":"At least 8 characters with an uppercase letter, a digit and a special character; at most 128.\n","example":"NewPassword1!"}}}}}},"responses":{"200":{"description":"Password updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/auth_reset_token_verdict"},"example":{"value":true,"message":"Password updated"}}}},"400":{"description":"No `code`. Joi: no body or a missing field ('\"user_id\" is required', '\"token\" is required', '\"password\" is required'), '\"user_id\" must be a valid GUID', an unknown key, a password over 128 characters, or one that fails the policy (\"Password must be at least 8 characters long, and contain at least one uppercase letter, one number, and one special character\"). The verdict \"Invalid Token\" needs a token signed with this user's current password hash that names another user - the API never mints one; another user's link fails its signature check (the 500 below).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The user's org is deleted (\"This organization is no longer active\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Today's behaviour, not masked in production: a link that is expired (\"jwt expired\"), already used or for a different user (\"invalid signature\") or mangled (\"jwt malformed\"), or a user_id that matches no live user (\"Cannot read properties of undefined (reading 'password')\"), answers 500 with that message and code INTERNAL.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/password/reset-token/validate":{"post":{"security":[],"summary":"Check a reset link before showing the new-password form","description":"Runs the same checks as POST /password/reset without changing anything. Except for a Joi failure, the answer is NOT the error envelope: it is `{ value, message }` at the verdict's status, and the reset page reads `value`. Unknown keys are refused ('\"x\" is not allowed').\n","tags":["Auth"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["user_id","token"],"properties":{"user_id":{"type":"string","format":"uuid","example":"3f2c1a9e-8b7d-4c6e-9f0a-1b2c3d4e5f60"},"token":{"type":"string","example":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJJRCI6IjNmMmMxYTllIn0.c2lnbmF0dXJl"}}}}}},"responses":{"200":{"description":"The link is valid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/auth_reset_token_verdict"},"example":{"value":true,"message":"valid token"}}}},"400":{"description":"Either a Joi failure in the error envelope with no `code` (no body, '\"user_id\" is required', '\"token\" is required', '\"user_id\" must be a valid GUID', an unknown key), or the verdict `{ value: false, message: \"Invalid Token\" }` for a token signed with this user's current password hash that names another user - the API never mints one; another user's link fails its signature check (the 500 below).\n","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/auth_reset_token_verdict"},{"$ref":"#/components/schemas/ErrorResponse"}]},"example":{"success":false,"status":"fail","statusCode":400,"error":"\"token\" is required"}}}},"403":{"description":"The user's org is deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/auth_reset_token_verdict"},"example":{"value":false,"message":"This organization is no longer active"}}}},"500":{"description":"Today's behaviour: an expired, used or mangled link, or a user_id that matches no live user, answers 500 with `value: false` and the raw error as `message` (\"jwt expired\", \"invalid signature\", \"jwt malformed\", \"Cannot read properties of undefined (reading 'password')\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/auth_reset_token_verdict"},"example":{"value":false,"message":"jwt expired"}}}}}}},"/password/change":{"post":{"summary":"Change the signed-in user's password","description":"Any signed-in user (no permission check), except a service API credential (401). Checks `old_password` against the stored hash, then, in one transaction, writes the new password and revokes every AI-assistant (OAuth) connection of the user (reason PASSWORD_CHANGED). Login JWTs, the caller's own included, stay live. The new password may equal the old one. Unknown keys are refused ('\"x\" is not allowed').\n","tags":["Auth"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["old_password","new_password"],"properties":{"old_password":{"type":"string","description":"The current password (no policy check).","example":"Password123!"},"new_password":{"type":"string","minLength":8,"maxLength":128,"pattern":"^(?=.*[A-Z])(?=.*\\d)(?=.*[^a-zA-Z\\d\\s]).{8,}$","description":"At least 8 characters with an uppercase letter, a digit and a special character; at most 128.\n","example":"NewPassword1!"}}}}}},"responses":{"200":{"description":"Password updated","content":{"application/json":{"schema":{"type":"string","example":"Password updated"}}}},"400":{"description":"Joi, code VALIDATION: no body or a missing field ('\"old_password\" is required', '\"new_password\" is required'), an unknown key, a new password over 128 characters or failing the policy (\"Password must be at least 8 characters long, and contain at least one uppercase letter, one number, and one special character\"). Without a `code`: a wrong old password (\"Incorrect Password\"), or both signing headers and a Bearer token (\"Conflicting authentication credentials\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/AuthSessionUnauthorized"},"403":{"description":"An AI-connector (MCP) token (\"Not available to AI connectors\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The signed-in user has been deleted (\"User doesn't exist\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/categories":{"get":{"summary":"Get the category tree","description":"The org's categories as a tree: each super category carries `children` (its categories), and each category carries `children` (its sub-categories). Sub-categories carry no `children` key. Rows are sorted by `weight`, then `name`, at every level. The tree is built from the filtered rows, so with `hidden=false` a visible category under a hidden super category is left out with it. Same handler as GET /categories/search. Requires PLATFORM_CATEGORY_VIEW. On the AI-connector (MCP) allowlist since DOZ-8318, for the connector's list_categories tool (the /categories/search alias is not).\n","tags":["Category"],"parameters":[{"$ref":"#/components/parameters/category_hidden_filter"}],"responses":{"200":{"description":"The super categories, each with its subtree","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/category_tree_node"}}}}},"400":{"description":"The category read failed; the message is the database error's own text.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"An AI-connector (MCP) session that lacks PLATFORM_CATEGORY_VIEW (the route is on the connector allowlist)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"summary":"Update a category","description":"Same handler and permissions as POST /categories/update (the web calls that one); see it for the full behaviour. Despite the method on the collection path this never creates a category: create is POST /categories/create.\n","tags":["Category"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/category_update"}}}},"responses":{"200":{"$ref":"#/components/responses/CategoryUpdated"},"400":{"$ref":"#/components/responses/CategoryWriteBadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/CategoryDepreciationDisabled"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/categories/search":{"get":{"summary":"Get the category tree (the web's call)","description":"Identical to GET /categories: same handler, same `hidden` filter, same tree. The web reads the category tree from this path. Requires PLATFORM_CATEGORY_VIEW.\n","tags":["Category"],"parameters":[{"$ref":"#/components/parameters/category_hidden_filter"}],"responses":{"200":{"description":"The super categories, each with its subtree","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/category_tree_node"}}}}},"400":{"description":"The category read failed; the message is the database error's own text.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/category_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/categories/create":{"post":{"summary":"Create a category","description":"Creates a super category, category or sub-category; the level comes from the parent ids sent (see `category_fields`). In one transaction it inserts the row, attaches the form templates, depreciation schedules and attachment links, checks the parents, and writes the `inherited` cache from them. A child's `add_on` is overwritten with its super category's.\n\nSide effects: when the new category is not hidden, its hidden parents are un-hidden; a journal CREATE row; and, when it sets a GL number, COGS number or rental rates, a CATEGORY_CONFIG_UPDATED financial audit row.\n\nRequires PLATFORM_CATEGORY_CREATE. While the org has depreciation switched on, sending `depreciation_schedules` (even `[]`) also needs SCHEDULE_CATEGORY_ASSIGNMENT, or the request is a 400 and nothing is created. While it is off, a `[]` is dropped before that check and a non-empty list is the 403.\n\nKnown issue: a `category_id` sent without `super_category_id` matches no level, so the row is stored with no parent check and the answer is \"New undefined created\".\n","tags":["Category"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/category_create"}}}},"responses":{"200":{"description":"The level of the new category","content":{"application/json":{"schema":{"type":"string","enum":["New SUPER_CAT created","New CAT created","New SUB_CAT created"],"example":"New SUB_CAT created"}}}},"400":{"description":"Validation failed (Joi; a missing body is \"name\" is required), or one of: \"Invalid form templates attached\" (a template id not in the org); \"Form template (<description>) is the New Asset Detail Checklist template and cannot be attached to a category\"; \"Send a weight on every form template or on none\"; \"You're Not Allowed To Attach Depreciation Schedules To Categories\"; the attachment-link messages listed on `category_write_fields`; \"New sub-category is missing a parent\" (a parent id not in the org); \"Invalid parents lineage for this sub-category\" (the category is not under the super category sent); \"New category is missing a parent\"; \"Invalid data type provided\" (a fraction in an integer column).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/CategoryDepreciationDisabled"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/categories/update":{"post":{"summary":"Update a category","description":"Updates one category's own settings (the web's call; POST /categories runs the same handler). Its parents can't be changed here: `category_id`, `super_category_id` and `org` are accepted and dropped. Move a category with POST /categories/batch.\n\nLink lists are replaced when sent and left alone when omitted: `form_templates` (templates no longer listed are detached; with no weights sent, templates already attached keep their order and new ones go on top), `depreciation_schedules`, and `child_attachments` / `parent_categories`.\n\nSide effects, in one transaction: when the category is not hidden, its hidden parents are un-hidden. For a sub-category, its assets' `inherited_rental_rates` (its own rates, else the inherited ones) and depreciation schedules are rewritten. For a category or super category, every child's `inherited` cache is recalculated, and so are the rental rates and schedules of the assets under each child sub-category. A journal UPDATE row when anything changed, and a CATEGORY_CONFIG_UPDATED financial audit row when the GL number, COGS number or rental rates changed.\n\nRequires PLATFORM_CATEGORY_UPDATE. While the org has depreciation switched on, sending `depreciation_schedules` (even `[]`) also needs SCHEDULE_CATEGORY_ASSIGNMENT, or the request is a 400 and nothing is saved. While it is off, a `[]` is dropped before that check and a non-empty list is the 403.\n\nKnown issue: saving a sub-category that has no rental rates of its own and whose `inherited` cache is null, or has no `rental_rates` key (such as the column default `{}`), is a masked 500 (it reads `inherited.rental_rates`).\n","tags":["Category"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/category_update"}}}},"responses":{"200":{"$ref":"#/components/responses/CategoryUpdated"},"400":{"$ref":"#/components/responses/CategoryWriteBadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/CategoryDepreciationDisabled"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/categories/batch":{"post":{"summary":"Reorder, re-parent and renumber categories in bulk","description":"Applies the category table's drag-and-drop edits: each item sets a category's `weight` and `gl_number` and can move it under a new parent. An omitted `weight` or `gl_number` keeps the stored value; `null` clears it. An empty array is accepted: it changes no category, but the org-wide recompute below still runs.\n\nMoving (`parent_id` set and different from the current parent): a sub-category can move under another category, and a category under another super category, taking its sub-categories along. The new parent must be of the level above and have the same `add_on`. A super category can't move, so its `parent_id` is only checked for existence. `parent_id: null` moves nothing.\n\nSide effects, in one transaction: the `inherited` cache is recalculated for every moved category, for the sub-categories a moved category takes along, and for the new parent category of a moved sub-category (never for a super category); for each of those that is a sub-category, its assets' `inherited_rental_rates` and depreciation schedules are rewritten. A `weight` or `gl_number` change alone recalculates nothing. Then `use_count`, `assignment_count` and `hidden` are recomputed for every category of the org: a category with no active, in-stock asset under it is hidden and every other one is shown, overwriting any `hidden` set by hand. A journal UPDATE row per changed category, sharing one batch id, and a CATEGORY_CONFIG_UPDATED financial audit row where the GL number changed.\n\nRequires PLATFORM_CATEGORY_UPDATE.\n\nKnown issues: an item without an `id` fails the whole batch with a 400 whose message is knex's own, SQL included (\"Undefined binding(s) detected when compiling WHERE ...\"). A new `gl_number` on a category or super category doesn't reach its children's `inherited.gl_number` (an update through POST /categories/update does), because only a move recalculates.\n","tags":["Category"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/category_batch_item"}},"example":[{"id":"c254a782-6a6f-46f4-bd91-99a6c9b39dbc","parent_id":"b42bcbf9-54fc-49a5-9977-7ecfac71e4a6","weight":0,"gl_number":"4100"}]}}},"responses":{"200":{"description":"Done","content":{"application/json":{"schema":{"type":"string","enum":["Categories batch update done"]}}}},"400":{"description":"Validation failed (Joi; a missing body is \"value\" must be an array), or one of: \"Category with id <id> not found\"; \"Parent category with id <id> not found\"; \"Category '<parent>' has a different add_on status and can not be a parent to the sub-category '<name>'\" (or \"to the category\"); \"Category '<parent>' is not a valid parent for the sub-category '<name>'\" (or \"for the category\"); \"Invalid data type provided\" (a fractional weight). An item without an `id` is a 400 carrying knex's own message and the SQL (a known issue).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/category_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/categories/merge":{"post":{"summary":"Merge two categories into a new one","description":"Creates a new category from the body, moves everything under the two categories in `categories_to_be_merged` onto it, and deletes them. Both must be of one level and one `add_on`, and the body's `add_on` must match; they need not share a parent.\n\nThe new category's parent comes from the merged ones: for sub-categories, `category_id` must be the parent of one of the two (its super category is taken from that one); for categories, `super_category_id` must be the parent of one of the two; super categories take neither.\n\nIn one transaction: the two categories' templates, schedules and attachment links are removed, and the body's are attached to the new one; their assets and child categories are moved onto it. The `inherited` cache of the new category and of every moved child category is recalculated, and so are the depreciation schedules of the assets under the new category (when it is a sub-category) and under each moved child sub-category. The moved assets' rental rates: see the known issues. `use_count`, `assignment_count` and `hidden` are recomputed for every category of the org (a category with no active, in-stock asset under it is hidden and every other one is shown, overwriting any `hidden` set by hand). After that recompute, the new category's hidden parents are un-hidden unless the body sent `hidden` true - even when the recompute has just hidden the new category itself. Journal: a MERGE row for the new category, an UPDATE row per moved child category, a DELETE row per merged category and an asset UPDATE row per moved asset, in one batch; plus the CATEGORY_CONFIG_UPDATED financial audit rows.\n\nRequires PLATFORM_CATEGORY_UPDATE.\n\nKnown issues:\n  - The assets moved onto a merged sub-category get the body's `rental_rates`\n    as their `inherited_rental_rates` only when the body sends them. Without\n    them, the new category's `inherited` is read before it is written, so the\n    assets keep the rates of the category they came from. Assets under a moved\n    child sub-category (a category or super category merge) keep their rates\n    either way.\n  - Merging two categories under different super categories: the\n    sub-categories of the one not under the chosen `super_category_id` get the\n    new `category_id` but keep their old `super_category_id`.\n  - Unlike create and update, merge attaches `depreciation_schedules` without\n    checking SCHEDULE_CATEGORY_ASSIGNMENT.\n","tags":["Category"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/category_merge"}}}},"responses":{"200":{"description":"Merged","content":{"application/json":{"schema":{"type":"string","enum":["Categories merged successfully"]}}}},"400":{"description":"Validation failed (Joi; a missing body is \"name\" is required), or one of: \"Some categories to be merged do not exist\" (either id is unknown in the org; there is no 404); \"Categories to be merged must be from the same level\"; \"Categories to be merged must have the same add-on status\"; \"The new category must have the same add-on status as the categories being merged.\"; \"Provided category_id is not a valid parent for the sub-categories being merged\"; \"Provided super_category_id is not a valid parent for the categories being merged\"; \"Super categories can not have parents\"; \"Invalid form templates attached\"; the New Asset Detail Checklist template message; \"Send a weight on every form template or on none\"; the attachment-link messages listed on `category_write_fields`; \"Invalid data type provided\".\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/CategoryDepreciationDisabled"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/categories/{id}":{"get":{"summary":"Get one category","description":"One category of the caller's org, without `children`. Requires PLATFORM_CATEGORY_READ.\n","tags":["Category"],"parameters":[{"$ref":"#/components/parameters/category_id_path"}],"responses":{"200":{"description":"The category","content":{"application/json":{"schema":{"$ref":"#/components/schemas/category_detail"}}}},"400":{"description":"\"value\" must be a valid GUID (a malformed id), or \"Category does not exist\" (no such category in the org; there is no 404).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/category_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"summary":"Delete a category","description":"Hard-deletes a category with its template, depreciation schedule and attachment links. A super category or category can only be deleted with no children. A sub-category with assets (soft-deleted ones count) needs `replacement_category`: its assets move there and take that sub-category's rental rates (see the known issues) and depreciation schedules.\n\nSide effects, in one transaction: `use_count`, `assignment_count` and `hidden` are recomputed for every category of the org (a category with no active, in-stock asset under it is hidden and every other one is shown, overwriting any `hidden` set by hand); a journal DELETE row, an asset UPDATE row per moved asset, and a CATEGORY_CONFIG_UPDATED financial audit row when the category had a GL number, COGS number or rental rates.\n\nRequires PLATFORM_CATEGORY_DELETE.\n\nKnown issues:\n  - A `replacement_category` equal to the category being deleted is accepted:\n    its assets are \"moved\" onto itself, then the category is deleted, leaving\n    them on a category id that no longer exists.\n  - A replacement that has no rental rates of its own and whose `inherited`\n    cache is null is a masked 500; when the cache has no `rental_rates` key\n    (such as the column default `{}`), the assets move but keep their old\n    `inherited_rental_rates`.\n","tags":["Category"],"parameters":[{"$ref":"#/components/parameters/category_id_path"},{"in":"query","name":"replacement_category","required":false,"description":"The sub-category to move the deleted sub-category's assets to. Checked (it must exist in the org and be a sub-category) whenever it is sent, but only used when a sub-category is deleted. Not checked against the `id` being deleted (see the known issues).\n","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The level of the deleted category","content":{"application/json":{"schema":{"type":"string","enum":["SUPER_CAT deleted","CAT deleted","SUB_CAT deleted"],"example":"SUB_CAT deleted"}}}},"400":{"description":"\"value\" must be a valid GUID (a malformed id or replacement), or one of: \"Category not found\"; \"Replacement category not found\"; \"Replacement category must be a sub-category\"; \"Super category with children can not be deleted\"; \"Category with children can not be deleted\"; \"To delete a sub-category with children assets please provide a replacement category\".\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/category_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/categories/{id}/template":{"get":{"summary":"Get a category's effective form template","description":"The questions an inspection or asset-details form of this category asks: the union of the org's default template of that type and every template attached to the category or inherited from its parents, for `scope` and GENERIC. A question on several templates appears once, with the nearest template's `weight`, `required` and `block`. Inspection questions are ordered by block, then required before optional, then template (org default, super category, category, sub-category; on one level the top of the list last), then weight. Requires PLATFORM_CATEGORY_READ.\n\nKnown issue: only 15 templates are read, the first 15 by template description (the org default included), so a category whose walk holds more loses the questions of the rest, whatever their level.\n","tags":["Category"],"parameters":[{"$ref":"#/components/parameters/category_id_path"},{"in":"query","name":"type","required":true,"schema":{"type":"string","enum":["inspection","asset_details"]}},{"in":"query","name":"scope","required":false,"description":"Template scope, GENERIC when omitted. The retired PURCHASE scope is refused with a 400.\n","schema":{"type":"string","enum":["GENERIC","RENTAL","SALE","INDEPENDENT"],"default":"GENERIC"}}],"responses":{"200":{"description":"The merged questions, in form order","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/category_template_item"}}}}},"400":{"description":"Validation failed (\"id\" must be a valid GUID, \"type\" is required, \"type\" must be one of [inspection, asset_details], or a `scope` outside the list), or \"Category doesn't exist\".\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/category_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/categories/{id}/template_disagreements":{"get":{"summary":"The questions a category's own templates disagree on (DOZ-7676)","description":"A question carried by two or more of the category's own templates of one type, where two that can apply together (same scope, or either GENERIC) put it in a different block (inspection templates) or disagree on whether it is required. A different position alone is not a disagreement. Each question's templates are listed in the category's order, top first; the first one that applies wins. Requires PLATFORM_CATEGORY_READ.\n","tags":["Category"],"parameters":[{"$ref":"#/components/parameters/category_id_path"},{"in":"query","name":"type","required":false,"description":"Both types when omitted.","schema":{"type":"string","enum":["inspection","asset_details"]}}],"responses":{"200":{"description":"Success; an empty array when the templates agree","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/category_template_disagreement"}}}}},"400":{"description":"\"id\" must be a valid GUID, \"value\" must be one of [inspection, asset_details] (a bad `type`), or \"Category does not exist\".\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/category_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/platform/checklists":{"get":{"summary":"List the org's checklists with their items","description":"Every live (not deleted) checklist of the caller's org. Each checklist carries its live top-level items (parent_id null), and each top-level item carries its live child items of the same org in `items` - two levels only: a child has no `items` key, and an item nested deeper than that is not returned. No order is applied to checklists, items or children. An org with no checklists answers []. Read only. Requires PLATFORM_CHECKLISTS_READ.\n","tags":["Checklists"],"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/checklist_detail"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"An AI-connector (MCP) session, the route is not on the connector allowlist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/platform/checklists/data_references":{"get":{"summary":"List the asset fields a checklist item may reference","description":"The catalogue of values for a checklist item's data_reference - the asset field the item's answer is meant to fill in. A fixed list (37 entries, always in the same order) with two org-dependent removals - depreciation_schedules is left out when the org setting ASSET.ENABLE_DEPRECIATION is false, and capitalization when ASSET.ENABLE_CAPITALIZATION is false. The item create and update routes accept every key of the full list whatever those settings say, so items already pointing at a hidden key stay saveable. The API only publishes the list; writing the referenced field on sign-off is done by the client through POST /assets/{id}. Read only. Requires PLATFORM_CHECKLISTS_READ.\n","tags":["Checklists"],"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/checklist_data_reference"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"An AI-connector (MCP) session, the route is not on the connector allowlist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/platform/checklists/items/update":{"post":{"summary":"Update checklist items","description":"Partial update of one or more checklist items. The body is an array; each element names an item by id and carries only the fields to change (name, setting, input_type, options, data_reference). Every id must be an item of the caller's org (404 \"Checklist item not found\" otherwise, checked before anything is saved), and the elements run in one transaction - if any element fails, nothing is saved. When an element sends an input_type other than select, the item's options are cleared (stored as null) whatever options says. When it does not send input_type, options is written as sent and must still fit the stored type (a non-empty list for a select item, null for the others); a combination the database refuses fails the whole batch with a 400 carrying the database message. An item cannot be moved, re-parented, deleted or restored here. [] is accepted and answers []. Side effects on assets - an asset's checklist reads the item definition live, so the change shows at once on every asset that already carries the checklist, including checklists already signed off (they are not re-validated). After an input_type change, the next save of an asset's checklist keeps only the answer column that fits the new type (value for checkbox, value_text for text and select). One `checklist` journal row is written per checklist touched (the rows of one request share a batch_id), with an `items[<item id>]` entry per item that changed; none when nothing changed (DOZ-7264). Requires PLATFORM_CHECKLISTS_UPDATE.\n","tags":["Checklists"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/checklist_item_update"}}}}},"responses":{"200":{"description":"One entry per element sent, in the same order - the item's full stored row after the update.\n","content":{"application/json":{"schema":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/checklist_item"}],"nullable":true}}}}},"400":{"description":"Validation failed (code VALIDATION) - including a missing body or a non-array body ('\"value\" must be an array'), an unknown key ('\"[0].deleted\" is not allowed'), a select item without options ('\"[0].options\" is required') or an unknown data_reference. Also a 400 without a code when the batch itself fails - an element without id (\"Column 'id' is required for batch updates\"), an element with nothing but its id (\"Nothing to update for 'id' = '<id>'\"), or any database error, whose message is passed through as is (e.g. a violation of the checklist_items_options_check constraint).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"An AI-connector (MCP) session, the route is not on the connector allowlist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"An id is not an item of the caller's org (\"Checklist item not found\"); nothing is saved\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/platform/checklists/items/create":{"post":{"summary":"Create a checklist item","description":"Adds one item to a checklist. org is always the caller's org (it cannot be sent). Send parent_id to add a child of a top-level item; GET /platform/checklists shows only two levels, so a parent should itself be a top-level item. checklist_id must be a live checklist of the caller's org (404 \"Checklist not found\"), and parent_id a live item of the org (404 \"Parent checklist item not found\") in that same checklist (400 \"The parent item belongs to a different checklist\"). input_type defaults to text and setting to required; options is stored only for a select item and is null for every other type, whatever was sent. Side effects on assets - the new item is not added to assets that already carry the checklist; it is attached to assets that receive the checklist afterwards (a Buy checklist when an asset is created, a Sale checklist when it is provisioned for a sale). Writes one `checklist` journal row for the item's checklist, with the new item's `items[<item id>]` entry (DOZ-7264). Requires PLATFORM_CHECKLISTS_ADD_ITEMS.\n","tags":["Checklists"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/checklist_item_create"}}}},"responses":{"200":{"description":"The inserted row","content":{"application/json":{"schema":{"$ref":"#/components/schemas/checklist_item"}}}},"400":{"description":"Validation failed (code VALIDATION) - including a missing body ('\"name\" is required'), a missing checklist_id, a select item without options ('\"options\" is required'), an unknown key ('\"org\" is not allowed') or an unknown data_reference; or a parent_id from a different checklist (\"The parent item belongs to a different checklist\", no code).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"An AI-connector (MCP) session, the route is not on the connector allowlist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"checklist_id is not a live checklist of the caller's org (\"Checklist not found\"), or parent_id not a live item of it (\"Parent checklist item not found\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The id sent is already used by another item (\"Duplicate field value entered for ...\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/platform/checklists/items/{id}":{"delete":{"summary":"Delete a checklist item","description":"Soft-deletes the item (deleted set to true) when it belongs to the caller's org. Always answers 200 with the same message - also when no item of the caller's org has this id, or when it is already deleted. Its child items are not deleted: they no longer appear in GET /platform/checklists (their parent is not listed), but they are still attached to assets that receive the checklist afterwards. Side effects on assets - the item is not attached to assets that receive the checklist afterwards; assets that already carry the checklist keep it, and their checklist reads still return it (with deleted true), so an unanswered required item still has to be answered before that asset's checklist can be validated. Deleting a live item writes one `checklist` journal row for its checklist, with the item's `items[<item id>]` entry going to null (DOZ-7264); nothing is written when no live item matched. Requires PLATFORM_CHECKLISTS_DELETE_ITEMS.\n","tags":["Checklists"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"The checklist item id."}],"responses":{"200":{"description":"Request Successful. The body is a bare JSON string.","content":{"application/json":{"schema":{"type":"string","example":"Checklist item deleted"}}}},"400":{"description":"id is not a valid uuid ('\"value\" must be a valid GUID', code VALIDATION)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"An AI-connector (MCP) session, the route is not on the connector allowlist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/customers/payment_method_client_secret/{id}":{"get":{"summary":"Get a Stripe client secret to set up a card for a customer","description":"Requires CUSTOMER_CC_UPDATE. Checks run in this order: a customer-portal session may only ask for its own customer (401), the customer must be one of the org's (deleted ones included), then the org must have a Stripe account. Then it creates a Stripe SetupIntent for the customer's card form and returns its client secret. Not read only: when the customer has no Stripe customer yet, one is created in the org's Stripe account and its id is saved on customers.stripe_id straight away, outside any transaction and with no journal row, so it stays saved even when the SetupIntent call after it fails. A Stripe error is a 500 where error details are hidden, as in production; elsewhere it keeps Stripe's own status (see 500).\n","tags":["Customer"],"parameters":[{"in":"path","required":true,"name":"id","description":"The customer id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The SetupIntent's client secret","content":{"application/json":{"schema":{"type":"object","required":["client_secret"],"properties":{"client_secret":{"type":"string"}}}}}},"400":{"description":"id is not a uuid (\"Invalid data type provided\"), or the org has no Stripe account (\"Stripe not configured for this Org\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token, the caller lacks CUSTOMER_CC_UPDATE, or a customer-portal session asked for another customer (\"Operation not authorized for an customer user\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"$ref":"#/components/responses/customer_ai_connector_forbidden"},"404":{"description":"No such customer in the caller's org (\"Customer not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/customer_stripe_error"}}}},"/customers/with-custom-roles":{"get":{"summary":"List the customers that have their own roles","description":"Requires CUSTOMER_VIEW. One row per distinct roles.customer_id among the org's live roles, ordered by customer then company name. An org-wide role has no customer, so it adds one row with customer_id and company_name both null. The company name is joined without skipping deleted customers, so a deleted customer with live roles is listed too. Not available to a customer-portal session. Read only.\n","tags":["Customer"],"responses":{"200":{"description":"One row per customer with roles (and one all-null row for org-wide roles)","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","required":["customer_id","company_name"],"properties":{"customer_id":{"type":"string","format":"uuid","nullable":true},"company_name":{"type":"string","nullable":true,"description":"Null on the org-wide row."}}}}}}},"401":{"description":"Missing or invalid token, the caller lacks CUSTOMER_VIEW, or a customer-portal session (\"You CAN'T Do That\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"$ref":"#/components/responses/customer_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/customers/set_default_payment_method":{"post":{"summary":"Set a customer's default card","description":"Requires CUSTOMER_CC_UPDATE. Checks run in this order: the body; a customer-portal session may only name its own customer (401); the customer must be one of the org's (deleted ones included) and have a Stripe customer; the org must have a Stripe account; then Stripe is asked for the payment method, which must belong to that Stripe customer. Then, in one transaction, sets customers.default_payment_method and writes one customer UPDATE journal row (default_payment_method), after a BASELINE row first, once, for a customer with legacy snapshots, plus the PAYMENT_METHOD_UPDATED financial audit row. Naming the card that already is the default changes nothing and writes no UPDATE row, but the audit row (from and to that same card) is still written. A Stripe error, e.g. an unknown payment method id, is a 500 where error details are hidden, as in production; elsewhere it keeps Stripe's own status (see 500).\n","tags":["Customer"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/customer_payment_method_body"}}}},"responses":{"200":{"description":"Default card set","content":{"application/json":{"schema":{"type":"string","example":"default payment method set"}}}},"400":{"description":"No body, or customer_id / payment_method missing or malformed (Joi message, code VALIDATION); the customer has no Stripe customer (\"No payment methods are attached to this customer\"); the org has no Stripe account (\"Stripe not configured for this Org\"); or the card belongs to another Stripe customer (\"This payment method is not attached to this customer\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token, the caller lacks CUSTOMER_CC_UPDATE, or a customer-portal session named another customer (\"Not Authorized\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"$ref":"#/components/responses/customer_ai_connector_forbidden"},"404":{"description":"No such customer in the caller's org (\"Customer not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/customer_stripe_error"}}}},"/customers/payment_method/update":{"post":{"summary":"Update a card's Stripe metadata (its label)","description":"Requires CUSTOMER_CC_UPDATE. The org's Stripe account is checked FIRST, so on an org without one every request, a bad body or no body included, is answered 400 \"Stripe not configured for this Org\". Then the body is validated and the card must be a live one of the org (and, for a customer-portal session, of its own customer). The metadata sent is merged into the card's Stripe metadata by Stripe's own rule: keys sent are set, a key sent with an empty value is removed, keys left out are kept; without metadata nothing changes in Stripe. The stored card details are then rewritten from Stripe's answer in one transaction, with one customer_payment_method journal row of the lean card fields that changed (normally the label; none when nothing changed), after the customer's BASELINE row first, once, for a customer with legacy snapshots. A Stripe error is a 500 where error details are hidden, as in production; elsewhere it keeps Stripe's own status (see 500).\n","tags":["Customer"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"Unknown keys are refused (400 \"x\" is not allowed).","required":["payment_method_id"],"properties":{"payment_method_id":{"type":"string","description":"The Stripe payment method id (customer_payment_methods.payment_method_id). '' is refused.","example":"pm_1Example"},"metadata":{"type":"object","description":"Stripe metadata to merge into the card's (see above). The web sends the card's label here. null is refused (\"metadata\" must be of type object).\n","example":{"label":"Office card"}}}}}}},"responses":{"200":{"description":"The card as stored after the update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/customer_payment_method"}}}},"400":{"description":"The org has no Stripe account (\"Stripe not configured for this Org\", checked before the body), or the body failed validation (Joi message, code VALIDATION, e.g. no body is \"payment_method_id\" is required, an unknown key \"x\" is not allowed)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token, or the caller lacks CUSTOMER_CC_UPDATE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"$ref":"#/components/responses/customer_ai_connector_forbidden"},"404":{"description":"No live card with that id in the caller's org, or not the portal session's own customer's (\"Payment method not found\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/customer_stripe_error"}}}},"/customers/detach_payment_method":{"post":{"summary":"Remove a card from a customer","description":"Requires CUSTOMER_CC_UPDATE. Checks run in this order: the body; a customer-portal session may only name its own customer (401); the customer must be one of the org's (deleted ones included), have a Stripe customer and live cards, and the card must be one of them; the org must have a Stripe account. Then, in one transaction, soft-deletes the card with one customer_payment_method DELETE journal row and, when it was the default, clears default_payment_method with one customer UPDATE row, after a BASELINE row first, once, for a customer with legacy snapshots. The Stripe detach runs last in the same transaction, so a Stripe error changes nothing (a 500 where error details are hidden, as in production; elsewhere Stripe's own status, see 500). After the commit the customer is revalidated (validated / validation_failures rewritten). That revalidation skips deleted customers, so on a deleted customer that still has live cards the answer is 404 \"Customer not found\" although the card is already removed, from the DB and from Stripe.\n","tags":["Customer"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/customer_payment_method_body"}}}},"responses":{"200":{"description":"Card removed","content":{"application/json":{"schema":{"type":"string","example":"payment method detached"}}}},"400":{"description":"No body, or customer_id / payment_method missing or malformed (Joi message, code VALIDATION); the customer has no Stripe customer or no live card (\"No payment methods are attached to this customer\"); the card is not one of the customer's (\"This payment method is not attached to this customer\"); or the org has no Stripe account (\"Stripe not configured for this Org\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token, the caller lacks CUSTOMER_CC_UPDATE, or a customer-portal session named another customer (\"Not Authorized\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"$ref":"#/components/responses/customer_ai_connector_forbidden"},"404":{"description":"No such customer in the caller's org (\"Customer not found\"); or, after the card was removed, the customer is a deleted one (same message, see above)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/customer_stripe_error"}}}},"/customers/filter/company_name":{"get":{"summary":"List the distinct company names","description":"Requires CUSTOMER_VIEW. Every distinct company name of the org's live customers, in name order, unpaginated. A customer-portal session gets only its own customer's. Read only.\n","tags":["Customer"],"responses":{"200":{"description":"The distinct values","content":{"application/json":{"schema":{"type":"object","required":["count","results"],"properties":{"count":{"type":"integer","description":"Number of rows in results."},"results":{"type":"array","items":{"type":"object","required":["company_name","pagination_full_count"],"properties":{"company_name":{"type":"string"},"pagination_full_count":{"type":"integer","description":"The same total on every row (equals count)."}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/customer_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/customers/filter/customer_number":{"get":{"summary":"List the distinct customer numbers","description":"Requires CUSTOMER_VIEW. Every customer number of the org's live customers, in ascending order, unpaginated. A customer-portal session gets only its own customer's. Read only.\n","tags":["Customer"],"responses":{"200":{"description":"The distinct values","content":{"application/json":{"schema":{"type":"object","required":["count","results"],"properties":{"count":{"type":"integer","description":"Number of rows in results."},"results":{"type":"array","items":{"type":"object","required":["customer_number","pagination_full_count"],"properties":{"customer_number":{"type":"integer"},"pagination_full_count":{"type":"integer","description":"The same total on every row (equals count)."}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/customer_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/customers/filter/key":{"get":{"summary":"List the distinct customer keys","description":"Requires CUSTOMER_VIEW. Every distinct key of the org's live customers, in key order (null last), unpaginated. A customer-portal session gets only its own customer's. Read only.\n","tags":["Customer"],"responses":{"200":{"description":"The distinct values","content":{"application/json":{"schema":{"type":"object","required":["count","results"],"properties":{"count":{"type":"integer","description":"Number of rows in results."},"results":{"type":"array","items":{"type":"object","required":["key","pagination_full_count"],"properties":{"key":{"type":"string","nullable":true},"pagination_full_count":{"type":"integer","description":"The same total on every row (equals count)."}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/customer_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/customers/suggestions":{"post":{"summary":"Suggest customers for a filter autocomplete","description":"Requires CUSTOMER_SUGGESTION_READ. Live customers of the org whose search_for column contains search_string (case-insensitive ILIKE; % and _ in search_string are not escaped, so they act as wildcards), ordered by id. search_for and extra_fields accept only the columns listed below; any other name is a 400. paginate is checked before search. With task, only customers on a live work order or a live inspection (not HIDDEN) whose status is in task.status; an empty task.status list matches every status. When no work order or inspection matches at all, the task filter is dropped and every customer is suggested (today's behaviour, a known issue). A matching work order without a customer counts as a match but adds no customer, so when only such work orders match, nothing is suggested. task null or omitted means no task filter. Not scoped to a customer-portal session's customer: the code has no portal check, so a portal session holding CUSTOMER_SUGGESTION_READ would get the org's customers. Read only.\n","tags":["Customer"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"Only search and paginate are read; other top-level keys are ignored.","required":["search","paginate"],"properties":{"search":{"type":"object","description":"Unknown keys are refused (400 \"x\" is not allowed).","required":["search_string","search_for"],"properties":{"search_string":{"type":"string","description":"Case-insensitive substring; empty matches everything. % and _ act as wildcards.\n","example":""},"search_for":{"type":"string","enum":["company_name","customer_number"],"description":"The customers column to match against","example":"company_name"},"extra_fields":{"description":"Extra customers columns to return with each suggestion - one name, a comma-separated list, or an array. Each must be company_name, customer_number or id.\n","oneOf":[{"type":"string","example":"id"},{"type":"array","items":{"type":"string","enum":["company_name","customer_number","id"]}}]},"checklist_status":{"type":"array","description":"Accepted and ignored (the body schema is shared with the asset suggestions, which read it).\n","items":{"type":"string"}},"categories":{"type":"array","description":"Accepted and ignored (shared with the asset suggestions), but each item must still be a uuid (\"categories[0]\" must be a valid GUID).\n","items":{"type":"string","format":"uuid"}},"task":{"type":"object","nullable":true,"description":"Only suggest customers on a live work order or a live inspection whose status is in status. If nothing matches, the filter is dropped and every customer is suggested (see the operation). null or omitted: no task filter.\n","required":["status"],"properties":{"status":{"type":"array","description":"An empty list matches every status.","items":{"type":"string","enum":["PENDING","READY","IN_PROGRESS","PAUSED","READY_FOR_INSPECTION","PARTIALLY_INSPECTED","WORK_COMPLETED","DONE"]}}}}}},"paginate":{"$ref":"#/components/schemas/paginate"}}}}}},"responses":{"200":{"description":"One page of suggestions","content":{"application/json":{"schema":{"type":"object","required":["data","pagination"],"properties":{"data":{"type":"array","items":{"type":"object","description":"id, the search_for column and any extra_fields"}},"pagination":{"$ref":"#/components/schemas/customer_list_pagination"}}}}}},"400":{"description":"paginate missing or invalid (\"Paginate object is required\" when it or one of its keys is missing, Joi's message otherwise, e.g. \"perPage\" must be less than or equal to 100); no search object (\"search object is required\"); or search failed validation (Joi message, code VALIDATION, e.g. \"search_for\" must be one of [company_name, customer_number], \"x\" is not allowed, \"categories[0]\" must be a valid GUID)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/customer_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/customers/search":{"post":{"summary":"Search customers","description":"Requires CUSTOMER_VIEW. CUSTOMER_RESTRICTED_VIEW and CUSTOMER_RESTORE are forwarded (they never block). Without CUSTOMER_RESTRICTED_VIEW each row carries 12 customer columns instead of 21 (see customer_search_row). Without CUSTOMER_RESTORE, search.deleted is forced to false, so deleted customers are never returned. Rows are sorted by order_by, then by company name (case-insensitive), then id. A customer-portal session gets only its own customer. Only search and paginate are read; other top-level keys are ignored. Read only. Reachable by an AI-connector (MCP) session; for one, a missing permission is a 403, not a 401.\n","tags":["Customer"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["search","paginate"],"properties":{"search":{"type":"object","description":"Every filter is optional; {} lists every live customer. Unknown keys are refused (400 \"x\" is not allowed).\n","properties":{"validation_failure":{"type":"array","description":"Validation problems to match. [\"None\"] returns only validated customers, [\"All\"] only invalid ones; otherwise a customer matches when any of the named validation_failures keys (e.g. address, credit_card) is invalid. [] or omitted is no filter.\n","items":{"type":"string"}},"company_name":{"type":"string","nullable":true,"description":"Case-insensitive substring; null or '' is no filter. % and _ act as wildcards.\n"},"customer_number":{"type":"number","nullable":true,"minimum":0,"exclusiveMinimum":true,"description":"Exact match. The schema takes any number above 0, a numeric string such as \"7\" too, but the query then needs a whole number: a non-integer such as 1.5 is a 400 \"Invalid data type provided\". null or '' is no filter; 0 or less is a 400 (\"customer_number\" must be greater than 0).\n"},"banned":{"type":"boolean","description":"Omitted is no filter; null is refused (400)."},"key":{"type":"string","description":"Case-insensitive substring of the customer key; % and _ act as wildcards. Unlike the other text filters, '' and null are refused (400 \"key\" is not allowed to be empty / \"key\" must be a string).\n"},"sales_person":{"type":"string","format":"uuid","nullable":true,"description":"Exact match; null or '' is no filter."},"deleted":{"type":"boolean","nullable":true,"description":"true lists deleted customers only. Omitted or null lists live ones. Forced to false without CUSTOMER_RESTORE.\n"},"facility":{"type":"string","format":"uuid","nullable":true,"description":"Exact match; null or '' is no filter."},"customer_type":{"type":"string","nullable":true,"enum":["PAY_ON_OPEN","ON_ACCOUNT"],"description":"Omit, null or empty string returns both types"},"order_by":{"description":"sales_person sorts by the sales person's name, not by the id in the row; a customer with no sales person, or whose sales person is a deleted user, has no name and sorts where nulls says (last by default).\n","allOf":[{"$ref":"#/components/schemas/order_by"}]}},"example":{}},"paginate":{"$ref":"#/components/schemas/paginate"}}},"example":{"search":{},"paginate":{"perPage":10,"currentPage":1}}}}},"responses":{"200":{"description":"One page of customers","content":{"application/json":{"schema":{"type":"object","required":["data","pagination","orderByColumnNamesList"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/customer_search_row"}},"pagination":{"$ref":"#/components/schemas/customer_list_pagination"},"orderByColumnNamesList":{"allOf":[{"$ref":"#/components/schemas/orderByColumnNamesList"}],"example":["customer_number","company_name","sales_person","validated"]}}}}}},"400":{"description":"No search object (\"Search object is required\"); a filter failed validation (Joi message, code VALIDATION, e.g. \"x\" is not allowed, \"customer_number\" must be greater than 0, \"key\" is not allowed to be empty); paginate missing or invalid (\"Paginate object is required\" when it or one of its keys is missing, Joi's message otherwise); an order_by column not in orderByColumnNamesList (\"Invalid column name in order_by: <column>\"); or a non-integer customer_number (\"Invalid data type provided\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token, or the caller lacks CUSTOMER_VIEW","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"An AI-connector (MCP) session that lacks CUSTOMER_VIEW","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/customers":{"post":{"summary":"Create a customer","description":"Requires CUSTOMER_CREATE. CUSTOMER_RESTRICTED_UPDATE and CUSTOMER_UPDATE_FEES are forwarded (they never block): they decide which fields the body may carry (see customer_create). CUSTOMER_BLACKLIST_UPDATE is forwarded too but changes nothing here: the create refuses blacklisted and blacklist_reason for every caller. Checks run in this order: the org's customer field settings marked as a requirement for the customer type (pay_on_open true, false, or not sent), then the fee fields, then the body schema, then sales_person. A requirement is met only by a truthy value in the body, so false, 0 and '' count as missing, and only address and billing_address are body fields: a requirement on primary_contact, site, file_rental_app, credit_card, credit_application or insurance_information refuses every create (\"Field '<key>' is required to create a customer\" without the key, \"<key>\" is not allowed with it). Fee, tax, terms and discount fields left out take the org's defaults; customer_number is the org's highest, deleted customers included, plus one. In one transaction it inserts the customer, its contacts and sites, with one customer CREATE journal row (every non-null column of the new row from null, except the server-set id, org, created_at, updated_at, deleted, validated, validation_failures, media_count, balance and stripe_id, and key, which the body may set but is never journaled; and the customer's image) and one customer_contact / customer_site CREATE row per contact and site. Then the customer is validated against the org's field settings and validated / validation_failures are written. The code has no customer-portal check (CUSTOMER_CREATE is an org permission).\n","tags":["Customer"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/customer_create"}}}},"responses":{"200":{"description":"The new customer, read just before validated / validation_failures were written, so those two show their state before this request's validation (a new customer reads validated false, validation_failures null). Read GET /customers/{id} for the stored values.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/customer_detail"}}}},"400":{"description":"A field the org requires is missing or falsy (\"Field '<key>' is required to create a customer\"); a fee field sent without CUSTOMER_UPDATE_FEES (\"Not permitted to update fees & taxes\"); the body failed validation (Joi message, code VALIDATION, e.g. \"company_name\" is required, \"x\" is not allowed for an unknown key or a server-set column, \"credit_limit\" is not allowed without CUSTOMER_RESTRICTED_UPDATE, \"blacklisted\" is not allowed); sales_person is not a user of the org holding at least one permission (\"Sales person does not exist\"; a deleted user is still found) or lacks ALLOW_ORDER_OWNERSHIP (\"Customer can only be assigned to sales users\"); a contact key that is not a whole number, e.g. 1.5 (\"Invalid data type provided\": the schema takes any number, the column is an integer)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token, or the caller lacks CUSTOMER_CREATE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"$ref":"#/components/responses/customer_ai_connector_forbidden"},"409":{"description":"A site id in the body is already in use (\"Duplicate field value entered for: id. Please use another value.\"), or another create in the org took the same customer_number at the same time (\"Duplicate field value entered for: customer_number, org. Please use another value.\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Unexpected server error, including a date field sent as a number or as a digits-only string of other than 6 or 8 digits, which passes the schema but is refused by the column (see customer_fields)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/customers/notes":{"post":{"summary":"Add a note to a customer","description":"Requires CUSTOMER_NOTES_CREATE. Inserts the note as the caller's, with one customer_note CREATE journal row (staff-only, never shown in the portal), in one transaction. The customer must be one of the caller's org (deleted ones included). A customer-portal session may only add notes to its own customer.\n","tags":["Customer"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/customer_note_create"}}}},"responses":{"200":{"description":"Note created","content":{"application/json":{"schema":{"type":"string","example":"Note Created"}}}},"400":{"description":"No body or the body failed validation (Joi message, e.g. \"customer_id\" is required), or a customer-portal session named another customer (\"Bad Request. Can't create notes for other customers\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token, or the caller lacks CUSTOMER_NOTES_CREATE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"$ref":"#/components/responses/customer_ai_connector_forbidden"},"404":{"description":"No such customer in the caller's org (\"Customer not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/customers/notes/search":{"post":{"summary":"List a customer's notes","description":"Requires CUSTOMER_NOTES_VIEW. The customer's live notes in the caller's org, newest edit first, with the author's username and the linked Lotus order's number; a customer id of another org returns an empty page. A customer-portal session may only list its own customer's notes. Only search and paginate are read; other top-level keys are ignored. Read only.\n","tags":["Customer"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["search","paginate"],"properties":{"search":{"type":"object","description":"Unknown keys are refused (400 \"x\" is not allowed).","required":["id"],"properties":{"id":{"type":"string","format":"uuid","description":"The customer id.","example":"00000000-0000-0000-0000-000000000000"},"type":{"type":"string","nullable":true,"description":"Only notes of this type. Any string passes, so a type no note has (anything but service, sales, rental or collection) returns an empty page. The string \"null\" lists only notes without a type; null or omitted lists every type; '' is refused (400 \"type\" is not allowed to be empty).\n","example":"sales"},"order":{"type":"string","format":"uuid","nullable":true,"description":"Only notes linked to this Lotus order."}}},"paginate":{"$ref":"#/components/schemas/paginate"}}},"example":{"search":{"id":"00000000-0000-0000-0000-000000000000"},"paginate":{"perPage":10,"currentPage":1}}}}},"responses":{"200":{"description":"One page of notes","content":{"application/json":{"schema":{"type":"object","required":["data","pagination"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/customer_note_row"}},"pagination":{"$ref":"#/components/schemas/customer_list_pagination"}}}}}},"400":{"description":"No search object (\"search object is required\", code VALIDATION); search failed validation (Joi message, code VALIDATION, e.g. \"id\" is required, \"x\" is not allowed); paginate missing or invalid (\"Paginate object is required\" when it or one of its keys is missing, Joi's message otherwise); or a customer-portal session asked for another customer (\"Bad request. can't request for other customer's notes\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token, or the caller lacks CUSTOMER_NOTES_VIEW","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"$ref":"#/components/responses/customer_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/customers/notes/{id}":{"post":{"summary":"Edit a note","description":"Requires CUSTOMER_NOTES_UPDATE. UPDATE_ALL_CUSTOMER_NOTES is forwarded (it never blocks): without it a caller may only edit their own notes. Both fields are required on every edit. Writes one customer_note journal row of the fields that changed (none when nothing changed), in the same transaction. A customer-portal session reaches only its own customer's notes.\n","tags":["Customer"],"parameters":[{"in":"path","required":true,"name":"id","description":"The note id (note_id).","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/customer_note_update"}}}},"responses":{"200":{"description":"Note updated","content":{"application/json":{"schema":{"type":"string","example":"Note updated"}}}},"400":{"description":"No body or the body failed validation (Joi message, e.g. \"note_content\" is required), id is not a uuid (Joi message), or another user's note without UPDATE_ALL_CUSTOMER_NOTES (\"You can not update other people' notes\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token, or the caller lacks CUSTOMER_NOTES_UPDATE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"$ref":"#/components/responses/customer_ai_connector_forbidden"},"404":{"description":"No live note with that id in the caller's org, or not the portal session's own customer's (\"Note not found\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"summary":"Delete a note","description":"Requires CUSTOMER_NOTES_DELETE. DELETE_ALL_CUSTOMER_NOTES is forwarded (it never blocks): without it a caller may only delete their own notes. Soft-deletes the note with one customer_note DELETE journal row carrying the removed text, in one transaction. A customer-portal session reaches only its own customer's notes.\n","tags":["Customer"],"parameters":[{"in":"path","required":true,"name":"id","description":"The note id (note_id).","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Note deleted","content":{"application/json":{"schema":{"type":"string","example":"Note deleted"}}}},"400":{"description":"id is not a uuid (Joi message), or another user's note without DELETE_ALL_CUSTOMER_NOTES (\"You can not delete other people' notes\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token, or the caller lacks CUSTOMER_NOTES_DELETE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"$ref":"#/components/responses/customer_ai_connector_forbidden"},"404":{"description":"No live note with that id in the caller's org, or not the portal session's own customer's (\"Note not found\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/customers/sites/{id}":{"post":{"summary":"Edit a site","description":"Requires CUSTOMER_UPDATE. A partial update: only the fields sent change, but every edit needs tax_exemption_status false or a tax_exemption_expiry. Writes one customer_site journal row of the fields that changed (none when nothing changed), in the same transaction, after the customer's BASELINE row first, once, for a customer with legacy snapshots. When the site is its customer's default site, the customer is revalidated after the commit, outside the transaction; a failure there is only logged and the 200 still goes out. A customer-portal session reaches only its own customer's sites.\n","tags":["Customer"],"parameters":[{"in":"path","required":true,"name":"id","description":"The site id.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/customer_site_update"}}}},"responses":{"200":{"description":"Site updated","content":{"application/json":{"schema":{"type":"string","example":"site updated"}}}},"400":{"description":"No body, or the body failed validation (Joi message, e.g. \"tax_exemption_expiry\" is required, which is also the answer to an empty body), or id is not a uuid (\"Invalid data type provided\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token, or the caller lacks CUSTOMER_UPDATE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"$ref":"#/components/responses/customer_ai_connector_forbidden"},"404":{"description":"No live site with that id in the caller's org, or not the portal session's own customer's (\"site not found\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The body's id is another existing site's id (\"Duplicate field value entered for: id. Please use another value.\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"summary":"Delete a site","description":"Requires CUSTOMER_UPDATE. In one transaction: the customer's BASELINE row first, once, for a customer with legacy snapshots, then the soft delete with one customer_site DELETE journal row carrying the site's before image. After the commit, outside that transaction: when it was its customer's default site, customers.default_site is cleared (no journal row) and the customer revalidated; a failure there is only logged and the 200 still goes out. A customer-portal session reaches only its own customer's sites.\n","tags":["Customer"],"parameters":[{"in":"path","required":true,"name":"id","description":"The site id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Site deleted","content":{"application/json":{"schema":{"type":"string","example":"site deleted"}}}},"400":{"description":"id is not a uuid (Joi message)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token, or the caller lacks CUSTOMER_UPDATE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"$ref":"#/components/responses/customer_ai_connector_forbidden"},"404":{"description":"No live site with that id in the caller's org, or not the portal session's own customer's (\"site not found\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/customers/contacts/{id}":{"post":{"summary":"Edit a contact","description":"Requires CUSTOMER_UPDATE. A partial update, except that name is required on every edit. Writes one customer_contact journal row of the fields that changed (none when nothing changed), in the same transaction, after the customer's BASELINE row first, once, for a customer with legacy snapshots. The customer is not revalidated. A customer-portal session reaches only its own customer's contacts.\n","tags":["Customer"],"parameters":[{"in":"path","required":true,"name":"id","description":"The contact id.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/customer_contact_update"}}}},"responses":{"200":{"description":"Contact updated","content":{"application/json":{"schema":{"type":"string","example":"customer contact updated"}}}},"400":{"description":"No body or the body failed validation (Joi message, e.g. \"name\" is required), or \"Invalid data type provided\": id is not a uuid, or key is not a whole number (e.g. 1.5; the schema takes any number, the column is an integer)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token, or the caller lacks CUSTOMER_UPDATE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"$ref":"#/components/responses/customer_ai_connector_forbidden"},"404":{"description":"No live contact with that id in the caller's org, or not the portal session's own customer's (\"customer contact not found\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The body's id is another existing contact's id (\"Duplicate field value entered for: id. Please use another value.\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"summary":"Delete a contact","description":"Requires CUSTOMER_UPDATE. In one transaction: the customer's BASELINE row first, once, for a customer with legacy snapshots, then the soft delete with one customer_contact DELETE journal row carrying the contact's before image. customers.default_contact is left as it was, so it can point at the deleted contact, and the customer is not revalidated. A customer-portal session reaches only its own customer's contacts.\n","tags":["Customer"],"parameters":[{"in":"path","required":true,"name":"id","description":"The contact id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Contact deleted","content":{"application/json":{"schema":{"type":"string","example":"customer contact deleted"}}}},"400":{"description":"id is not a uuid (Joi message)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token, or the caller lacks CUSTOMER_UPDATE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"$ref":"#/components/responses/customer_ai_connector_forbidden"},"404":{"description":"No live contact with that id in the caller's org, or not the portal session's own customer's (\"customer contact not found\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/customers/validate/{id}":{"get":{"summary":"Check a customer against the org's validation rules","description":"Requires CUSTOMER_READ. Runs the same check that sets validated / validation_failures on save, against the org's customer and customer_site field settings, and returns the verdict without storing it. Read only. A customer-portal session may only check its own customer.\n","tags":["Customer"],"parameters":[{"in":"path","required":true,"name":"id","description":"The customer id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The verdict","content":{"application/json":{"schema":{"type":"object","required":["validationStatus","validationFailureObject"],"properties":{"validationStatus":{"type":"boolean","description":"False when any entry of validationFailureObject is invalid."},"validationFailureObject":{"$ref":"#/components/schemas/customer_validation_failures"}}}}}},"400":{"description":"id is not a uuid (\"Invalid data type provided\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token, the caller lacks CUSTOMER_READ, or a customer-portal session asked for another customer (\"Not Authorized\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"$ref":"#/components/responses/customer_ai_connector_forbidden"},"404":{"description":"No live customer with that id in the caller's org (\"Customer not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/customers/restore/{id}":{"post":{"summary":"Restore a deleted customer","description":"Requires CUSTOMER_RESTORE. Sets deleted back to false and writes one customer RESTORE journal row (deleted true to false), after a BASELINE row first, once, for a customer with legacy snapshots, in one transaction. The customer's sites, contacts and cards deleted with it stay deleted. No body. The code has no customer-portal check (CUSTOMER_RESTORE is an org permission).\n","tags":["Customer"],"parameters":[{"in":"path","required":true,"name":"id","description":"The customer id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Customer restored","content":{"application/json":{"schema":{"type":"string","example":"Customer restored successfully"}}}},"400":{"description":"id is not a uuid (Joi message)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token, or the caller lacks CUSTOMER_RESTORE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"$ref":"#/components/responses/customer_ai_connector_forbidden"},"404":{"description":"No deleted customer with that id in the caller's org (\"Customer not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"A live customer of the org already has this customer's number (\"Duplicate field value entered for: customer_number, org. Please use another value.\"); nothing is restored\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/customers/{id}":{"get":{"summary":"Get a customer","description":"Requires CUSTOMER_READ. CUSTOMER_RESTRICTED_VIEW and CUSTOMER_RESTORE are forwarded (they never block). Without CUSTOMER_RESTRICTED_VIEW the customer carries 28 columns instead of every column (see customer_detail). Without CUSTOMER_RESTORE a deleted customer is a 404. A customer-portal session may only read its own customer. Read only. Reachable by an AI-connector (MCP) session; for one, a missing permission is a 403, not a 401.\n","tags":["Customer"],"parameters":[{"in":"path","required":true,"name":"id","description":"The customer id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The customer with its live sites, cards and contacts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/customer_detail"}}}},"400":{"description":"id is not a uuid (Joi message)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token, the caller lacks CUSTOMER_READ, or a customer-portal session asked for another customer (\"Not Authorized\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"An AI-connector (MCP) session that lacks CUSTOMER_READ","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No customer with that id in the caller's org, or a deleted one without CUSTOMER_RESTORE (\"Customer not found\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"summary":"Update a customer","description":"Requires CUSTOMER_UPDATE. CUSTOMER_RESTRICTED_UPDATE, CUSTOMER_BLACKLIST_UPDATE, CUSTOMER_CASH_VS_ACCOUNT_UPDATE and CUSTOMER_UPDATE_FEES are forwarded (they never block): they decide which fields the body may carry (see customer_update). The update is partial; contacts and sites, when sent, are the full desired lists. A customer-portal session may update only its own customer, and may send banned only at its stored value: banning is the org's call (the web's customer popup sends banned on every save, so an unchanged value passes). Checks run in this order: a customer-portal session's own customer, a body was sent, id is a uuid, the customer is live in the org, the fee fields, then pay_on_open, the body schema, then banned for a customer-portal session, sales_person, contact / site ids, default_contact. A refusal writes nothing. In one transaction it writes a BASELINE row first, once, for a customer with legacy snapshots, then one customer UPDATE journal row of the columns that changed and one customer_contact / customer_site row per contact or site that changed, under one batch_id, plus a CUSTOMER_CREDIT_UPDATED financial audit row when a credit field (banned, banned_reason, credit_limit, credit_limit_action, terms, discount, cash_discount_percentage, cash_discount_days, balance_application_setting, blacklisted, blacklist_reason) changed. No UPDATE rows when nothing changed. Then the customer is revalidated. An empty object {} changes nothing and only revalidates (200), though it can still write the one-time BASELINE row. When company_name was sent and the customer has a Stripe customer, the name is copied to Stripe last, after the update is saved: on an org without a Stripe account that step answers 400 \"Stripe not configured for this Org\", and a Stripe error there is a 500 where error details are hidden, as in production (Stripe's own status elsewhere, see 500), although the update is already saved.\n","tags":["Customer"],"parameters":[{"in":"path","required":true,"name":"id","description":"The customer id.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/customer_update"}}}},"responses":{"200":{"description":"The customer after the update and its revalidation","content":{"application/json":{"schema":{"type":"object","required":["message","customer"],"properties":{"message":{"type":"string","description":"\"Customer <id> updated\", or \"Customer <id> updated but is invalid\" when validation failed."},"customer":{"description":"The stored customer columns (every column with CUSTOMER_RESTRICTED_UPDATE, else the 28 listed in customer_detail), plus its live sites and contacts. No payment_methods, active_orders_count, sales_person_details or facility_details.\n","allOf":[{"$ref":"#/components/schemas/customer"},{"type":"object","properties":{"sites":{"type":"array","items":{"$ref":"#/components/schemas/customer_site"}},"contacts":{"type":"array","items":{"$ref":"#/components/schemas/customer_contact_summary"}}}}]}}}}}},"400":{"description":"No body (\"Nothing to update\"); id is not a uuid (Joi message); a fee field sent without CUSTOMER_UPDATE_FEES (\"Not permitted to update fees & taxes\"); pay_on_open sent without CUSTOMER_CASH_VS_ACCOUNT_UPDATE (\"Not Authorized. You can't set 'pay_on_open' field on a customer\"); the body failed validation (Joi message, code VALIDATION, e.g. \"x\" is not allowed for an unknown key or a server-set column, \"credit_limit\" is not allowed without CUSTOMER_RESTRICTED_UPDATE, \"blacklisted\" is not allowed without CUSTOMER_BLACKLIST_UPDATE, \"A reason must be provided when blacklisting a customer\"); a customer-portal session sent a banned other than the stored value (\"Not Authorized. You can't set 'banned' field on a customer\"); sales_person is not a user of the org holding at least one permission (\"Sales person does not exist\"; a deleted user is still found) or lacks ALLOW_ORDER_OWNERSHIP (\"Customer can only be assigned to sales users\"); a contact or site id that is not this customer's (\"Unknown contact id: <id>\", \"Unknown site id: <id>\"); default_contact names a stored contact that fails the contact schema (\"Can not set an invalid contact as default contact\", see customer_fields.default_contact); gallagher_verified sent as '' or a contact key that is not a whole number, e.g. 1.5 (\"Invalid data type provided\", see customer_update and customer_contact_fields.key; nothing is saved); or the Stripe name sync on an org without Stripe, after the update is saved (see above)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token, the caller lacks CUSTOMER_UPDATE, or a customer-portal session asked for another customer (\"Not Authorized\"; nothing is written)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"$ref":"#/components/responses/customer_ai_connector_forbidden"},"404":{"description":"No live customer with that id in the caller's org (\"Customer not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/customer_stripe_error"}}},"delete":{"summary":"Delete a customer","description":"Requires CUSTOMER_DELETE. Soft-deletes the customer and every site, contact and card of it, in one transaction under one batch_id: one customer DELETE journal row (deleted false to true, the customer's image as snapshot.before) and one DELETE row per live site, contact and card deleted with it, after a BASELINE row first, once, for a customer with legacy snapshots. POST /customers/restore/{id} brings back the customer only. The code has no customer-portal check (CUSTOMER_DELETE is an org permission).\n","tags":["Customer"],"parameters":[{"in":"path","required":true,"name":"id","description":"The customer id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Customer deleted","content":{"application/json":{"schema":{"type":"string","example":"Customer deleted successfully"}}}},"400":{"description":"id is not a uuid (Joi message)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token, or the caller lacks CUSTOMER_DELETE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"$ref":"#/components/responses/customer_ai_connector_forbidden"},"404":{"description":"No live customer with that id in the caller's org (\"Customer not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/form_template/items":{"get":{"summary":"List the org's form template items","description":"Every live item in the org's pool, sorted by description, not paginated. Each row carries `templates`: the live templates it is linked into, of any type. Requires PLATFORM_FORM_TEMPLATES_VIEW.\n","tags":["Form Templates"],"parameters":[{"in":"query","name":"name","description":"Case-insensitive substring of the description (ILIKE `%name%`; a `%` or `_` in it acts as a wildcard).\n","schema":{"type":"string"},"example":"trailer"},{"in":"query","name":"type","description":"An item type key. VERIFIED_TEXT and VERIFIED_READING are keys too, so they are accepted and match nothing (200 `[]`): no form-template item has either type. Anything else, an old integer included, is a 400 \"Unknown item type (<type>)\".\n","schema":{"type":"string","enum":["TEXT","NUMBER","DROPDOWN","PASS_FAIL","YES_NO","YES_NO_AND_GAUGE","CHECK_AND_GAUGE","DAMAGE_AND_PHOTO","GAUGE_AND_PHOTO","VERIFIED_TEXT","VERIFIED_READING"]},"example":"DROPDOWN"},{"in":"query","name":"template_type","description":"Only items that may be attached to this template type (its `allowed_form_templates` holds it). Not validated: an unknown value returns `[]`.\n","schema":{"type":"string"},"example":"inspection"}],"responses":{"200":{"description":"The items","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/form_template_item_search_row"}}}}},"400":{"description":"`type` is not an item type key: \"Unknown item type (<type>)\".","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/form_template_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/form_template/items/create":{"post":{"summary":"Create a form template item","description":"Adds a question to the org's pool. `allowed_form_templates` is derived from `item_type`, never taken from the body: every type but the two photo types may go on all three template types; DAMAGE_AND_PHOTO and GAUGE_AND_PHOTO on inspection templates only. Journals a CREATE row (entity form_template_question). Requires PLATFORM_FORM_TEMPLATES_CREATE.\n","tags":["Form Templates"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/form_template_item_create"}}}},"responses":{"200":{"description":"The stored item","content":{"application/json":{"schema":{"$ref":"#/components/schemas/form_template_item"}}}},"400":{"description":"No body (`\"value\" is required`), a missing description or item_type, an item_type outside the list (an integer included), a DROPDOWN without `elements` or with an empty list, or an unknown key.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/form_template_ai_connector_forbidden"},"409":{"description":"\"A form template item of the same type already exists.\", whichever key collided: a live item of the org with the same description and item_type, or a sent `id` that is already a live item's id in the org.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/form_template/items/update":{"post":{"summary":"Update a form template item","description":"Rewrites a live item: description, item_type and, when sent, elements. `allowed_form_templates` is derived again from the (possibly new) item_type. The item is shared, so the change reaches every template it is linked into. Changing the type does not unlink the item from a template whose type no longer allows it (known issue). Journals an UPDATE row of what changed (nothing for a no-op). Requires PLATFORM_FORM_TEMPLATES_UPDATE.\n","tags":["Form Templates"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/form_template_item_update"}}}},"responses":{"200":{"description":"The stored item","content":{"application/json":{"schema":{"$ref":"#/components/schemas/form_template_item"}}}},"400":{"description":"No body or no `id` (\"Form template item ID is required.\"), a Joi failure (as on create), or an id that is not a live item of the org: \"Form template item does not exist.\" (a 400, not a 404).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/form_template_ai_connector_forbidden"},"409":{"description":"Another live item already has this description and item_type: \"A form template item of the same type already exists\".\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/form_template/items/{id}":{"delete":{"summary":"Delete a form template item","description":"Soft-deletes the item. Its links stay in the junction, but every client-facing template read skips a deleted item, so it disappears from each template it was on (a template's journal snapshots still list it). Journals a DELETE row keeping the item. Requires PLATFORM_FORM_TEMPLATES_DELETE.\n","tags":["Form Templates"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"example":"8a1a7c38-edee-41b6-84ff-d878f2473706"}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"string","example":"Form template item deleted"}}}},"400":{"description":"A malformed id: `\"value\" must be a valid GUID`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/form_template_ai_connector_forbidden"},"404":{"description":"A missing, foreign or already-deleted item: \"Form template item does not exist.\".\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/form_template/search":{"post":{"summary":"Search form templates","description":"The org's live templates, paginated, sorted by description unless `order_by` says otherwise. With `order_by` the row id breaks ties; the default description sort has no tiebreaker, so two templates sharing a description (of different types) can swap between pages. The nadc template is left out unless `type` names it. Requires PLATFORM_FORM_TEMPLATES_VIEW. On the AI-connector (MCP) allowlist since DOZ-8319, for the connector's list_form_templates tool.\n","tags":["Form Templates"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/form_template_search_body"}}}},"responses":{"200":{"description":"One page of templates","content":{"application/json":{"schema":{"$ref":"#/components/schemas/form_template_search_result"}}}},"400":{"description":"No body or no paginate (\"Paginate object is required\"), a perPage above 100, an unknown key in `search`, a `type` outside the list, a malformed category id, or an `order_by` column outside orderByColumnNamesList (\"Invalid column name in order_by: <column>\"); or the read failed (the database error's own text).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"An AI-connector (MCP) session that lacks PLATFORM_FORM_TEMPLATES_VIEW (the route is on the connector allowlist)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/form_template/create":{"post":{"summary":"Create a form template","description":"Creates the template and links the items in `items`, each with its weight, required flag and block (a link without a block starts in GENERAL). Every item must be a live item of the org whose `allowed_form_templates` holds the template's type. Journals a CREATE row with the template and its questions. Requires PLATFORM_FORM_TEMPLATES_CREATE.\n","tags":["Form Templates"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/form_template_create"}}}},"responses":{"200":{"description":"The new template, read back as the search returns it","content":{"application/json":{"schema":{"$ref":"#/components/schemas/form_template_search_row"}}}},"400":{"description":"No body (`\"value\" is required`), a Joi failure (missing description or type, no items, an unknown key), type nadc (\"The New Asset Detail Checklist template is created from Platform Settings, not here\"), an item that is not a live item of the org (\"Some items do not exist. Please verify the items you are attaching to this form template.\"), an item its type may not go on (\"Item (<description>) cannot be attached to <type> form template.\"), or an unknown block (\"Unknown question block: <block>. Call GET /v1/form_template/blocks for the list.\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/form_template_ai_connector_forbidden"},"409":{"description":"\"A form template item of the same type already exists\", whichever key collided: a live template of the org with the same description and type, the same `item_id` twice in `items`, or a sent `id` that is already a live template's id in the org.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Also answered, masked, for a `template_id` in the body: Joi accepts it, but form_templates has no such column (known issue).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/form_template/update":{"post":{"summary":"Update a form template","description":"Rewrites the template's description and type and replaces its item list wholesale: links not in `items` are removed. A link sent without a block (absent or null) keeps the block it had on this template (GENERAL for a new link). The type may move between asset_details and inspection; the template's category attachments keep their old template_type (known issue). Journals an UPDATE row of what changed. Requires PLATFORM_FORM_TEMPLATES_UPDATE.\n","tags":["Form Templates"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/form_template_update"}}}},"responses":{"200":{"description":"The template, read back as the search returns it","content":{"application/json":{"schema":{"$ref":"#/components/schemas/form_template_search_row"}}}},"400":{"description":"No body or no `id` (\"Form template ID is required\"), a Joi failure (as on create), the nadc template sent with another type (\"The New Asset Detail Checklist template cannot change type\"), another template sent as nadc (\"A form template cannot become the New Asset Detail Checklist template\"), or one of create's item and block refusals.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/form_template_ai_connector_forbidden"},"404":{"description":"The id is not a live template of the org: \"Form template does not exist\".\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"\"A form template of the same type already exists\", whichever key collided: another live template with the same description and type, or the same `item_id` twice in `items`.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Also answered, masked, for a `template_id` in the body (known issue, as on create).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/form_template/blocks":{"get":{"summary":"The inspection question block vocabulary, in render order","description":"The areas of the machine a question can sit in (DOZ-7591), sorted by weight. The keys are the only values a link's `block` accepts. Global, not per org. Requires PLATFORM_FORM_TEMPLATES_VIEW.\n","tags":["Form Templates"],"responses":{"200":{"description":"Blocks","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/form_template_block"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/form_template_ai_connector_forbidden"},"500":{"description":"Also answered when the block table is empty or has no GENERAL row.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/form_template/{id}":{"get":{"summary":"Get a form template","description":"One live template of the org, any type (the nadc template included), with its categories and items. Each item also carries `used_by`: the other live templates it is linked into, so the editor can say what a rename touches. Requires PLATFORM_FORM_TEMPLATES_READ (not _VIEW). On the AI-connector (MCP) allowlist since DOZ-8319, for the connector's get_form_template tool.\n","tags":["Form Templates"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"example":"1cae4a1a-1da7-4518-826a-46c3baeabf1e"}],"responses":{"200":{"description":"The template. A missing, foreign or deleted id also answers 200, with the JSON string \"Form template does not exist\".\n","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/form_template_detail"},{"type":"string","enum":["Form template does not exist"]}]}}}},"400":{"description":"A malformed id (`\"id\" must be a valid GUID`), or the read failed (the database error's own text).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"An AI-connector (MCP) session that lacks PLATFORM_FORM_TEMPLATES_READ (the route is on the connector allowlist)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"summary":"Delete a form template","description":"Soft-deletes the template. Its category attachments (category_form_templates) are left in place. The effective-template resolver (GET /categories/{id}/template and the inspections built from it), template search and GET, the item list's `templates` and the template-disagreements check skip a deleted template. Known issue: four readers don't, and keep showing or counting the attachment: the category list, search and detail (`form_templates`, by name), the category journal view, the category report, and the asset search's `has_inspection_template`; the categories' `inherited` cache keeps its entry too. Journals a DELETE row with the template and its questions. Requires PLATFORM_FORM_TEMPLATES_DELETE.\n","tags":["Form Templates"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"example":"1cae4a1a-1da7-4518-826a-46c3baeabf1e"}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"string","example":"Form template deleted"}}}},"400":{"description":"A malformed id (`\"value\" must be a valid GUID`), or the nadc template (\"The New Asset Detail Checklist template cannot be deleted; remove its questions instead\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/form_template_ai_connector_forbidden"},"404":{"description":"A missing, foreign or already-deleted template: \"Form template does not exist\".\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/freight_companies/search":{"post":{"summary":"Search freight companies","description":"Live (not deleted) freight companies of the caller's org, paginated. Default order is case-insensitive by name (with no id tiebreaker); with `search.order_by` the requested columns come first and id breaks ties. Requires FREIGHT_COMPANY_VIEW. On the AI-connector (MCP) allowlist since DOZ-8320, for the connector's search_freight_companies tool.\n","tags":["Freight Companies"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/freight_company_search_body"}}}},"responses":{"200":{"description":"One page of freight companies. knex-paginate is not length-aware here: `pagination.total`, `lastPage`, `prevPage` and `nextPage` come back on page 1 only, and `prevPage` / `nextPage` can be null. Known issue: the validated `paginate` is discarded and the raw one is passed on, so numeric strings (`currentPage: \"1\"`) pass validation but come back as strings in `pagination`, and page 1 then carries no `total` / `lastPage` / `prevPage` / `nextPage` (knex-paginate compares `\"1\" === 1`). Send numbers.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/freight_company_search_result"}}}},"400":{"description":"`paginate` missing, or missing `currentPage` or `perPage` (all three answer \"Paginate object is required\"); `paginate: null` (`\"value\" must be of type object`); a paginate value that is not an integer or out of range (`currentPage` below 1, `perPage` outside 1-100); an unknown key inside `search` (`\"x\" is not allowed`); an unknown key inside `paginate` (`\"x\" is not allowed`); a `paginate` or `search` that is not an object (`\"value\" must be of type object`); an `order_by` entry without a `column` (`\"order_by[0].column\" is required`); a `no_fee` that is neither a boolean nor the string \"true\" / \"false\" (`\"no_fee\" must be a boolean`; Joi converts those two strings); an `order_by` entry with a bad `order` or `nulls` (`\"order_by[0].order\" must be one of [asc, desc, ASC, DESC]`); or an `order_by` column other than name (\"Invalid column name in order_by: <column>\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"An AI-connector (MCP) session that lacks FREIGHT_COMPANY_VIEW (the route is on the connector allowlist)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/freight_companies":{"post":{"summary":"Create a freight company","description":"Creates a freight company in the caller's org. `no_fee` defaults to false. Journals one CREATE row. Requires FREIGHT_COMPANY_CREATE.\n","tags":["Freight Companies"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/freight_company_create"}}}},"responses":{"200":{"description":"The stored freight company","content":{"application/json":{"schema":{"$ref":"#/components/schemas/freight_company"}}}},"400":{"description":"A schema error: no body or no `name` (`\"name\" is required`), an empty name (`\"name\" is not allowed to be empty`), a `no_fee` that is neither a boolean nor the string \"true\" / \"false\" (`\"no_fee\" must be a boolean`; Joi converts those two strings), or any other key (`\"org\" is not allowed`).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/freight_company_ai_connector_forbidden"},"409":{"description":"A live freight company in the org already has this exact name (\"Duplicate field value entered for: name, org. Please use another value.\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/freight_companies/{id}":{"get":{"summary":"Get a freight company","description":"The live freight company. Requires FREIGHT_COMPANY_READ.","tags":["Freight Companies"],"parameters":[{"$ref":"#/components/parameters/freight_company_id"}],"responses":{"200":{"description":"The freight company","content":{"application/json":{"schema":{"$ref":"#/components/schemas/freight_company"}}}},"400":{"description":"A malformed id (`\"id\" must be a valid GUID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/freight_company_ai_connector_forbidden"},"404":{"description":"No live freight company with this id in the caller's org (\"Freight company not found\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"summary":"Update a freight company","description":"Writes `name` (required on every update) and, when sent, `no_fee`; an omitted `no_fee` keeps its value. Sets `updated_at`. Journals one UPDATE row of the fields that changed. Requires FREIGHT_COMPANY_UPDATE.\n","tags":["Freight Companies"],"parameters":[{"$ref":"#/components/parameters/freight_company_id"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/freight_company_update"}}}},"responses":{"200":{"description":"The stored freight company","content":{"application/json":{"schema":{"$ref":"#/components/schemas/freight_company"}}}},"400":{"description":"A schema error (checked first: no body or no `name` is `\"name\" is required`, an empty name is `\"name\" is not allowed to be empty`, a `no_fee` that is neither a boolean nor the string \"true\" / \"false\" (`\"no_fee\" must be a boolean`; Joi converts those two strings), any other key is `\"<key>\" is not allowed`), or a malformed id (`\"value\" must be a valid GUID`).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/freight_company_ai_connector_forbidden"},"404":{"description":"No live freight company with this id in the caller's org (\"Freight company not found\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Another live freight company in the org already has this exact name (\"Duplicate field value entered for: name, org. Please use another value.\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"summary":"Delete a freight company","description":"Soft-deletes the freight company (sets `deleted = true`); its name becomes free for a new one. Journals one DELETE row. Requires FREIGHT_COMPANY_DELETE.\n","tags":["Freight Companies"],"parameters":[{"$ref":"#/components/parameters/freight_company_id"}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"string","example":"Freight company deleted"}}}},"400":{"description":"A malformed id (`\"value\" must be a valid GUID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/freight_company_ai_connector_forbidden"},"404":{"description":"No live freight company with this id in the caller's org (\"Freight company not found\"), so a repeat delete is a 404.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/journal/history":{"post":{"summary":"Search the change history","description":"The journal rows of the caller's org, newest first (DOZ-7261). Every filter is optional and ANDed: types defaults to every component the caller may see, start / end to the last 30 days. Asking for a retired component name (task_update, lotus_order, lotus_invoice_delete) also returns its successor's rows, and the other way round, each labelled with the name that was asked for. Org users need JOURNAL_VIEW. A customer-portal session needs no permission; it reads only its own customer's rows, only from the components its portal shows, whatever the body says. Pagination is opt-in - without paginate every matching row is returned and there is no pagination block. FE guide - docs/FE-integration-docs/DOZ-7261_JOURNAL_HISTORY_READER_FE_INTEGRATION.md.\n","tags":["Journal"],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/journal_history_search"}}}},"responses":{"200":{"description":"The matching rows","content":{"application/json":{"schema":{"$ref":"#/components/schemas/journal_history_result"}}}},"400":{"description":"Validation failed (a non-uuid id, an unknown entity_type or action, a start or end that is not a date, paginate.perPage over 100, an unknown key), or start after end\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token, an org user without JOURNAL_VIEW, or a service API credential (machine principals are refused)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"An AI-connector (MCP) session - this route is not on its allowlist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/media/platform/resource/rental_app_document":{"post":{"summary":"Upload The Org's Rental Application Document","description":"Stores a PDF under `<bucket>/<org>/platform_media/<random id>` and saves that path as the org's CUSTOMER.RENTAL_APP_DOCUMENT platform setting, through the settings service (its own module and permission checks, validation and journal row). The file is uploaded before the setting is written, so a refused setting leaves the object behind. The previous document object is not deleted. Requires PLATFORM_GENERAL_UPDATE.\n","tags":["Media"],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["rental_app_document"],"properties":{"rental_app_document":{"type":"string","format":"binary","description":"The PDF (mimetype application/pdf). A file sent under any other field name is refused by the upload middleware with a 500.\n"}}}}}},"responses":{"200":{"description":"The org (every organizations column except the Stripe secrets, plus `modules`) with `rental_app_document` set to the new storage path.\n","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/platform_org"},{"type":"object","properties":{"rental_app_document":{"type":"string","description":"The new object path. This is the one response that returns it: GET /platform/general and POST /platform never do.\n","example":"erm-media/b67aba8b-9819-4179-b2a1-384968cfa963/platform_media/0b7d4f3e-3c1a-4f7e-9a51-6d2f5c8e9a10"}}}]}}}},"400":{"description":"No file under rental_app_document (\"Please attach the rental_app_document media\", code VALIDATION), or a file that is not a PDF (\"Only PDF files are allowed for rental application document\", code VALIDATION), or a value the settings service refuses.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"An AI-connector (MCP) session (\"Not available to AI connectors\"); or the settings service refusing the write - \"Setting CUSTOMER.RENTAL_APP_DOCUMENT is not available to this organization's modules\" (an org without the module) or \"You are not allowed to change CUSTOMER.RENTAL_APP_DOCUMENT\" (the caller's roles lack the setting's permission, checked even when RBAC is relaxed). The file is already uploaded by then.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/media/platform/resource/{path}":{"get":{"summary":"Stream A Stored File","description":"Streams one stored object, with the Content-Type it was stored with (application/octet-stream when it has none). `path` is a row's `full_path`, slashes included, e.g. `erm-media/<org>/asset_media/<asset_id>/<media_id>`; add `.thumbnail` or `.original` for an image's other copies.\n\nTwo ways in, tried in this order:\n\n1. **Inspection link** (`access-token` header, no login). The token must verify as a JWT and match an unexpired lotus_inspection_links row, and the path must be one of that inspection's (or its dispatch inspection's) DAMAGE_AND_PHOTO images or hour meter image. The file is then streamed for the link's org. A request without the header goes on to the logged-in mode.\n\n2. **Logged in** (bearer token). The path is looked up among the caller's org's asset, customer, order and PO vendor invoice media; a customer file's `.thumbnail` and `.original` resolve to that file's row. Customer media needs CUSTOMER_READ, and sensitive customer media also CUSTOMER_MEDIA_READ_SENSITIVE; a customer-portal session reads only its own customer's media (DOZ-8110). Asset media needs ASSET_READ; PO vendor invoice media needs PURCHASE_ORDERS_READ; order media needs no further permission. A path found in none of them (part, inspection, note and platform media, and an asset or order image's `.thumbnail` or `.original`) is streamed when its second segment is the caller's org. The route guard admits any authenticated caller who holds at least one permission; the four permissions above are only checked here.\n","tags":["Media"],"security":[{"bearerAuth":[]},{"inspectionLinkToken":[]}],"parameters":[{"in":"path","name":"path","required":true,"description":"The object path (a row's full_path), slashes included","schema":{"type":"string","example":"erm-media/b67aba8b-9819-4179-b2a1-384968cfa963/asset_media/08861081-b4b8-4209-95b6-70b26dba7c76/ebd35005-1c60-455d-b1c1-df61de34133f"}}],"responses":{"200":{"description":"The file's bytes","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}},"image/*":{"schema":{"type":"string","format":"binary"}},"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Inspection link only - an access token that does not verify (\"Link has expired or is malformed, Please request a new link from the owner\", code VALIDATION), or no unexpired link for it (\"No link against this access token was found, Please request a new link from the owner\", code VALIDATION).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Logged-in mode with no or an invalid token, or a caller with no permissions at all (\"Not Authorized. Your account holds no permissions\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Logged-in mode - an AI-connector (MCP) session (\"Not available to AI connectors\"); a path of another org (\"You do not have permission to read this media\"); customer media without CUSTOMER_READ, or of another customer for a customer-portal session (\"You do not have permission to read customer media\"); sensitive customer media without CUSTOMER_MEDIA_READ_SENSITIVE (\"You can not access sensitive media\"); asset media without ASSET_READ (\"You do not have permission to read asset media\"); PO vendor invoice media without PURCHASE_ORDERS_READ (\"You do not have permission to read purchase order media\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No object at the path (\"Media not found\"). Inspection link only - the link's inspection is gone (\"Inspection not found\"), or the path is not one of its images (\"Image not found\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/media/platform/logo/stream/{id}":{"get":{"summary":"Stream An Org's Logo","description":"Streams the logo object of the organization `id`. No login - it is linked from emails. The response carries `Cache-Control: public, max-age=172800` and no Content-Type. A storage read failure (e.g. the object is gone) ends the response with an empty 404.\n","tags":["Media"],"security":[],"parameters":[{"in":"path","name":"id","required":true,"description":"The organization id","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The logo's bytes","headers":{"Cache-Control":{"schema":{"type":"string","example":"public, max-age=172800"}}},"content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"id is not a uuid (\"Invalid data type provided\", code VALIDATION)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such org or it has no logo (\"Logo not found\"), or the logo object cannot be read (empty body).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/media/assets/media_types":{"get":{"summary":"List The Asset Media Types","description":"The fixed list of media types an asset file can be filed under. Requires ASSET_MEDIA_CREATE.","tags":["Media"],"responses":{"200":{"description":"The asset media types","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/media_type_option"}},"example":[{"desc":"Marketing Photo","value":"marketing_photo"},{"desc":"Marketing Video","value":"marketing_video"},{"desc":"Factory Spec Sheet","value":"factory_spec_sheet"},{"desc":"Operator Manual","value":"operator_manual"},{"desc":"Invoice From Vendor","value":"invoice_from_vendor"},{"desc":"Trucking Invoice","value":"trucking_invoice"},{"desc":"BOL","value":"bol"},{"desc":"Machine Invoice","value":"machine_invoice"},{"desc":"Document","value":"document"}]}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/media_ai_connector_forbidden"}}}},"/media/assets/{asset_id}":{"get":{"summary":"List An Asset's Media","description":"Every asset2media row of the asset in the caller's org that is not deleted, in no set order. Rows whose upload never finished (`valid: false`) are included. An unknown asset gives an empty list. Requires ASSET_READ.\n","tags":["Media"],"parameters":[{"in":"path","name":"asset_id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The asset's media rows","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/asset_media"}}}}},"400":{"description":"asset_id is not a uuid (\"Invalid data type provided\", code VALIDATION)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/media_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/media/assets":{"post":{"summary":"Upload Asset Media","description":"Uploads files to an asset of the caller's org (deleted assets included). Each file is filed under the media type at the same index in `media_types[]`; a file whose type is not an asset media type is skipped without an error. For each stored file a row is written first with `valid: false`, then the object(s), then the row is set `valid: true`, and the asset's media count goes up by one. Once every file is processed, one ASSET journal row (action UPDATE) is written in its own transaction with a `media[<media_id>]` entry per stored file (`from: null`, `to: { file_name, media_type, description }`); it is written even when a later file fails (500), so the earlier files are stored, counted and journaled and the failing file's row stays `valid: false`. The asset is then revalidated, and the response is that result. Requires ASSET_MEDIA_CREATE.\n","tags":["Media"],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/asset_media_upload"}}}},"responses":{"200":{"description":"The asset's validation result after the upload","content":{"application/json":{"schema":{"$ref":"#/components/schemas/media_validation_result"}}}},"400":{"description":"No files (\"Please attach at least one file\", code VALIDATION); files without media_types[] (\"Please provide a media_types entry for each file\", code VALIDATION); no asset_id ('\"asset_id\" is required', code VALIDATION) or one that is not a uuid ('\"asset_id\" must be a valid GUID', code VALIDATION).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/media_ai_connector_forbidden"},"404":{"description":"No such asset in the caller's org (\"Asset not found <asset_id>\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Unexpected server error, including a file sent under a field name other than files[].\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/media/assets/resource/{media_id}":{"post":{"summary":"Update An Asset Media Row","description":"Sets the media type and description of an asset media row in the caller's org that is not deleted, and returns the updated row. The row is locked for the update, and when the description or the media type changed, one ASSET journal row (action UPDATE, a `media[<media_id>]` entry with the row before and after) is written in the same transaction. Requires ASSET_MEDIA_CREATE.\n","tags":["Media"],"parameters":[{"in":"path","name":"media_id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/media_update"}}}},"responses":{"200":{"description":"The updated row","content":{"application/json":{"schema":{"$ref":"#/components/schemas/asset_media"}}}},"400":{"description":"The body fails validation (Joi's message, e.g. '\"media_type\" is required' or '\"sensitive\" is not allowed', code VALIDATION); a media_type that is not an asset media type (\"Invalid media type, please provide a valid media type\", code VALIDATION); media_id is not a uuid (\"Invalid data type provided\", code VALIDATION).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/media_ai_connector_forbidden"},"404":{"description":"No such row in the caller's org, or it is deleted (\"Media not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"summary":"Delete An Asset Media Row","description":"Marks the row deleted (the stored objects are kept) and writes the asset's journal row (component ASSET, action UPDATE, a `media[<media_id>]` entry with `to: null`) in the same transaction, then revalidates the asset and lowers its media count by one. Only a live row matches, so a repeat delete is a 404. Requires ASSET_MEDIA_DELETE.\n","tags":["Media"],"parameters":[{"in":"path","name":"media_id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The asset's validation result after the delete","content":{"application/json":{"schema":{"$ref":"#/components/schemas/media_validation_result"}}}},"400":{"description":"media_id is not a uuid (\"Invalid data type provided\", code VALIDATION)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/media_ai_connector_forbidden"},"404":{"description":"No such row in the caller's org, or it is already deleted (\"Media not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/media/parts/media_types":{"get":{"summary":"List The Part Media Types","description":"The fixed list of media types a part file can be filed under. Requires PART_MEDIA_CREATE.","tags":["Media"],"responses":{"200":{"description":"The part media types","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/media_type_option"}},"example":[{"desc":"Marketing Photo","value":"marketing_photo"},{"desc":"Marketing Video","value":"marketing_video"},{"desc":"Invoice From Vendor","value":"invoice_from_vendor"},{"desc":"Trucking Invoice","value":"trucking_invoice"},{"desc":"Parts Invoice","value":"parts_invoice"},{"desc":"Factory Spec Sheet","value":"factory_spec_sheet"},{"desc":"Machine Invoice","value":"machine_invoice"}]}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/media_ai_connector_forbidden"}}}},"/media/parts/{part_id}":{"get":{"summary":"List A Part's Media","description":"Every parts_media row of the part in the caller's org that is not deleted, in no set order. Rows whose upload never finished (`valid: false`) are included. An unknown part gives an empty list. Requires PARTS_READ.\n","tags":["Media"],"parameters":[{"in":"path","name":"part_id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The part's media rows","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/part_media"}}}}},"400":{"description":"part_id is not a uuid (\"Invalid data type provided\", code VALIDATION)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/media_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/media/parts":{"post":{"summary":"Upload Part Media","description":"Uploads files to a part of the caller's org (deleted parts included). Each file is filed under the media type at the same index in `media_types[]`; a file whose type is not a part media type is skipped without an error. For each stored file a row is written first with `valid: false`, then the object(s), then the row is set `valid: true`. Requires PART_MEDIA_CREATE.\n","tags":["Media"],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/part_media_upload"}}}},"responses":{"200":{"description":"Always null","content":{"application/json":{"schema":{"nullable":true,"example":null}}}},"400":{"description":"No files (\"Please attach at least one file\", code VALIDATION); files without media_types[] (\"Please provide a media_types entry for each file\", code VALIDATION); no part_id ('\"part_id\" is required', code VALIDATION) or one that is not a uuid ('\"part_id\" must be a valid GUID', code VALIDATION).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/media_ai_connector_forbidden"},"404":{"description":"No such part in the caller's org (\"Part not found <part_id>\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Unexpected server error, including a file sent under a field name other than files[].\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/media/parts/resource/{media_id}":{"post":{"summary":"Update A Part Media Row","description":"Sets the media type and description of a part media row in the caller's org that is not deleted, and returns the updated row. Requires PART_MEDIA_CREATE.\n","tags":["Media"],"parameters":[{"in":"path","name":"media_id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/media_update"}}}},"responses":{"200":{"description":"The updated row","content":{"application/json":{"schema":{"$ref":"#/components/schemas/part_media"}}}},"400":{"description":"The body fails validation (Joi's message, e.g. '\"media_type\" is required', code VALIDATION); a media_type that is not a part media type (\"Invalid media type, please provide a valid media type\", code VALIDATION); media_id is not a uuid (\"Invalid data type provided\", code VALIDATION).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/media_ai_connector_forbidden"},"404":{"description":"No such row in the caller's org, or it is deleted (\"Media not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"summary":"Delete A Part Media Row","description":"Marks the row deleted (the stored objects are kept). A row that is already deleted is matched again and answers 200. Requires PART_MEDIA_DELETE.\n","tags":["Media"],"parameters":[{"in":"path","name":"media_id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Always null","content":{"application/json":{"schema":{"nullable":true,"example":null}}}},"400":{"description":"media_id is not a uuid (\"Invalid data type provided\", code VALIDATION)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/media_ai_connector_forbidden"},"404":{"description":"No such row in the caller's org (\"Media not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/media/po_invoices/{invoice_id}":{"get":{"summary":"List A PO Vendor Invoice's Files","description":"Every po_invoice_media row of the vendor invoice (lotus_po_vendor_invoices) in the caller's org that is not deleted, in no set order. Rows whose upload never finished (`valid: false`) are included. An unknown invoice gives an empty list. Requires PURCHASE_ORDERS_READ.\n","tags":["Media"],"parameters":[{"in":"path","name":"invoice_id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The invoice's file rows","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/po_invoice_media"}}}}},"400":{"description":"invoice_id is not a uuid (\"Invalid data type provided\", code VALIDATION)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/media_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/media/po_invoices":{"post":{"summary":"Upload PO Vendor Invoice Files","description":"Uploads files to a vendor invoice of the caller's org that is not deleted. Every file is stored as one object (images too - no thumbnail or original copy). For each file a row is written first with `valid: false`, then the object, then the row is set `valid: true`. Requires PURCHASE_ORDERS_UPDATE.\n","tags":["Media"],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/po_invoice_media_upload"}}}},"responses":{"200":{"description":"One entry per stored file","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/po_invoice_media_uploaded"}}}}},"400":{"description":"No files (\"Please attach at least one file\", code VALIDATION); no invoice_id ('\"invoice_id\" is required', code VALIDATION) or one that is not a uuid ('\"invoice_id\" must be a valid GUID', code VALIDATION).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/media_ai_connector_forbidden"},"404":{"description":"No such invoice in the caller's org, or it is deleted (\"Vendor invoice not found <invoice_id>\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Unexpected server error, including a file sent under a field name other than files[].\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/media/po_invoices/resource/{media_id}":{"delete":{"summary":"Delete A PO Vendor Invoice File","description":"Marks the row deleted (the stored object is kept). Requires PURCHASE_ORDERS_UPDATE.\n","tags":["Media"],"parameters":[{"in":"path","name":"media_id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Always null","content":{"application/json":{"schema":{"nullable":true,"example":null}}}},"400":{"description":"media_id is not a uuid (\"Invalid data type provided\", code VALIDATION)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/media_ai_connector_forbidden"},"404":{"description":"No such row in the caller's org, or it is already deleted (\"Media not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/media/customers/media_types":{"get":{"summary":"List The Customer Media Types","description":"The fixed list of media types a customer file can be filed under. Requires CUSTOMER_VIEW.","tags":["Media"],"responses":{"200":{"description":"The customer media types","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/media_type_option"}},"example":[{"desc":"Drivers License","value":"drivers_license"},{"desc":"Insurance","value":"insurance"},{"desc":"Rental App","value":"rental_app"},{"desc":"Credit Application","value":"credit_application"},{"desc":"Sales Tax Exemption Document","value":"sales_tax_exemption_document"},{"desc":"W-9","value":"w9"},{"desc":"Contract","value":"contract"},{"desc":"Other","value":"other"}]}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/media_ai_connector_forbidden"}}}},"/media/customers":{"post":{"summary":"Upload Customer Media","description":"Uploads files to a customer of the caller's org that is not deleted. Each file takes the media type, description, expiration date and sensitive flag at its index; a file whose type is not a customer media type is skipped without an error. A missing expiration date is stored as the upload time, and a missing sensitive flag as false. For each stored file a row is written first with `valid: false`, then the object(s), then the row is set `valid: true`, and the customer's media count goes up by one. Once every file is processed, one CUSTOMER journal row (action UPDATE) is written in its own transaction with a `media[<media_id>]` entry per stored file; a sensitive file's entry carries no file name or description. It is written even when a later file fails (500), so the earlier files are stored, counted and journaled. The customer is then revalidated, and the response is that result. For a customer-portal session the customer is always the session's own, whatever customer_id says (DOZ-8110). Requires CUSTOMER_MEDIA_CREATE.\n","tags":["Media"],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/customer_media_upload"}}}},"responses":{"200":{"description":"The customer's validation result after the upload","content":{"application/json":{"schema":{"$ref":"#/components/schemas/media_validation_result"}}}},"400":{"description":"No files (\"Please attach at least one file\", code VALIDATION); files without media_types[] (\"Please provide a media_types entry for each file\", code VALIDATION); from a staff session, no customer_id ('\"customer_id\" is required', code VALIDATION) or one that is not a uuid ('\"customer_id\" must be a valid GUID', code VALIDATION).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/media_ai_connector_forbidden"},"404":{"description":"No such customer in the caller's org, or it is deleted (\"Customer not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Unexpected server error, including a file sent under a field name other than files[].\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/media/customers/{customer_id}/signed-documents":{"get":{"summary":"List A Customer's Signed Documents","description":"The e-signed contracts (orders_media rows with media_type `signed_artifact` that are valid and not deleted) of the customer's rental and sale orders that are not deleted or imported, newest first. Rentals are listed only for a caller holding RENTAL_VIEW and sales only for SALE_VIEW; a caller with neither, or asking for an order_type they cannot see, gets an empty list. For a customer-portal session the customer is always the session's own, whatever customer_id says. Requires CUSTOMER_READ.\n","tags":["Media"],"parameters":[{"in":"path","name":"customer_id","required":true,"schema":{"type":"string","format":"uuid"}},{"in":"query","name":"order_type","required":false,"description":"Only this order type (case-insensitive). Other query keys are ignored.","schema":{"type":"string","enum":["RENTAL","SALE"]}}],"responses":{"200":{"description":"The signed documents","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/customer_signed_document"}}}}},"400":{"description":"customer_id is not a uuid (\"Invalid customer_id\", code VALIDATION), or an order_type other than RENTAL or SALE (Joi's message, code VALIDATION).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/media_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/media/customers/{customer_id}":{"get":{"summary":"List A Customer's Media","description":"Every customer2media row of the customer in the caller's org that is not deleted, in no set order; sensitive rows only for a caller holding CUSTOMER_MEDIA_READ_SENSITIVE. Rows whose upload never finished (`valid: false`) and expired rows are included. An unknown customer gives an empty list. For a customer-portal session the customer is always the session's own, whatever customer_id says (DOZ-8110). Requires CUSTOMER_READ.\n","tags":["Media"],"parameters":[{"in":"path","name":"customer_id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The customer's media rows","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/customer_media"}}}}},"400":{"description":"customer_id is not a uuid, from a staff session (\"Invalid data type provided\", code VALIDATION)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/media_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/media/customers/resource/{media_id}":{"post":{"summary":"Update A Customer Media Row","description":"Sets the media type, sensitive flag, expiration date and description of a customer media row in the caller's org that is not deleted, and returns the updated row. All four are required, and the expiration date must be in the future. The row is locked for the update, and a CUSTOMER journal row (action UPDATE, a `media[<media_id>]` entry with the row's journal view before and after) is written in the same transaction only when that view changed; a row that is sensitive before and after shows no description in its view, so a description-only edit of a sensitive row writes no journal row. A customer-portal session's row must be its own customer's, and a sensitive row is not found for a caller without CUSTOMER_MEDIA_READ_SENSITIVE, as it is absent from their list (DOZ-8110). Requires CUSTOMER_MEDIA_CREATE.\n","tags":["Media"],"parameters":[{"in":"path","name":"media_id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/customer_media_update"}}}},"responses":{"200":{"description":"The updated row","content":{"application/json":{"schema":{"$ref":"#/components/schemas/customer_media"}}}},"400":{"description":"The body fails validation (Joi's message, e.g. '\"media_type\" is required' or '\"expiration_date\" must be greater than \"now\"', code VALIDATION); a media_type that is not a customer media type (\"Invalid media type, please provide a valid media type\", code VALIDATION); media_id is not a uuid (\"Invalid data type provided\", code VALIDATION).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/media_ai_connector_forbidden"},"404":{"description":"No such row in the caller's org, or it is deleted, another customer's for a customer-portal session, or sensitive for a caller without CUSTOMER_MEDIA_READ_SENSITIVE (\"Media not found\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"summary":"Delete A Customer Media Row","description":"Marks the row deleted (the stored objects are kept) and writes the customer's journal row (component CUSTOMER, action UPDATE, a `media[<media_id>]` entry with `to: null`; a sensitive file's entry carries no file name or description) in the same transaction, then revalidates the customer and lowers its media count by one. Only a live row matches, so a repeat delete is a 404. A customer-portal session's row must be its own customer's, and a sensitive row is not found for a caller without CUSTOMER_MEDIA_READ_SENSITIVE (DOZ-8110). Requires CUSTOMER_MEDIA_DELETE. If the row's customer is deleted, the row is still marked deleted and journaled, then the answer is 404 \"Customer not found\" and the count is not lowered.\n","tags":["Media"],"parameters":[{"in":"path","name":"media_id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The customer's validation result after the delete","content":{"application/json":{"schema":{"$ref":"#/components/schemas/media_validation_result"}}}},"400":{"description":"media_id is not a uuid (\"Invalid data type provided\", code VALIDATION)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/media_ai_connector_forbidden"},"404":{"description":"No such row in the caller's org, or it is already deleted, another customer's for a customer-portal session, or sensitive for a caller without CUSTOMER_MEDIA_READ_SENSITIVE (\"Media not found\"); the row's customer is deleted (\"Customer not found\", after the row was marked deleted and journaled).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/media/inspection/inspection_item":{"post":{"summary":"Upload Images To An Inspection Item","description":"Adds images to a DAMAGE_AND_PHOTO or GAUGE_AND_PHOTO item of an inspection (lotus_inspections) in the caller's org, and sets the item's other values sent with them (`damage` / `description` on a damage item, `reading` on a gauge item). The item's `passed` flag follows the values sent: a damage item passes when `damage` is `no` and fails for any other damage, a gauge item fails when `reading` is above 0 and passes otherwise, and it is unchanged when neither is sent; the type's default value shape is merged under the stored value (DOZ-7496). The objects are stored under `<bucket>/<org>/form_template_media/inspection_media/...`, each with its capture time (captured_at_dates[], or now) drawn onto the image; the item update and an inspection snapshot are written in one transaction, and the objects are not removed if it fails. A completed (DONE) inspection accepts images only when the item is marked for a deferred upload (its stored `lazy_upload`); the inspection.completed event is then published again. Same handler as POST /media/inspection/inspection_item/new. Requires INSPECTION_UPDATE.\n","tags":["Media"],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/inspection_item_media_upload"}}}},"responses":{"200":{"description":"The updated inspection item","content":{"application/json":{"schema":{"$ref":"#/components/schemas/inspection_template_item"}}}},"400":{"description":"No files (\"Please at least attach one image\", code VALIDATION); the fields fail validation (Joi's message, code VALIDATION); a field the item type does not take (\"'reading' is not allowed for type DAMAGE_AND_PHOTO items\", code VALIDATION); an item that is not a photo item (\"Asset inspection item is of the wrong type\", code VALIDATION); a file that is not an image (\"Only images are allowed\", code VALIDATION); a PENDING inspection (\"Asset is not yet ready for inspection\", code CONFLICT); a DONE inspection without a deferred upload (\"Asset inspection has already completed\", code CONFLICT).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/media_ai_connector_forbidden"},"404":{"description":"\"Asset inspection record does not exist\" - no such inspection in the caller's org, it is deleted or HIDDEN, or the item is not on it.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/media/inspection/inspection_item/new":{"post":{"summary":"Upload Images To An Inspection Item (Web)","description":"The same handler as POST /media/inspection/inspection_item; this is the path the web app calls. Requires INSPECTION_UPDATE.\n","tags":["Media"],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/inspection_item_media_upload"}}}},"responses":{"200":{"description":"The updated inspection item","content":{"application/json":{"schema":{"$ref":"#/components/schemas/inspection_template_item"}}}},"400":{"description":"As POST /media/inspection/inspection_item","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/media_ai_connector_forbidden"},"404":{"description":"As POST /media/inspection/inspection_item","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/media/inspection/inspection_item/{inspection_id}/{inspection_item_id}/{image_id}":{"delete":{"summary":"Remove An Image From An Inspection Item","description":"Removes the image whose path ends in image_id from the item's images, with an inspection snapshot, in one transaction. The stored objects are kept. Requires INSPECTION_UPDATE.\n","tags":["Media"],"parameters":[{"in":"path","name":"inspection_id","required":true,"schema":{"type":"string","format":"uuid"}},{"in":"path","name":"inspection_item_id","required":true,"schema":{"type":"string","format":"uuid"}},{"in":"path","name":"image_id","required":true,"description":"The last segment of the image's path","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Done","content":{"application/json":{"schema":{"type":"string","example":"Image deleted from the inspection item"}}}},"400":{"description":"An id is not a uuid (Joi's message, code VALIDATION); a PENDING inspection (\"Asset is not yet ready for inspection\", code CONFLICT); an item that is not a photo item (\"Asset inspection item is of the wrong type\", code VALIDATION); a DONE inspection (\"Asset inspection has already completed\", code CONFLICT).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/media_ai_connector_forbidden"},"404":{"description":"No such inspection in the caller's org, it is deleted or HIDDEN, or the item is not on it (\"Asset inspection record does not exist\"); the item has no images (\"Asset inspection item does not have any images\"); no image with that id (\"This image does not exist on the inspection item\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/media/inspection/hour_meter_image":{"post":{"summary":"Upload An Inspection's Hour Meter Image","description":"Stores the image (as the three image objects) under `<bucket>/<org>/form_template_media/inspection_media/<inspection_record>/hour_meter_image-<time>` and sets the inspection's hour_meter_image file_path and thumbnail_path (its other keys are kept), with an inspection snapshot, in one transaction. The capture time (captured_at, or now) is drawn onto the image in the org's timezone. The previous image objects are kept. Requires INSPECTION_UPDATE.\n","tags":["Media"],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file","inspection_record"],"properties":{"file":{"type":"string","format":"binary","description":"The image (any image/* type). A file sent under any other field name is refused by the upload middleware with a 500.\n"},"inspection_record":{"type":"string","format":"uuid","description":"The inspection (lotus_inspections) id","example":"3f6c1d2e-8a4b-4c5d-9e7f-1a2b3c4d5e6f"},"captured_at":{"type":"string","format":"date-time","description":"When the photo was taken; drawn onto the image, not stored"}}}}}},"responses":{"200":{"description":"The updated inspection","content":{"application/json":{"schema":{"$ref":"#/components/schemas/inspection_record"}}}},"400":{"description":"No file (\"Please attach a valid file.\", code VALIDATION); inspection_record missing or not a uuid (Joi's message about \"value\", code VALIDATION); a DONE inspection (\"Asset inspection has already completed\", code CONFLICT); a PENDING inspection (\"Asset inspection is  pending and is not ready for inspection\", code CONFLICT); an asset without an hour meter (\"Asset does not have an hour meter value\", code CONFLICT); a file that is not an image (\"Only images are allowed\", code VALIDATION).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/media_ai_connector_forbidden"},"404":{"description":"No such inspection in the caller's org, or it is deleted (\"Asset inspection record does not exist\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/media/orders/media_types":{"get":{"summary":"List The Order Media Types","description":"The fixed list of media types an order file can be uploaded under. Requires SALE_CREATE or SALE_UPDATE.\n","tags":["Media"],"responses":{"200":{"description":"The order media types","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/media_type_option"}},"example":[{"desc":"Sales Tax Exemption Document","value":"sales_tax_exemption_document"}]}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/media_ai_connector_forbidden"}}}},"/media/orders":{"post":{"summary":"Upload Order Media","description":"Uploads files to an order (lotus_orders) of the caller's org that is not deleted. Every media type is checked before anything is stored. Each file is then stored (the object(s) first, then its orders_media row) under the media type and expiration date at its index. A file that fails is logged and skipped, so the answer is 200 even when no file was stored. Requires SALE_CREATE or SALE_UPDATE.\n","tags":["Media"],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/order_media_upload"}}}},"responses":{"200":{"description":"Done","content":{"application/json":{"schema":{"type":"string","example":"Order media uploaded"}}}},"400":{"description":"No files (\"Please attach at least one file\", code VALIDATION); no media_types[] (\"Please provide a media_types entry for each file\", code VALIDATION); a media type that is not an order media type (\"Invalid order media type\", code VALIDATION); no order_id ('\"order_id\" is required', code VALIDATION) or one that is not a uuid ('\"order_id\" must be a valid GUID', code VALIDATION).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/media_ai_connector_forbidden"},"404":{"description":"No such order in the caller's org, or it is deleted (\"Order not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Unexpected server error, including a file sent under a field name other than files[].\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/media/orders/{order_id}":{"get":{"summary":"List An Order's Media","description":"Every orders_media row of the order in the caller's org that is not deleted, in no set order - uploaded files and the SignNow signed contracts alike, including rows whose `valid` is false (a replaced, declined or expired signed document). An unknown order gives an empty list. Requires SALE_CREATE or SALE_UPDATE.\n","tags":["Media"],"parameters":[{"in":"path","name":"order_id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The order's media rows","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/order_media"}}}}},"400":{"description":"order_id is not a uuid (\"Invalid data type provided\", code VALIDATION)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/media_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/media/orders/resource/{media_id}":{"delete":{"summary":"Delete An Order Media Row","description":"Marks the row deleted (the stored objects are kept). Requires SALE_CREATE or SALE_UPDATE.\n","tags":["Media"],"parameters":[{"in":"path","name":"media_id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Done","content":{"application/json":{"schema":{"type":"string","example":"Order media deleted"}}}},"400":{"description":"media_id is not a uuid (Joi's message about \"value\", code VALIDATION)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/media_ai_connector_forbidden"},"404":{"description":"No such row in the caller's org, or it is already deleted (\"Order media not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/media/orders/note":{"post":{"summary":"Upload Images To An Order Note","description":"Adds images to a note (lotus_order_notes) of the caller's org that is not deleted, stored as the three image objects under `<bucket>/<org>/orders/notes/<order_id>/<note_id>/...`, and returns the updated note. The caller needs `<ORDER_TYPE>_NOTES_UPDATE` for the note's order type, and `UPDATE_ALL_<ORDER_TYPE>_NOTES` when the note is someone else's. The route guard only forwards WORK_ORDER_NOTES_UPDATE, RENTAL_NOTES_UPDATE and UPDATE_ALL_WORK_ORDER_NOTES, so another user's rental note is always refused.\n","tags":["Media"],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["files[]","note_id"],"properties":{"files[]":{"type":"array","items":{"type":"string","format":"binary"},"description":"The images (any image/* type). A file sent under any other field name is refused by the upload middleware with a 500.\n"},"note_id":{"type":"string","format":"uuid","example":"dc21b23f-a6ff-43c2-9fc9-2d3f97c07520"}}}}}},"responses":{"200":{"description":"The updated note","content":{"application/json":{"schema":{"$ref":"#/components/schemas/work_order_note"}}}},"400":{"description":"note_id missing or not a uuid (Joi's message about \"value\", code VALIDATION); no files (\"Please attach at least one image\", code VALIDATION); a file that is not an image (\"Only images are allowed\", code VALIDATION).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No or an invalid token, or a caller with no permissions at all (\"Not Authorized. Your account holds no permissions\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"An AI-connector (MCP) session (\"Not available to AI connectors\"); no notes permission for the order type (\"You are not allowed to update <ORDER_TYPE> notes\"); another user's note without the update-all permission (\"You can not update other people' notes\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such note in the caller's org, or it is deleted (\"Note not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/media/orders/note/{note_id}/{image_id}":{"delete":{"summary":"Remove An Image From An Order Note","description":"Removes the image whose path ends in image_id from the note's images. The stored objects are kept. Same permission rules as POST /media/orders/note.\n","tags":["Media"],"parameters":[{"in":"path","name":"note_id","required":true,"schema":{"type":"string","format":"uuid"}},{"in":"path","name":"image_id","required":true,"description":"The last segment of the image's path","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Done","content":{"application/json":{"schema":{"type":"string","example":"Image deleted from order note"}}}},"400":{"description":"An id is not a uuid (Joi's message, code VALIDATION)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No or an invalid token, or a caller with no permissions at all (\"Not Authorized. Your account holds no permissions\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"As POST /media/orders/note","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such note in the caller's org, or it is deleted (\"Note not found\"), or no image with that id on it (\"This image does not exist on this order note\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/oauth/grants":{"get":{"summary":"List the current user's AI-assistant connections","description":"The caller's live connections, newest first (created_at descending). Revoked connections and connections past their expires_at are not listed. No pagination. Read only. Any signed-in user (no permission check); a service-credential session is refused with 401, as it has no connections of its own.\n","tags":["OAuth"],"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/oauth_grant"}}}}},"401":{"description":"Missing or invalid token, or a service-credential session (\"Unauthorized\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"An AI-connector (MCP) session, the route is not on the connector allowlist (\"Not available to AI connectors\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/oauth/grants/{id}":{"delete":{"summary":"Revoke one of the current user's AI-assistant connections","description":"Revokes a live connection owned by the caller. The grant row is kept as the audit trail (revoked_at, revoked_by = the caller, revoked_reason USER_REVOKED), and in the same transaction the connection's stored refresh tokens and authorization codes are deleted, so it can no longer obtain tokens; an access token already issued for it is refused from its next API call. Revoking also ends the caller's OAuth sign-in - their saved OAuth browser sessions and pending sign-ins are deleted, so approving a connection again needs a fresh login. The caller's other connections stay active. A connection past its expires_at that was never revoked is not listed by GET, but can still be revoked here (200). No notification or journal entry. Any signed-in user (no permission check); a service-credential session is refused with 401.\n","tags":["OAuth"],"parameters":[{"$ref":"#/components/parameters/oauth_grant_id"}],"responses":{"200":{"description":"Revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/oauth_grant_revoked"}}}},"400":{"description":"id does not have the connection id format (\"Invalid connection id\", code VALIDATION)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token, or a service-credential session (\"Unauthorized\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"An AI-connector (MCP) session, the route is not on the connector allowlist (\"Not available to AI connectors\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"\"Connection not found\" - no such connection, a connection of another user (it is not disclosed as existing), or one already revoked. Nothing is changed.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/platform/oauth-grants":{"get":{"summary":"List every AI-assistant connection in the org","description":"The live connections of every user in the caller's org, newest first (created_at descending), each with its owner's user_id and name. Revoked connections and connections past their expires_at are not listed. No pagination. Read only. Requires MCP_CONNECTIONS_MANAGE.\n","tags":["OAuth"],"responses":{"200":{"description":"Request Successful","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/oauth_grant"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"An AI-connector (MCP) session, the route is not on the connector allowlist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/platform/oauth-grants/{id}":{"delete":{"summary":"Revoke any AI-assistant connection in the org","description":"Revokes a live connection of any user in the caller's org. The grant row is kept as the audit trail (revoked_at, revoked_by = the caller, revoked_reason ADMIN_REVOKED), and in the same transaction the connection's stored refresh tokens and authorization codes are deleted, so it can no longer obtain tokens; an access token already issued for it is refused from its next API call. Revoking also ends the connection owner's OAuth sign-in - their saved OAuth browser sessions and pending sign-ins are deleted, so approving a connection again needs a fresh login. The owner's other connections stay active. A connection past its expires_at that was never revoked is not listed by GET, but can still be revoked here (200). No notification or journal entry. Requires MCP_CONNECTIONS_MANAGE.\n","tags":["OAuth"],"parameters":[{"$ref":"#/components/parameters/oauth_grant_id"}],"responses":{"200":{"description":"Revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/oauth_grant_revoked"}}}},"400":{"description":"id does not have the connection id format (\"Invalid connection id\", code VALIDATION)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"An AI-connector (MCP) session, the route is not on the connector allowlist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"\"Connection not found\" - no such connection in the caller's org (a connection of another org is not disclosed as existing), or one already revoked. Nothing is changed.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/parts/search":{"post":{"summary":"Search parts","description":"Live (not deleted) parts of the caller's org, paginated. Every filter is ANDed with the others. Requires PARTS_VIEW.\n","tags":["Parts"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/part_search_body"}}}},"responses":{"200":{"description":"One page of parts. knex-paginate is not length-aware here: `pagination.total`, `lastPage`, `prevPage` and `nextPage` come back on page 1 only, and `prevPage` / `nextPage` can be null. Known issue: the validated `paginate` is discarded and the raw one is passed on, so numeric strings (`currentPage: \"1\"`) pass validation but come back as strings in `pagination`, and page 1 then carries no `total` / `lastPage` / `prevPage` / `nextPage` (knex-paginate compares `\"1\" === 1`). Send numbers.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/part_search_result"}}}},"400":{"description":"`search` is checked before `paginate`, so with both wrong the `search` error is the one answered. `search`: an unknown key (`\"x\" is not allowed`); a filter of the wrong type, including `name: null` (`\"name\" must be a string`), `type: null` (`\"type\" must be an array`), `part_number: ''` (`\"part_number\" is not allowed to be empty`), a `type` entry outside the three values (`\"type[0]\" must be one of [supplies, inventoried, non_inventoried]`) and a `query` over 200 characters; an `order_by` entry with a bad `order` or `nulls`. `paginate`: missing, or missing `currentPage` or `perPage` (all three answer \"Paginate object is required\"); `paginate: null` (`\"value\" must be of type object`); a value that is not an integer or out of range (`currentPage` below 1, `perPage` outside 1-100), or an unknown key inside it (`\"isLengthAware\" is not allowed`). A `search` that is not an object (`\"value\" must be of type object`) is refused before either. Last, an `order_by` column outside the list (\"Invalid column name in order_by: <column>\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/part_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/parts":{"post":{"summary":"Create a part","description":"Creates a part in the caller's org. The checks run in this order: `type` (absent counts as `default`) must be one of supplies, inventoried, non_inventoried, default; every field the org's part field settings mark \"requirement\" for that type must be present and truthy (`0`, `false` and `''` count as missing); then the body schema. `quantity` is stored as 0 when PART.PARTS_MANUAL_INVENTORY is off or `purchase_order` is sent. With `purchase_order`, the part is added as a line of that purchase order in the same transaction: the line's quantity_ordered (a smallint) is the posted `quantity` when above 0, else 1, and its unit price is `purchase_price`. Then `validated` / `validation_failures` are computed and one CREATE row is journaled. Requires PARTS_UPDATE.\n","tags":["Parts"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/part_create"}}}},"responses":{"200":{"description":"The stored part","content":{"application/json":{"schema":{"$ref":"#/components/schemas/part_write_result"},"example":{"message":"Part created","data":{"id":"7d1c2f4e-5a6b-4c3d-8e9f-0a1b2c3d4e5f","name":"Hydraulic fitting","category_import":null,"type":"inventoried","type_import":null,"part_number":"HF-1216","item_number":1042,"manufacture":"Parker","model_number":null,"samt":null,"serial_number":null,"model_year":null,"description":null,"bulk_item":null,"bought_used":null,"rental_rates":null,"purchase_price":12.5,"extra_charges":null,"quantity":0,"sell_price":18,"time_out":null,"time_in":null,"purchase_date":"2026-09-30","sold_date":null,"last_date":null,"org":"5e6f7a8b-9c0d-4e1f-8a2b-3c4d5e6f7a8b","deleted":false,"validated":true,"validation_failures":{"manufacture":{"valid":true,"reason":null,"name":"Make"},"model_number":{"valid":true,"reason":null,"name":"Model"},"model_year":{"valid":true,"reason":null,"name":"Year"},"serial_number":{"valid":true,"reason":null,"name":"Serial Number"},"part_number":{"valid":true,"reason":null,"name":"Part Number"},"bin_location":{"valid":true,"reason":null,"name":"Bin Location"},"sell_price":{"valid":true,"reason":null,"name":"Sell Price"},"quantity":{"valid":true,"reason":null,"name":"Quantity Price"},"purchase_price":{"valid":true,"reason":null,"name":"Purchase Price"},"purchase_date":{"valid":true,"reason":null,"name":"Purchase Date"},"purchase_order":{"valid":true,"reason":null,"name":"Purchase Order"}},"bin_location":"A-12","capitalization":false,"purchase_order":null,"is_taxable":true}}}}},"400":{"description":"One of, in check order: a `type` outside the four values (`\"value\" must be one of [supplies, inventoried, non_inventoried, default]`); a field the org's settings require for this type is missing (\"Field '<key>' is required to create a part\"; when several are missing, which one is named is unspecified); a schema error, including no body or no `name` (`\"name\" is required`), no `type` (`\"type\" is required`), an empty string where the schema doesn't allow one (`\"name\" is not allowed to be empty`), a `bought_used` that is neither a boolean nor the string \"true\" / \"false\" (`\"bought_used\" must be a boolean`), a `quantity` of -1 or less (`\"quantity\" must be greater than -1`) or any other key (`\"item_number\" is not allowed`). Then \"Invalid data type provided\" for a `quantity` the integer column can't hold: without `purchase_order`, a fraction or a value between -1 and 0 while manual inventory is on (with it off the part stores 0); with `purchase_order` the part stores 0, and only a positive fraction is refused, after the purchase-order checks below, because it goes to the line's smallint quantity_ordered (0 or less orders 1). With `purchase_order`: no live purchase order with that id in the org (\"Purchase order doesn't exist\"); one that is no longer 'new' (\"Purchase order is finalized. Please select an existing open purchase order or create a new one\"); a supplies part (\"Item(<name>) is a 'supplies' type part which can't be attached to POs\"); or, unless the org setting PO.ASSET_PART_MIX_PO is on, a purchase order that already orders assets (\"Both parts and assets can't be ordered via a single PO\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/part_ai_connector_forbidden"},"500":{"description":"Any unexpected error. Known issues: `asset_number` passes the schema but is not a parts column, so the insert fails; a `bin_location` over 255 characters (varchar(255)) and a `quantity` out of its column's range (above 32767 with `purchase_order`, which goes to the line's smallint quantity_ordered) raise Postgres errors the handler doesn't map.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/parts/{id}":{"get":{"summary":"Get a part","description":"The live part, with its purchase order's number. Requires PARTS_READ.","tags":["Parts"],"parameters":[{"$ref":"#/components/parameters/part_id"}],"responses":{"200":{"description":"The part","content":{"application/json":{"schema":{"$ref":"#/components/schemas/part_detail"}}}},"400":{"description":"A malformed id (\"Invalid data type provided\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/part_ai_connector_forbidden"},"404":{"description":"No live part with this id in the caller's org (\"Part doesn't exist\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"summary":"Update a part","description":"Writes the sent fields; omitted fields keep their values. The checks run in this order: `type` (absent counts as `default`) must be one of the four values; every field the org's part field settings mark \"requirement\" for that type must be present and truthy in the BODY (`0`, `false` and `''` count as missing). Known issue: the stored part is not consulted, so a partial update is refused in an org with requirements, and without `type` the `default` column applies even to an inventoried part; the web app sends the whole part back on every save. Then the id, then the body schema. `purchase_order` and `org` are accepted but dropped (the purchase-order link is create-only and the session decides the org); `quantity` is dropped while PART.PARTS_MANUAL_INVENTORY is off. The part is then revalidated (`validated`, `validation_failures`) and one UPDATE row of the changed fields is journaled. Requires PARTS_UPDATE.\n","tags":["Parts"],"parameters":[{"$ref":"#/components/parameters/part_id"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/part_update"}}}},"responses":{"200":{"description":"The stored part","content":{"application/json":{"schema":{"$ref":"#/components/schemas/part_write_result"},"example":{"message":"Part updated","data":{"id":"7d1c2f4e-5a6b-4c3d-8e9f-0a1b2c3d4e5f","name":"Hydraulic fitting 3/4in","category_import":null,"type":"inventoried","type_import":null,"part_number":"HF-1216","item_number":1042,"manufacture":"Parker","model_number":null,"samt":null,"serial_number":null,"model_year":null,"description":null,"bulk_item":null,"bought_used":null,"rental_rates":null,"purchase_price":12.5,"extra_charges":null,"quantity":0,"sell_price":18,"time_out":null,"time_in":null,"purchase_date":"2026-09-30","sold_date":null,"last_date":null,"org":"5e6f7a8b-9c0d-4e1f-8a2b-3c4d5e6f7a8b","deleted":false,"validated":true,"validation_failures":{"manufacture":{"valid":true,"reason":null,"name":"Make"},"model_number":{"valid":true,"reason":null,"name":"Model"},"model_year":{"valid":true,"reason":null,"name":"Year"},"serial_number":{"valid":true,"reason":null,"name":"Serial Number"},"part_number":{"valid":true,"reason":null,"name":"Part Number"},"bin_location":{"valid":true,"reason":null,"name":"Bin Location"},"sell_price":{"valid":true,"reason":null,"name":"Sell Price"},"quantity":{"valid":true,"reason":null,"name":"Quantity Price"},"purchase_price":{"valid":true,"reason":null,"name":"Purchase Price"},"purchase_date":{"valid":true,"reason":null,"name":"Purchase Date"},"purchase_order":{"valid":true,"reason":null,"name":"Purchase Order"}},"bin_location":"A-14","capitalization":false,"purchase_order":null,"is_taxable":true}}}}},"400":{"description":"One of, in check order: a `type` outside the four values (`\"value\" must be one of [supplies, inventoried, non_inventoried, default]`); a field the org's settings require is missing from the body (\"Field '<key>' is required for parts\"); a malformed id (`\"id\" must be a valid GUID`); a schema error, including a null where the schema refuses one (`\"name\" must be a string`, `\"capitalization\" must be a boolean`) or any other key (`\"item_number\" is not allowed`); nothing left to write: no body, `{}`, only `org` / `purchase_order`, or only `quantity` while manual inventory is off (\"No part fields to update\"); a `quantity` the integer column can't hold, i.e. a fraction or a value between -1 and 0 while manual inventory is on (\"Invalid data type provided\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/part_ai_connector_forbidden"},"404":{"description":"No live part with this id in the caller's org (\"Part doesn't exist\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Any unexpected error. Known issues: `asset_number` passes the schema but is not a parts column, so the update fails; a `bin_location` over 255 characters (varchar(255)) and a `quantity` above the integer column's range raise Postgres errors the handler doesn't map.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"summary":"Delete a part","description":"Soft-deletes the part (`deleted` = true) and journals one DELETE row. Its purchase-order lines, if any, are left as they are. Afterwards GET, update and delete answer 404 for it, and search leaves it out. Requires PARTS_DELETE.\n","tags":["Parts"],"parameters":[{"$ref":"#/components/parameters/part_id"}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","required":["message"],"properties":{"message":{"type":"string","example":"Part deleted"}}}}}},"400":{"description":"A malformed id (\"Invalid data type provided\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/part_ai_connector_forbidden"},"404":{"description":"No live part with this id in the caller's org, including one already deleted (\"Part doesn't exist\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/platform/logo":{"post":{"summary":"Upload The Organization Logo","description":"Stores the uploaded file in cloud storage under <bucket>/<org>/platform_media/<random uuid> and writes that path to the organization's `logo` column. Any file type and size is accepted (the whole file is held in memory); the previous logo object is not deleted. Writes one `platform_settings` journal row with a `logo` change (DOZ-7269). Requires PLATFORM_GENERAL_UPDATE.\n","tags":["Platform"],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["logo_file"],"properties":{"logo_file":{"type":"string","format":"binary","description":"The logo image. A file sent under any other field name is refused by the upload middleware with a 500.\n"}}}}}},"responses":{"200":{"description":"The updated organizations row as stored (every column except stripe_private_key and stripe_webhook_secret), with `logo` set to the new storage path. Unlike GET /platform/general it has no `modules`, `cards` or asset counts.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/platform_org"}}}},"400":{"description":"No file under logo_file (\"Please attach the logo_file media\", code VALIDATION)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/platform_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/platform/assets_liability_file":{"post":{"summary":"Import The Assets Liability File","description":"Replaces ALL of the org's assets liability rows (the loan / payoff records listed by POST /assets/liability) with the rows of the uploaded Excel workbook, in one transaction - the old rows are kept when the insert fails. The first worksheet is read; row 1 (the header) and the last row (treated as a footer) are skipped. Columns A-K are, in order - account_number, asset_number, year_make_model, loan_date, loan_amount, interest_rate, current_principal_balance, accrued_interest, current_payoff, per_diem, paid_off_date. Cells are stored as read, with no header or per-cell check. Each row's `asset` column is meant to link the org's asset with that asset_number; today it is always stored null. The file itself is not kept. No journal row. Requires PLATFORM_GENERAL_UPDATE.\n","tags":["Platform"],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary","description":"An .xlsx workbook. A CSV, .xls or unreadable file, a workbook without a first worksheet, or a file under another field name answers 500.\n"}}}}}},"responses":{"200":{"description":"Imported. The body is a bare JSON string.","content":{"application/json":{"schema":{"type":"string","example":"Liability file imported"}}}},"400":{"description":"No file (\"No file was attached\"); no data row between the header and the last row (\"No liability records found in the uploaded file\") - both code VALIDATION, and nothing is deleted; a cell whose value does not fit its column (\"Invalid data type provided\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/platform_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/platform":{"post":{"summary":"Update The Organization Profile","description":"Partial update of the caller's organization profile - name, logo path, address, the notification recipient lists, daily summary report preferences, accounts, Slack, the Stripe publishable key, the default facility and allow_fake_login. Only the keys sent are written. Unknown keys are dropped silently at every level (stripUnknown), so read-only columns echoed back from GET /platform/general (id, stripe_id, cards, modules, ...) are ignored rather than refused.\n\nOrg settings are no longer stored here (DOZ-7318). A key that used to be a settings column of the organization (timezone, the taxes and fees, cc_processing_fee, payment terms, delivery rates, the creation settings, work_order_settings, ...) is dropped with a server-side warning and the call still answers 200 - save settings with PUT /lotus/platform-settings. required_customer_fields is validated and then ignored. A body with nothing left to write (no body, {}, only unknown or settings keys) is a no-op: 200 with the organization, nothing written.\n\njsonb bags (accounts, address, slack_urls, the recipient bags, daily_summary_report_preferences) are stored as sent, replacing the whole stored value; accounts and address require every inner key once the bag is sent.\n\nrental_app_document is the one setting still accepted here. It is taken out of the body before validation and saved to the CUSTOMER.RENTAL_APP_DOCUMENT setting through the settings store: a non-string, empty or blank value clears it (null), and a change writes the settings store's journal row. It is never part of the response. It is saved after the profile columns and outside their write, so when the settings store refuses it (the 403s below) the profile columns are already saved.\n\nRequires PLATFORM_GENERAL_UPDATE. Side effects - when at least one profile key is left after validation, a journal row (component platform_general) holding the organization row as it was before the save (Stripe secrets removed), then the organizations update.\n","tags":["Platform"],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/platform_org_update"}}}},"responses":{"200":{"description":"The organization after the save - the GET /platform/general object without `cards`, motorized_assets_count and non_motorized_assets_count.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/platform_org"}}}},"400":{"description":"Validation failed on a profile key (code VALIDATION), e.g. an address missing one of its five keys or a default_facility that is not a uuid; or a rental_app_document the setting's definition refuses\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"An AI-connector (MCP) session (\"Not available to AI connectors\"); or, for rental_app_document, the settings store refusing it - \"Setting CUSTOMER.RENTAL_APP_DOCUMENT is not available to this organization's modules\" (an org without the module) or \"You are not allowed to change CUSTOMER.RENTAL_APP_DOCUMENT\" (the caller's roles lack the setting's permission, checked even when RBAC is relaxed). The profile columns are already saved by then.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/platform/fields/settings":{"get":{"summary":"List The Field Settings","description":"The org's field settings - which fields of an asset, part, customer, customer site or vendor are required, validated, optional or omitted, per record type - plus the asset-cloning settings when asked for by name. Without `entity` every row except the asset_cloning ones is returned. The `depreciation_schedules` row is left out while the org has depreciation switched off, and the `capitalization` row while it has capitalization switched off (except for entity=asset_cloning). Ordered by name. Read only. Requires PLATFORM_FIELD_SETTINGS_READ.\n","tags":["Platform"],"parameters":[{"in":"query","name":"entity","required":false,"schema":{"type":"string","enum":["asset","part","customer","vendor","customer_site","asset_cloning"]},"description":"Only this entity's rows. Any other query parameter is refused with 400 ('\"x\" is not allowed').\n"}],"responses":{"200":{"description":"The field settings","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/field_settings_schema"}}}}},"400":{"description":"An unknown entity or an unknown query parameter (code VALIDATION)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/platform_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"summary":"Update Field Settings In Bulk","description":"Sets the `setting` of existing field-settings rows of the caller's org, matched by key + entity (the `org` sent is replaced by the session org). Rows are never created: an item that matches no row changes nothing and comes back as null in `data`. All items are written in one transaction; when any item fails (for example an item without `setting`) nothing is saved and the answer is still 200, with `data` empty. The body is stored as sent (Joi's conversions, such as \"true\" to true, are not applied). An empty array answers 200 with nothing changed.\n\nSide effects - when the saved rows of an entity differ from before, a background re-validation of every record of that type in the org starts (not awaited) - customer or customer_site changes re-validate all customers, asset all assets, part all parts, vendor all vendors; asset_cloning changes trigger nothing. Each re-validation updates the records' validated / validation_failures and ends with a system notification (email and Slack) to the org. Requires PLATFORM_FIELD_SETTINGS_UPDATE.\n","tags":["Platform"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/field_settings_update_item"},"example":[{"key":"serial_number","entity":"asset","org":"b67aba8b-9819-4179-b2a1-384968cfa963","setting":{"default":"requirement","normal":"validation","add_on":"optional"}}]}}}},"responses":{"200":{"description":"Saved (or nothing saved, see above)","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","example":"Field Settings Updated"},"data":{"type":"array","description":"The updated rows in request order; null for an item that matched no row; empty when the batch failed.\n","items":{"allOf":[{"$ref":"#/components/schemas/field_settings_schema"}],"nullable":true}}}}}}},"400":{"description":"Validation failed (code VALIDATION) - no body or not an array ('\"value\" must be an array'), a missing key / entity / org, an unknown item key ('\"[0].name\" is not allowed'), a setting that does not fit its entity ('\"[0].setting.default\" is required'); a tuple without a setting (\"Nothing to update for 'key' = '<key>'\", DOZ-7269: the save is one transaction and a failure answers its real status instead of a 200 with an empty data array)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/platform_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/platform/entity/validate":{"post":{"summary":"Validate A Record Against The Field Settings","description":"Checks a record (usually a form being filled in) against the org's field settings for its entity and reports which required or validated fields fail. Nothing is saved. The context used is the record's own type (`type` for assets and parts, `pay_on_open` for customers) and `default` when it has none; vendors always use `default`. For an asset with a `category`, the category's rental rates and, when `depreciation_schedules` is an empty array, its depreciation schedules are used as the inherited values; the depreciation and capitalization rules are skipped while those features are off for the org. Some asset and customer rules look rows up by `data.id` (media, depreciation schedules, sites, rental and credit applications), so send the record's id; without it, or with a category that is not the org's, the request fails with 500. Requires any one of ASSET_READ, PARTS_READ, CUSTOMER_READ, VENDOR_READ (any of them for any entity).\n","tags":["Platform"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/entity_validate_body"}}}},"responses":{"200":{"description":"The validation result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/entity_validation_result"}}}},"400":{"description":"entity missing or not one of asset, part, customer, vendor (\"Invalid entity for validations\", also for no body); data missing or not an object (\"data object is required\") - both code VALIDATION; a malformed id inside data (\"Invalid data type provided\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/platform_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/platform/general":{"get":{"summary":"Get The Organization Profile","description":"The caller's organization profile - every organizations column except stripe_private_key and stripe_webhook_secret, the module catalog with this org's enabled flags, the latest asset counts and an always-empty `cards`. Org settings are not part of it (DOZ-7318): read them with GET /lotus/platform-settings. Read only. Requires PLATFORM_GENERAL_VIEW.\n","tags":["Platform"],"responses":{"200":{"description":"The organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/platform_org_general"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/platform_ai_connector_forbidden"},"404":{"description":"\"Organization not found\" - also the answer when the organization read fails with a database error\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/platform/notifications":{"get":{"summary":"Get The Notification Settings","description":"Every notification of the global catalog, each with this org's email recipients, Slack webhook and the two on/off switches (empty list, null and false when the org never saved that notification). Ordered by weight, then notification_key. Read only. Requires PLATFORM_GENERAL_VIEW.\n","tags":["Platform"],"responses":{"200":{"description":"The notification settings","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotificationSettingsResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/platform_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/platform/notifications/update":{"post":{"summary":"Update Notification Settings In Bulk","description":"Saves the org's settings for the notifications sent, creating the org's row on a first save. Only emails, slack_webhook, emails_active and slack_active are written, and only the ones present in an item - an omitted field keeps its stored value; org_id, notification_name, notification_group and weight are accepted and ignored. A key sent twice - the last item wins. One transaction. An empty array answers 200 with [] and changes nothing. Effect - changes who receives the org's notification emails and which Slack webhook receives its posts. Writes one `platform_settings` journal row with a `notifications.<key>.<column>` entry per sent value that changed, and none when nothing did (DOZ-7269). Requires PLATFORM_GENERAL_UPDATE.\n","tags":["Platform"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/notification_setting_update"},"example":[{"notification_key":"off_rent_inspection","emails":["jane.doe@example.com"],"slack_webhook":"https://hooks.slack.com/services/T000/B000/XXXX","emails_active":true,"slack_active":false}]}}}},"responses":{"200":{"description":"The full list after the save, exactly as GET /platform/notifications returns it","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotificationSettingsResponse"}}}},"400":{"description":"Validation failed (code VALIDATION) - no body or not an array ('\"value\" must be an array'), an item without notification_key, an invalid email or webhook URI, an unknown item key; a key that is not in the catalog (\"Unknown notification keys: a, b\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/platform_ai_connector_forbidden"},"500":{"description":"Unexpected error, e.g. a slack_webhook longer than 255 characters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/platform/roles/create":{"post":{"summary":"Create A Role","description":"Creates an empty role (no permissions) in the caller's org. A customer-portal session always creates it for its own customer (customer_id is taken from the session); for an org user customer_id comes from the body. A role with a customer_id must be of type cust. Role names are unique among the org's live roles. Writes one `role` CREATE journal row (DOZ-7267). Requires PLATFORM_ROLE_UPDATE.\n","tags":["Platform"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/role_create_body"}}}},"responses":{"200":{"description":"The new role (permissions empty, assignment_count \"0\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/role"}}}},"400":{"description":"Validation failed (code VALIDATION) - including no body ('\"name\" is required') and unknown keys; a customer_id with type org (\"Invalid role type\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/platform_ai_connector_forbidden"},"409":{"description":"A live role of the org already has this name (\"Duplicate field value entered for: name, org_id. Please use another value.\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Unexpected error, e.g. a name longer than 255 characters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/platform/roles/clone":{"post":{"summary":"Clone A Role","description":"Creates a new role in the caller's org from an existing one - a global default role or one of the org's own, live and visible under the org's enabled modules. The copy takes the source's type and those of its permissions that belong to the org's enabled modules, with the name and description sent; its modules are null (visible to the org whatever its modules). customer_id is the session's for a customer-portal user, else the body's. Writes one `role` CLONE journal row naming the source role and every permission the copy inherited (DOZ-7267). Requires PLATFORM_ROLE_UPDATE.\n","tags":["Platform"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/role_clone_body"}}}},"responses":{"200":{"description":"The new role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/role"}}}},"400":{"description":"Validation failed (code VALIDATION) - including no body ('\"role_id\" is required') and an empty description; role_id is not a uuid (\"Invalid data type provided\"); a customer clone of an org role (\"Customers can not clone org roles | Org roles can not be assigned a customer ID\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/platform_ai_connector_forbidden"},"404":{"description":"No such live role visible to the org (\"Role to be cloned was not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"A live role of the org already has this name (\"Duplicate field value entered for: name, org_id. Please use another value.\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/platform/roles/{id}":{"post":{"summary":"Rename A Role","description":"Changes the name and/or description of one of the org's own live roles (global default roles answer 404). A customer-portal session can only change a role of its own customer; any other role answers 404. Permissions are set with POST /platform/roles. Writes one `role` UPDATE journal row of the fields that changed, and none when nothing did (DOZ-7267). Requires PLATFORM_ROLE_UPDATE.\n","tags":["Platform"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"The role id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/role_update_body"}}}},"responses":{"200":{"description":"The role after the change. The body is empty when the role is outside the org's enabled modules.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/role"}}}},"400":{"description":"id is not a uuid ('\"id\" must be a valid GUID'); validation failed (an empty or null value, an unknown key); no body or {} (\"Nothing to update\") - all code VALIDATION\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/platform_ai_connector_forbidden"},"404":{"description":"Not a live role of the caller's org or, for a customer-portal session, not one of its customer's roles (\"Role not found\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Another live role of the org has this name (\"Duplicate field value entered for: name, org_id. Please use another value.\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"summary":"Delete A Role","description":"Soft-deletes one of the org's own live roles (deleted = true; its permission links are kept and its name becomes free again). Refused while any user row holds the role - human or service-credential user, deleted users included; unassign it first. A customer-portal session can only delete a role of its own customer. Global default roles cannot be deleted. Writes one `role` DELETE journal row whose snapshot keeps the role and its permissions (DOZ-7267). Requires PLATFORM_ROLE_UPDATE.\n","tags":["Platform"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"The role id."}],"responses":{"200":{"description":"Deleted. The body is a bare JSON string.","content":{"application/json":{"schema":{"type":"string","example":"Role deleted"}}}},"400":{"description":"id is not a uuid ('\"id\" must be a valid GUID', code VALIDATION)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/platform_ai_connector_forbidden"},"404":{"description":"Not a live role of the caller's org (or of the customer) - \"Role not found\"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Someone holds the role (\"Can't delete assigned roles\"). This is checked before the org check, so a shared default role or another org's role that any user holds answers 409, not 404.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/platform/roles":{"get":{"summary":"List Roles","description":"The org's own live roles plus the global default roles, sorted by name, each with its permissions and how many people hold it. A role tagged with modules the org has not enabled is left out, and each role's permission list only shows permissions of the org's enabled modules. A customer-portal session only sees cust roles that are its customer's or have no customer. Read only. Requires PLATFORM_ROLE_VIEW.\n","tags":["Platform"],"parameters":[{"in":"query","name":"type","required":false,"schema":{"type":"string","enum":["org","cust"]},"description":"Only roles of this type. Ignored when `customer` is sent."},{"in":"query","name":"default","required":false,"schema":{"type":"string","enum":["true","false"]},"description":"true - the global org roles and the cust roles with no customer; false - the org's own org roles and the cust roles of a customer. Ignored when `customer` is sent.\n"},{"in":"query","name":"customer","required":false,"schema":{"type":"string","format":"uuid"},"description":"Only the roles of this customer."}],"responses":{"200":{"description":"The roles","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/role"}}}}},"400":{"description":"A filter value outside its allowed values (code VALIDATION)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"A customer-portal session sending `customer` or type=org (\"Unauthorized: You can't do that\"), or an AI-connector (MCP) session\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"summary":"Set The Permissions Of Roles","description":"Saves the permission list of one or more roles; each entry's `permissions` becomes that role's list as far as the org can see it, and an empty list clears it. Every entry is checked before anything is saved - the role must exist and be live (\"Role doesn't exist\", 404), must not be a global default role (\"Default roles can not be edited/updated\"), must belong to the caller's org (\"Role does not belong to your organization\"); for a customer-portal user it must not be an org role ('You are not allowed to update an \"org\" role') and must be one of its own customer's roles (\"Role does not belong to your customer\"); a cust role may only get cust permissions (\"'org' permissions can not be assigned to 'customer' roles\"), and every permission must belong to a module the org has enabled (\"The following permissions belong to modules that are not enabled for this organization: A, B\"). One refused entry refuses the whole request and nothing is saved. Then, in one transaction, each role's links to permissions of the org's enabled modules that are not in its list are removed (links to permissions outside those modules are kept) and all listed links are added (existing ones are left as they are). Permission ids are not checked for existence. The change applies to every holder of the role from their next request. Writes one `role` UPDATE journal row per role whose permissions changed, with one `permissions[<NAME>]` entry per permission granted or revoked; the rows of one request share a batch_id (DOZ-7267). Requires PLATFORM_ROLE_UPDATE.\n","tags":["Platform"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/role_permissions_body"}}}},"responses":{"200":{"description":"Saved. The body is a bare JSON string.","content":{"application/json":{"schema":{"type":"string","example":"Permissions for roles are updated"}}}},"400":{"description":"Validation failed (code VALIDATION) - no body ('\"value\" is required'), an empty array, a missing id or permissions, a non-uuid id; one of the role checks above\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/platform_ai_connector_forbidden"},"404":{"description":"An entry's role does not exist or is deleted (\"Role doesn't exist\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/platform/permissions":{"get":{"summary":"List Permissions","description":"Every permission of the global catalog that belongs to a module the org has enabled (or to no module), ordered by permission_group then name. A customer-portal session gets only the cust permissions. Read only. Requires PLATFORM_ROLE_VIEW.\n","tags":["Platform"],"responses":{"200":{"description":"The permissions","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/permission"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/platform_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/platform/payment_method_client_secret":{"get":{"summary":"Get A Stripe SetupIntent Client Secret For The Organization","description":"Starts saving a card for the organization itself, in the org's OWN Stripe account (the account configured on the org, not a CloudERM platform account). On the first call it creates a Stripe customer named \"<org name>-[org]\" in that account and stores its id in organizations.stripe_id; every call then creates a new card SetupIntent for that customer and returns its client secret, for Stripe.js to confirm. No audit row. Any signed-in user of the org, customer-portal users included (no permission check); a service-credential session is refused.\n","tags":["Platform"],"responses":{"200":{"description":"The SetupIntent client secret","content":{"application/json":{"schema":{"type":"object","properties":{"client_secret":{"type":"string","example":"seti_1PqR2sT3uV4wX5yZ_secret_AbCdEfGhIjKlMnOp"}}}}}},"400":{"description":"The org has no Stripe account configured (\"Stripe not configured for this Org\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Not signed in, or a service-credential session (\"Unauthorized\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"$ref":"#/components/responses/platform_ai_connector_forbidden"},"404":{"description":"Organization not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Unexpected error, including any Stripe API error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/platform/set_default_payment_method":{"post":{"summary":"Set The Organization's Default Payment Method","description":"Records a card already saved to the organization's Stripe customer (in the org's own Stripe account) as organizations.default_payment_method. The payment method is read from Stripe and must belong to that customer. Only the column is written; the Stripe customer's own default is not changed. Writes a PAYMENT_METHOD_UPDATED financial audit row with the previous and new value. Requires PLATFORM_GENERAL_UPDATE.\n","tags":["Platform"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/platform_payment_method_body"}}}},"responses":{"200":{"description":"Saved. The body is a bare JSON string.","content":{"application/json":{"schema":{"type":"string","example":"default payment method set for org"}}}},"400":{"description":"payment_method missing, also for no body (\"payment_method is required\", code VALIDATION); the org has no Stripe customer yet (\"No payment methods are attached to this organization\"); no Stripe account configured (\"Stripe not configured for this Org\"); the payment method belongs to another customer (\"This payment method is not attached to this organization\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/platform_ai_connector_forbidden"},"404":{"description":"Organization not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Unexpected error, including any Stripe API error (e.g. an unknown payment method id)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/platform/detach_payment_method":{"post":{"summary":"Remove A Payment Method From The Organization","description":"Detaches a card from the organization's Stripe customer (in the org's own Stripe account) - irreversible on the Stripe side. The payment method must belong to that customer. When it was organizations.default_payment_method, that column is cleared. Writes a PAYMENT_METHOD_UPDATED financial audit row (detached id, was_default). Requires PLATFORM_GENERAL_UPDATE.\n","tags":["Platform"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/platform_payment_method_body"}}}},"responses":{"200":{"description":"Detached. The body is a bare JSON string.","content":{"application/json":{"schema":{"type":"string","example":"payment method detached from org"}}}},"400":{"description":"payment_method missing, also for no body (\"payment_method is required\", code VALIDATION); the org has no Stripe customer yet (\"No payment methods are attached to this organization\"); no Stripe account configured (\"Stripe not configured for this Org\"); the payment method belongs to another customer (\"This payment method is not attached to this organization\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/platform_ai_connector_forbidden"},"404":{"description":"Organization not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Unexpected error, including any Stripe API error (e.g. an unknown payment method id)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/platform/facilities":{"get":{"summary":"List Facilities","description":"The org's live facilities (locations / yards), in no particular order. Read only. Requires PLATFORM_FACILITY_VIEW. On the AI-connector (MCP) allowlist since DOZ-8322, for the connector's list_facilities tool.\n","tags":["Platform"],"responses":{"200":{"description":"The facilities","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/facility"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"An AI-connector (MCP) session that lacks PLATFORM_FACILITY_VIEW (the route is on the connector allowlist)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"summary":"Create Or Update A Facility","description":"Without `id` a facility is created in the caller's org. With the id of one of the org's live facilities, that facility is updated (only the fields sent; zip keeps its value when omitted). An id that exists nowhere creates a facility with that id; the id of another org's facility or of a deleted one answers 404. Facility names need not be unique. A create writes one `facility` CREATE journal row; an update writes one `facility` UPDATE row of the fields that changed, and none when nothing did (DOZ-7271). Requires PLATFORM_FACILITY_UPDATE.\n","tags":["Platform"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/facility_body"}}}},"responses":{"200":{"description":"Created or updated (200 for both)","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","example":"Facility created/updated"},"data":{"$ref":"#/components/schemas/facility_record"}}}}}},"400":{"description":"Validation failed (code VALIDATION) - including no body ('\"name\" is required'), an empty name/address/state/city and unknown keys\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/platform_ai_connector_forbidden"},"404":{"description":"id is another org's facility or a deleted one (\"Facility not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Unexpected error, e.g. a value longer than 255 characters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/platform/facilities/{id}":{"delete":{"summary":"Delete A Facility","description":"Soft-deletes one of the org's live facilities (deleted = true). Records that point at it (assets, customers, cash drawers, the org's default_facility) are not changed. Writes one `facility` DELETE journal row whose snapshot keeps the facility (DOZ-7271). Requires PLATFORM_FACILITY_DELETE.\n","tags":["Platform"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"The facility id."}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","example":"Facility deleted"}}}}}},"400":{"description":"id is not a uuid ('\"id\" must be a valid GUID', code VALIDATION)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/platform_ai_connector_forbidden"},"404":{"description":"Not a live facility of the caller's org (\"Facility not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/platform/part_bundles":{"post":{"summary":"Create A Part Bundle","description":"Creates a named set of parts with their quantities and billing flags, used to add several parts to a task at once. Every part must be a live part of the caller's org, listed once. Only parts of type supplies may have a decimal quantity. The bundle and its items are inserted in one transaction, with one `part_bundle` CREATE journal row whose snapshot holds the bundle and its items (DOZ-7271). Requires PLATFORM_PART_BUNDLES_CREATE.\n","tags":["Platform"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/part_bundle_body"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","example":"Part bundle created successfully"},"data":{"$ref":"#/components/schemas/part_bundle_created"}}}}}},"400":{"description":"Validation failed (code VALIDATION) - including no body ('\"name\" is required'), no items, the same part twice, an unknown key; a part that is not a live part of the org (\"One or more parts do not exist\"); a decimal (or missing) quantity on a part that is not of type supplies (\"Invalid quantity for part '<name>'. Only parts of type 'supplies' can have decimal quantities. You provided <quantity>.\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/platform_ai_connector_forbidden"},"409":{"description":"The org already has a bundle with this exact name (\"A part bundle with the name '<name>' already exists. Please choose a different name.\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/platform/part_bundles/search":{"post":{"summary":"Search Part Bundles","description":"Paginated part bundles of the caller's org, each with its items and the item's part details. name matches anywhere in the bundle name, case-insensitive. Default order - name ascending. Read only. Requires PLATFORM_PART_BUNDLES_VIEW.\n","tags":["Platform"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/part_bundle_search_body"}}}},"responses":{"200":{"description":"One page of bundles","content":{"application/json":{"schema":{"$ref":"#/components/schemas/part_bundle_search_result"}}}},"400":{"description":"paginate missing or not an object, also for no body (\"paginate object is required\"); search missing or not an object (\"search object is required\") - both without code; validation failed (code VALIDATION - \"Paginate object is required\" when currentPage or perPage is missing, '\"perPage\" must be less than or equal to 100', an unknown search key); an order_by column outside orderByColumnNamesList (\"Invalid column name in order_by: x\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/platform_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/platform/part_bundles/{id}":{"get":{"summary":"Get A Part Bundle","description":"One part bundle of the caller's org with its items and the item's part details (also for parts deleted since). Read only. Requires PLATFORM_PART_BUNDLES_VIEW.\n","tags":["Platform"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"The part bundle id."}],"responses":{"200":{"description":"The bundle (not wrapped in data)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/part_bundle"}}}},"400":{"description":"id is not a uuid ('\"id\" must be a valid GUID', code VALIDATION)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/platform_ai_connector_forbidden"},"404":{"description":"No bundle with this id in the caller's org (\"Part bundle not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"summary":"Update A Part Bundle","description":"Replaces a bundle - name and task_type are set (an omitted task_type keeps the stored one) and every item is deleted and re-inserted from `items`, in one transaction. Same part rules as the create. Writes one `part_bundle` UPDATE journal row of what changed - name, task_type and an `items[<part_id>]` entry per part added, removed or changed - and none when nothing did (DOZ-7271). Requires PLATFORM_PART_BUNDLES_UPDATE.\n","tags":["Platform"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"The part bundle id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/part_bundle_body"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","example":"Part bundle updated successfully"},"data":{"$ref":"#/components/schemas/part_bundle_updated"}}}}}},"400":{"description":"id is not a uuid ('\"id\" must be a valid GUID'); validation failed (code VALIDATION - including no body ('\"name\" is required')); \"One or more parts do not exist\"; a decimal (or missing) quantity on a part not of type supplies (\"Invalid quantity for part '<name>'. ...\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/platform_ai_connector_forbidden"},"404":{"description":"No bundle with this id in the caller's org (\"Part bundle not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Another bundle of the org has this exact name (\"A part bundle with the name '<name>' already exists. Please choose a different name.\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"summary":"Delete A Part Bundle","description":"Permanently deletes a bundle and its items (hard delete, one transaction). Writes one `part_bundle` DELETE journal row whose snapshot keeps the bundle and its items (DOZ-7271). Requires PLATFORM_PART_BUNDLES_DELETE.\n","tags":["Platform"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"The part bundle id."}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","example":"Part bundle deleted successfully"}}}}}},"400":{"description":"id is not a uuid ('\"id\" must be a valid GUID', code VALIDATION)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/platform_ai_connector_forbidden"},"404":{"description":"No bundle with this id in the caller's org (\"Part bundle not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/platform/service-credentials":{"get":{"summary":"List Service API Credentials","description":"The org's service API credentials (pre-shared secrets an internal service uses to call this API with HMAC-signed requests), newest first, revoked ones included. The secret is never returned. role_id / role_name come from the credential's service user and are null once the credential is revoked or its role deleted. Read only. Requires SERVICE_CREDENTIAL_READ.\n","tags":["Platform"],"responses":{"200":{"description":"The credentials","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/service_credential"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/platform_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"summary":"Create A Service API Credential","description":"Mints a credential - a new key id (cerm_k_ + 32 hex characters) and a server- generated secret (cerm_s_ + 64 hex characters), stored encrypted. The secret is in this response and nowhere else, ever; if it is lost, rotate the credential. The credential acts as a dedicated service user created with it (no username, no login, no email sent) that holds `role_id`; what the credential can reach is exactly that role's permissions. The role must be an org-type role whose modules the org has enabled. The service user, the credential and its secret are inserted in one transaction. Requires SERVICE_CREDENTIAL_CREATE.\n","tags":["Platform"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/service_credential_create_body"}}}},"responses":{"201":{"description":"Created - copy the secret now","content":{"application/json":{"schema":{"$ref":"#/components/schemas/service_credential_created"}}}},"400":{"description":"Validation failed (code VALIDATION) - including unknown keys; a request with no body answers \"expires_at is not a valid date\"; expires_at in the past (\"expires_at must be in the future\") or too far ahead (\"expires_at cannot be more than 390 days in the future\"); role_id is not an org-type role (\"Invalid role\") or its modules are not enabled (\"The following roles belong to modules that are not enabled for this organization: X\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/platform_ai_connector_forbidden"},"500":{"description":"Unexpected error, or the server's credential encryption key is missing or malformed (the message says so)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/platform/service-credentials/{id}":{"patch":{"summary":"Update A Service API Credential","description":"Renames, re-describes, re-roles or changes the expiry of a credential. A new name is also written to its service user; a new role replaces the service user's role (it applies from the credential's next request). An expired credential can be given a new expiry and works again. Revoked credentials cannot be changed. A request with no body at all changes only updated_by / updated_at and answers 200. The secret is not returned. Requires SERVICE_CREDENTIAL_UPDATE.\n","tags":["Platform"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"The credential id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/service_credential_update_body"}}}},"responses":{"200":{"description":"The updated credential row","content":{"application/json":{"schema":{"$ref":"#/components/schemas/service_credential_row"}}}},"400":{"description":"id is not a uuid (\"Invalid service credential id\"); validation failed - {} (\"Provide at least one field to update\"), an unknown key; an expires_at or role_id refused as on the create - all code VALIDATION; a revoked credential (\"A revoked service credential cannot be updated\", code CONFLICT)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/platform_ai_connector_forbidden"},"404":{"description":"No live credential with this id in the caller's org (\"Service credential not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"summary":"Revoke A Service API Credential","description":"Revokes a credential for good - its stored secret is destroyed, revoked_at / revoked_by are set and its service user is marked deleted with its roles emptied (so the role is no longer assigned), all in one transaction. The credential row is kept (it still lists, with status REVOKED) and its key id is never reissued. Revoking an already revoked credential changes nothing and answers 200 with the original revoked_at. Requires SERVICE_CREDENTIAL_DELETE.\n","tags":["Platform"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"The credential id."}],"responses":{"200":{"description":"Revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/service_credential_revoked"}}}},"400":{"description":"id is not a uuid (\"Invalid service credential id\", code VALIDATION)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/platform_ai_connector_forbidden"},"404":{"description":"No live credential with this id in the caller's org (\"Service credential not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/platform/service-credentials/{id}/rotate":{"post":{"summary":"Rotate A Service API Credential's Secret","description":"Issues a new secret under the SAME key id (the service user, role and history stay), returned in this response only. The old secret stops working the moment this commits. Expired credentials can be rotated; revoked ones cannot. No body. Requires SERVICE_CREDENTIAL_CREATE.\n","tags":["Platform"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"The credential id."}],"responses":{"200":{"description":"Rotated - copy the new secret now","content":{"application/json":{"schema":{"$ref":"#/components/schemas/service_credential_rotated"}}}},"400":{"description":"id is not a uuid (\"Invalid service credential id\", code VALIDATION); a revoked credential (\"A revoked service credential cannot be rotated\", code CONFLICT)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/platform_ai_connector_forbidden"},"404":{"description":"No live credential with this id in the caller's org (\"Service credential not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Unexpected error, or the server's credential encryption key is missing or malformed (the message says so)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/purchase_orders/search":{"post":{"summary":"Search Purchase Orders","description":"One page of the org's live (not voided) purchase orders. Unless search.order_by says otherwise they are sorted by the numeric prefix of po_number, highest first; numbers that don't start with a digit sort ahead of all of them. Each row carries the vendor's name and contacts, the line aggregates, the landed totals and the calling user's can_* flags. Filters on lines (item_ids, has_allocatable_items) pick whole POs; the aggregates always cover every line of the PO. Requires PURCHASE_ORDERS_VIEW; the APPROVE, RECEIVE, CLOSE and both INVOICE_*_VARIANCE_APPROVE permissions are only read to fill the can_* flags.\n","tags":["Purchase Orders"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/purchase_order_search_body"}}}},"responses":{"200":{"description":"One page of purchase orders","content":{"application/json":{"schema":{"$ref":"#/components/schemas/purchase_order_search_result"}}}},"400":{"description":"No search object (\"Search object is required\"), an unknown or invalid filter (e.g. \"x\" is not allowed), a missing or invalid paginate (\"Paginate object is required\"), or an order_by column outside orderByColumnNamesList (\"Invalid column name in order_by: <column>\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/purchase_order_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/purchase_orders/not_received":{"post":{"summary":"Search Not Received Items On POs","description":"One page of the org's purchase order lines still waiting to be received (received false) on released or partially_received purchase orders, in no guaranteed order. Lines of every item_type are listed unless search.item_type narrows them. Requires PARTS_VIEW.\n","tags":["Purchase Orders"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/purchase_order_not_received_body"}}}},"responses":{"200":{"description":"One page of lines not yet received","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/purchase_order_not_received_row"}},"pagination":{"$ref":"#/components/schemas/pagination"}}}}}},"400":{"description":"A missing or invalid paginate (\"Paginate object is required\"), no search object (\"Search object is required\"), or an unknown key or item_type in it.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/purchase_order_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/purchase_orders":{"post":{"summary":"Create A Purchase Order","description":"Creates the purchase order and its lines in one transaction and writes a \"created\" audit row. A catalog line's item_type is taken from the catalog record, not the payload, and its price defaults to the part's or asset's purchase_price; a CUSTOM line's price defaults to 0. persist_price writes a line's price back onto the part or asset. Parts and assets can share a PO only when the org's PO.ASSET_PART_MIX_PO setting allows it. Requires PURCHASE_ORDERS_CREATE. Creating it already released (state released) additionally needs PURCHASE_ORDERS_APPROVE and the org's PO.ALLOW_PURCHASER_EQUALS_APPROVER setting, unless booking is true.\n","tags":["Purchase Orders"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/purchase_order_create"}}}},"responses":{"200":{"description":"Purchase order created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/purchase_order_write_result"}}}},"400":{"description":"Joi refused the body (e.g. \"vendor\" is required, \"items[0].item_id\" is required, \"x\" is not allowed); po_number is all zeros (\"Invalid purchase order number\"); parts and assets on one PO when the org forbids it (\"Both parts and assets can't be ordered via a single PO\"); a 'supplies' type part; part_asset_ref on an asset line (\"Only part items can reference an 'asset'\"); or a quantity_ordered that is not a whole number from 1 to 32767.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"state released without PURCHASE_ORDERS_APPROVE (\"You do not have permission to release purchase orders.\"), or while the org forbids the purchaser releasing their own purchase order (\"Company policy prohibits the purchaser from releasing their own purchase order.\"). booking true skips both. An AI-connector (MCP) session gets this status before either (\"Not available to AI connectors\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The vendor is not one of the org's (\"Vendor doesn't exist\"), a catalog item is not a live part or asset of the org (\"Item (<id>) doesn't exist\"), or a part_asset_ref is not a live asset of the org.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"po_number is already used in the org, voided purchase orders included (\"A purchase order with this number already exists\"). Two lines naming the same catalog item_id answer the same 409 with the same message: a purchase order holds at most one line per catalog item.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/purchase_orders/next_po_number":{"get":{"summary":"Suggest the next PO number(s) for the org","description":"Read-only: it reserves nothing, so two users creating a PO at once are offered the same number and the later create answers 409. When pattern is empty and the org has no legacy zero-padded numbers (no purchase orders yet, or only patterned ones), 000001 is offered first. Requires PURCHASE_ORDERS_CREATE.\n","tags":["Purchase Orders"],"parameters":[{"in":"query","name":"pattern","schema":{"type":"string"},"description":"The prefix the user is typing. Matched as a substring of the org's known number patterns, not parsed as a sample number. Omit to get every pattern the org uses.\n"}],"responses":{"200":{"description":"Suggestions","content":{"application/json":{"schema":{"type":"object","properties":{"po_number":{"type":"string","description":"The first suggestion, or '' if none matched."},"exact_match":{"type":"boolean","description":"True if pattern is already a PO number in this org, voided purchase orders included (their numbers stay taken).\n"},"suggestions":{"type":"array","description":"One per matching pattern, ordered by pattern: the legacy '' sequence (type legacy, sorting first; synthesised as 000001 when pattern is empty and the org has none) and each pattern that contains a '#' (a number with no digits has nothing to increment and is skipped).\n","items":{"type":"object","properties":{"pattern":{"type":"string","description":"The stored pattern, '#' standing for the digits ('' for the legacy zero-padded sequence)"},"suggestion":{"type":"string"},"type":{"type":"string","enum":["legacy","pattern"]}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/purchase_order_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/purchase_orders/delete":{"post":{"summary":"Void A Purchase Order","description":"Soft-deletes a new or released purchase order and its lines, records who voided it and why in deletion_information, and writes a \"deleted\" audit row. Parts and assets that were created on this purchase order are deleted with it (an asset through the regular asset delete, which refuses one on an open rental, sale, work order or inspection). The number stays taken. Refused while any line has received anything, is allocated to a work order, or the PO carries a live vendor invoice. Requires PURCHASE_ORDERS_UPDATE; a released PO also needs PURCHASE_ORDERS_UPDATE_RELEASED.\n","tags":["Purchase Orders"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["id","deletion_notes"],"description":"Unknown keys are refused with 400.","properties":{"id":{"type":"string","format":"uuid"},"deletion_notes":{"type":"string","minLength":1,"description":"Why; stored as deletion_information.reason"}},"example":{"id":"00000000-0000-4000-8000-000000000000","deletion_notes":"Ordered in error"}}}}},"responses":{"200":{"description":"Voided. A bare JSON string - \"Purchase order voided\", or \"Purchase order voided. <n> item(s) created on this purchase order were deleted\" when the void deleted parts or assets with it.\n","content":{"application/json":{"schema":{"type":"string"}}}},"400":{"description":"Joi refused the body; the PO is past released (\"You can only delete new or released POs\", or for partially_received \"Purchase order can't be voided: item(s) have already been received. Close the PO short instead\", which also answers a released PO with a received line); a line is allocated to a work order; a vendor invoice is recorded against it; or an asset created on it is on a rental or otherwise can't be deleted (details names what holds it). code is CONFLICT for the state refusals.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"A released PO without PURCHASE_ORDERS_UPDATE_RELEASED (\"You don't have permission to void a 'released' purchase order\"), or an AI-connector (MCP) session (\"Not available to AI connectors\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No live purchase order with this id in the caller's org (\"Purchase order doesn't exist\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/purchase_orders/{id}":{"post":{"summary":"Update And/Or Receive A Purchase Order","description":"Unified endpoint that applies structural edits (header + line add/edit/delete) and receives items in a single atomic transaction. When `items` is sent it is the full desired set: an existing line missing from it is removed. Omit `items` to edit only the header. A line may carry structural fields AND receive fields (quantity_received / received / receiving_notes) in the same request; structural edits apply first, then receiving runs against the updated line set. Edits write an \"updated\" audit row and receipts a \"received\" row; reconciliation_notes lands on the \"updated\" row only.\n\nLine identity - a line with `id` edits that stored line. A catalog line with no `id` but an item_id matching exactly one stored line edits that line; otherwise it is added. A line with neither is a new CUSTOM line, whatever its item_type. A new line needs quantity_ordered. A purchase order holds at most one line per catalog item, so two new lines with the same item_id answer 409.\n\nReceiving - a line receives when quantity_received rises above what is stored, or when `received` flips. `received` true closes the line to further receiving, even short; closing it short also fails the line's work-order allocations that are still short (status FAILED, voided, the work-order line FAILED) and returns to inventory the units they had received before this request; units this request credits to such an allocation are not restocked. Re-sending the stored quantity_received and received values is not a receipt, so a client can send every line's stored receive fields back unchanged. The line object itself is not the GET line: unknown keys (item_price, item_total, item_details, ...) are refused. Receiving requires the PO to be released or partially_received, and moves the PO to partially_received or received. On a line allocated to work orders, the units received are credited to its open allocations in this order: the allocation named by the line's `allocation_id`, then work-order priority (highest first), then the oldest allocation. Units left over on a catalog line go to stock; on a CUSTOM line they are received and left at that. A catalog line being received must carry its item_id as well as its id: the inventory writes key on the item_id sent, so a line received by id alone does not increment the part's or asset's quantity (a STOCK allocation row is still written, so the line's quantity_stocked rises).\n\nRe-opening - raising a received line's quantity_ordered above both its stored quantity_ordered and what it received re-opens it, and so does adding a line to a received or closed PO: the PO falls back to partially_received (or released) and loses its closing_information.\n\nPermissions - requires PURCHASE_ORDERS_UPDATE. A structural edit (header field, added, removed or changed line; sending estimated_arrival_date at all, even unchanged, or persist_price true counts as a line change) to a PO past new also needs the state's permission PURCHASE_ORDERS_UPDATE_RELEASED, _PARTIALLY_RECEIVED, _RECEIVED or _CLOSED. Receiving more needs PURCHASE_ORDERS_RECEIVE and, when the caller created the PO, the org's PO.ALLOW_PURCHASER_EQUALS_RECEIVER setting. Re-opening a closed PO needs PURCHASE_ORDERS_CLOSE. A reconciliation_notes-only save needs none of them.\n","tags":["Purchase Orders"],"parameters":[{"in":"path","required":true,"name":"id","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/purchase_order_update"}}}},"responses":{"200":{"description":"Purchase order updated (also for a request that changed nothing)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/purchase_order_write_result"}}}},"400":{"description":"Joi refused the body or id (e.g. \"received\" missing required peer \"quantity_received\"); po_number is all zeros (\"Invalid purchase order number\"); parts and assets mixed when the org forbids it; a new 'supplies' part; part_asset_ref on an asset line; a removed line that has received (\"Item line (<id>) has been received and cannot be removed\"), is allocated to a work order or carries vendor invoice lines; quantity_received below the stored value (\"Received quantity (<n>) cannot be below the previously received quantity (<n>) for line (<id>)\") or above quantity_ordered (\"Item(<line id>) received quantity can not be greater than ordered quantity(<n>)\", where the Item is the line id, or the item_id when the line was addressed by item_id); a quantity_ordered or quantity_received that is not a whole number (quantity_ordered at most 32767); quantity_ordered below what was received; a new line without quantity_ordered; receiving on a PO that is not released or partially_received (\"Can't receive items for an unreleased, already fully received, or closed PO\"); or an allocation_id that is not an open work-order allocation of the line (\"Item(<id>): allocation <id> is not a work-order allocation of this PO line\", \"... has been voided\", \"... is already fully received\", \"... can't be received (status <status>)\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"A structural edit without the state's permission (\"You don't have permission to update a '<state>' purchase order\", or \"Purchase order in state '<state>' cannot be modified\" for a state that has none); raising a line's quantity_received without PURCHASE_ORDERS_RECEIVE (\"You do not have permission to receive purchase order items.\" - closing a line short needs no RECEIVE); the purchaser receiving their own PO while the org forbids it (\"Company policy prohibits the purchaser from receiving their own purchase order.\"); or re-opening a closed PO without PURCHASE_ORDERS_CLOSE (\"You do not have permission to re-open a closed purchase order.\"). An AI-connector (MCP) session gets this status before any of these (\"Not available to AI connectors\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No live purchase order with this id in the caller's org; the vendor is not one of the org's (\"Vendor doesn't exist\"); a line id is not on this PO (\"Item line (<id>) not found on this PO\"); a new catalog item is not a live part or asset of the org; or a part_asset_ref is not a live asset of the org.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The new po_number is already used in the org, voided purchase orders included (\"A purchase order with this number already exists\"). Two new lines with the same catalog item_id answer the same 409 with the same message.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}},"get":{"summary":"Get A Purchase Order By Id","description":"The purchase order with its vendor, every live line (with its catalog details and its allocations), the 20 newest audit rows, its vendor invoices with the per-line invoicing position and the reconciliation, the landed totals and the calling user's can_* flags. Requires PURCHASE_ORDERS_READ; the APPROVE, RECEIVE, CLOSE and both INVOICE_*_VARIANCE_APPROVE permissions are only read to fill the can_* flags.\n","tags":["Purchase Orders"],"parameters":[{"in":"path","required":true,"name":"id","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The purchase order","content":{"application/json":{"schema":{"$ref":"#/components/schemas/purchase_order_detail"}}}},"400":{"description":"id is not a uuid (\"id\" must be a valid GUID)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/purchase_order_ai_connector_forbidden"},"404":{"description":"No live purchase order with this id in the caller's org (\"Purchase order doesn't exist\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/purchase_orders/history/{item_id}":{"get":{"summary":"Get the PO lines for a part/asset across all past POs (any state)","description":"One row per PO line, newest PO first; deleted lines and POs are excluded. A line is returned when it IS the part/asset (linked_via ITEM), was bought for the asset (part_asset_ref, linked_via ASSET_REFERENCE), or has a live allocation on a work order for the asset (linked_via WORK_ORDER, DOZ-7829). A part id only ever matches ITEM. An id that matches nothing answers 200 with count 0. Requires PURCHASE_ORDERS_READ.\n","tags":["Purchase Orders"],"parameters":[{"in":"path","required":true,"name":"item_id","description":"A part id or an asset id","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The matching PO lines","content":{"application/json":{"schema":{"type":"object","properties":{"count":{"type":"integer"},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"The PO line id"},"po_id":{"type":"string","format":"uuid"},"po_number":{"type":"string"},"item_type":{"type":"string","enum":["part","asset","CUSTOM"]},"price":{"type":"number","description":"The line's item_price"},"quantity_ordered":{"type":"integer"},"quantity_received":{"type":"integer"},"received_at":{"type":"string","format":"date-time","nullable":true},"date":{"type":"string","format":"date-time","description":"When the purchase order was created"},"vendor":{"type":"string","nullable":true,"description":"The vendor's name"},"item_id":{"type":"string","format":"uuid","nullable":true,"description":"Catalog part/asset id; null on a CUSTOM line"},"item_name":{"type":"string","nullable":true,"description":"Part/asset name, or the CUSTOM line's own name"},"part_asset_ref":{"type":"string","format":"uuid","nullable":true,"description":"The asset this line was bought for, if set on the PO"},"linked_via":{"type":"array","description":"Why this line is in the list; one or more values","items":{"type":"string","enum":["ITEM","ASSET_REFERENCE","WORK_ORDER"]}},"work_orders":{"type":"array","description":"Work orders on the requested asset holding a live allocation of this line, by order_number; always empty for a part id\n","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"order_number":{"type":"string"},"status":{"type":"string"},"quantity_allocated":{"type":"number","description":"This line's units allocated to the work order"}}}}}}}}}}}},"400":{"description":"item_id is not a uuid (\"id\" must be a valid GUID)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/purchase_order_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/purchase_orders/{id}/audit_log":{"get":{"summary":"Get the audit trail (who/what/when) for a purchase order","description":"Every audit row of the purchase order, newest first (GET /purchase_orders/{id} embeds only the 20 newest). An id with no rows, including one that is no purchase order at all, answers 200 with count 0, not 404. Requires PURCHASE_ORDERS_READ.\n","tags":["Purchase Orders"],"parameters":[{"in":"path","required":true,"name":"id","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The audit rows","content":{"application/json":{"schema":{"type":"object","properties":{"count":{"type":"integer"},"data":{"type":"array","items":{"$ref":"#/components/schemas/purchase_order_audit_log"}}}}}}},"400":{"description":"id is not a uuid (\"id\" must be a valid GUID)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/purchase_order_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/purchase_orders/receipt/{id}":{"get":{"summary":"Download the purchase order document","description":"The vendor-facing purchase order document as a file download. format=pdf renders it through the Typst service as po-<po_number>.pdf; any other value, or none, sends the HTML version (the same page the vendor email carries) as invoice-<id>.html. Requires PURCHASE_ORDERS_READ.\n","tags":["Purchase Orders"],"parameters":[{"in":"path","required":true,"name":"id","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"format","schema":{"type":"string","default":"html"},"description":"pdf for the PDF; anything else sends the HTML"}],"responses":{"200":{"description":"The document, sent as an attachment (Content-Disposition)","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}},"text/html":{"schema":{"type":"string"}}}},"400":{"description":"id is not a uuid (\"Invalid data type provided\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/purchase_order_ai_connector_forbidden"},"404":{"description":"No live purchase order with this id in the caller's org (\"Purchase order doesn't exist\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/purchase_orders/email/{id}/{receiver_type}":{"get":{"summary":"Send out purchase order email","description":"Emails the purchase order document (the same HTML as GET /purchase_orders/receipt/{id}) and answers without waiting for the mail service; the delivery outcome is journaled afterwards. A GET with a side effect. vendor sends to the vendor contact named by contact_id, or to every contact of the vendor; it is refused (403) when the deployment's CUSTOMER_EMAILS flag or the org's send-external-emails setting is off, but only after the vendor's contacts have passed the checks (a contact problem answers its 404 or 409 first). self sends to the caller. custom sends to the addresses in custom_email. Requires PURCHASE_ORDERS_READ.\n","tags":["Purchase Orders"],"parameters":[{"in":"path","required":true,"name":"id","schema":{"type":"string","format":"uuid"}},{"in":"path","required":true,"name":"receiver_type","description":"vendor, self or custom. The route marks it optional, but only the slash-terminated form `/purchase_orders/email/{id}/` reaches the handler, which answers 400 \"Unknown receiver type\" once the custom_email check and the purchase order lookup have passed; without the trailing slash it is a 404 route-not-found.\n","schema":{"type":"string","enum":["vendor","self","custom"]}},{"in":"query","name":"contact_id","description":"vendor only - one contact of the vendor to send to; omit to send to all of them (ignored otherwise)","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"custom_email","description":"Comma-separated email addresses; required for custom and only used there, but validated for every receiver_type, so a malformed value is a 400 on vendor and self too.\n","schema":{"type":"string"},"example":"buyer@example.com,ap@example.com"}],"responses":{"200":{"description":"The email was handed to the mail service","content":{"application/json":{"schema":{"type":"object","properties":{"msg":{"type":"string","example":"Email sent"},"emailRes":{"type":"object","description":"Always {} - the send is not awaited, so this is a pending promise serialised."}}}}}},"400":{"description":"receiver_type is not vendor, self or custom (\"Unknown receiver type\"); custom without custom_email (\"Custom email addresses are not provided\"); an address in custom_email is not an email; or id is not a uuid (\"Invalid data type provided\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"vendor while external email is switched off (\"Vendor emails to real vendors is disabled\"), or an AI-connector (MCP) session (\"Not available to AI connectors\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No live purchase order with this id in the caller's org, or contact_id is not a contact of the vendor (\"Vendor contact not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"vendor, but the vendor has no contacts (\"No contacts are attached to this vendor\"), or a contact's stored email is missing or invalid (\"Vendor contact email is not valid\" for the contact_id contact; \"Vendor contact email(<email>) is not valid\" when sending to every contact).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/purchase_orders/status/update":{"post":{"summary":"Release or close a purchase order","description":"Moves a new purchase order to released, or a partially_received or received one to closed (re-closing a closed one refreshes its close). Writes a \"status_changed\" audit row. Requires PURCHASE_ORDERS_UPDATE. Releasing also needs PURCHASE_ORDERS_APPROVE and, when the caller created the PO, the org's PO.ALLOW_PURCHASER_EQUALS_APPROVER setting. Closing also needs PURCHASE_ORDERS_CLOSE.\n\nClosing settles the vendor-invoice reconciliation (DOZ-7163). The server recomputes it as of the close, freezes it into closing_information.reconciliation and returns it. It is refused while an invoice variance awaits its approver (409), except on a re-close of a closed PO, which has nothing pending and stamps nothing. Variances that exist only because the PO closes short (billed ahead of a delivery that will now never come) are signed in the closer's name. A close whose reconciliation status is OUTSTANDING or MATCHED_WITH_VARIANCE also needs PURCHASE_ORDERS_CLOSE_DISCREPANCY; NOT_INVOICED and FULLY_MATCHED close on PURCHASE_ORDERS_CLOSE alone.\n","tags":["Purchase Orders"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["id","state"],"description":"Unknown keys are refused with 400.","properties":{"id":{"type":"string","format":"uuid"},"state":{"type":"string","enum":["released","closed"]},"closing_notes":{"type":"string","nullable":true,"description":"On a close, stored as closing_information.reason. Accepted on a release too, where it only lands in the \"status_changed\" audit row's detail.notes.\n"},"reconciliation":{"type":"object","description":"DOZ-7163, close only. The vendor-invoice figures the user was looking at when they closed. The server recomputes the reconciliation itself and stores that; these are compared against it only to detect a stale screen, and a mismatch (or a different invoice set) answers 409. Un-invoiced receipts and accepted variances do NOT block the close. Each figure is optional; one left out is not compared. Unknown keys are refused with 400.\n","properties":{"invoice_ids":{"type":"array","description":"The live invoice ids, in any order","items":{"type":"string","format":"uuid"}},"acknowledged_invoiced_total":{"type":"number"},"acknowledged_uninvoiced_receipts":{"type":"number"},"acknowledged_difference":{"type":"number"},"note":{"type":"string","nullable":true,"description":"Stored with the frozen reconciliation"}}}},"example":{"id":"00000000-0000-4000-8000-000000000000","state":"released"}}}}},"responses":{"200":{"description":"A bare JSON string (\"Purchase order status updated\") for a release; on a close, an object carrying the reconciliation status and the reconciliation frozen with it.\n","content":{"application/json":{"schema":{"oneOf":[{"type":"string","example":"Purchase order status updated"},{"type":"object","properties":{"message":{"type":"string","example":"Purchase order status updated"},"reconciliation_status":{"type":"string","enum":["NOT_INVOICED","OUTSTANDING","MATCHED_WITH_VARIANCE","FULLY_MATCHED"]},"reconciliation":{"$ref":"#/components/schemas/po_reconciliation"}}}]}}}},"400":{"description":"Joi refused the body (e.g. \"state\" must be one of [released, closed]), or the PO's state does not allow the move (\"Can't convert a purchase order from '<state>' to '<state>'\", code CONFLICT).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Releasing without PURCHASE_ORDERS_APPROVE (\"You do not have permission to release purchase orders.\"), the purchaser releasing their own PO while the org forbids it (\"Company policy prohibits the purchaser from releasing their own purchase order.\"), closing without PURCHASE_ORDERS_CLOSE (\"You do not have permission to close purchase orders.\"), or closing an OUTSTANDING or MATCHED_WITH_VARIANCE PO without PURCHASE_ORDERS_CLOSE_DISCREPANCY (\"You do not have permission to close purchase orders with invoice discrepancies.\"). An AI-connector (MCP) session gets this status before any of these (\"Not available to AI connectors\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No live purchase order with this id in the caller's org (\"Purchase order doesn't exist\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"A vendor invoice variance is still awaiting its approver (details.pending_approval with invoice_count, line_count, amount and the per-kind price and quantity counters - DOZ-7758, split by DOZ-7848), or the PO was invoiced since the client loaded it (details.stale_fields - each field with what was acknowledged and the actual value - plus details.reconciliation). code CONFLICT.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/reports/accounting/payment_register":{"post":{"summary":"EOD Payment Register","description":"Every payment posted in the window (P-008) - succeeded, non-refund manual, invoice and card-deposit payments; system-minted deposits stay out - oldest first, with the customer, the cash drawer and who recorded it, plus subtotals by the groups accounting reconciles against (P-009) - Cash & Checks (the drawer), Credit Cards (the processor), ACH & Wires (the bank) and Other - and the grand total. With no dates the window is today in the org's timezone. The subtotals, grand_total and payment_count always cover the whole window; only rows page. Requires REPORTS_ACCOUNTING. The csv, excel and pdf formats return the whole window as an attachment and ignore paginate; the excel carries the pdf's columns, org-timezone dates and group labels, with the subtotals and the grand total beneath (DOZ-8008).\n","tags":["Reports"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/payment_register_body"}}}},"responses":{"200":{"description":"json - the page of rows, the window's subtotals, grand total and payment count, and the window used. csv / excel / pdf - every payment as an attachment (payment_register.csv, payment_register_MM-DD-YYYY.xlsx / .pdf).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/payment_register_result"}},"text/csv":{"schema":{"type":"string"}},"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":{"schema":{"type":"string","format":"binary"}},"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Validation failed (format, a non-ISO date, date_to before date_from, a non-uuid cash_drawer_id, payment_type, an unknown search key, or a bad paginate)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/reports/assets/price_sheet":{"post":{"summary":"Assets Price Sheet","description":"Every live asset with its category tree, make, model, sell price and day, week and 4-week rental rates, ordered by the category tree and asset number. A rate comes from the asset, else from its category, else reads \"No Rates\". Requires REPORTS_ASSETS. The csv, excel and pdf formats return the whole set as an attachment and ignore paginate; the excel and pdf group the assets by category node with the count in the heading (DOZ-8020).\n","tags":["Reports"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/asset_report_body"}}}},"responses":{"200":{"description":"json - the page of assets (or every asset with paginate false). csv / excel / pdf - every asset as an attachment (assets_price_sheet_report-YYYY-MM-DD.csv, assets_price_sheet_report_YYYY-MM-DD.xlsx / .pdf).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/price_sheet_result"}},"text/csv":{"schema":{"type":"string"}},"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":{"schema":{"type":"string","format":"binary"}},"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Validation failed (format, a non-uuid category, a non-boolean stock, an unknown search key, or a bad paginate)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/reports/fleet_utilization":{"post":{"summary":"Fleet Utilization Report","description":"Time utilization (on-rent days / available days) and dollar utilization (rental income / OEC, the purchase price) per category node and calendar month in the org's timezone, with the whole window's totals (DOZ-7813). A unit is in the fleet from its In Service Date, or its first contract rental if earlier; a never-rented unit with no In Service Date is not; total_assets counts every unit owned, in service or not (DOZ-7872), and a month with owned units but no fleet is still a row. Rent counts in its invoice month. Months before the org's first contract rental or first issued rental invoice report that half as null, with the dates in coverage. Every row and the totals carry the yearly benchmark they are measured against and the gap of their annualized figure to it - the request's benchmark_percent, else the category node's own or inherited target, else the org's ASSET.FINANCIAL_UTILIZATION_BENCHMARK_PERCENT setting, else 35. Requires REPORTS_ASSETS. The csv, excel and pdf formats return the whole set as an attachment and ignore paginate; the csv and excel carry every column, the pdf the web page's columns (no data as a dash) with a TOTAL row and the coverage notes (DOZ-8005). FE guides - docs/FE-integration-docs/DOZ-7813_FLEET_UTILIZATION_REPORT_FE_INTEGRATION.md and docs/FE-integration-docs/DOZ-7872_FLEET_UTILIZATION_TOTAL_ASSETS_FE_INTEGRATION.md.\n","tags":["Reports"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/fleet_utilization_body"}}}},"responses":{"200":{"description":"json - the rows, totals, window and coverage. csv / excel / pdf - the same rows and a TOTAL row as an attachment (fleet_utilization_MM-DD-YYYY.csv / .xlsx / .pdf).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/fleet_utilization_result"}},"text/csv":{"schema":{"type":"string"}},"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":{"schema":{"type":"string","format":"binary"}},"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Validation failed (format, category, a non-ISO date, date_to before date_from), an order_by column outside the list, or a window over 120 months\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/reports/customers/complete":{"post":{"summary":"Complete Customer Report","description":"One row per customer (live ones only unless the caller may restore customers): identity, addresses, default site and contact, type, ban and blacklist state, terms, fee and tax statuses, discounts, credit limit with current balance, open orders and total exposure (exposure_basis), auto payment, insurance, credit and rental applications, the card on file, and validation. Requires REPORTS_CUSTOMERS. The credit-class columns (blacklisted, blacklist_reason, terms, the fee and tax statuses, the early-pay pair, credit_limit, current_balance, open_orders, total_exposure, credit_application, file_rental_app, credit_card) need CUSTOMER_RESTRICTED_VIEW too; without it they are absent from every format, and naming one in columns is a 400. columns picks and orders the columns in every format. The csv, excel and pdf formats return the whole set as an attachment and ignore paginate. The excel carries the csv's columns; the pdf, unless columns is sent, a compact set (customer #, company, address, banned, terms, default site and contact, type, credit limit, balance, exposure, validated), and is limited to 20 columns and 56,000 cells (customers x columns: 4,666 customers with the compact set) - above either it is a 400, and the excel and csv stay uncapped (DOZ-8021).\n","tags":["Reports"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/complete_customer_body"}}}},"responses":{"200":{"description":"json - the page of customers (or every customer with paginate false); with columns, each row is id plus the picked keys. csv / excel / pdf - every customer as an attachment (complete_customer_report-MM-DD-YYYY.csv, complete_customer_report_MM-DD-YYYY.xlsx / .pdf, dated in the org's timezone).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/complete_customer_result"}},"text/csv":{"schema":{"type":"string"}},"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":{"schema":{"type":"string","format":"binary"}},"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Validation failed (format, a non-numeric customer_number, customer_type, exposure_basis, an unknown missing_fields value, an unknown search key, or a bad paginate), a column the caller may not see or that does not exist, or a pdf over 20 columns or 56,000 cells\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/reports/categories/checklists":{"post":{"summary":"Categories Checklists Report","description":"Every category of the org with its lineage (super category -> category -> base category) and each asset-detail checklist template assigned to it through the hierarchy - attached directly or inherited from its category or super category. The org default template is left out; a category with no assigned template still gets one row with blank template columns. Requires REPORTS_CATEGORY. The csv, excel and pdf formats return the whole set as an attachment and ignore paginate; the csv carries the excel's columns in its order. date_from and date_to are accepted (both or neither) but do not filter the rows.\n","tags":["Reports"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/categories_checklists_body"}}}},"responses":{"200":{"description":"json - the page of rows and the pagination. csv / excel / pdf - every row as an attachment (categories_checklist_MM-DD-YYYY.csv, categories_checklist.xlsx, Categories_Checklist_Report_MM-DD-YYYY.pdf).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/categories_checklists_result"}},"text/csv":{"schema":{"type":"string"}},"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":{"schema":{"type":"string","format":"binary"}},"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Validation failed (format, only one of date_from / date_to, a non-date value, an unknown search key, or a bad paginate)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/reports/service/maintenance_report":{"post":{"summary":"Maintenance Report","description":"Every live asset with its category tree and identity, current hours, its last maintenance (date and hour meter) and when the next falls due (by hours and by date, from the category's maintenance interval), its last inspection and the next due date, and its active recurring maintenance work orders and inspections. Dates arrive formatted MM-DD-YYYY in the org's timezone, and a value that can't be worked out reads \"--\". Requires REPORTS_SERVICE. The csv, excel and pdf formats return the whole set as an attachment and ignore paginate; the csv counts the active work orders and inspections, the excel and pdf list the work orders by number and count the inspections, grouped by category node (DOZ-8022).\n","tags":["Reports"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/asset_report_body"}}}},"responses":{"200":{"description":"json - the page of assets (or every asset with paginate false). csv / excel / pdf - every asset as an attachment (maintenance_report-YYYY-MM-DD.csv, maintenance_report_YYYY-MM-DD.xlsx / .pdf, dated in the org's timezone).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/maintenance_report_result"}},"text/csv":{"schema":{"type":"string"}},"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":{"schema":{"type":"string","format":"binary"}},"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Validation failed (format, a non-uuid category, a non-boolean stock, an unknown search key, or a bad paginate)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/reports/org_fleet":{"post":{"summary":"Org Fleet Report","description":"The Fleet Utilization report rolled up (DOZ-7891) - the whole rental fleet's count, OEC, rental income, time utilization and dollar utilization (monthly, annualized, trailing 12 months) and its gap to the yearly benchmark, one row per calendar month in the org's timezone. search.level drills down - org (default, one row per month, even a month with no fleet), super_category, category or sub_category (exactly the Fleet Utilization report's rows) - and each row's drill names the next request. The same assets, rules and formulas as POST /reports/fleet_utilization, so the totals are identical at every level and to that report's for the same filters, and a level's counts, OEC and income add up to the level above. An org row takes the totals' benchmark (the request's, else the filtered category node's, else the org setting); any other row its category node's own or inherited target. Requires REPORTS_ASSETS. The csv, excel and pdf formats return the whole level as an attachment and ignore paginate; the csv and excel carry every column, the pdf the web page's columns (no data as a dash) with a TOTAL row and the coverage notes (DOZ-8006). FE guide - docs/FE-integration-docs/DOZ-7891_ORG_FLEET_REPORT_FE_INTEGRATION.md.\n","tags":["Reports"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/org_fleet_body"}}}},"responses":{"200":{"description":"json - the level's rows, the totals, window and coverage. csv / excel / pdf - the same rows and a TOTAL row as an attachment (org_fleet_MM-DD-YYYY.csv / .xlsx / .pdf); at level org the file has no category columns.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org_fleet_result"}},"text/csv":{"schema":{"type":"string"}},"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":{"schema":{"type":"string","format":"binary"}},"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Validation failed (format, level, category, a non-ISO date, date_to before date_from), a level above the category filter's node (super_category with a category's id, or super_category / category with a sub-category's), an order_by column outside the list, or a window over 120 months\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/reports/accounting/inventory_report":{"post":{"summary":"Asset Inventory Report","description":"Every live asset with its category tree (super category, category, sub category, each with its GL number), identity, purchase date and purchase price, ordered by the category tree and asset number. The totals roll the purchase prices up per sub category, category and super category plus a grand total, grouped on category ids so same-named siblings stay apart (DOZ-6865); each carries asset_count and priced_asset_count, so an unpriced group reads as unpriced rather than worthless. When the org has depreciation on, every row also carries the depreciated_value and depreciation_schedules placeholders. Requires REPORTS_ACCOUNTING. The csv, excel and pdf formats return the whole set as an attachment and ignore paginate: the csv appends a SUMMARY TOTALS block; the excel and pdf group the assets by category node (the node's GL number and count in the heading, its priced total as the subtotal) and follow them with a Summary Totals table and the GRAND TOTAL (DOZ-8018).\n","tags":["Reports"],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/asset_report_body"}}}},"responses":{"200":{"description":"json - the page of assets (or every asset with paginate false) and the roll-up totals over the whole set. csv / excel / pdf - every asset as an attachment (asset_inventory_report-YYYY-MM-DD.csv, asset_inventory_report_YYYY-MM-DD.xlsx / .pdf, dated in the org's timezone).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/inventory_report_result"}},"text/csv":{"schema":{"type":"string"}},"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":{"schema":{"type":"string","format":"binary"}},"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Validation failed (format, a non-uuid category, a non-boolean stock, an unknown search key, or a bad paginate)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Not authenticated, or the user lacks REPORTS_ACCOUNTING. An MCP connector session is refused with 403 instead.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"An MCP connector session - \"Not available to AI connectors\"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/reports/delivery_subsidy":{"post":{"summary":"Delivery Subsidy Report","description":"What freight cost us with the carrier minus what we charged the customer for it (DOZ-7816), per delivery, pickup or additional leg on a rental or a sale. A leg charged above its cost is a negative subsidy. search.group_by picks the view - by customer (default), salesperson (the order's assignee) or machine, or one row per leg with its split. A multi-machine leg is split by each line's 4-week rate (else week x 4, else day x 28), or equally when no line is rated. Legs are dated from the order's own events (first dispatch, last off-rent once the contract and its renewals have closed, the sale's close), because no ticket is ever marked completed; a leg with no charge, no cost and no carrier is not a leg, and neither is one whose carrier cost was never recorded (freight_fee NULL - imported history, DOZ-8059), which is left out rather than counted at $0 cost. Rental income is the Asset Information Sheet's per-machine figure (gross rent on issued invoices, by invoice date); a customer's or salesperson's income is their machines' rent on their orders, and the totals' income is every machine invoiced in the window. Requires REPORTS_SALES. The csv, excel and pdf formats return the whole set as an attachment and ignore paginate; the csv and excel carry every column, the pdf the web page's columns for the view (no data as a dash) with a TOTAL row and the income note (DOZ-8007). FE guide - docs/FE-integration-docs/DOZ-7816_DELIVERY_SUBSIDY_REPORT_FE_INTEGRATION.md.\n","tags":["Reports"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/delivery_subsidy_body"}}}},"responses":{"200":{"description":"json - the rows for the chosen view, the window's totals and the window used. csv / excel / pdf - the same rows and a TOTAL row as an attachment (delivery_subsidy_<group_by>_MM-DD-YYYY.csv / .xlsx / .pdf).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/delivery_subsidy_result"}},"text/csv":{"schema":{"type":"string"}},"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":{"schema":{"type":"string","format":"binary"}},"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Validation failed (format, group_by, a non-uuid customer_id or asset_id, a salesperson_id that is neither a uuid nor unassigned, a non-ISO date, date_to before date_from), or an order_by column outside the view's list\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/reports/assets/complete":{"post":{"summary":"Complete Asset Report","description":"Every live asset with a full category path (super category, category and sub category) and its record - identity, maintenance and inspection dates, service and purchase dates and price, rental rates (its own and those inherited from its category), sell price, current hours, validation state and media count. With capitalization on for the org, rows carry capitalization; with depreciation on, the schedules, this month's accumulated depreciation and ending book value. Dates arrive formatted MM-DD-YYYY in the org's timezone. Requires REPORTS_ASSETS. The csv, excel and pdf formats return the whole set as an attachment and ignore paginate; the excel and pdf carry the web page's columns (rental rates as one line, e.g. \"$150.00/day, $400.00/week\"), the pdf on an A3 landscape sheet (DOZ-8019).\n","tags":["Reports"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/complete_asset_body"}}}},"responses":{"200":{"description":"json - the page of assets (or every asset with paginate false). csv / excel / pdf - every asset as an attachment (complete_asset_report-MM-DD-YYYY.csv, complete_asset_report_MM-DD-YYYY.xlsx / .pdf, dated in the org's timezone).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/complete_asset_result"}},"text/csv":{"schema":{"type":"string"}},"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":{"schema":{"type":"string","format":"binary"}},"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Validation failed (format, a non-uuid category, an unknown missing_fields value, an unknown search key, or a bad paginate)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/reports/accounting/credit_notes_report":{"post":{"summary":"Credit Notes Report","description":"Every live credit note - the negative-total invoice minted when an order shrinks - newest first, with its order, customer and the REFUND_ABSORB deposit that carried the credit into the customer's balance (absorption_amount, 0 when none). The totals cover the whole filtered set - per order type, the grand total and the total absorbed. Requires REPORTS_ACCOUNTING. The csv, excel and pdf formats return the whole set as an attachment and ignore paginate; the excel carries the pdf's columns, title-cased status and org-timezone date, with the totals beneath - each order type and the grand total under Credit Amount, the absorbed total under Absorbed (DOZ-8009).\n","tags":["Reports"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/credit_notes_body"}}}},"responses":{"200":{"description":"json - the page of rows, the totals over every row, and the pagination. csv / excel / pdf - every credit note as an attachment (credit_notes_report.csv, credit_notes_report_MM-DD-YYYY.xlsx / .pdf).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/credit_notes_result"}},"text/csv":{"schema":{"type":"string"}},"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":{"schema":{"type":"string","format":"binary"}},"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Validation failed (format, a non-uuid customer_id, order_type, a non-ISO date, date_to before date_from, an unknown search key, or a bad paginate)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/reports/accounting/average_days_to_pay":{"post":{"summary":"Average Days to Pay Report","description":"One row per customer with a fully-paid invoice settled in the window - the invoices paid, the average days from invoice date to the last payment applied (avg_days_to_pay), the same average over the preceding window of equal length (prev_avg_days_to_pay), the change between them (trend_days, negative = paying faster), the share paid by the due date (on_time_pct) and the last payment. The window selects invoices by settled date and defaults to the 365 days ending now. The totals are pooled over every paid invoice, not averaged over the customers, and echo the resolved window. Defaults to the slowest payer first. Requires REPORTS_ACCOUNTING; an MCP connector session reads it too. The csv, excel and pdf formats return the whole set as an attachment and ignore paginate; the excel and pdf carry the web page's columns (the trend signed, null as an em dash, the last payment as the org's day) under the window and customer filter, then a TOTAL row (DOZ-8015).\n","tags":["Reports"],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/average_days_to_pay_body"}}}},"responses":{"200":{"description":"json - the page of rows, the totals over every row with the window, and the pagination. csv - every row as an attachment (average_days_to_pay_report.csv), with a TOTAL line beneath. excel / pdf - every row (average_days_to_pay_report_MM-DD-YYYY.xlsx / .pdf, dated in the org's timezone).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/average_days_to_pay_result"}},"text/csv":{"schema":{"type":"string"}},"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":{"schema":{"type":"string","format":"binary"}},"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Validation failed (format, a non-ISO date, date_to before date_from, a non-uuid customer_id, an unknown search key, an order_by column the report doesn't have, or a bad paginate)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Not authenticated, or the user lacks REPORTS_ACCOUNTING. An MCP connector session is refused with 403 instead.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"An MCP connector session whose user lacks REPORTS_ACCOUNTING (the route is on the connector allowlist)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/reports/dispatched_assets":{"post":{"summary":"Dispatched Assets","description":"Every dispatch that is not RELEASED, on a rental order that is not deleted (reservations, open and closed contracts alike), with its asset, the line item's expected dispatch and return, the actual dispatch and off-rent timestamps, the dispatch status as stored, and the order's customer. The asset is the one stamped on the dispatch, else the line item's planned asset. The window filters on the actual dispatched_date, so a dispatch not yet sent out (no dispatched_date) falls outside any bounded window. Requires REPORTS_ASSETS. The csv, excel and pdf formats return the whole set as an attachment and ignore paginate; the csv carries the excel's columns in its order, with the four timestamps in the org's timezone.\n","tags":["Reports"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/dispatched_assets_body"}}}},"responses":{"200":{"description":"json - the page of rows and the pagination. csv / excel / pdf - every row as an attachment (dispatch_assets_MM-DD-YYYY.csv, dispatch_assets.xlsx, Dispatch_Report_MM-DD-YYYY.pdf).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/dispatched_assets_result"}},"text/csv":{"schema":{"type":"string"}},"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":{"schema":{"type":"string","format":"binary"}},"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Validation failed (format, a non-ISO date, date_to before date_from, an unknown search key, or a bad paginate)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/reports/service/vendor_invoice_export_report":{"post":{"summary":"Vendor Invoice Export Report","description":"The PO Export report mirrored for the vendor invoices recorded against POs (DOZ-7903) - one row per live invoice line, then the invoice's freight, sales tax and miscellaneous as rows of their own when above zero, newest invoice first. Amount is the stored line total (quantity x unit price, rounded to cents), so the Amount column sums to each invoice's total; quantity x price can differ by fractions of a cent because unit prices carry 4 decimals. Soft-deleted invoices and lines are left out. The date window is calendar days in the org's timezone; invoice_date is the stored timestamp in json and its org-local day (YYYY-MM-DD) in the files. status is the invoice's own status. Requires REPORTS_SERVICE and PURCHASE_ORDERS_READ. The excel, csv and pdf formats return the whole set as an attachment and ignore paginate. FE guide - docs/FE-integration-docs/DOZ-7903_VENDOR_INVOICE_EXPORT_REPORT_FE_INTEGRATION.md.\n","tags":["Reports"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/vendor_invoice_export_body"}}}},"responses":{"200":{"description":"json - the page of rows and its pagination. excel / csv / pdf - every row as an attachment (vendor_invoice_export_report_MM-DD-YYYY.xlsx / .csv, Vendor_Invoice_Export_Report_MM-DD-YYYY.pdf).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/vendor_invoice_export_result"}},"text/csv":{"schema":{"type":"string"}},"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":{"schema":{"type":"string","format":"binary"}},"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Validation failed (format, status, a non-ISO date, date_to before date_from, an unknown search key, or a bad paginate)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/reports/accounting/gl_transactions":{"post":{"summary":"GL Transaction Report","description":"A journal derived from the A/R money events in a window - payments, deposit applications, discounts, refund absorptions, invoice revenue and drawer closes - each expanded into its DR/CR lines on the org's GL accounts. Nothing is posted; QuickBooks stays the ledger. The default order is date, then journal #, debit first, which keeps each entry's lines together. The window defaults to month to date in the org's timezone (with date_to alone, the month of date_to). With no account filter the journal should balance (totals.balanced); an account or entry-type scoped view legitimately does not. When the request resolves to exactly one account (accounts or gl_code), lines carry running_balance and totals.ledger the period net, plus opening and closing balances with with_opening_balance. Requires REPORTS_ACCOUNTING. The csv, excel and pdf formats return the whole set as an attachment and ignore paginate; the excel and pdf carry the web page's journal (dates as the org's day, a TOTAL row tying to the totals) and its By Account, By Entry Type and Customer Deposits by Segment tables, each on its own sheet in the excel (DOZ-8016).\n","tags":["Reports"],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/gl_transactions_body"}}}},"responses":{"200":{"description":"json - the page of lines, the totals over every line, and the pagination. csv - every line, then the per-account rollup and the debit / credit totals (gl_transaction_report.csv). excel / pdf - every line (gl_transaction_report_MM-DD-YYYY.xlsx / .pdf, dated in the org's timezone).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/gl_transactions_result"}},"text/csv":{"schema":{"type":"string"}},"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":{"schema":{"type":"string","format":"binary"}},"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Validation failed (format, a non-ISO date, date_to before date_from, a non-uuid customer_id, an unknown entry type, an accounts item missing category or label or with an unknown category, a gl_code over 64 characters, a non-boolean include_legacy or with_opening_balance, an unknown search key, an order_by column the report doesn't have, or a bad paginate), or \"Unknown GL account: <category>.<label>\" for a pair the GL catalog doesn't have - a lowercase pair included\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Not authenticated, or the user lacks REPORTS_ACCOUNTING. An MCP connector session is refused with 403 instead.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"An MCP connector session - \"Not available to AI connectors\"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/reports/accounting/parts_inventory_report":{"post":{"summary":"Parts Inventory Report","description":"The parts sibling of the Asset Inventory Report (DOZ-7949) - one row per live inventoried part with its make, model, model year, serial number, bin location and on-hand quantity. Supplies and non-inventoried parts are left out, and so are deleted parts. Quantity is the stored count (an empty one reads 0) and can be negative. Default order is part name, then part number. Requires REPORTS_ACCOUNTING. The csv, excel and pdf formats return the whole set as an attachment and ignore paginate. FE guide - docs/FE-integration-docs/DOZ-7949_PARTS_INVENTORY_REPORT_FE_INTEGRATION.md.\n","tags":["Reports"],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/parts_inventory_body"}}}},"responses":{"200":{"description":"json - the page of rows and its pagination. csv / excel / pdf - every row as an attachment (parts_inventory_report_MM-DD-YYYY.csv / .xlsx / .pdf).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/parts_inventory_result"}},"text/csv":{"schema":{"type":"string"}},"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":{"schema":{"type":"string","format":"binary"}},"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Validation failed (format, a non-boolean stock, a query or bin_location over 200 characters, an unknown search key, an order_by column outside the list, or a bad paginate)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/reports/accounting/financial_audit":{"post":{"summary":"Financial Activity Audit Journal","description":"Who did what and when against the money - deposits and payments received, applications, card events, discounts, invoice and order lifecycle, statements, cash drawers and financial configuration changes - one row per audit event, each with a composed summary sentence. The window is on event_at and defaults to month to date in the org's timezone (with date_to alone, the month of date_to). Newest first by default. The totals count every event of the filtered set, with the count and amount per action. Requires FINANCIAL_AUDIT_READ. The csv, excel and pdf formats return the whole set as an attachment and ignore paginate; the excel and pdf carry the web page's journal (event time as the org's, action label, summary, customer, amount, source) and a By Action table with the event total (DOZ-8017). The pdf is limited to 5,000 events; excel and csv are not.\n","tags":["Reports"],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/financial_audit_body"}}}},"responses":{"200":{"description":"json - the page of rows, the totals over every row with the window, and the pagination. csv - every row with its detail JSON, then the per-action rollup (financial_audit.csv). excel / pdf - every row (financial_audit_MM-DD-YYYY.xlsx / .pdf, dated in the org's timezone).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/financial_audit_result"}},"text/csv":{"schema":{"type":"string"}},"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":{"schema":{"type":"string","format":"binary"}},"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Validation failed (format, a non-ISO date, date_to before date_from, a non-uuid id, an unknown action, entity_type or source, an entity_number over 64 characters, an unknown search key, an order_by column the report doesn't have, or a bad paginate), or a pdf requested for more than 5,000 events\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Not authenticated, or the user lacks FINANCIAL_AUDIT_READ. An MCP connector session is refused with 403 instead.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"An MCP connector session - \"Not available to AI connectors\"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/reports/accounting/po_status_report":{"post":{"summary":"PO Status Report","description":"Every purchase order's state and invoicing position (DOZ-7756) - ordered, received and invoiced value, the receipts not yet invoiced, and invoiced minus ordered. A closed PO also gets the reconciliation bucket its close landed in - NO_VARIANCE, WITH_VARIANCE (a variance, or a freight / tax / misc charge billed over the org's tolerance) or NOT_RECONCILED (closed with no invoice, or with received goods left unbilled). A closed PO is read from the snapshot its close froze, not re-derived; a close from before vendor invoices existed has none and is summarized live (reconciliation_source LIVE). The reconciliation filter narrows the closed rows only. date_basis picks the date the window applies to; CLOSED also drops every PO that is not closed. Requires REPORTS_ACCOUNTING. The csv, excel and pdf formats return the whole set as an attachment and ignore paginate; the csv and excel carry every column, the pdf the web page's columns with a TOTAL row and the PO counts (DOZ-8004).\n","tags":["Reports"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/po_status_body"}}}},"responses":{"200":{"description":"json - the page of rows, the totals over every row, and the pagination. csv / excel / pdf - every row as an attachment (po_status_report_MM-DD-YYYY.csv / .xlsx / .pdf).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/po_status_result"}},"text/csv":{"schema":{"type":"string"}},"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":{"schema":{"type":"string","format":"binary"}},"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Validation failed (format, states, reconciliation, date_basis, a non-ISO date, date_to before date_from, a non-uuid vendor_id, a po_number over 64 characters, an unknown search key, or a bad paginate), or an order_by column outside the list\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/reports/accounting/transaction_history":{"post":{"summary":"Transaction History (QuickBooks export)","description":"Per day, the org's GL buckets from the live (non-stale) Lotus billable charges of the orders created that day - rental income (4100, rental and sale orders' base lines net of discounts), parts billed to customers (4771, work orders' base lines net of discounts), trucking fees (4761), the environmental fee (4770) and the state, county, city and special sales taxes (2300-2303) - and per day the assets sold (sold_date) with their sell prices. Days are calendar days in the org's timezone; with no dates the window is today. Requires REPORTS_ACCOUNTING. The csv and iif formats are the QuickBooks journal lines (one per account per day, one per sold asset and a 5500 line per sale day), dated by the same calendar day (DOZ-8013 fixed the csv printing the day before west of the server). The excel and pdf print the web page's two tables - Order Details and Sold Assets, each with a TOTAL row (DOZ-8013); the excel puts each on its own sheet.\n","tags":["Reports"],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/transaction_history_body"}}}},"responses":{"200":{"description":"json - the day buckets and the sold assets under data, with a pagination block covering them all (paginate is ignored; total counts the day rows plus the sold-day groups). csv / excel / pdf / iif - the whole window as an attachment (transaction_history_YYYY-MM-DD.csv / .xlsx / .pdf / .iif).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/transaction_history_result"}},"text/csv":{"schema":{"type":"string"}},"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":{"schema":{"type":"string","format":"binary"}},"application/pdf":{"schema":{"type":"string","format":"binary"}},"application/qbooks":{"schema":{"type":"string"}}}},"400":{"description":"Validation failed (format, a non-date bound, date_to before date_from, an unknown search key, or a bad paginate - paginate is validated although it is ignored)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Not authenticated, or the user lacks REPORTS_ACCOUNTING. An MCP connector session is refused with 403 instead.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"An MCP connector session - \"Not available to AI connectors\"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Unexpected error - including a date sent as a number (epoch milliseconds), which passes validation and then fails the query\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/reports/rental_revenue_report":{"post":{"summary":"Rental Revenue Analysis","description":"Per machine, what it has earned on rent against what it has cost - depreciation, repair and maintenance, and a flat overhead on its rent - plus its current monthly rate and income and how many contracts it went out on. Lists every non-deleted machine with quantity above 0 whose category is a sub-category (super category -> category -> sub-category); a machine filed directly under a category or super category is not listed. The json format (DOZ-7911) carries the same figures as the excel sheet's machine rows and adds subtotals for every sub-category, category and super category plus a grand total, over the whole result rather than the page. Rental income is the history imported from the previous system up to the org's cutover (its first issued CloudERM rental invoice on an order that was not imported), then CloudERM billing from that day. Repair and maintenance is the imported repairs plus every completed work order's internal cost; depreciation is the internal-value schedule's, and is left out when the org has depreciation turned off. Requires REPORTS_SALES. The excel format returns the whole set as an attachment and ignores paginate. FE guide - docs/FE-integration-docs/DOZ-7911_RENTAL_REVENUE_REPORT_FE_INTEGRATION.md.\n","tags":["Reports"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/rental_revenue_body"}}}},"responses":{"200":{"description":"json - the page of machine rows, the whole result's subtotals and the window used. excel - the Rental Revenue Analysis workbook as an attachment (revenue_report.xlsx).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/rental_revenue_result"}},"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Validation failed (format, a non-date date_from or date_to, only one of the two dates, date_to before date_from, an unknown search key, or an invalid paginate)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/reports/daily_summary":{"post":{"summary":"Daily Summary Report","description":"The desks' activity in a window - the high-level summary counts per desk, then the sales desk (new quotes, open and closed sales orders), the rental desk (new quotes, reservations, open and closed contracts) and the service desk (new work orders, dispatch and off-rent inspections, completed inspections) - the same payload the nightly email renders. With no dates the window is the trailing 24 hours; send date_from and date_to (or the older start_date and end_date) together for a custom one. The caller's merged report preferences decide which sections are shown, and hideEmptySections hides the empty ones. Requires REPORTS_DAILY_SUMMARY. paginate is ignored. The csv, excel and pdf formats return the whole report as an attachment; the csv is the excel workbook written out as text - the same sections, header rows and blank spacer rows, a link cell as its visible text (DOZ-8012).\n","tags":["Reports"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/daily_summary_body"}}}},"responses":{"200":{"description":"json - the report payload under data. csv / excel / pdf - the whole report as an attachment (daily_summary_report-MM-DD-YYYY.csv / .xlsx / .pdf).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/daily_summary_result"}},"text/csv":{"schema":{"type":"string"}},"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":{"schema":{"type":"string","format":"binary"}},"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Validation failed (format, a non-date bound, only one bound of a pair, an unknown search key), or the report data could not be assembled\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/reports/customers/sold_assets":{"post":{"summary":"Customer Sold Asset Report","description":"Every asset sold to one customer (DOZ-8106) - one row per ASSET line on the customer's closed sales orders, add-ons included (an attachment that went free with a machine is a $0 row), oldest first. Rows are grouped by sold month (default), sold year or order, and groups lists every group's count and revenue over the whole filtered set, so a group cut by paginate still shows its full figures. Revenue is the sale price frozen at close (the line total, less the line discount, for sales closed before snapshots existed); no cost, book value or profit field is returned. Only sales closed in CloudERM name a buyer, so sales from before go-live never appear. Requires REPORTS_CUSTOMERS. The csv, excel and pdf formats return the whole set as an attachment and ignore paginate - each group's heading, its rows, its subtotal (unless subtotals is false) and a GRAND TOTAL. FE guide - docs/FE-integration-docs/DOZ-8106_CUSTOMER_SOLD_ASSET_REPORT_FE_INTEGRATION.md.\n","tags":["Reports"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/customer_sold_assets_body"}}}},"responses":{"200":{"description":"json - the customer, the page of rows, every group, the totals, the window and grouping used, and the pagination. csv / excel / pdf - the whole set as an attachment (customer_sold_assets_MM-DD-YYYY.csv / .xlsx / .pdf).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/customer_sold_assets_result"}},"text/csv":{"schema":{"type":"string"}},"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":{"schema":{"type":"string","format":"binary"}},"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Validation failed (a missing or non-uuid customer_id, format, group_by, a non-ISO date, date_to before date_from, an unknown search key, an order_by column outside the list, or an invalid paginate)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No customer with this customer_id in the caller's organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/reports/accounting/over_credit_limit":{"post":{"summary":"Over Credit Limit Report","description":"One row per customer with a credit limit - the limit, the current balance, the open (uninvoiced) orders, the total exposure under the chosen basis, how far over the limit it is (over_by, negative when under) and the limit's action (WARN or BLOCK). The totals cover every row over the limit, independent of paginate. Defaults to the most over-limit customer first. The whole report is credit data, so it requires BOTH REPORTS_ACCOUNTING and CUSTOMER_RESTRICTED_VIEW (DOZ-8062); a user holding only one gets 401 (403 through an MCP connector session). The csv, excel and pdf formats return the whole filtered set as an attachment and ignore paginate.\n","tags":["Reports"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/over_credit_limit_body"}}}},"responses":{"200":{"description":"json - the page of rows, the totals over every row and the pagination. csv - every row as an attachment (over_credit_limit_report.csv), with a GRAND TOTAL OVER LIMIT line beneath. excel / pdf - the web page's columns (the customer as \"[number] name\", the money columns, the title-cased status and the lock state) under the filters in force, then a TOTAL OVER LIMIT row naming the count (over_credit_limit_report_MM-DD-YYYY.xlsx / .pdf, dated in the org's timezone).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/over_credit_limit_result"}},"text/csv":{"schema":{"type":"string"}},"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":{"schema":{"type":"string","format":"binary"}},"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Validation failed (format, exposure_basis, an order_type outside RENTAL, SALE and WORK_ORDER, an unknown search key, an order_by column the report doesn't have, or an invalid paginate)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Not authenticated, or the user lacks REPORTS_ACCOUNTING or CUSTOMER_RESTRICTED_VIEW - both are required. An MCP connector session is refused with 403 instead.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/reports":{"get":{"summary":"Reports catalog","description":"The reports registry as the web's reports pages list it - every enabled report the caller may open, ordered by report_name. The registry is global (seeded by migrations only, DOZ-8053), so every organization sees the same rows, narrowed per caller: a report shows when its permissions column is null or shares at least one permission with the caller's roles (DOZ-7055); the inspection_analysis/* reports only when the organization has the Inspection Analysis module; and the reports the environment hides (a fixed list in controller/report.js - fleet_margin_report everywhere, plus the reports whose web page hasn't shipped on production) never. Hiding removes a report from this list, from top reports and from the global search's reports bucket - its own route still answers. available_online says the web has a page for the report; available_offline that it can be downloaded from the list. Requires REPORT_VIEW. Also open to an MCP connector session (DOZ-8062).\n","tags":["Reports"],"parameters":[{"in":"query","name":"report_group","schema":{"type":"string"},"description":"Only this group (e.g. accounting, customers, assets). Empty = every group.","example":"accounting"},{"in":"query","name":"single_item_report","schema":{"type":"boolean"},"description":"true lists only the single-item reports (one customer or asset, opened from its record), false only the others. Omitted = both. Postgres reads the value, so t/f, 1/0 and yes/no work too; anything else is a 400.\n","example":false}],"responses":{"200":{"description":"The visible reports and their count.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/report_catalog_result"}}}},"400":{"description":"single_item_report is not a boolean Postgres can read - \"Invalid data type provided\" (an empty single_item_report= included)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Not authenticated, or the user lacks REPORT_VIEW. An MCP connector session is refused with 403 instead.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"An MCP connector session whose user lacks REPORT_VIEW","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/reports/user/top":{"get":{"summary":"The caller's most-run reports","description":"The reports the caller has run most often, most-run first (ties in no set order), counted from the user report journal - every run of a report route that journals (writeUserReportJournal), including a run that then failed validation. Counts the caller's runs from the app only: runs through an MCP connector (source mcp, DOZ-8062) and single-item reports never count. The same catalog filters apply - enabled, permission overlap and the environment's hidden list - except the Inspection Analysis module check. At most TOP_N_REPORTS rows (30 in the Cloud Build deploy config); with that variable unset there is no limit. The runs are matched by user, not by organization. Requires REPORT_VIEW.\n","tags":["Reports"],"responses":{"200":{"description":"A bare array (not wrapped in results); empty when the caller has run none.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/top_report_entry"}}}}},"401":{"description":"Not authenticated, or the user lacks REPORT_VIEW. An MCP connector session is refused with 403 instead.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"An MCP connector session - \"Not available to AI connectors\" (the route isn't on the connector allowlist)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/reports/accounting/liability_report":{"post":{"summary":"Customer Deposit Liability Report","description":"Every customer deposit the organization still owes - succeeded, non-refund payments with purpose DEPOSIT whose amount is not yet fully applied to invoices (unconsumed above 0) - oldest paid first, with the customer and the order it was taken on (none for an unlinked deposit). The totals cover the whole filtered set, independent of paginate. There is no date filter - it is the liability as of now. Requires REPORTS_ACCOUNTING. The csv, excel and pdf formats return the whole set as an attachment and ignore paginate. Known issues - order_type is matched as sent, so a lowercase rental or sale passes validation but matches no rows; order_type together with linkage UNLINKED is always empty (an unlinked deposit has no order type).\n","tags":["Reports"],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/liability_report_body"}}}},"responses":{"200":{"description":"json - the page of rows, the totals and the pagination. pdf / excel - every row under the org name and today's date, with Rental, Sale, Linked, Unlinked and grand totals beneath (liability_report_MM-DD-YYYY.pdf / .xlsx, dated and with Paid At in the org's timezone). csv - every row with the same total lines (liability_report.csv, undated; Paid At as MM/DD/YYYY in the server's timezone).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/liability_report_result"}},"text/csv":{"schema":{"type":"string"}},"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":{"schema":{"type":"string","format":"binary"}},"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Validation failed (format, a non-uuid customer_id, an order_type other than RENTAL or SALE, a linkage other than LINKED or UNLINKED, an unknown search key - date_from included, or an invalid paginate), or the PDF failed to render (\"Typst Error: ...\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Not authenticated, or the user lacks REPORTS_ACCOUNTING. An MCP connector session is refused with 403 instead.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"An MCP connector session - \"Not available to AI connectors\"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/reports/accounts_receivables":{"post":{"summary":"A/R Report (aging)","description":"One row per customer with open invoices (UNPAID or PARTIALLY_PAID, on a live order), each invoice's balance due aged by its due date in the org's timezone into current (due after today, or no due date), <=30 (due today through 30 days ago), 31-60, 61-90, 91-120 and >120 days past due. A due date on a boundary falls in the younger bucket. total sums the past-due buckets only - current is left out. Rows are ordered by customer id. With detailed, each row also lists its invoices in orders. Requires REPORTS_ACCOUNTING. paginate pages the json only; the csv, excel and pdf formats return every customer as an attachment. Known issues - interval_type and sort_by are validated but ignored (the buckets are always 30/60/90/120 and the order is always by customer id), and the web's Sort select sends sort_by null when cleared, which is refused.\n","tags":["Reports"],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ar_aging_body"}}}},"responses":{"200":{"description":"json - the customers (a page when paginate is sent) and the pagination. pdf - the aging per customer with grand totals (AR_Report_MM-DD-YYYY.pdf). csv / excel - one line per customer (Company Name, Customer Number, Sales Person, Current, 1 - 30 Days, 31 - 60 Days, 61 - 90 Days, 91 - 120 Days, 120+ Days, Total) and a Grand Total line; with detailed, one line per invoice adding Invoice ID (the invoice number), Status, Amount, Created At, Customer PO and Approved (AR_Report_MM-DD-YYYY.csv / .xlsx). File names are dated in the org's timezone.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ar_aging_result"}},"text/csv":{"schema":{"type":"string"}},"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":{"schema":{"type":"string","format":"binary"}},"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Validation failed (format, a contract_status outside the list or a null contract_status, a non-uuid customer_id, an empty customer_name, a non-numeric customer_number, a non-boolean flag, interval_type, a sort_by outside the list (null included), an unknown search key, or an invalid paginate - \"Paginate object is required\" when currentPage or perPage is missing), or the PDF failed to render (\"Typst Error: ...\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Not authenticated, or the user lacks REPORTS_ACCOUNTING. An MCP connector session is refused with 403 instead.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"An MCP connector session - \"Not available to AI connectors\"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/reports/accounts_receivables/search":{"post":{"summary":"A/R transaction search","description":"Locates A/R transactions in the financial activity journal (DOZ-7092) - one row per invoice, payment or payment-application action, newest first. Drawer, statement, customer-credit and configuration events are left out, so an action_types value from those families matches nothing. Every filter is optional and they combine with AND. The who-did-what fields (actor, source, detail, summary) need FINANCIAL_AUDIT_READ and are never returned here - use the Financial Activity Audit Journal for them. JSON only. Requires REPORTS_ACCOUNTING. Not journaled, so it never counts toward top reports and carries no supported_formats.\n","tags":["Reports"],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ar_search_body"}}}},"responses":{"200":{"description":"The page of journal rows and the pagination.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ar_search_result"}}}},"400":{"description":"Validation failed (a non-uuid customer_id, a non-string customer_number - send it as a string, a non-ISO date_from or date_to, a non-numeric amount bound, an action_types value outside the journal's vocabulary, an unknown search key - format included (\"format\" is not allowed), or an invalid paginate)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Not authenticated, or the user lacks REPORTS_ACCOUNTING. An MCP connector session is refused with 403 instead.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"An MCP connector session - \"Not available to AI connectors\"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/reports/accounts_receivables/details":{"post":{"summary":"A/R details for a customer","description":"The drill-down behind an A/R aging cell - one row per order of the customer that has an open (UNPAID or PARTIALLY_PAID) invoice in the window, with the order's invoiced total, paid and unpaid balances. Invoices are summed per order AND approved flag, so an order holding both approved and unapproved invoices comes back as two rows. The rows carry no ORDER BY, so their order (and the split across pages) is not stable. start and end bound the invoices' due dates the other way round from their names - start is the LATEST due date counted, end the EARLIEST - matching an aging bucket read from today backwards. An unknown or other-org customer_id returns no rows. JSON only. Requires REPORTS_ACCOUNTING. Not journaled.\n","tags":["Reports"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ar_details_body"}}}},"responses":{"200":{"description":"The orders and the pagination.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ar_details_result"}}}},"400":{"description":"Validation failed (customer_id missing - \"\\\"customer_id\\\" is required\", also for a request with no body - or not a uuid, a non-uuid order_id, a start or end that isn't a date, a non-boolean approved, a format other than json, an unknown search key, or an invalid paginate)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Not authenticated, or the user lacks REPORTS_ACCOUNTING. An MCP connector session is refused with 403 instead.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"An MCP connector session - \"Not available to AI connectors\"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/reports/accounts_receivables/notes":{"post":{"summary":"A/R call notes for a customer","description":"The customer's notes (customer_notes) attached to an order that has at least one invoice (any status), each with its author's username and the order number. order_id narrows to one order; start and end keep the notes whose order has an invoice due in the window, read as on the details route (start the latest due date, end the earliest). No ORDER BY, so the order is not stable. Notes not tied to an order never appear. An unknown or other-org customer_id returns no rows. JSON only. Requires REPORTS_ACCOUNTING. Not journaled.\n","tags":["Reports"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ar_details_body"}}}},"responses":{"200":{"description":"The notes and the pagination. There is no total_minutes_logged key, though the web's notes panel reads one.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ar_notes_result"}}}},"400":{"description":"Validation failed (as on the details route - customer_id missing or not a uuid, a non-uuid order_id, a start or end that isn't a date, a format other than json, an unknown search key, or an invalid paginate)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Not authenticated, or the user lacks REPORTS_ACCOUNTING. An MCP connector session is refused with 403 instead.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"An MCP connector session - \"Not available to AI connectors\"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/reports/accounts_receivables/invoices":{"post":{"summary":"A customer's open invoices","description":"Every open (UNPAID or PARTIALLY_PAID) invoice of one customer, earliest due date first, each flagged past_due when its due date is today or earlier in the org's timezone (an invoice with no due date is not past due) - the selection list of the A/R report's export dialog (DOZ-6186). Never paged; paginate is ignored. An unknown or other-org customer_id returns no invoices. Requires REPORTS_ACCOUNTING. Not journaled.\n","tags":["Reports"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ar_invoices_body"}}}},"responses":{"200":{"description":"The customer id as sent, the counts and the invoices.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ar_invoices_result"}}}},"400":{"description":"Validation failed (customer_id missing or not a uuid, or any other search key - format included (\"format\" is not allowed))\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Not authenticated, or the user lacks REPORTS_ACCOUNTING. An MCP connector session is refused with 403 instead.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"An MCP connector session - \"Not available to AI connectors\"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/reports/accounts_receivables/invoices/export":{"post":{"summary":"Export a customer's open invoices as one PDF","description":"Renders the selected open invoices of one customer as one PDF, one invoice per page, in due-date order - the invoice document the invoice routes print (DOZ-6186). mode picks from the customer's open set; with ids, an id outside that set (paid, voided, another customer's or unknown) is dropped without an error. paginate is ignored. Requires REPORTS_ACCOUNTING. Not journaled.\n","tags":["Reports"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ar_invoice_export_body"}}}},"responses":{"200":{"description":"The merged PDF (invoices-N.pdf, N = the number of invoices in it).","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Validation failed (customer_id missing or not a uuid, mode, invoice_ids missing or empty with mode ids - \"\\\"invoice_ids\\\" is required\" / \"\\\"invoice_ids\\\" must contain at least 1 items\", a non-uuid invoice id, or an unknown search key - format included)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Not authenticated, or the user lacks REPORTS_ACCOUNTING. An MCP connector session is refused with 403 instead.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"An MCP connector session - \"Not available to AI connectors\"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"\"No open invoices match the selection\" - the customer has no open invoice in the chosen mode (an unknown or other-org customer included), or none of the ids is in its open set\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The PDF failed to render (\"Invoice PDF generation failed: ...\"), or an unexpected error\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/reports/accounts_receivables/invoices/email":{"post":{"summary":"Email a customer's open invoices","description":"Emails the selected open invoices of one customer as one merged PDF attachment (invoices-N.pdf), with the subject \"Invoices — <company name>\" (DOZ-6186). The selection works as on the export route. Recipients by receiver_type - customer: the customer's contacts opted in to new invoice emails (new_invoices_email); self: the caller's own address; custom: custom_emails as sent. A customer send is skipped - still a 200, status skipped_customer_emails_disabled - when the CUSTOMER_EMAILS environment switch or the org's send-external-emails setting is off; self and custom always send. Every 200 writes a journal event (email_delivery_ar_invoices) and one INVOICE_EMAILED row in the financial activity journal naming the invoices and recipients. status sent means the send was handed to Mailgun - a Mailgun failure is logged, not returned, and with ENABLE_EMAILS off nothing leaves the server either way. Requires REPORTS_ACCOUNTING. Not journaled as a report run.\n","tags":["Reports"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ar_invoice_email_body"}}}},"responses":{"200":{"description":"Sent (or skipped for the customer), with the recipients.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ar_invoice_email_result"}}}},"400":{"description":"Validation failed (the export route's causes, receiver_type missing or outside customer, self and custom, custom_emails missing or empty with receiver_type custom, an invalid address in custom_emails, or an unknown search key - contact_ids included); \"Current user has no email address\" (self); \"No recipients to send to\" (customer, when no contact is opted in)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Not authenticated, or the user lacks REPORTS_ACCOUNTING. An MCP connector session is refused with 403 instead.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"An MCP connector session - \"Not available to AI connectors\"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"\"No open invoices match the selection\", as on the export route. Nothing is sent.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The PDF failed to render (\"Invoice PDF generation failed: ...\"), or an unexpected error\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/reports/accounts_receivables/invoices/{id}/pdf":{"get":{"summary":"One invoice as a PDF","description":"Renders one invoice of the caller's organization as a PDF - the per-invoice download the A/R export dialog loops over (DOZ-6186). Any invoice that isn't deleted is served, whatever its status (draft and paid included) or customer - the open set does not apply here. A voided invoice is soft-deleted by every void path (invoice delete, rental and sale order void), so it answers 404. Requires REPORTS_ACCOUNTING. Not journaled.\n","tags":["Reports"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"The invoice id (lotus_invoices).","example":"3f9a8b7c-2d4e-4f61-9a0b-5c7d8e9f1a2b"}],"responses":{"200":{"description":"The invoice (invoice-<invoice number>.pdf).","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"The id is not a uuid - \"Invalid data type provided\"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Not authenticated, or the user lacks REPORTS_ACCOUNTING. An MCP connector session is refused with 403 instead.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"An MCP connector session - \"Not available to AI connectors\"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"\"Invoice not found\" - no such invoice in the caller's organization, or it is deleted\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The PDF failed to render (\"Invoice PDF generation failed: ...\"), or an unexpected error\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/saved_filters":{"post":{"summary":"Create a saved filter","description":"Stores a tab for the caller. A `type` of `task` is stored as `service` (the DOZ-7399 back-compat shim). The raw body is written, not the validated one, so Joi's conversions (\"3\" for `weight`, \"true\" for `replaces_default_view`) pass and Postgres casts them, except where the raw text isn't a valid integer: `weight: \"3.0\"` passes Joi and then fails the cast (a 400, below). Journals one CREATE row. Requires RENTAL_VIEW, SALE_VIEW or WORK_ORDER_VIEW.\n","tags":["Saved Filters"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/saved_filter_create"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/saved_filter_write_result"},"example":{"message":"Saved filter created successfully","data":{"id":"5a7c3e19-8b2d-4f61-a0e4-9c1d7b3f2e86","org":"6e1b3f8d-2a7c-4c59-8e40-1d9a5b7c3f62","user_id":"2f8a6c19-5e4b-4d7a-9c03-8b1e7f6a2d54","type":"rental","name":"Due back this week","filters":{"status":["on_rent"],"period":"This Week"},"weight":3,"replaces_default_view":false,"created_at":"2026-03-14T09:30:00.000Z","updated_at":"2026-03-14T09:30:00.000Z"}}}}},"400":{"description":"A schema error: no body or no `name` (`\"name\" is required`), a missing `type` or `filters`, an empty `name` or `type` (`\"name\" is not allowed to be empty`), `filters` that is not an object (`\"filters\" must be of type object`), a `weight` that is not a whole number (`\"weight\" must be an integer`) or is below 0 (`\"weight\" must be greater than or equal to 0`), or any other key (`\"foo\" is not allowed`). After validation, a `weight` string Postgres can't cast to an integer (`\"3.0\"`) answers \"Invalid data type provided\".\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/saved_filter_ai_connector_forbidden"},"409":{"description":"The caller already has a tab with this `type` and `name` (\"Duplicate field value entered for: org, user_id, type, name. Please use another value.\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/saved_filters/search":{"post":{"summary":"List the caller's saved filters","description":"The caller's tabs, lowest `weight` first (no tiebreaker). The first search by a user who was never provisioned (`users.saved_filters_seeded_at` is null) first inserts the default tabs of every module and then stamps the user (DOZ-7509); a stamped user is never seeded again, so `[]` is a valid answer once they deleted their tabs. A seeding failure is logged, leaves the user unstamped (the next search tries again) and the search still answers. The body is optional: no body, or no `search`, lists every tab. Requires RENTAL_VIEW, SALE_VIEW or WORK_ORDER_VIEW.\n","tags":["Saved Filters"],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/saved_filter_search_body"}}}},"responses":{"200":{"description":"A bare array of tabs, never a `{ data, pagination }` envelope: with `paginate` it is that page's rows only, and nothing says how many there are in all. Known issue: the handler starts knex-paginate's own query and discards its promise, so every paginated search (the web sends `paginate` on every tab load) runs the page query a second time, plus a count on page 1, in a detached transaction.\n","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/saved_filter"}}}}},"400":{"description":"`paginate` is checked before `search`. `paginate`: missing `currentPage` or `perPage` (\"Paginate object is required\") or out of range (`\"perPage\" must be less than or equal to 100`); `paginate: null` means not sent. `search`: any other key (`\"foo\" is not allowed`).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/saved_filter_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/saved_filters/{id}":{"parameters":[{"name":"id","in":"path","required":true,"description":"The saved filter's id.","schema":{"type":"string","format":"uuid"},"example":"5a7c3e19-8b2d-4f61-a0e4-9c1d7b3f2e86"}],"get":{"summary":"Get a saved filter","description":"One of the caller's tabs. Requires RENTAL_VIEW, SALE_VIEW or WORK_ORDER_VIEW.\n","tags":["Saved Filters"],"responses":{"200":{"description":"The tab","content":{"application/json":{"schema":{"$ref":"#/components/schemas/saved_filter"}}}},"400":{"description":"A malformed id (`\"id\" must be a valid GUID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/saved_filter_ai_connector_forbidden"},"404":{"description":"No tab with this id belongs to the caller, including another user's (\"Saved filter not found\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"summary":"Update a saved filter","description":"Writes the sent fields of one of the caller's tabs; omitted fields keep their value, and `filters` replaces the whole object. `type` can't be changed. `updated_at` is set on every call, but a call that changes nothing journals no row; otherwise one UPDATE row of the changed fields. Requires RENTAL_VIEW, SALE_VIEW or WORK_ORDER_VIEW.\n","tags":["Saved Filters"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/saved_filter_update"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/saved_filter_write_result"},"example":{"message":"Saved filter updated successfully","data":{"id":"5a7c3e19-8b2d-4f61-a0e4-9c1d7b3f2e86","org":"6e1b3f8d-2a7c-4c59-8e40-1d9a5b7c3f62","user_id":"2f8a6c19-5e4b-4d7a-9c03-8b1e7f6a2d54","type":"rental","name":"Due back next week","filters":{"status":["on_rent"],"period":"Next Week"},"weight":3,"replaces_default_view":false,"created_at":"2026-03-14T09:30:00.000Z","updated_at":"2026-03-15T11:02:00.000Z"}}}}},"400":{"description":"The id is checked first: a malformed id (`\"value\" must be a valid GUID`). Then the body: no body or `{}` (`\"value\" must have at least 1 key`), `type` or any other key (`\"type\" is not allowed`; the \"Type cannot be updated\" message validSavedFilterUpdate defines never fires, because Joi reports `forbidden()` as `any.unknown`), an empty `name`, `filters` that is not an object, or a `weight` below 0.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/saved_filter_ai_connector_forbidden"},"404":{"description":"No tab with this id belongs to the caller, including another user's (\"Saved filter not found\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"A `name` the caller already uses on another tab of the same `type` (\"Duplicate field value entered for: org, user_id, type, name. Please use another value.\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"summary":"Delete a saved filter","description":"Deletes one of the caller's tabs for good (not a soft delete) and journals one DELETE row. A deleted default tab is not provisioned again. Requires RENTAL_VIEW, SALE_VIEW or WORK_ORDER_VIEW.\n","tags":["Saved Filters"],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","required":["message"],"properties":{"message":{"type":"string","example":"Saved filter deleted successfully"}}}}}},"400":{"description":"A malformed id (`\"value\" must be a valid GUID`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/saved_filter_ai_connector_forbidden"},"404":{"description":"No tab with this id belongs to the caller, including another user's (\"Saved filter not found\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/schedules":{"get":{"summary":"List depreciation schedules","description":"Every schedule of the caller's org, soft-deleted ones included (check `deleted`), ordered by method, type and term with no tiebreaker. `[]` when depreciation is disabled for the org. Requires PLATFORM_DEPRECIATION_SCHEDULES.\n","tags":["Schedules"],"responses":{"200":{"description":"The schedules","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/schedule"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/schedule_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"summary":"Create a depreciation schedule","description":"Creates a schedule in the caller's org. Journals one CREATE row and one financial audit row. Requires PLATFORM_DEPRECIATION_SCHEDULES.\n","tags":["Schedules"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/schedule_create"}}}},"responses":{"200":{"description":"The stored schedule","content":{"application/json":{"schema":{"$ref":"#/components/schemas/schedule"}}}},"400":{"description":"A schema error: no body or no `name` (`\"name\" is required`), a missing `method`, `type` or `term`, a `method` or `type` outside its list (`\"method\" must be one of [STRAIGHT_LINE, DOUBLE_DECLINING_BALANCE, MACRS]`), a `term` or `calculation_window_days` that is not a whole number above 0 (`\"term\" must be greater than 0`), a `convention` other than FULL_MONTH (`\"convention\" must be [FULL_MONTH]`), or any other key (`\"deleted\" is not allowed`).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Depreciation is disabled for the org (\"Depreciation feature is disabled for this organization\"), or an AI-connector (MCP) session (\"Not available to AI connectors\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The sent `id` is already a schedule's id (\"Duplicate field value entered for: id. Please use another value.\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Any unexpected error. Known issue: a `name` over 255 characters (varchar(255)) raises a Postgres error the handler doesn't map.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/schedules/update":{"post":{"summary":"Update a depreciation schedule","description":"Writes `name`, `description` and `type` of the schedule `id`; an omitted `name` or `description` keeps its value, but `type` must be sent every time. `method`, `term`, `calculation_window_days` and `convention` can't be changed. A soft-deleted schedule is updated like a live one. Journals one UPDATE row and one financial audit row of the changed fields. Requires PLATFORM_DEPRECIATION_SCHEDULES.\n","tags":["Schedules"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/schedule_update"}}}},"responses":{"200":{"description":"The stored schedule","content":{"application/json":{"schema":{"$ref":"#/components/schemas/schedule"}}}},"400":{"description":"One of, in check order: a schema error, including a malformed id (`\"id\" must be a valid GUID`), a `type` outside the list (`\"type\" must be one of [FEDERAL_TAX, INTERNAL_VALUE, BANK_VALUE, TAX_VALUE, PROPERTY]`), an empty `name` and any other key (`\"method\" is not allowed`); no body or no `id` (`\"id\" is required`); no `type` (\"Please provide a valid type from [Federal Tax, Internal Value, Bank Value, Tax Value, Property]\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Depreciation is disabled for the org (\"Depreciation feature is disabled for this organization\"), or an AI-connector (MCP) session (\"Not available to AI connectors\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No schedule with this id in the caller's org (\"Schedule not found\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Any unexpected error. Known issue: a `name` over 255 characters (varchar(255)) raises a Postgres error the handler doesn't map.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/schedules/types":{"get":{"summary":"List depreciation schedule types","description":"The five fixed schedule types (utils/data/system_types.json), whether or not depreciation is enabled. Requires PLATFORM_DEPRECIATION_SCHEDULES.\n","tags":["Schedules"],"responses":{"200":{"description":"The types","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/schedule_type"}},"example":[{"desc":"Federal Tax","value":"FEDERAL_TAX"},{"desc":"Internal Value","value":"INTERNAL_VALUE"},{"desc":"Bank Value","value":"BANK_VALUE"},{"desc":"Tax Value","value":"TAX_VALUE"},{"desc":"Property","value":"PROPERTY"}]}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/schedule_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/server_logs/mobile/create":{"post":{"summary":"Create a mobile device log","description":"Stores one log row under the caller's own org and user (`org` and `user` come from the session, never the body). Needs no permission, since the app logs for every user, but a service API credential is refused with a 401. Top-level keys other than the documented ones pass validation and are dropped (logged as \"Extra fields that will not be stored in the database\"); extra keys inside `device_information`, `error_information`, `api_endpoint_information` and each metric are stored. The validated body is not what is written: the raw values are, so a numeric string that Joi accepts as a number (`error_information.code: \"500\"`, `size_bytes: \"5\"` in a metric) is stored as a string. Every log needs `error_information.message`, including one that only reports `metrics`.\n","tags":["Server Logs"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/mobile_log_create"}}}},"responses":{"200":{"description":"Stored. The body is this JSON string, not the record.","content":{"application/json":{"schema":{"type":"string","example":"Mobile log has been created"}}}},"400":{"description":"A schema error, first failing key in schema order: no body (`\"value\" is required`), a missing `timestamp` or one Joi can't parse (`\"timestamp\" must be a valid date`), a missing `api_env`, `device_information` or one of its four keys (`\"device_information.os\" is required`), no `error_information` (`\"error_information\" is required`) or no `error_information.message`, a value outside a list (`\"api_env\" must be one of [staging, production, non-production]`, `\"client_type\" must be one of [mobile, web]`), a non-number `error_information.code`, an `api_endpoint_information` without `endpoint`, `request_body` or `query_params` (`\"api_endpoint_information.request_body\" is required`), `metrics: []` (`\"metrics\" must contain at least 1 items`), a metric with a `type` outside its list, a null `entity_id` (`\"metrics[0].entity_id\" must be a string`) or a non-uuid one (`\"metrics[0].entity_id\" must be a valid GUID`), or a size, duration or throughput of 0 or less (`\"metrics[0].size_bytes\" must be greater than 0`).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/mobile_log_ai_connector_forbidden"},"500":{"description":"Any unexpected error. Known issue: `timestamp` as a number or a numeric string (epoch milliseconds, `1710408600000` or `\"1710408600000\"`) passes Joi, but the raw value is not a valid Postgres timestamp (\"date/time field value out of range\"), so the insert fails.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/server_logs/error/create":{"post":{"deprecated":true,"summary":"Create a mobile device log (deprecated path)","description":"The original path of `POST /server_logs/mobile/create`: same handler, same guards and same contract. Kept for older mobile app builds; call /server_logs/mobile/create instead.\n","tags":["Server Logs"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/mobile_log_create"}}}},"responses":{"200":{"description":"Stored. The body is this JSON string, not the record.","content":{"application/json":{"schema":{"type":"string","example":"Mobile log has been created"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/mobile_log_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/server_logs/mobile/search":{"post":{"summary":"Search the org's mobile device logs","description":"Every log row of the caller's org (all users), newest `timestamp` first with no tiebreaker, so rows sharing a timestamp can move between pages. Every filter sent is ANDed. Requires PLATFORM_SERVER_LOGS_READ.\n","tags":["Server Logs"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/mobile_log_search_body"}}}},"responses":{"200":{"description":"One page of logs. knex-paginate is not length-aware here: `pagination.total`, `lastPage`, `prevPage` and `nextPage` only come back on page 1, and `prevPage` / `nextPage` can be null.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/mobile_log_search_result"}}}},"400":{"description":"`search` is checked before `paginate`, so with both wrong the `search` error is the one returned. `search`: missing, or no body at all (`\"value\" is required`), any other key (`\"foo\" is not allowed`), an `api_env` item outside the list (`\"api_env[0]\" must be one of [staging, production, non-production]`), a non-uuid `user` (`\"user\" must be a valid GUID`), an unparseable `from` / `to` (`\"from\" must be a valid date`), or an empty string filter (`\"os\" is not allowed to be empty`). `paginate`: missing, or missing `currentPage` or `perPage` (all three answer \"Paginate object is required\"), `paginate: null` (`\"value\" must be of type object`), or out of range (`\"perPage\" must be less than or equal to 100`).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/mobile_log_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/server_logs/error/search":{"post":{"deprecated":true,"summary":"Search the org's mobile device logs (deprecated path)","description":"The original path of `POST /server_logs/mobile/search`: same handler, same guards (PLATFORM_SERVER_LOGS_READ) and same contract. Kept for older mobile app builds; call /server_logs/mobile/search instead.\n","tags":["Server Logs"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/mobile_log_search_body"}}}},"responses":{"200":{"description":"One page of logs, as `POST /server_logs/mobile/search` answers it","content":{"application/json":{"schema":{"$ref":"#/components/schemas/mobile_log_search_result"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/mobile_log_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/signnow/user/details":{"get":{"summary":"Get the platform's SignNow account","description":"Fetches the profile of the platform's own SignNow account (the same for every org) from SignNow and passes it through unchanged, getting or refreshing the platform's SignNow token first. The shape is SignNow's contract, not this API's: only the top-level keys seen in a live call are listed, and SignNow can add more. Requires PLATFORM_GENERAL_UPDATE.\n","tags":["SignNow"],"responses":{"200":{"description":"The account, as SignNow returns it","content":{"application/json":{"schema":{"$ref":"#/components/schemas/signnow_user"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/signnow_ai_connector_forbidden"},"500":{"description":"SignNow refused the credentials, was unreachable, or answered an error; the handler maps none of these.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/types":{"get":{"summary":"List item types","description":"Every row of the global `types` table, in no defined order; the same list for every org. Requires PLATFORM_CATEGORY_VIEW.\n","tags":["Types"],"responses":{"200":{"description":"The types","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/item_type"}}}}},"400":{"description":"Known issue: a database error is answered 400 with the raw database message instead of a 500 (utils/assetsType.js doGetTypes catches it).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/types_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/platform/users/fcm/update_token":{"post":{"summary":"Save the signed-in user's push-notification (FCM) token","description":"Any signed-in user (no permission check), except a service API credential, which is refused with 401 (it has no device to receive pushes). Stores the Firebase Cloud Messaging token the server pushes notifications to (used by the mobile app for task assignments). A user holds ONE token: the row is keyed by user, so a new token (from another device too) replaces the previous one. There is no Joi schema - the only check is that `token` is present and has a length; other keys are ignored. Writes one row in users_fcm_tokens (user, fcm_token, org, timestamp); nothing else changes.\n","tags":["Platform"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["token"],"properties":{"token":{"type":"string","minLength":1,"description":"The device's FCM registration token.","example":"dGVzdC1mY20tdG9rZW46QVBBOTFiRXhhbXBsZVRva2Vu"}}}}}},"responses":{"200":{"description":"Token saved","content":{"application/json":{"schema":{"type":"string","example":"FCM token updated for user"}}}},"400":{"description":"No body, or `token` missing, empty, or a value without a length such as a number (\"Please provide a valid FCM token\", no `code`)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token, or a service API credential (\"Unauthorized\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"An AI-connector (MCP) session (\"Not available to AI connectors\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/platform/users/notifications/search":{"get":{"summary":"List the signed-in user's own email notification switches","description":"Requires PLATFORM_GENERAL_VIEW. USER_NOTIFICATION_RENTAL_UPDATE, USER_NOTIFICATION_INVOICING_UPDATE, USER_NOTIFICATION_SERVICE_UPDATE, USER_NOTIFICATION_SALE_UPDATE and USER_NOTIFICATION_SYSTEM_UPDATE are forwarded (they never block): they decide `can_edit` per row. Returns every notification of the global catalogue (notification_definitions, the same rows for every org), ordered by weight then key, merged with the user's own setting in the caller's org. Each catalogue row carries a required_permission (not returned) - Rentals and Service.Rental rows need USER_NOTIFICATION_RENTAL_UPDATE, Service, Service.Independent and Service.Purchase rows USER_NOTIFICATION_SERVICE_UPDATE, Service.Sale rows USER_NOTIFICATION_SALE_UPDATE, Invoicing rows USER_NOTIFICATION_INVOICING_UPDATE and System rows USER_NOTIFICATION_SYSTEM_UPDATE. All rows are returned whether or not the user holds the permission. No query parameters; read only.\n","tags":["Platform"],"responses":{"200":{"description":"One entry per catalogue notification","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/user_notification_setting"}}}}},"401":{"description":"Missing or invalid token, or the session lacks PLATFORM_GENERAL_VIEW","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"An AI-connector (MCP) session (\"Not available to AI connectors\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/platform/users/notifications/update":{"post":{"summary":"Switch the signed-in user's own email notifications on or off","description":"Requires PLATFORM_GENERAL_VIEW. The five USER_NOTIFICATION_*_UPDATE permissions are forwarded and decide which notifications may be switched ON: turning a row from off (or never set) to on needs that row's required_permission (see GET /platform/users/notifications/search); without it the whole request is refused with 401. Turning a row off, or re-sending a row that is already on, is always allowed, so the settings page can echo the full list back. The body is an array; an empty array changes nothing and answers 200. Repeated keys collapse to the last one. Each entry is upserted into user_notification_settings for the caller's user and org (one statement) and only email_active is overwritten. An entry without email_active is written as false (switched off). These rows decide who receives the org's notification emails. In the same transaction, one `user` UPDATE journal row lists each `notifications.<key>` that switched (from / to email_active), and none is written when nothing switched (DOZ-7268).\n","tags":["Platform"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/user_notification_setting_update"}},"example":[{"notification_key":"quote","email_active":true},{"notification_key":"invoice_payment","email_active":false}]}}},"responses":{"200":{"description":"Saved (also for an empty array)","content":{"application/json":{"schema":{"type":"string","example":"User notifications updated"}}}},"400":{"description":"No body or not an array ('\"value\" must be an array'), an item without notification_key, a non-boolean email_active or an unknown item key (Joi, code VALIDATION); a key that is not in the catalogue (\"Unknown notification keys: a, b\", no `code`)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token, the session lacks PLATFORM_GENERAL_VIEW, or an entry switches on a notification whose required permission the user lacks (\"Not Authorized to enable these notifications: quote (needs USER_NOTIFICATION_RENTAL_UPDATE), ...\"); nothing is saved then\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"An AI-connector (MCP) session (\"Not available to AI connectors\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/platform/users/self":{"get":{"summary":"Get the signed-in user's own record","description":"Requires USER_SELF_READ. Reads the session user's row in the session org and returns every column except `password` and `override_pin` (replaced by the has_override_pin boolean). `preferences` is merged with the shipped defaults (utils/data/defaults/default_user_preferences.json): keys the stored value lacks are filled in, stored arrays replace the default arrays, and a user with no stored preferences gets the defaults - so the answer always has the full shape. `roles` is the list of role ids. There is no `deleted` filter: a soft-deleted user whose login token is still valid gets 200 with deleted true.\n","tags":["Platform"],"responses":{"200":{"description":"The user's record, preferences merged with the defaults","content":{"application/json":{"schema":{"$ref":"#/components/schemas/user"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"An AI-connector (MCP) session (\"Not available to AI connectors\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No user with the session's id in the session org (\"User not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"summary":"Update the signed-in user's own name, phone or preferences","description":"Requires USER_SELF_UPDATE. Only name, phone and preferences can be changed here; any other key (roles, username, password, override_pin, org, customer_id ...) is refused with 400 (\"x\" is not allowed). The sent keys are written as they are: `preferences` replaces the stored value as a whole (no merge), so send the complete object - the web sends the store's current preferences with its change applied. A phone must not belong to another live user in any org (409). Writes one `user` UPDATE journal row of the fields that changed, and none when nothing did (DOZ-7268); no other side effects. Answers with the updated row, password and override_pin removed, and preferences as stored (NOT merged with defaults, unlike GET).\n","tags":["Platform"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/user_self_update"}}}},"responses":{"200":{"description":"The updated record","content":{"application/json":{"schema":{"$ref":"#/components/schemas/user"}}}},"400":{"description":"Joi failure (an unknown key, a phone not in E.164 form, preferences without columnSettings or with a wrong key or value; code VALIDATION), or no body / an empty object (\"Nothing to update\", no `code`)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"An AI-connector (MCP) session (\"Not available to AI connectors\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The phone belongs to another live user (\"Phone already exists\"); a concurrent save caught by the database index answers \"Duplicate field value entered for: phone. Please use another value.\"\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/platform/users/search":{"post":{"summary":"Search the users of the caller's org","description":"Requires USER_VIEW. Lists the users of the session org, paginated. Service-API credential actors (is_service) are never listed. All search filters are optional and combine with AND; leaving `deleted` out lists live and deleted users together. Each row's roles are resolved to {id, name}. A customer-portal session lists only its own customer's users - never the org's staff or another customer's portal users - and may not filter by search.customer (403). Read only.\n","tags":["Platform"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/user_search_body"}}}},"responses":{"200":{"description":"One page of users","content":{"application/json":{"schema":{"$ref":"#/components/schemas/user_search_result"}}}},"400":{"description":"No paginate object (\"paginate object is required\") or no search object (\"search object is required\") - both without `code`; a paginate or search value Joi rejects (e.g. \"Paginate object is required\" when currentPage or perPage is missing, '\"perPage\" must be less than or equal to 100', an unknown search key; code VALIDATION); an order_by column outside the list (\"Invalid column name in order_by: x\", no `code`)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"An AI-connector (MCP) session (\"Not available to AI connectors\"), or a customer-portal session that sends search.customer (\"You CAN'T do that\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/platform/users":{"post":{"summary":"Create a user in the caller's org","description":"Requires USER_CREATE. A user needs a login key: a valid email `username`, a valid E.164 `phone`, or both. Checks run in this order - (1) neither is valid: 400; (2) a username that is not a valid email while the phone is valid is dropped silently and the user is created phone-only with username ''; (3) the body is validated (unknown keys refused); (4) the username and the phone must not belong to another live user in ANY org (exact match, 409); (5) roles are checked. `org` is always replaced by the session org. A customer-portal session's user always gets that session's customer; an org session's customer_id must be a customer of the org (404 \"Customer not found\"), checked after the body. A password policy applies when `password` is sent; without one the account gets a random password nobody is told (an environment with USE_DUMMY_PASSWORD_FOR_NEW_USERS set uses its fixed dummy password instead). The insert and a `user` CREATE journal row (the new user, password and override_pin masked, plus one `roles[<id>]` entry per role granted) share a transaction (DOZ-7268). Side effects, after it: the user's default saved filters are seeded (every template of utils/data/defaults/default_saved_filters.json, then users.saved_filters_seeded_at is stamped; a failure here is only logged); and when the user has a username, an email with a one-time password-setup link valid for 15 minutes is sent to it (CLIENT_URL/password/new/{id}/{token}) and a journal row (email_delivery_user_management) is written - a mail failure is only logged. Phone-only users get no email; they sign in by SMS code. The answer is the string \"User created\" - the new id is not returned; find the user with POST /platform/users/search.\n","tags":["Platform"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/user_create"}}}},"responses":{"200":{"description":"Created","content":{"application/json":{"schema":{"type":"string","example":"User created"}}}},"400":{"description":"Neither a valid email username nor a valid phone (\"Please provide username or phone to register a new user\"), a Joi failure (unknown key, weak password, pin not 6 digits (\"Override pin must be exactly 6 digits\"), bad preferences), \"Invalid role\", or \"The following roles belong to modules that are not enabled for this organization: Role A, Role B\" - all with code VALIDATION\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"An AI-connector (MCP) session (\"Not available to AI connectors\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"customer_id is not a customer of the caller's org (\"Customer not found\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The phone (\"Phone already exists\") or the username (\"Username already exists\") belongs to another live user in any org; the phone message wins when both clash. A concurrent create caught by the database index also answers \"Phone already exists\".\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/platform/users/{id}":{"parameters":[{"in":"path","name":"id","required":true,"description":"The user's id","schema":{"type":"string","format":"uuid","example":"3f6c2a9e-8b1d-4e57-9a0c-2d4b6e8f1a35"}}],"get":{"summary":"Get one user of the caller's org","description":"Requires USER_READ. Returns the user with that id in the session org; a user of another org and a service-API credential actor answer 404. Soft-deleted users are returned (deleted true). Roles are resolved to {id, name}, preferences are merged with the shipped defaults (like GET /platform/users/self), and the customer is attached as customer_details. password and override_pin are never returned (only has_override_pin). A customer-portal session reads only its own customer's users; any other user answers 404. Read only.\n","tags":["Platform"],"responses":{"200":{"description":"The user (the object itself, not wrapped)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/user_detail"}}}},"400":{"description":"The id is not a uuid (\"Invalid data type provided\", code VALIDATION)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"An AI-connector (MCP) session (\"Not available to AI connectors\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such user in the caller's org, or, for a customer-portal session, not one of its customer's users (\"User not found\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"summary":"Update a user of the caller's org","description":"Requires USER_UPDATE. Partial update: only the sent keys are written, exactly as validated (see user_update for each field's rules; `password` is not accepted). The target must be a non-service user of the session org (404 otherwise). Username: it can only be set while the user has none - sending a non-empty username to a user who already has one answers 400 \"User already has a username set\", even when it is the same value (the web leaves username out when editing such a user); '' or null stores that value, which removes the username. Setting a username on a user without one also replaces the stored password with a random one and, after the update, emails a one-time password-setup link valid for 15 minutes to the new username and writes a journal row (email_delivery_user_management). A sent username or phone must not belong to another live user in any org (409). Customer: an org session's customer_id must be a customer of the org (404 \"Customer not found\"). Roles: an empty array removes all roles; otherwise the roles the user will hold are checked like on create whenever roles or customer_id is sent, against the user as it will be saved (the customer_id sent, else the stored one) - so a customer change also re-checks the roles kept - with the org and module checks applied only to roles the user does not already hold. In the update's transaction, one `user` UPDATE journal row lists what changed - each role gained or lost as a `roles[<id>]` entry, password and override_pin masked - and none is written when nothing changed (DOZ-7268). The user's existing login and OAuth sessions are not touched (new roles apply from the user's next login or token refresh). A soft-deleted target answers 404. A user can never be moved to another org (`org` is refused). For a customer-portal session, customer_id is forced to the session's customer (a sent value is ignored) and only that customer's users can be updated (any other user answers 404); when the session's customer no longer exists the answer is 401 \"Customer with this customer_id not found\".\n","tags":["Platform"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/user_update"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"type":"object","properties":{"msg":{"type":"string","example":"User Updated"},"user":{"$ref":"#/components/schemas/user"}}}}}},"400":{"description":"The id is not a uuid ('\"id\" must be a valid GUID'), a Joi failure (unknown key such as password, a bad email / phone / pin / preferences), \"Invalid role\" or \"The following roles belong to modules that are not enabled for this organization: ...\" (no `code`), \"User already has a username set\" (no `code`), or no body / an empty object (\"Nothing to update\", no `code`). Joi failures carry code VALIDATION.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid token, or a customer-portal session whose customer no longer exists (\"Customer with this customer_id not found\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"An AI-connector (MCP) session (\"Not available to AI connectors\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No live non-service user with that id in the caller's org (for a customer-portal session, among its customer's users) (\"User not found\"), or customer_id is not a customer of the org (\"Customer not found\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The phone (\"Phone already exists\") or the username (\"Username already exists\") belongs to another live user in any org; a concurrent phone save caught by the database index answers \"Duplicate field value entered for: phone. Please use another value.\"\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"summary":"Delete (soft) a user of the caller's org","description":"Requires USER_DELETE. A user cannot delete themselves (403). In one transaction: the user row is soft-deleted (deleted true, roles emptied; the row, username and phone stay, and the username and phone become free for a new user), and every live OAuth (AI-connector) grant of the user is revoked with reason USER_DELETED - the grants' tokens are removed, the user's OAuth browser sessions end and an oauth_session_resets watermark is written; and a `user` DELETE journal row records the user as it was (secrets masked) with a `roles[<id>]` entry per role lost (DOZ-7268). Only live, non-service users of the session org can be deleted; anything else is 404. The user's login (JWT) sessions, saved filters, notification settings and FCM token are not touched; a deleted user is skipped by notification emails and push notifications.\n","tags":["Platform"],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"string","example":"User deleted"}}}},"400":{"description":"The id is not a uuid ('\"id\" must be a valid GUID', no `code`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The id is the caller's own (\"You can't delete your self\"), or an AI-connector (MCP) session (\"Not available to AI connectors\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No live, non-service user with that id in the caller's org (also for a user who is already deleted) (\"User not found\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/vendors/search":{"post":{"summary":"Search the org's vendors","description":"Live (not deleted) vendors of the caller's org, paginated. Each row is the stored vendor record without its contacts. Default order is by id; with `search.order_by` the requested columns come first and id breaks ties. Requires VENDOR_VIEW.\n","tags":["Vendors"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/vendor_search_body"}}}},"responses":{"200":{"description":"One page of vendors. knex-paginate is not length-aware here: `pagination.total`, `lastPage`, `prevPage` and `nextPage` come back on page 1 only, and `prevPage` / `nextPage` can be null. Known issue: the validated `paginate` is discarded and the raw one is passed on, so numeric strings (`currentPage: \"1\"`) pass validation but come back as strings in `pagination`, and page 1 then carries no `total` / `lastPage` / `prevPage` / `nextPage` (knex-paginate compares `\"1\" === 1`). Send numbers.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/vendor_search_result"}}}},"400":{"description":"`paginate` missing, or missing `currentPage` or `perPage` (all three answer \"Paginate object is required\"); `paginate: null` (`\"value\" must be of type object`); a paginate value that is not an integer or out of range (`currentPage` below 1, `perPage` outside 1-100); an unknown key inside `search` (`\"x\" is not allowed`); an unknown key inside `paginate` (`\"x\" is not allowed`); a `paginate` or `search` that is not an object (`\"value\" must be of type object`); an `order_by` entry without a `column` (`\"order_by[0].column\" is required`); an `order_by` entry with a bad `order` or `nulls` (`\"order_by[0].order\" must be one of [asc, desc, ASC, DESC]`); or an `order_by` column outside the list (\"Invalid column name in order_by: <column>\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/vendor_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/vendors":{"post":{"summary":"Create a vendor","description":"Creates a vendor and the contacts sent with it. Checks, in this order: the body against the schema; every vendor field setting at \"requirement\" other than primary_contact must be truthy in the body; with primary_contact at \"requirement\", at least one contact must be sent. `vendor_number` is assigned by the server as the org's highest vendor_number + 1 (1 for the org's first vendor). `default_contact` is the sent value, or the first sent contact when none is given. A sent value is stored unchecked, with or without contacts (known issue: any uuid is accepted, even one that is not this vendor's contact). Then `validated` / `validation_failures` are computed from the field settings. Journals one CREATE row. Requires VENDOR_CREATE.\n","tags":["Vendors"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/vendor_create"}}}},"responses":{"200":{"description":"The stored vendor. `contacts` holds the inserted contact rows, `[]` after `contacts: []`, and is absent when `contacts` was left out.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/vendor_write_result"}}}},"400":{"description":"A schema error: no body or no `name` (`\"name\" is required`), `name: ''` (`\"name\" is not allowed to be empty`), an unknown key such as `vendor_number` (`\"vendor_number\" is not allowed`) or inside a contact (`\"contacts[0].deleted\" is not allowed`), or a contact email that is not an email. Then a field setting at \"requirement\" that is empty or missing in the body (\"Field '<setting name>' is required to create a vendor\", e.g. \"Field 'Primary Address' is required to create a vendor\"; only one is named, and which one comes first is unspecified). Then primary_contact at \"requirement\" with no contacts (\"Field 'Primary Contact' is required to create a vendor. Please attach at least one contact\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/vendor_ai_connector_forbidden"},"409":{"description":"The body's `id`, or a contact's `id`, is already taken (\"Duplicate field value entered for: id. Please use another value.\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/vendors/{id}":{"get":{"summary":"Get a vendor with its contacts","description":"The live vendor with its live contacts. Requires VENDOR_READ.","tags":["Vendors"],"parameters":[{"$ref":"#/components/parameters/vendor_id"}],"responses":{"200":{"description":"The vendor","content":{"application/json":{"schema":{"$ref":"#/components/schemas/vendor_detail"}}}},"400":{"description":"A malformed id (`\"id\" must be a valid GUID`), or no live vendor with this id in the caller's org (\"vendor not found\"; a missing vendor is a 400, not a 404).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/vendor_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"summary":"Update a vendor and its contacts","description":"Writes the sent fields; an omitted field keeps its value, but `name` is required on every update (the create schema is reused). A sent `id` is replaced by the path id and a sent `org` is dropped.\n\n`contacts`, when sent, is the vendor's whole contact list: a contact with an `id` (it must be one of this vendor's live contacts) is updated, one without an `id` is added, and every live contact left out is soft-deleted; `[]` soft-deletes them all. Left out, the contacts are not touched. Send each contact whole: the contacts are written as one multi-row upsert, so a key one contact leaves out while another sent contact carries it is written as null.\n\n`default_contact`: with `contacts: []` it becomes null. With a non-empty `contacts` list only a sent `default_contact` is compared: one that names a sent contact's id is kept; otherwise - including when it is left out - the default becomes the first sent contact, even if the stored default is in the list. Known issue: when `default_contact` is not sent and any sent contact has no `id`, the stored default is kept (the code compares `undefined === undefined`), even when this same request soft-deletes that contact; with no stored default, the first sent contact becomes it. With `contacts` left out, a sent `default_contact` is stored unchecked (known issue: any uuid is accepted, even one that is not this vendor's contact).\n\nThe field settings are not enforced (no \"requirement\" 400); `validated` / `validation_failures` are recomputed instead. Journals one UPDATE row of the fields and contacts that changed. Requires VENDOR_UPDATE.\n","tags":["Vendors"],"parameters":[{"$ref":"#/components/parameters/vendor_id"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/vendor_update"}}}},"responses":{"200":{"description":"The stored vendor. `contacts` holds the sent contacts as stored (the vendor's full live list), `[]` after `contacts: []`, and is absent when `contacts` was left out.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/vendor_write_result"}}}},"400":{"description":"A schema error (checked first: no body or no `name` is `\"name\" is required`, `name: ''` is `\"name\" is not allowed to be empty`, an unknown key such as `vendor_number` is `\"vendor_number\" is not allowed`, one inside a contact is `\"contacts[0].deleted\" is not allowed`), a malformed id (`\"value\" must be a valid GUID`), no live vendor with this id in the caller's org (\"Vendor not found\"), or a contact id that is not one of this vendor's live contacts (\"Unknown contact id: <id>\", code VALIDATION).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/vendor_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"summary":"Delete a vendor","description":"Soft-deletes the vendor and all of its contacts. Nothing is refused: purchase orders that name the vendor keep pointing at it and still show it. Journals one DELETE row. Requires VENDOR_DELETE.\n","tags":["Vendors"],"parameters":[{"$ref":"#/components/parameters/vendor_id"}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"string","example":"Vendor deleted"}}}},"400":{"description":"A malformed id (`\"value\" must be a valid GUID`), or no live vendor with this id in the caller's org (\"Vendor not found\"), so a repeat delete is a 400.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/vendor_ai_connector_forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}}}},"components":{"schemas":{"ErrorResponse":{"type":"object","description":"The envelope globalErrorHandler/errorHandler.js sends for every error.","properties":{"success":{"type":"boolean","example":false},"status":{"type":"string","description":"\"fail\" for a 4xx AppError, \"error\" otherwise.","example":"fail"},"statusCode":{"type":"integer","example":400},"error":{"type":"string","description":"Human-readable message. Masked to a generic one for an unexpected 500."},"code":{"type":"string","enum":["VALIDATION","NOT_FOUND","UNAUTHORIZED","FORBIDDEN","CONFLICT","INTERNAL"],"description":"Machine-readable code (DOZ-7648). Omitted on a 400 that named none."},"requestId":{"type":"string"},"details":{"type":"object","nullable":true,"description":"Structured context an endpoint chose to attach, when it did."}}},"lotus_order_pagination":{"type":"object","description":"knex-paginate without isLengthAware - total, lastPage, prevPage and nextPage are sent on page 1 only.\n","properties":{"perPage":{"type":"integer","example":25},"currentPage":{"type":"integer","example":1},"from":{"type":"integer","example":0},"to":{"type":"integer","example":25},"total":{"type":"integer","description":"Page 1 only.","example":41},"lastPage":{"type":"integer","description":"Page 1 only.","example":2},"prevPage":{"type":"integer","nullable":true,"description":"Page 1 only (always null there)."},"nextPage":{"type":"integer","nullable":true,"description":"Page 1 only; null when there is no second page.","example":2}}},"lotus_order_customer_details":{"type":"object","description":"The order's customer (hydrateCustomerDetails), with its live contacts. A sales order without a customer still gets this object, with every field null and contacts empty.\n","properties":{"id":{"type":"string","format":"uuid","nullable":true},"company_name":{"type":"string","nullable":true},"customer_number":{"type":"integer","nullable":true},"address":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/invoice_postal_address"}]},"billing_address":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/invoice_billing_address"}]},"default_contact":{"type":"string","format":"uuid","nullable":true},"default_site":{"type":"string","format":"uuid","nullable":true},"default_payment_method":{"type":"string","nullable":true,"description":"Stripe payment method id (pm_...) of the saved default card."},"stripe_id":{"type":"string","nullable":true,"description":"Stripe customer id (cus_...)."},"auto_payment":{"type":"boolean","nullable":true,"description":"True when the customer's saved card is charged automatically."},"pay_on_open":{"type":"boolean","nullable":true},"terms":{"type":"integer","nullable":true,"description":"Payment terms in days."},"validated":{"type":"boolean","nullable":true,"description":"False for a non-compliant customer."},"env_fee_status":{"type":"boolean","nullable":true},"damage_waiver_status":{"type":"boolean","nullable":true},"state_sales_tax_status":{"type":"boolean","nullable":true},"county_sales_tax_status":{"type":"boolean","nullable":true},"city_sales_tax_status":{"type":"boolean","nullable":true},"special_sales_tax_status":{"type":"boolean","nullable":true},"deleted":{"type":"boolean","nullable":true},"contacts":{"type":"array","description":"The customer's non-deleted contacts (full customer_contacts rows).","items":{"$ref":"#/components/schemas/lotus_order_customer_contact"}}}},"lotus_order_customer_contact":{"type":"object","description":"A customer_contacts row. The *_email flags say which notifications the contact receives.","properties":{"id":{"type":"string","format":"uuid"},"org":{"type":"string","format":"uuid"},"customer":{"type":"string","format":"uuid"},"key":{"type":"integer","nullable":true},"name":{"type":"string","nullable":true},"email":{"type":"string","nullable":true},"title":{"type":"string","nullable":true},"department":{"type":"string","nullable":true},"office_phone":{"type":"string","nullable":true},"cell_phone":{"type":"string","nullable":true},"fax":{"type":"string","nullable":true},"note":{"type":"string","nullable":true},"new_quotes_email":{"type":"boolean"},"new_reservations_email":{"type":"boolean"},"open_contracts_email":{"type":"boolean"},"closed_contracts_email":{"type":"boolean"},"completed_contracts_email":{"type":"boolean"},"new_invoices_email":{"type":"boolean"},"invoice_payments_email":{"type":"boolean"},"asset_offrents_email":{"type":"boolean"},"statements_email":{"type":"boolean"},"deleted":{"type":"boolean"},"created_at":{"type":"string","format":"date-time","nullable":true},"updated_at":{"type":"string","format":"date-time","nullable":true}}},"lotus_order_site_details":{"type":"object","nullable":true,"description":"The order's site, or null when the order has none. Keys whose value is null are left out (jsonb_strip_nulls) - most sites carry no address, city, state or zip.\n","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string"},"address":{"type":"string"},"city":{"type":"string"},"state":{"type":"string"},"zip":{"type":"string"},"deleted":{"type":"boolean"}}},"lotus_order_contact_details":{"type":"object","nullable":true,"description":"The order's contact, or null when none is set. Null-valued keys are left out.","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"email":{"type":"string"},"title":{"type":"string"},"deleted":{"type":"boolean"}}},"lotus_order_assignee_details":{"type":"object","nullable":true,"description":"The user the order is assigned to, or null when unassigned. Null-valued keys are left out.","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"username":{"type":"string","description":"The user's email."},"phone":{"type":"string"}}},"lotus_order_ancestor":{"type":"object","description":"One order up the parent_id chain, oldest first.","properties":{"id":{"type":"string","format":"uuid"},"order_number":{"type":"string"},"status":{"type":"string"},"type":{"type":"string","enum":["RENTAL","SALE","WORK_ORDER"]}}},"lotus_order_adjustment":{"type":"object","description":"A tax, fee or discount rule stored on the order (lotus_order_adjustments).","properties":{"id":{"type":"string","format":"uuid"},"type":{"type":"string","description":"e.g. STATE_TAX, COUNTY_TAX, CITY_TAX, SPECIAL_TAX, DAMAGE_WAIVER, ENV_FEE, ORDER_DISCOUNT, CUSTOMER_DISCOUNT, FLAT_DISCOUNT.","example":"STATE_TAX"},"category":{"type":"string","enum":["TAX","FEE","DISCOUNT"]},"value":{"type":"number","description":"A percentage when is_percentage, else a dollar amount.","example":4},"is_percentage":{"type":"boolean"},"calculation_priority":{"type":"integer","description":"Lower runs first."}}},"lotus_order_applied_adjustment":{"description":"An adjustment with the amount it came to on this order.","allOf":[{"$ref":"#/components/schemas/lotus_order_adjustment"},{"type":"object","properties":{"calculated_dollar_value":{"type":"number","description":"Rounded to 2 decimals - the amount billed as a charge. Subtracted for a DISCOUNT."},"calculated_percentage_value":{"type":"number","nullable":true,"description":"Null for a flat discount on a $0 subtotal."},"taxable_base":{"type":"number"},"calculation_basis_description":{"type":"string"},"org":{"type":"string","format":"uuid","description":"POST /lotus/rentals/charges_summary and POST /lotus/sales/charges_summary only. No charges_summary route returns an id (its adjustments are not stored).\n"},"description":{"type":"string","nullable":true,"description":"POST /lotus/sales/charges_summary and POST /lotus/work-orders/charges_summary only; always null there."},"tax_state_code":{"type":"string","nullable":true,"description":"POST /lotus/sales/charges_summary only - where a tax rate came from."},"tax_county_name":{"type":"string","nullable":true,"description":"POST /lotus/sales/charges_summary only."},"tax_city_name":{"type":"string","nullable":true,"description":"POST /lotus/sales/charges_summary only."},"tax_source_zip":{"type":"string","nullable":true,"description":"POST /lotus/sales/charges_summary only."}}}]},"lotus_order_fulfillment_ticket":{"type":"object","description":"A delivery, pickup or additional freight job on the order.","properties":{"id":{"type":"string","format":"uuid"},"type":{"type":"string","enum":["DELIVERY","PICKUP","ADDITIONAL"]},"status":{"type":"string","example":"SCHEDULED"},"site_id":{"type":"string","format":"uuid","nullable":true},"freight_company_id":{"type":"string","format":"uuid","nullable":true},"quoted_freight_fee":{"type":"number","description":"Charged to the customer."},"freight_fee":{"type":"number","description":"What the freight company charges (internal)."}}},"lotus_order_invoice_summary":{"type":"object","description":"One of the order's invoices, oldest first.","properties":{"id":{"type":"string","format":"uuid"},"invoice_number":{"type":"string","example":"INV-R-20260923-001-01"},"approved":{"type":"boolean"},"total_amount":{"type":"number"},"amount_paid":{"type":"number"},"balance_due":{"type":"number"},"payment_status":{"type":"string","enum":["DRAFT","UNPAID","PARTIALLY_PAID","PAID","VOIDED"],"description":"The invoice's status."},"created_at":{"type":"string","format":"date-time"}}},"lotus_order_signed_artifact":{"type":"object","nullable":true,"description":"The current SignNow-signed PDF of the order, or null when it has none.","properties":{"media_id":{"type":"string","format":"uuid"},"full_path":{"type":"string"},"file_name":{"type":"string"},"file_type":{"type":"string"},"file_size":{"type":"number"},"created_at":{"type":"string","format":"date-time"}}},"lotus_order_financials":{"type":"object","description":"Projected totals computed on read (calculateRentalOrderFinancials for rentals, calculateSaleOrderFinancials for sales); nothing is stored.","properties":{"projected_line_item_sub_total":{"type":"number","description":"Line items after their own discounts, before order adjustments."},"projected_fulfillment_sub_total":{"type":"number","description":"Sum of the tickets' quoted_freight_fee."},"projected_total":{"type":"number","description":"Both subtotals plus taxes and fees, less discounts, rounded to 2 decimals."},"applied_adjustments":{"type":"object","properties":{"line_item_adjustments":{"type":"array","items":{"$ref":"#/components/schemas/lotus_order_applied_adjustment"}},"fulfillment_ticket_adjustments":{"type":"array","description":"Only the taxes apply to freight.","items":{"$ref":"#/components/schemas/lotus_order_applied_adjustment"}}}}}},"lotus_credit_decision":{"type":"object","description":"The customer's credit position against their credit limit (customerCreditService.evaluateCreditForConversion).\n","properties":{"current_balance":{"type":"number","nullable":true,"description":"Open A/R. Null when the customer has no limit and nothing was computed (charges summary only)."},"open_orders":{"type":"number","nullable":true,"description":"Projected value of committed, not yet invoiced orders. Null as above."},"total_exposure":{"type":"number","nullable":true,"description":"current_balance + open_orders + this order's projection. Null as above."},"limit":{"type":"number","nullable":true,"description":"The credit limit; null when the customer has none."},"action":{"type":"string","enum":["WARN","BLOCK",null],"nullable":true,"description":"What happens over the limit. Null only for a sale without a customer."},"over_by":{"type":"number"},"decision":{"type":"string","enum":["OK","WARN","BLOCK"]},"reason":{"type":"string","enum":["no_limit","within_limit","no_customer"],"description":"Sent with decision OK only. no_customer - GET /lotus/sales/{id}/credit-check on a sale without a customer."}}},"paginate":{"type":"object","description":"The page to return (validPaginateBody). When it is sent, both keys are required integers and unknown keys are refused. A missing key answers 400 \"Paginate object is required\"; any other problem answers Joi's own message, e.g. \"perPage\" must be less than or equal to 100, or \"x\" is not allowed. Whether paginate itself is required is up to each operation.\n","required":["currentPage","perPage"],"properties":{"currentPage":{"type":"integer","minimum":1,"example":1},"perPage":{"type":"integer","minimum":1,"maximum":100,"example":10}}},"pagination":{"type":"object","description":"The page block of a paginated list. Which keys come back, and when they are null, depends on the list; each operation says so.\n","properties":{"total":{"type":"integer","description":"Rows matching the request across every page."},"lastPage":{"type":"integer","nullable":true},"prevPage":{"type":"integer","nullable":true,"description":"Null on page 1."},"nextPage":{"type":"integer","nullable":true,"description":"Null on the last page."},"perPage":{"type":"integer","nullable":true},"currentPage":{"type":"integer"},"from":{"type":"integer","nullable":true},"to":{"type":"integer","nullable":true}}},"order_by":{"type":"array","description":"Server-side sort, applied in order (the second entry breaks ties in the first). Each column must be in the endpoint's orderByColumnNamesList, else 400 \"Invalid column name in order_by: <column>\". The server appends the row id as the last key. Omitted or empty, the list keeps its default order.\n","items":{"type":"object","required":["column"],"properties":{"column":{"type":"string"},"order":{"type":"string","enum":["asc","desc","ASC","DESC"],"default":"asc"},"nulls":{"type":"string","enum":["first","last","FIRST","LAST"],"default":"last","description":"Where empty values go."}}}},"orderByColumnNamesList":{"type":"array","description":"The columns this endpoint can sort by - the same list the server checks order_by against.","items":{"type":"string"}},"invoice":{"type":"object","description":"A stored invoice (lotus_invoices), as the search and the detail read return it. Every field is server-set. A credit note is an invoice with a negative total_amount; it is born PAID with no due date.\n","properties":{"id":{"type":"string","format":"uuid","readOnly":true},"org":{"type":"string","format":"uuid","readOnly":true},"order_id":{"type":"string","format":"uuid","readOnly":true},"order_type":{"type":"string","enum":["RENTAL","SALE","WORK_ORDER"],"readOnly":true},"customer_id":{"type":"string","format":"uuid","readOnly":true},"invoice_number":{"type":"string","example":"INV-R-20260923-001-01","readOnly":true},"status":{"type":"string","enum":["DRAFT","UNPAID","PARTIALLY_PAID","PAID","VOIDED"],"readOnly":true},"approved":{"type":"boolean","readOnly":true},"total_amount":{"type":"number","example":533.18,"description":"Negative for a credit note.","readOnly":true},"amount_paid":{"type":"number","nullable":true,"readOnly":true},"balance_due":{"type":"number","nullable":true,"readOnly":true},"currency":{"type":"string","example":"USD","readOnly":true},"due_date":{"type":"string","format":"date-time","nullable":true,"description":"Null on credit notes.","readOnly":true},"invoice_date":{"type":"string","format":"date-time","readOnly":true},"created_at":{"type":"string","format":"date-time","nullable":true,"readOnly":true},"updated_at":{"type":"string","format":"date-time","nullable":true,"readOnly":true},"deleted":{"type":"boolean","description":"True once voided (status VOIDED).","readOnly":true},"customer_snapshot":{"type":"object","nullable":true,"description":"The customer as billed, frozen when the invoice was issued.","readOnly":true,"properties":{"id":{"type":"string","format":"uuid"},"company_name":{"type":"string"},"customer_number":{"type":"integer"},"address":{"allOf":[{"$ref":"#/components/schemas/invoice_postal_address"}],"nullable":true},"billing_address":{"allOf":[{"$ref":"#/components/schemas/invoice_billing_address"}],"nullable":true}}}}},"invoice_billing_address":{"description":"A postal address plus the billing contact.","allOf":[{"$ref":"#/components/schemas/invoice_postal_address"},{"type":"object","properties":{"billing_name":{"type":"string","nullable":true},"email":{"type":"string","nullable":true},"phone":{"type":"string","nullable":true}}}]},"invoice_postal_address":{"type":"object","properties":{"street1":{"type":"string","nullable":true},"street2":{"type":"string","nullable":true},"city":{"type":"string","nullable":true},"state":{"type":"string","nullable":true},"zip":{"type":"string","nullable":true}}},"unbilled_search":{"type":"object","description":"Body of POST /lotus/invoices/unbilled/search. Both keys are optional. Unlike the invoice search, unknown keys are refused with a 400, not stripped.\n","properties":{"search":{"type":"object","description":"Every filter is optional; omitted means no filter.","properties":{"customer_id":{"type":"string","format":"uuid","example":"3f1c1d6e-2b7a-4c1e-9d55-0a6f3b2c8e11","description":"Ignored for a customer-portal session, which is always limited to its own customer."},"order_type":{"type":"array","minItems":1,"description":"Case-insensitive. Intersected with the order types the caller may view. An empty array is a 400.","items":{"type":"string","enum":["RENTAL","SALE","WORK_ORDER"]},"example":["RENTAL"]},"order_id":{"type":"string","format":"uuid","example":"8a0b4c52-7d6e-4f3a-b1c9-5e2d9f7a6b30"},"order_number":{"type":"string","minLength":1,"maxLength":100,"example":"R-20260923-001","description":"Exact order number."},"include_settled":{"type":"boolean","default":false,"example":false,"description":"True also lists orders with nothing left to bill, and those over-invoiced."},"order_by":{"type":"array","description":"Applied in order; each later entry breaks ties in the one before, then the order number ascending. Empty values sort last in either direction. Default is unbilled descending.\n","maxItems":4,"items":{"type":"object","required":["column"],"properties":{"column":{"type":"string","enum":["unbilled","projected_total","invoiced_total","order_number"],"example":"unbilled"},"order":{"type":"string","enum":["asc","desc"],"default":"asc","example":"desc"}}}}}},"paginate":{"type":"object","properties":{"currentPage":{"type":"integer","minimum":1,"default":1,"example":1},"perPage":{"type":"integer","minimum":1,"maximum":100,"default":25,"example":25}}}}},"unbilled_row":{"type":"object","description":"One open order with what is projected, invoiced and left to bill. Every field is server-set.","properties":{"order_id":{"type":"string","format":"uuid","readOnly":true},"order_number":{"type":"string","example":"R-20260923-001","readOnly":true},"order_type":{"type":"string","enum":["RENTAL","SALE","WORK_ORDER"],"readOnly":true},"order_status":{"type":"string","example":"ACTIVE","readOnly":true},"parent_order_number":{"type":"string","nullable":true,"description":"For a rental the renewal predecessor, for a work order the rental that spawned it; null if none.","readOnly":true},"customer_id":{"type":"string","format":"uuid","nullable":true,"readOnly":true},"customer_number":{"type":"integer","nullable":true,"readOnly":true},"company_name":{"type":"string","nullable":true,"readOnly":true},"projected_total":{"type":"number","nullable":true,"description":"Contract value of the order. Null when projection_error.","readOnly":true},"invoiced_total":{"type":"number","description":"Net of card fees; DRAFT invoices count, voided ones do not, credit notes reduce it.","readOnly":true},"draft_invoiced_total":{"type":"number","description":"The part of invoiced_total that is still on DRAFT invoices.","readOnly":true},"invoice_count":{"type":"integer","readOnly":true},"last_invoice_at":{"type":"string","format":"date-time","nullable":true,"readOnly":true},"unbilled":{"type":"number","nullable":true,"description":"projected_total minus invoiced_total. Negative means over-invoiced. Null when projection_error.","readOnly":true},"projection_error":{"type":"boolean","description":"True when the order could not be priced; projected_total and unbilled are then null.","readOnly":true}}},"unbilled_search_result":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/unbilled_row"}},"totals":{"type":"object","description":"Over every computed order, before the include_settled filter and before paging.","properties":{"unbilled_total":{"type":"number","description":"Sum of the positive unbilled figures."},"over_invoiced_total":{"type":"number","description":"Sum of the magnitudes of the negative unbilled figures."},"orders_computed":{"type":"integer"},"orders_with_errors":{"type":"integer"}}},"pagination":{"type":"object","properties":{"currentPage":{"type":"integer"},"perPage":{"type":"integer"},"total":{"type":"integer"},"from":{"type":"integer","nullable":true},"to":{"type":"integer","nullable":true},"lastPage":{"type":"integer"},"prevPage":{"type":"integer","nullable":true},"nextPage":{"type":"integer","nullable":true}}},"currency":{"type":"string","enum":["USD"]}}},"invoice_search_body":{"type":"object","description":"Body of POST /lotus/invoices/search. Both keys are optional; unknown keys are stripped.","properties":{"search":{"type":"object","description":"Every filter is optional; omitted means no filter. order_by sorts by invoice_number, company_name, total_amount, status, due_date or order_number; the default order is status, then newest first.\n","properties":{"order_type":{"type":"array","description":"Case-insensitive. Intersected with the order types the caller may view.","items":{"type":"string","enum":["RENTAL","SALE","WORK_ORDER"]}},"status":{"type":"array","description":"Case-insensitive. Asking for VOIDED also lists the deleted (voided) invoices.","items":{"type":"string","enum":["DRAFT","UNPAID","PARTIALLY_PAID","PAID","VOIDED"]}},"approved":{"type":"boolean","nullable":true,"description":"Null means no filter."},"deleted":{"type":"boolean","description":"Without it deleted invoices are hidden, unless status asks for VOIDED."},"customer_id":{"type":"string","format":"uuid","description":"Ignored for a customer-portal session, which is always limited to its own customer."},"company_name":{"type":"string","nullable":true,"description":"Case-insensitive substring of the customer's company name."},"invoice_number":{"type":"string","nullable":true,"description":"Case-insensitive substring."},"date_from":{"type":"string","format":"date-time","description":"Invoice date from (inclusive)."},"date_to":{"type":"string","format":"date-time","description":"Invoice date to (inclusive). Requires date_from and must be after it."},"order_id":{"type":"string","format":"uuid"},"is_credit_note":{"type":"boolean","nullable":true,"description":"True for credit notes only (negative total); false for every other invoice, zero-total ones included; null or omitted for both."},"order_by":{"$ref":"#/components/schemas/order_by"}}},"paginate":{"type":"object","properties":{"currentPage":{"type":"integer","minimum":1,"default":1},"perPage":{"type":"integer","minimum":1,"maximum":100,"default":25}}}}},"invoice_search_result":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/invoice_search_row"}},"pagination":{"type":"object","description":"perPage, currentPage, from and to are always sent. total, lastPage, prevPage and nextPage are only sent for page 1, where prevPage is always null (and nextPage is null on the last page). When none of the requested order types is permitted the page is empty, total and lastPage are 0 on any page, and prevPage and nextPage are not sent.\n","properties":{"total":{"type":"integer"},"lastPage":{"type":"integer"},"prevPage":{"type":"integer","nullable":true},"nextPage":{"type":"integer","nullable":true},"perPage":{"type":"integer"},"currentPage":{"type":"integer"},"from":{"type":"integer"},"to":{"type":"integer"}}},"orderByColumnNamesList":{"$ref":"#/components/schemas/orderByColumnNamesList"}}},"invoice_search_row":{"description":"A search result - the stored invoice plus its order, customer and display figures.","allOf":[{"$ref":"#/components/schemas/invoice"},{"type":"object","properties":{"order_number":{"type":"string","nullable":true,"example":"R-20260923-001","readOnly":true},"order_status":{"type":"string","nullable":true,"readOnly":true},"customer_company_name":{"type":"string","nullable":true,"description":"The customer's current company name (customer_snapshot holds the billed one).","readOnly":true},"customer_number":{"type":"integer","nullable":true,"readOnly":true},"customer_deleted":{"type":"boolean","nullable":true,"readOnly":true},"balance_application_setting":{"type":"string","enum":["ORDER_ONLY","ORDER_AND_FLOATING","ALL",null],"nullable":true,"readOnly":true},"cash_discount_percentage":{"type":"number","nullable":true,"readOnly":true},"cash_discount_days":{"type":"integer","nullable":true,"readOnly":true},"billing_type":{"type":"string","enum":["ADVANCE","ON_ACCOUNT",null],"nullable":true,"description":"Rental invoices only; null for sale and work-order invoices.","readOnly":true},"applicable_balance":{"type":"number","description":"Customer balance that can be applied to this invoice now; 0 when nothing is due.","readOnly":true},"early_payment_discount":{"type":"number","description":"Discount the customer still earns by paying this invoice in full within the window; 0 when none.","readOnly":true}}}]},"invoice_detail":{"type":"object","description":"Body of GET /lotus/invoices/{id}.","properties":{"invoice":{"allOf":[{"$ref":"#/components/schemas/invoice"},{"type":"object","properties":{"org_snapshot":{"type":"object","nullable":true,"readOnly":true},"adjustments_snapshot":{"type":"object","nullable":true,"readOnly":true},"actual_amount_paid":{"type":"number","description":"Real money paid (cash, check, card, ...), excluding refund credit and discounts.","readOnly":true},"refund_applied":{"type":"number","description":"Refund credit absorbed by this invoice.","readOnly":true},"early_payment_discount_applied":{"type":"number","description":"Early payment discount already granted on this invoice.","readOnly":true},"early_payment_discount":{"type":"number","description":"Same as the top-level early_payment_discount.","readOnly":true}}}]},"order":{"$ref":"#/components/schemas/invoice_detail_order"},"customer":{"$ref":"#/components/schemas/invoice_detail_customer"},"site":{"$ref":"#/components/schemas/invoice_detail_site"},"org":{"$ref":"#/components/schemas/invoice_detail_org"},"applicable_balance":{"type":"number","description":"Customer balance that can be applied to this invoice now; 0 when nothing is due."},"early_payment_discount":{"type":"number","description":"Discount the customer still earns by paying this invoice in full within the window; 0 when none."},"charges":{"type":"object","description":"The charges this invoice billed, in sequence order. all holds every row; the other keys split the same rows for display, each row going to the first group it matches - rental (RENTAL and INITIAL_RENTAL rows), ccFee (CC_PROCESSING_FEE rows; the sales tax on a fee is a tax row and lands in adjustment), freight (FREIGHT rows and any row tied to a fulfillment ticket), delta (the prior-balance zero-out row of a re-bill), workOrderParts (PART rows of a child work order billed on a rental), parts (one-time PART and OTHER rows tied to a line item - a work order's own parts and custom items) and adjustment (everything else - taxes, fees, discounts, and the item lines of a sale invoice, which are stored as SALE charges).\n","properties":{"all":{"type":"array","items":{"$ref":"#/components/schemas/invoice_charge"}},"rental":{"type":"array","items":{"$ref":"#/components/schemas/invoice_charge"}},"parts":{"type":"array","items":{"$ref":"#/components/schemas/invoice_charge"}},"workOrderParts":{"type":"array","items":{"$ref":"#/components/schemas/invoice_charge"}},"freight":{"type":"array","items":{"$ref":"#/components/schemas/invoice_charge"}},"adjustment":{"type":"array","items":{"$ref":"#/components/schemas/invoice_charge"}},"delta":{"type":"array","items":{"$ref":"#/components/schemas/invoice_charge"}},"ccFee":{"type":"array","items":{"$ref":"#/components/schemas/invoice_charge"}}}},"payments_applied":{"type":"array","description":"Successful payments applied to this invoice, oldest first.","items":{"$ref":"#/components/schemas/invoice_payment_applied"}}}},"invoice_detail_order":{"type":"object","description":"The order the invoice belongs to.","properties":{"id":{"type":"string","format":"uuid"},"order_number":{"type":"string","example":"R-20260923-001"},"type":{"type":"string","enum":["RENTAL","SALE","WORK_ORDER"]},"status":{"type":"string","example":"OPEN_CONTRACT"},"customer_id":{"type":"string","format":"uuid","nullable":true},"site_id":{"type":"string","format":"uuid","nullable":true},"external_notes":{"type":"string","nullable":true},"internal_notes":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time","nullable":true}}},"invoice_detail_customer":{"type":"object","nullable":true,"description":"The billed customer as it is now.","properties":{"id":{"type":"string","format":"uuid"},"company_name":{"type":"string"},"customer_number":{"type":"integer"},"address":{"allOf":[{"$ref":"#/components/schemas/invoice_postal_address"}],"nullable":true},"billing_address":{"allOf":[{"$ref":"#/components/schemas/invoice_billing_address"}],"nullable":true},"terms":{"type":"integer","description":"Payment terms in days."},"cash_discount_percentage":{"type":"number"},"cash_discount_days":{"type":"integer"},"balance_application_setting":{"type":"string","enum":["ORDER_ONLY","ORDER_AND_FLOATING","ALL"]},"default_contact":{"type":"string","format":"uuid","nullable":true}}},"invoice_detail_site":{"type":"object","nullable":true,"description":"The order's job site; null when the order has none.","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","nullable":true},"address":{"type":"string","nullable":true},"city":{"type":"string","nullable":true},"state":{"type":"string","nullable":true},"zip":{"type":"string","nullable":true},"tax_exemption_status":{"type":"boolean","nullable":true}}},"invoice_detail_org":{"type":"object","description":"The issuing organization, with the settings the invoice document uses.","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","nullable":true},"address":{"allOf":[{"$ref":"#/components/schemas/invoice_postal_address"}],"nullable":true},"logo":{"type":"string","nullable":true},"invoice_address":{"type":"string","nullable":true,"description":"The org's INVOICE.INVOICE_ADDRESS setting."},"timezone":{"type":"string","example":"America/New_York","description":"The org's GENERAL.TIMEZONE setting."},"early_payment_discount_status":{"type":"boolean","description":"The org's INVOICE.EARLY_PAYMENT_DISCOUNT_STATUS setting."}}},"invoice_charge":{"type":"object","description":"One billed charge (lotus_billable_charges), with its line item, dispatched asset and fulfillment ticket when it has them.","properties":{"id":{"type":"string","format":"uuid"},"charge_type":{"type":"string","example":"RENTAL","description":"e.g. RENTAL, SALE, PART, FREIGHT, STATE_TAX, COUNTY_TAX, ENV_FEE, DAMAGE_WAIVER, CC_PROCESSING_FEE, OTHER."},"charge_category":{"type":"string","example":"RECURRING","description":"e.g. RECURRING, ONE_TIME, TAX, FEE, DISCOUNT, CREDIT, PENALTY."},"line_item_id":{"type":"string","format":"uuid","nullable":true},"fulfillment_ticket_id":{"type":"string","format":"uuid","nullable":true},"period_start":{"type":"string","format":"date-time","nullable":true},"period_end":{"type":"string","format":"date-time","nullable":true},"amount":{"type":"number","description":"Negative for credits and discounts."},"description":{"type":"string","nullable":true},"sequence_no":{"type":"integer","description":"The billing sequence of the order that produced this charge."},"created_at":{"type":"string","format":"date-time","nullable":true},"line_item_name":{"type":"string","nullable":true},"item_type":{"type":"string","nullable":true,"enum":["ASSET","PART","CUSTOM","WARRANTY",null]},"quantity":{"type":"number","nullable":true},"unit_price":{"type":"number","nullable":true},"rental_rates":{"type":"object","nullable":true},"line_item_order_id":{"type":"string","format":"uuid","nullable":true,"description":"Order owning the line item - differs from the invoice's order for a child work order's parts."},"line_item_order_number":{"type":"string","nullable":true},"asset_id":{"type":"string","format":"uuid","nullable":true,"description":"The asset actually dispatched for the line item."},"dispatched_date":{"type":"string","format":"date-time","nullable":true},"off_rent_date":{"type":"string","format":"date-time","nullable":true},"asset_name":{"type":"string","nullable":true},"asset_number":{"type":"string","nullable":true},"fulfillment_ticket_type":{"type":"string","nullable":true,"enum":["DELIVERY","PICKUP","ADDITIONAL",null]}}},"invoice_payment_applied":{"type":"object","description":"One successful payment applied to the invoice.","properties":{"paid_at":{"type":"string","format":"date-time","nullable":true},"amount":{"type":"number","description":"The part of the payment applied to this invoice."},"origin_type":{"type":"string","example":"INVOICE_PAYMENT","description":"e.g. INVOICE_PAYMENT, REFUND_ABSORB, EARLY_PAYMENT_DISCOUNT, OVERPAYMENT_CREDIT, MANUAL, CC_DEPOSIT."},"payment_type":{"type":"string","enum":["CASH","CHECK","CREDIT_CARD","ACH","WIRE","OTHER"]},"reference_number":{"type":"string","nullable":true},"card_brand":{"type":"string","nullable":true},"notes":{"type":"string","nullable":true},"display_label":{"type":"string","example":"Credit Card · Visa","description":"Ready-to-render label, e.g. Cash, Check, Credit Card · Visa, Refund Credit, Early Payment Discount, Credit Applied."}}},"invoice_approve_body":{"type":"object","description":"Unknown keys are stripped.","required":["invoice_id"],"properties":{"invoice_id":{"type":"string","format":"uuid","example":"0b6f3c2e-5d1a-4e8b-9f7c-2a4d6e8b1c3f"}}},"invoice_approve_result":{"type":"object","properties":{"message":{"type":"string","example":"Invoice approved"},"invoice_id":{"type":"string","format":"uuid"},"auto_charged":{"type":"boolean","description":"True when a card charge was started on the customer's auto-payment card; it is confirmed or failed later by the Stripe webhook."},"charge_error":{"type":"string","nullable":true,"description":"Why starting the auto-charge failed; the invoice stays approved (UNPAID or PARTIALLY_PAID)."},"balance_applied":{"type":"number","description":"Customer balance applied to the invoice on approval (an early payment discount granted with it is not included)."},"balance_applications":{"type":"array","items":{"type":"object","properties":{"application_id":{"type":"string","format":"uuid"},"payment_id":{"type":"string","format":"uuid","description":"The deposit the balance came from."},"invoice_id":{"type":"string","format":"uuid"},"amount":{"type":"number"}}}}}},"invoice_pay_cc_body":{"type":"object","description":"Unknown keys are stripped. At least one of amount and use_balance must be above 0 after rounding.","required":["invoice_id"],"properties":{"invoice_id":{"type":"string","format":"uuid","example":"0b6f3c2e-5d1a-4e8b-9f7c-2a4d6e8b1c3f"},"payment_method_id":{"type":"string","example":"pm_1Q2w3E4r5T6y7U8i9O0pAsDf","description":"The Stripe id (pm_...) of one of the customer's saved cards. Required when amount is above 0."},"amount":{"type":"number","minimum":0,"maximum":99999999.99,"example":100.5,"description":"Amount to pay by card, before the card processing fee and its tax; rounded to 2 decimals."},"use_balance":{"type":"number","minimum":0,"maximum":99999999.99,"description":"Customer balance to apply first, rounded to 2 decimals."}}},"invoice_pay_cc_result":{"type":"object","properties":{"message":{"type":"string","example":"Payment initiated — awaiting webhook confirmation.","description":"\"Payment initiated — awaiting webhook confirmation.\", or \"Customer balance applied\" when no card was charged."},"payment_id":{"type":"string","format":"uuid","nullable":true,"description":"Null when no card was charged."},"payment_intent_id":{"type":"string","nullable":true,"description":"The Stripe PaymentIntent; null when no card was charged."},"amount":{"type":"number","description":"The amount asked for, before the card processing fee and its tax; the balance applied when no card was charged."},"balance_applied":{"type":"number"},"cash_amount":{"type":"number","description":"The amount asked for, before the card processing fee and its tax; 0 when no card was charged."}}},"invoice_pay_externally_body":{"type":"object","description":"Unknown keys are stripped. At least one of amount and use_balance must be above 0 after rounding.","required":["invoice_id"],"properties":{"invoice_id":{"type":"string","format":"uuid","example":"0b6f3c2e-5d1a-4e8b-9f7c-2a4d6e8b1c3f"},"payment_type":{"type":"string","enum":["CASH","CHECK","ACH","WIRE","OTHER"],"example":"CASH","description":"Case-insensitive. Required when amount is above 0."},"amount":{"type":"number","minimum":0,"maximum":99999999.99,"example":100.5,"description":"Rounded to 2 decimals."},"use_balance":{"type":"number","minimum":0,"maximum":99999999.99,"description":"Customer balance to apply first, rounded to 2 decimals."},"cash_drawer_id":{"type":"string","format":"uuid","nullable":true,"description":"For CASH and CHECK; omitted means the org's default drawer. Refused (400) with ACH, WIRE or OTHER, and ignored when payment_type is omitted."},"reference_number":{"type":"string","nullable":true,"maxLength":255},"transaction_date":{"type":"string","format":"date-time","nullable":true,"description":"When the money moved at the bank (a bare date is read as UTC midnight)."},"notes":{"type":"string","nullable":true,"maxLength":1000}}},"invoice_pay_externally_result":{"type":"object","properties":{"message":{"type":"string","example":"Payment recorded"},"payment_id":{"type":"string","format":"uuid","nullable":true,"description":"Null when only customer balance was applied."},"cash_amount":{"type":"number"},"balance_applied":{"type":"number"},"total_applied":{"type":"number","description":"Cash plus balance applied to the invoice."}}},"invoice_email_body":{"type":"object","description":"Unknown keys are stripped.","required":["invoice_id","receiver_type"],"properties":{"invoice_id":{"type":"string","format":"uuid","example":"0b6f3c2e-5d1a-4e8b-9f7c-2a4d6e8b1c3f"},"receiver_type":{"type":"string","enum":["customer","self","custom"],"example":"customer","description":"Case-insensitive."},"custom_emails":{"type":"array","description":"Required (at least one) for custom; accepted but ignored for customer and self.","items":{"type":"string","format":"email"}},"contact_ids":{"type":"array","minItems":1,"uniqueItems":true,"description":"customer only (refused with self / custom) - send to these contacts instead of every opted-in contact; each must be the invoice customer's, opted in and have an email.","items":{"type":"string","format":"uuid"}}}},"invoice_email_result":{"type":"object","properties":{"message":{"type":"string","enum":["Email sent","Email skipped (customer emails disabled)"]},"status":{"type":"string","enum":["sent","skipped_customer_emails_disabled"],"description":"sent means the email was handed to the mailer, not that it was delivered."},"recipients_count":{"type":"integer"},"recipients":{"type":"array","items":{"type":"string","format":"email"}}}},"invoice_delete_body":{"type":"object","description":"Unknown keys are stripped.","properties":{"reason":{"type":"string","nullable":true,"maxLength":500,"example":"Issued in error"}}},"platform_settings_group":{"type":"object","description":"One node of the settings page layout tree","properties":{"key":{"type":"string","description":"Dotted UPPER_SNAKE key, e.g. ORDERS.RENTALS","example":"ORDERS.RENTALS"},"parent_key":{"type":"string","nullable":true,"description":"The parent node, null for a top-level tab","example":"ORDERS"},"label":{"type":"string","example":"Rentals"},"component":{"type":"string","enum":["TAB","SUB_TAB","SECTION"]},"weight":{"type":"integer","description":"Sibling order, ascending","example":10},"custom_block":{"type":"boolean","description":"True for a bespoke screen the client renders against its own endpoints; the node carries no generic settings\n"}}},"platform_setting":{"type":"object","description":"One setting with its effective value for the org and the user's rights on it","properties":{"key":{"type":"string","description":"Dotted UPPER_SNAKE key, e.g. SERVICE.ALLOW_WORK_ORDERS","example":"SERVICE.ALLOW_WORK_ORDERS"},"label":{"type":"string","example":"Allow Work Orders"},"description":{"type":"string","nullable":true},"group_key":{"type":"string","description":"The layout node the setting sits under","example":"SERVICE.BASICS"},"weight":{"type":"integer","description":"Order inside the group, ascending"},"data_type":{"type":"string","enum":["BOOLEAN","ENUM","JSON","NUMBER","STRING","STRING_ARRAY","UUID"]},"control_type":{"type":"string","enum":["BTN_TOGGLE","COLOR","CURRENCY","CUSTOM","MULTI_SELECT","NUMBER","PERCENT","SELECT","TAX_FEE","TEXT","TEXTAREA","TOGGLE"]},"allowed_values":{"type":"array","nullable":true,"description":"The choices, null when the setting has none. For a MULTI_SELECT control each entry is one member and the setting's value is a non-empty list of members without repeats (e.g. ASSET.AUTO_GEN_NAME_FIELDS); for every other control each entry is a whole value the setting may take, which for a STRING_ARRAY setting is itself an array (e.g. ASSET.DEFAULT_TRANSACTION_GROUP). `value` is submitted as is\n","items":{"type":"object","properties":{"label":{"type":"string"},"value":{"description":"Any JSON value"}}}},"value":{"nullable":true,"description":"The effective value, the stored override or else the org's default. Any JSON value; its shape follows `data_type` (a JSON setting such as a tax or fee is an object)\n"},"default_value":{"nullable":true,"description":"The org's default for the key (tier-resolved)"},"is_overridden":{"type":"boolean","description":"True when the org stores its own value"},"editable":{"type":"boolean","description":"True when the user holds the key's edit permission (PLATFORM_GENERAL_UPDATE when the key names none)\n"},"updated_at":{"type":"string","format":"date-time","nullable":true,"description":"When the org's row for the key last changed; null only when the org has no row for the key. A key reset to the default keeps its row, so it can be set while `is_overridden` is false\n"}}},"platform_settings_page":{"type":"object","properties":{"groups":{"type":"array","items":{"$ref":"#/components/schemas/platform_settings_group"}},"settings":{"type":"array","items":{"$ref":"#/components/schemas/platform_setting"}}}},"platform_settings_update":{"type":"object","description":"Only the diffs; unknown keys are refused, in the envelope and in each diff","additionalProperties":false,"required":["settings"],"properties":{"settings":{"type":"array","minItems":1,"maxItems":200,"items":{"type":"object","additionalProperties":false,"required":["key","value"],"properties":{"key":{"type":"string","pattern":"^[A-Z][A-Z0-9_]*(\\.[A-Z][A-Z0-9_]*)*$","example":"SERVICE.ALLOW_WORK_ORDERS"},"value":{"nullable":true,"description":"The new value, or null to reset the key to the org's default"}}}}}},"po_allocation_result":{"type":"object","description":"What the PO screen needs to re-render the row without a refetch.","properties":{"work_order":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"order_number":{"type":"string"},"asset_id":{"type":"string","format":"uuid","nullable":true},"asset_name":{"type":"string","nullable":true},"asset_number":{"type":"string","nullable":true}}},"purchase_order_item":{"type":"object","description":"The PO line after the change","properties":{"id":{"type":"string","format":"uuid"},"quantity_ordered":{"type":"number"},"quantity_allocated":{"type":"number","description":"Every live allocation of the line, stock receipts included"},"quantity_allocatable":{"type":"number","description":"quantity_ordered - quantity_allocated, floored at 0; 0 once the line is received"},"consumed":{"type":"number","description":"Total received on the line, stock receipts included"},"part_asset_ref":{"type":"string","format":"uuid","nullable":true,"description":"The asset the part was purchased for - purchasing intent only"},"part_asset_ref_details":{"type":"object","nullable":true,"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","nullable":true},"asset_number":{"type":"string","nullable":true}}}}}}},"po_allocation_asset_mismatch":{"type":"object","nullable":true,"description":"Set when the PO line was bought for one asset (part_asset_ref) and the work order is on another - with bill_to ASSET the part is charged to the work order's asset. A warning, not an error - re-purposing a purchased part is legitimate. null when they agree or the line names no asset.\n","properties":{"purchased_for":{"$ref":"#/components/schemas/po_allocation_asset"},"allocated_to":{"$ref":"#/components/schemas/po_allocation_asset"}}},"po_allocation_asset":{"type":"object","properties":{"id":{"type":"string","format":"uuid","nullable":true},"name":{"type":"string","nullable":true},"asset_number":{"type":"string","nullable":true}}},"po_vendor_invoice_fields":{"type":"object","description":"The vendor invoice header fields a client writes.","properties":{"invoice_number":{"type":"string","minLength":1,"maxLength":255,"description":"Trimmed; blank or whitespace-only is refused. Unique among the purchase order's live invoices."},"invoice_date":{"type":"string","description":"An ISO 8601 calendar date or date-time in extended format on write (2026-10-24, 2026-10-24T09:30:00Z); basic-format, week and ordinal dates, an epoch number and a non-ISO string are refused. Stored and returned as a date-time.\n"},"freight":{"type":"number","minimum":0,"maximum":99999999.99,"nullable":true,"default":0,"description":"Freight billed on this invoice, rounded to 2 decimals; null and omitted are 0"},"sales_tax":{"type":"number","minimum":0,"maximum":99999999.99,"nullable":true,"default":0},"miscellaneous":{"type":"number","minimum":0,"maximum":99999999.99,"nullable":true,"default":0},"notes":{"type":"string","nullable":true}}},"po_vendor_invoice_write":{"description":"Body of recording (POST) and replacing (PATCH) a vendor invoice - a PATCH is a full replace, so send the whole invoice. Unknown keys, vendor included, are dropped silently.\n","allOf":[{"$ref":"#/components/schemas/po_vendor_invoice_fields"},{"type":"object","required":["invoice_number","invoice_date"],"properties":{"lines":{"type":"array","default":[],"description":"The billed PO lines. Each po_item_id at most once. A line with quantity_invoiced 0 is dropped, so the web can post every PO line every time.\n","items":{"$ref":"#/components/schemas/po_vendor_invoice_line_input"}}}}],"example":{"invoice_number":"INV-1001","invoice_date":"2026-10-24","freight":25,"lines":[]}},"po_vendor_invoice_line_input":{"type":"object","required":["po_item_id","quantity_invoiced","invoice_unit_price"],"description":"One billed PO line. variance_resolution cannot be sent with either per-kind resolution.\n","properties":{"po_item_id":{"type":"string","format":"uuid","description":"A purchase_order_items row id on this purchase order"},"quantity_invoiced":{"type":"number","minimum":0,"maximum":99999999.99,"description":"Rounded to 2 decimals"},"invoice_unit_price":{"type":"number","minimum":0,"maximum":99999999.99,"description":"Rounded to 4 decimals - parts are often quoted in tenths of a cent"},"variance_resolution":{"type":"string","enum":["ACCEPTED",null],"nullable":true,"description":"Accept every variance kind the line carries. Cannot be sent together with the two per-kind fields.\n"},"price_variance_resolution":{"type":"string","enum":["ACCEPTED",null],"nullable":true,"description":"DOZ-7848. Accept the line's price variance only."},"quantity_variance_resolution":{"type":"string","enum":["ACCEPTED",null],"nullable":true,"description":"DOZ-7848. Accept the line's quantity variance only."},"variance_reason":{"type":"string","nullable":true,"description":"The uploader's note to the approver, or the approver's words. Dropped (stored null) on a line that carries no live variance. On a PATCH, a line whose every variance kind keeps its acceptance (quantity and unit price unchanged) keeps its stored reason, unless a caller who could sign one of the kinds sends the key - then the value sent replaces it, and null or '' clears it.\n"}}},"po_vendor_invoice":{"description":"A stored vendor invoice (lotus_po_vendor_invoices) with its lines and attachments.","allOf":[{"$ref":"#/components/schemas/po_vendor_invoice_fields"},{"type":"object","properties":{"id":{"type":"string","format":"uuid","readOnly":true},"org":{"type":"string","format":"uuid","readOnly":true},"purchase_order":{"type":"string","format":"uuid","readOnly":true},"po_number":{"type":"string","nullable":true,"readOnly":true,"description":"The PO's number when the invoice was recorded"},"vendor":{"type":"string","format":"uuid","nullable":true,"readOnly":true,"description":"The PO's vendor when the invoice was recorded"},"invoice_date":{"type":"string","format":"date-time"},"freight":{"type":"number","description":"Never null once stored"},"sales_tax":{"type":"number","description":"Never null once stored"},"miscellaneous":{"type":"number","description":"Never null once stored"},"subtotal":{"type":"number","readOnly":true,"description":"Sum of the lines' line_total"},"total":{"type":"number","readOnly":true,"description":"subtotal plus the three charges"},"status":{"type":"string","enum":["MATCHED","AWAITING_RECEIPT","PENDING_APPROVAL","APPROVED"],"readOnly":true,"description":"Derived from the lines, worst first - PENDING_APPROVAL (a variance kind nobody has approved; blocks the close), APPROVED (every variance accepted), AWAITING_RECEIPT (billed ahead of delivery), MATCHED. Re-derived whenever a receipt or invoice changes.\n"},"match_status":{"type":"string","deprecated":true,"readOnly":true,"description":"DOZ-7758 compatibility alias, removed with DOZ-7759 - status, except APPROVED reads VARIANCE_ACCEPTED and PENDING_APPROVAL reads VARIANCE.\n"},"created_by":{"type":"string","format":"uuid","nullable":true,"readOnly":true},"created_at":{"type":"string","format":"date-time","readOnly":true},"updated_at":{"type":"string","format":"date-time","readOnly":true},"deleted":{"type":"boolean","readOnly":true},"lines":{"type":"array","items":{"$ref":"#/components/schemas/po_vendor_invoice_line"}},"attachments":{"type":"array","readOnly":true,"description":"The files attached through the media upload (po_invoice_media)","items":{"$ref":"#/components/schemas/po_vendor_invoice_attachment"}}}}]},"po_vendor_invoice_line":{"type":"object","description":"A stored invoice line (lotus_po_vendor_invoice_lines). Its quantity and price columns are frozen as of the save; `current` is where it stands against today's receipts.\n","properties":{"id":{"type":"string","format":"uuid","readOnly":true},"org":{"type":"string","format":"uuid","readOnly":true},"invoice_id":{"type":"string","format":"uuid","readOnly":true},"purchase_order":{"type":"string","format":"uuid","readOnly":true},"po_item_id":{"type":"string","format":"uuid"},"item_id":{"type":"string","format":"uuid","nullable":true,"readOnly":true},"item_type":{"type":"string","nullable":true,"enum":["part","asset","CUSTOM",null],"readOnly":true},"quantity_invoiced":{"type":"number"},"invoice_unit_price":{"type":"number"},"line_total":{"type":"number","readOnly":true},"po_unit_price":{"type":"number","nullable":true,"readOnly":true},"quantity_ordered":{"type":"number","nullable":true,"readOnly":true},"quantity_received":{"type":"number","nullable":true,"readOnly":true},"quantity_already_invoiced":{"type":"number","nullable":true,"readOnly":true,"description":"Billed on other invoices when this one was saved"},"quantity_variance":{"type":"number","readOnly":true,"description":"Over-invoiced quantity x PO price, as of the save"},"price_variance":{"type":"number","readOnly":true,"description":"(invoice price - PO price) x quantity invoiced; negative when in our favor"},"variance_amount":{"type":"number","readOnly":true,"description":"line_total minus what the line should have cost; quantity_variance + price_variance"},"variance_reason":{"type":"string","nullable":true},"price_resolved_by":{"type":"string","format":"uuid","nullable":true,"readOnly":true,"description":"Who approved the price variance. A legacy (pre-DOZ-7848) acceptance counts for both kinds, so it is set even when price_approval_status is null.\n"},"price_resolved_at":{"type":"string","format":"date-time","nullable":true,"readOnly":true},"price_resolved_by_username":{"type":"string","nullable":true,"readOnly":true},"quantity_resolved_by":{"type":"string","format":"uuid","nullable":true,"readOnly":true,"description":"Who approved the quantity variance. A legacy (pre-DOZ-7848) acceptance counts for both kinds, so it is set even when quantity_approval_status is null.\n"},"quantity_resolved_at":{"type":"string","format":"date-time","nullable":true,"readOnly":true},"quantity_resolved_by_username":{"type":"string","nullable":true,"readOnly":true},"approval_status":{"type":"string","nullable":true,"enum":["APPROVED","PENDING",null],"readOnly":true,"description":"The line as a whole; null when it carries no live variance"},"price_approval_status":{"type":"string","nullable":true,"enum":["APPROVED","PENDING",null],"readOnly":true,"description":"null when price is not a variance on the line"},"quantity_approval_status":{"type":"string","nullable":true,"enum":["APPROVED","PENDING",null],"readOnly":true,"description":"null when quantity is not a variance on the line"},"in_our_favor":{"type":"boolean","readOnly":true,"description":"DOZ-7786. Billed below the PO price with nothing over-invoiced - not a variance."},"variance_resolution":{"type":"string","nullable":true,"enum":["ACCEPTED",null],"deprecated":true,"readOnly":true,"description":"Compatibility - ACCEPTED exactly when approval_status is APPROVED"},"resolved_by":{"type":"string","format":"uuid","nullable":true,"deprecated":true,"readOnly":true,"description":"Compatibility - the latest approver once every kind is approved"},"resolved_at":{"type":"string","format":"date-time","nullable":true,"deprecated":true,"readOnly":true},"resolved_by_username":{"type":"string","nullable":true,"deprecated":true,"readOnly":true},"created_at":{"type":"string","format":"date-time","readOnly":true},"deleted":{"type":"boolean","readOnly":true},"current":{"type":"object","nullable":true,"readOnly":true,"allOf":[{"$ref":"#/components/schemas/po_invoice_receipt_allocation"}]}}},"po_invoice_receipt_allocation":{"type":"object","description":"Where an invoice line stands against today's receipts. Receipts are consumed by the PO's invoices oldest first, so one invoice's picture depends on every invoice on the PO. quantity_outstanding = quantity_awaiting_receipt + quantity_over_invoiced.\n","properties":{"invoice_id":{"type":"string","format":"uuid"},"po_item_id":{"type":"string","format":"uuid"},"po_unit_price":{"type":"number"},"quantity_invoiced":{"type":"number"},"quantity_settled":{"type":"number","description":"Covered by goods received"},"quantity_outstanding":{"type":"number","description":"Not covered by goods received yet"},"quantity_awaiting_receipt":{"type":"number","description":"The part of quantity_outstanding a delivery can still cover"},"quantity_over_invoiced":{"type":"number","description":"The part no delivery can cover"},"quantity_status":{"type":"string","enum":["SETTLED","AWAITING_RECEIPT","OVER_INVOICED"],"description":"OVER_INVOICED outranks AWAITING_RECEIPT when a line is both"},"quantity_variance":{"type":"number","description":"quantity_over_invoiced x PO price"},"quantity_awaiting":{"type":"number","description":"quantity_awaiting_receipt x PO price (a dollar figure, despite the name)"}}},"po_invoice_line_position":{"type":"object","description":"How much of one PO line has been billed, and how much of what arrived is still billable.","properties":{"po_item_id":{"type":"string","format":"uuid"},"item_id":{"type":"string","format":"uuid","nullable":true},"item_type":{"type":"string","nullable":true,"enum":["part","asset","CUSTOM",null]},"name":{"type":"string","nullable":true,"description":"The line's own name (set on CUSTOM lines); null on most catalog lines"},"po_unit_price":{"type":"number"},"quantity_ordered":{"type":"number"},"quantity_received":{"type":"number"},"quantity_already_invoiced":{"type":"number","description":"On live invoices (on a match preview, excluding the invoice being edited)"},"invoiceable_quantity":{"type":"number","description":"Received and not yet invoiced, floored at 0"},"uninvoiced_value":{"type":"number","description":"invoiceable_quantity x PO price"},"received_closed":{"type":"boolean","description":"The line's received flag - closed to further receiving"},"outstanding_quantity":{"type":"number","description":"Ordered and not yet received"},"open_for_receiving":{"type":"boolean"},"awaitable_quantity":{"type":"number","description":"How much can still be billed ahead of delivery - ordered units neither received nor billed; 0 on a closed line.\n"}}},"po_invoice_charge_match":{"type":"object","description":"One header charge (freight, sales_tax or miscellaneous) against the PO's own amount, across the PO's invoices. Advisory - never blocks a save.\n","properties":{"po_amount":{"type":"number"},"already_invoiced":{"type":"number"},"amount":{"type":"number","description":"Billed on this invoice; 0 in a reconciliation's charge_positions"},"invoiced_total":{"type":"number"},"variance":{"type":"number","description":"invoiced_total - po_amount"},"variance_percent":{"type":"number","nullable":true,"description":"null when the PO carries no such charge"},"tolerance_percent":{"type":"number","description":"The org's PO.CHARGE_VARIANCE_TOLERANCE_PERCENT"},"status":{"type":"string","enum":["NONE","NOT_ON_PO","OVER_TOLERANCE","FULLY_INVOICED","MATCHED","UNDER_INVOICED","WITHIN_TOLERANCE"]},"is_variance":{"type":"boolean","description":"true only for OVER_TOLERANCE"}}},"po_invoice_charge_matches":{"type":"object","properties":{"freight":{"$ref":"#/components/schemas/po_invoice_charge_match"},"sales_tax":{"$ref":"#/components/schemas/po_invoice_charge_match"},"miscellaneous":{"$ref":"#/components/schemas/po_invoice_charge_match"}}},"po_reconciliation_counter":{"type":"object","properties":{"invoice_count":{"type":"integer"},"line_count":{"type":"integer"},"amount":{"type":"number"}}},"po_reconciliation":{"type":"object","description":"DOZ-7163. The purchase order's vendor-invoice reconciliation - the summary strip, the close banner and the status the close is judged by.\n","properties":{"ordered":{"type":"number","description":"Lines at PO price plus the three header charges"},"received":{"type":"number","description":"The value of the lines received, at PO price; charges excluded"},"invoiced":{"type":"number","description":"Sum of the live invoices' totals"},"uninvoiced_receipts":{"type":"number","description":"Received goods not invoiced yet, at PO price"},"difference":{"type":"object","description":"Why invoiced differs from ordered, itemised","properties":{"total":{"type":"number"},"lines":{"type":"number"},"freight":{"type":"number"},"sales_tax":{"type":"number"},"miscellaneous":{"type":"number"}}},"charge_positions":{"$ref":"#/components/schemas/po_invoice_charge_matches"},"status":{"type":"string","enum":["NOT_INVOICED","OUTSTANDING","MATCHED_WITH_VARIANCE","FULLY_MATCHED"],"description":"NOT_INVOICED with no live invoice; OUTSTANDING while goods are unbilled or still to arrive; MATCHED_WITH_VARIANCE when a live variance (approved or pending) remains; else FULLY_MATCHED.\n"},"receiving_open":{"type":"boolean","description":"Goods can still arrive"},"awaiting_receipt":{"type":"object","description":"Billed ahead of delivery","properties":{"line_count":{"type":"integer"},"amount":{"type":"number"}}},"invoice_count":{"type":"integer"},"variance_count":{"type":"integer","description":"Live variances, approved or pending"},"variance_total":{"type":"number"},"favorable_variance":{"type":"object","description":"DOZ-7786. Lines billed below the PO price; amount is the (negative) saving.","properties":{"line_count":{"type":"integer"},"amount":{"type":"number"}}},"pending_approval":{"description":"DOZ-7758. The variances still awaiting an approver; line_count above 0 blocks the close. Split by kind (DOZ-7848).\n","allOf":[{"$ref":"#/components/schemas/po_reconciliation_counter"},{"type":"object","properties":{"price":{"$ref":"#/components/schemas/po_reconciliation_counter"},"quantity":{"$ref":"#/components/schemas/po_reconciliation_counter"}}}]},"short_receipt":{"type":"object","description":"Ordered line value that has not arrived - still to come while receiving_open, else what the PO closed short by","properties":{"is_short":{"type":"boolean"},"amount":{"type":"number"}}},"invoice_state":{"type":"string","enum":["not_invoiced","partially_invoiced","invoiced"]}}},"po_vendor_invoice_attachment":{"type":"object","properties":{"invoice_id":{"type":"string","format":"uuid","description":"On the list only"},"media_id":{"type":"string","format":"uuid"},"file_name":{"type":"string","nullable":true},"file_size":{"type":"integer","nullable":true},"file_type":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time","nullable":true},"full_path":{"type":"string","nullable":true,"description":"Stream the file through GET /media/platform/resource/{path} with this"}}},"po_vendor_invoice_and_reconciliation":{"type":"object","properties":{"invoice":{"$ref":"#/components/schemas/po_vendor_invoice"},"reconciliation":{"$ref":"#/components/schemas/po_reconciliation"},"invoice_state":{"type":"string","enum":["not_invoiced","partially_invoiced","invoiced"]}}},"po_vendor_invoice_write_result":{"type":"object","properties":{"message":{"type":"string","enum":["Vendor invoice recorded","Vendor invoice updated"]},"data":{"allOf":[{"$ref":"#/components/schemas/po_vendor_invoice_and_reconciliation"},{"type":"object","properties":{"charge_matches":{"$ref":"#/components/schemas/po_invoice_charge_matches"}}}]}}},"po_invoice_line_match":{"description":"One previewed line - the PO line's position, the match against it and the quantity sent.","allOf":[{"$ref":"#/components/schemas/po_invoice_line_position"},{"type":"object","properties":{"quantity_invoiced":{"type":"number"},"line_total":{"type":"number"},"expected_total":{"type":"number","description":"What the line should cost - received plus legitimately billed ahead, at PO price"},"quantity_variance":{"type":"number"},"quantity_awaiting":{"type":"number","description":"quantity_awaiting_receipt x PO price"},"price_variance":{"type":"number"},"variance_amount":{"type":"number"},"quantity_over_invoiced":{"type":"number"},"quantity_awaiting_receipt":{"type":"number"},"quantity_status":{"type":"string","enum":["SETTLED","AWAITING_RECEIPT","OVER_INVOICED"]},"has_variance":{"type":"boolean"},"variance_kinds":{"type":"array","items":{"type":"string","enum":["PRICE","QUANTITY"]}},"in_our_favor":{"type":"boolean"},"status":{"type":"string","enum":["MATCHED","AWAITING_RECEIPT","VARIANCE"]}}}]},"po_invoice_match_preview":{"type":"object","properties":{"lines":{"type":"array","items":{"$ref":"#/components/schemas/po_invoice_line_match"}},"charge_matches":{"$ref":"#/components/schemas/po_invoice_charge_matches"},"subtotal":{"type":"number"},"total":{"type":"number"},"unresolved_variance_count":{"type":"integer"},"unresolved_variance_total":{"type":"number"},"awaiting_receipt_count":{"type":"integer","description":"Lines with a quantity billed ahead of delivery. That part needs no answer, but a line counted here can also carry a variance (e.g. a wrong price) and be in the variance count too.\n"},"awaiting_receipt_total":{"type":"number"},"favorable_variance_count":{"type":"integer"},"favorable_variance_total":{"type":"number"},"positions":{"type":"array","description":"Every live PO line, so the table renders rows the payload did not mention","items":{"$ref":"#/components/schemas/po_invoice_line_position"}}}},"rental_order_row":{"type":"object","description":"The stored rental order (a lotus_orders row).","properties":{"id":{"type":"string","format":"uuid","readOnly":true},"org":{"type":"string","format":"uuid","readOnly":true},"order_number":{"type":"string","readOnly":true,"description":"R-YYYYMMDD-NNN. With the platform setting RENTAL.CONTINUATION_ALPHA_SUFFIX on (the default), a renewal keeps its contract's number with a letter suffix: the contract is renamed -A when it is first renewed, its renewals are -B, -C, and after -Z come -AA, -AB. With it off, a renewal gets a new number.\n","example":"R-20260923-001"},"type":{"type":"string","enum":["RENTAL"],"readOnly":true},"status":{"type":"string","enum":["QUOTE","RESERVATION","OPEN_CONTRACT","CLOSED_CONTRACT","COMPLETED_CONTRACT"],"readOnly":true},"customer_id":{"type":"string","format":"uuid","readOnly":true},"contact_id":{"type":"string","format":"uuid","nullable":true,"readOnly":true},"site_id":{"type":"string","format":"uuid","nullable":true,"readOnly":true},"assignee_id":{"type":"string","format":"uuid","nullable":true,"readOnly":true},"parent_id":{"type":"string","format":"uuid","nullable":true,"readOnly":true,"description":"The contract this one renews (set by the auto-renew job only)."},"priority":{"type":"integer","nullable":true,"readOnly":true},"customer_po":{"type":"string","nullable":true,"readOnly":true},"external_notes":{"type":"string","nullable":true,"readOnly":true},"internal_notes":{"type":"string","nullable":true,"readOnly":true},"metadata":{"type":"object","nullable":true,"readOnly":true,"description":"Where each tax and fee rate came from - keys ENV_FEE_SOURCE, DAMAGE_WAIVER_SOURCE, STATE_TAX_SOURCE, COUNTY_TAX_SOURCE, CITY_TAX_SOURCE, SPECIAL_TAX_SOURCE, SALES_TAX_SOURCE (string) and SALES_TAX_SOURCE_ZIP (string or null).\n","additionalProperties":{"type":"string","nullable":true},"example":{"SALES_TAX_SOURCE":"Organization Defaults","STATE_TAX_SOURCE":"CONTRACT","SALES_TAX_SOURCE_ZIP":null}},"deleted":{"type":"boolean","readOnly":true,"description":"True for a voided order."},"deletion_information":{"type":"object","nullable":true,"readOnly":true,"description":"Who voided the order, when and why; null on a live order.","properties":{"date":{"type":"string","format":"date-time"},"reason":{"type":"string","nullable":true},"user_id":{"type":"string","format":"uuid","nullable":true},"username":{"type":"string","nullable":true}}},"imported":{"type":"boolean","readOnly":true,"description":"True for history imported from the previous system."},"created_at":{"type":"string","format":"date-time","readOnly":true},"updated_at":{"type":"string","format":"date-time","readOnly":true},"status_updated_at":{"type":"string","format":"date-time","nullable":true,"readOnly":true},"created_by":{"type":"string","format":"uuid","nullable":true,"readOnly":true},"updated_by":{"type":"string","format":"uuid","nullable":true,"readOnly":true},"status_updated_by":{"type":"string","format":"uuid","nullable":true,"readOnly":true}}},"rental_details":{"type":"object","description":"The rental-only part of the order (a lotus_rental_details row).","properties":{"org":{"type":"string","format":"uuid"},"order_id":{"type":"string","format":"uuid"},"billing_type":{"type":"string","enum":["ON_ACCOUNT","ADVANCE"],"description":"Copied from the customer's pay_on_open (true - ADVANCE) when the order is created and on every PUT /lotus/rentals/update. ADVANCE customers pay before the contract opens; ON_ACCOUNT ones are billed as it runs.\n"},"valid_until":{"type":"string","format":"date-time","nullable":true,"description":"A quote's expiry; cleared once it is reserved."},"backdated_status":{"type":"string","nullable":true,"enum":["NO","PARTIAL","FULL",null],"description":"Whether some (PARTIAL) or all (FULL) of the line items' dates are in the past."},"day_rate_enabled":{"type":"boolean"},"signed_status":{"type":"string","enum":["UNSIGNED","SENT_TO_SIGNER","SIGNED","SIGNER_DECLINED","INVITE_EXPIRED","INVITE_EMAIL_DELIVERY_FAILED","SIGNER_INVITE_DELETED"]},"signnow_document_id":{"type":"string","nullable":true},"signnow_invite_sent_date":{"type":"string","format":"date-time","nullable":true},"signed_date":{"type":"string","format":"date-time","nullable":true},"can_complete":{"type":"object","description":"Whether the contract can be completed and, if not, why. Never null - a new order starts at status false with the off-rent inspection and unpaid invoice reasons.\n","properties":{"status":{"type":"boolean"},"reasons":{"type":"array","items":{"type":"string"},"example":["Not all invoices have been paid for the contract"]}}},"deleted":{"type":"boolean"}}},"rental_line_item":{"type":"object","description":"A line item (the booking) merged with its active dispatch (what physically happened), its inspections, the asset currently filling it and its price. CUSTOM lines have no dispatch - their dispatch fields are null and current_item_details holds nulls (except \"\" and empty lists).\n","properties":{"id":{"type":"string","format":"uuid"},"org":{"type":"string","format":"uuid"},"order_id":{"type":"string","format":"uuid"},"parent_id":{"type":"string","format":"uuid","nullable":true,"description":"The parent line item when this is an attachment."},"item_type":{"type":"string","enum":["ASSET","CUSTOM"]},"item_id":{"type":"string","format":"uuid","nullable":true,"description":"The asset now on the dispatch (the replacement after a swap); null for CUSTOM."},"parent_item_id":{"type":"string","format":"uuid","nullable":true,"description":"The parent line item's current asset id."},"name":{"type":"string","nullable":true},"quantity":{"type":"number","description":"On an ASSET line, the units of the asset it rents (DOZ-8243)."},"unit_price":{"type":"number","nullable":true,"description":"CUSTOM lines; 0 on ASSET lines (they price from rental_rates)."},"quoted_cost":{"type":"number","nullable":true,"description":"Internal per-unit cost of a CUSTOM line; never billed."},"percentage_discount":{"type":"number","nullable":true},"is_taxable":{"type":"boolean","nullable":true},"bill_to":{"type":"string","nullable":true,"example":"CUSTOMER"},"capitalized":{"type":"boolean","nullable":true},"rental_rates":{"type":"object","nullable":true,"description":"The asset's rates at booking time; null for CUSTOM.","properties":{"day":{"type":"number","nullable":true},"week":{"type":"number","nullable":true},"4week":{"type":"number","nullable":true}}},"rental_charge_details":{"type":"object","nullable":true,"description":"Always null on rentals."},"expected_dispatch_date":{"type":"string","format":"date-time","nullable":true},"expected_return_date":{"type":"string","format":"date-time","nullable":true},"billing_cycle_anchor":{"type":"string","format":"date-time","nullable":true},"approval_status":{"type":"string","nullable":true,"example":"QUOTED"},"lifecycle_status":{"type":"string","description":"The booking's stage - QUOTED, DISPATCH_INSPECTION, IN_TRANSIT, DISPATCHED, OFF_RENT, OFF_RENT_INSPECTION, RELEASED, REMOVED; NONE for CUSTOM lines.\n","example":"DISPATCHED"},"item_details":{"type":"object","nullable":true,"additionalProperties":true,"description":"Snapshot of the asset row taken when the line was booked (id, name, asset_number, category_name, has_hour_meter, ...); an empty object (rarely null) on CUSTOM lines."},"purchase_order":{"type":"string","format":"uuid","nullable":true},"asset_ref":{"type":"string","format":"uuid","nullable":true},"sold_at":{"type":"string","format":"date-time","nullable":true},"sale_snapshot":{"type":"object","nullable":true,"description":"Always null on rentals."},"warranty_source_option_id":{"type":"string","format":"uuid","nullable":true},"warranty_coverage_months":{"type":"integer","nullable":true},"warranty_coverage_hours":{"type":"integer","nullable":true},"deleted":{"type":"boolean"},"imported":{"type":"boolean","nullable":true},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time","nullable":true},"created_by":{"type":"string","format":"uuid","nullable":true},"updated_by":{"type":"string","format":"uuid","nullable":true},"dispatch_id":{"type":"string","format":"uuid","nullable":true,"description":"The active dispatch (REMOVED and REPLACED ones are not shown); null for CUSTOM and before the reservation."},"dispatch_status":{"type":"string","nullable":true,"enum":["DISPATCH_INSPECTION","IN_TRANSIT","DISPATCHED","OFF_RENT","OFF_RENT_INSPECTION","RELEASED",null]},"dispatched_date":{"type":"string","format":"date-time","nullable":true},"off_rent_date":{"type":"string","format":"date-time","nullable":true},"released_date":{"type":"string","format":"date-time","nullable":true},"dispatch_inspection_date":{"type":"string","format":"date-time","nullable":true},"off_rent_inspection_date":{"type":"string","format":"date-time","nullable":true},"replacement_for_dispatch_id":{"type":"string","format":"uuid","nullable":true,"description":"Set when this dispatch replaced another asset's."},"replaced_assets":{"type":"array","description":"Assets swapped out of this line item, oldest first.","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"asset_number":{"type":"string"}}}},"service_details":{"type":"object","description":"The dispatch and off-rent inspections of the active dispatch, and the asset's pending work orders.","properties":{"dispatch_inspection_id":{"type":"string","format":"uuid","nullable":true},"off_rent_inspection_id":{"type":"string","format":"uuid","nullable":true},"dispatch_inspection_status":{"type":"string","nullable":true,"example":"PENDING"},"off_rent_inspection_status":{"type":"string","nullable":true,"description":"HIDDEN until the asset goes off rent.","example":"HIDDEN"},"dispatch_inspection_passed":{"type":"boolean","nullable":true},"off_rent_inspection_passed":{"type":"boolean","nullable":true},"dispatch_inspection_quick_pass":{"type":"boolean","nullable":true},"off_rent_inspection_quick_pass":{"type":"boolean","nullable":true},"dispatch_inspection_assigned":{"type":"boolean"},"off_rent_inspection_assigned":{"type":"boolean"},"meter_readings":{"type":"object","properties":{"dispatch_meter":{"type":"string","nullable":true},"off_rent_meter":{"type":"string","nullable":true}}},"pending_tasks_count":{"type":"integer"},"pending_task_ids":{"type":"array","items":{"type":"string","format":"uuid"}}}},"current_item_details":{"type":"object","description":"The asset on the active dispatch, live. On CUSTOM lines every field is null except category_linage (\"\") and child_attachments / parent_categories ([]).\n","properties":{"id":{"type":"string","format":"uuid","nullable":true},"asset_number":{"type":"string","nullable":true},"name":{"type":"string","nullable":true},"out_of_service":{"type":"boolean","nullable":true},"current_rental_status":{"type":"string","nullable":true},"quantity":{"type":"number","nullable":true},"validated":{"type":"boolean","nullable":true},"category":{"type":"string","format":"uuid","nullable":true},"category_name":{"type":"string","nullable":true},"category_linage":{"type":"string","description":"Grandparent -> parent -> category; \"\" for CUSTOM."},"child_attachments":{"type":"array","items":{"type":"string","format":"uuid"}},"parent_categories":{"type":"array","items":{"type":"string","format":"uuid"}},"inherited_rental_rates":{"type":"object","nullable":true,"description":"The category's rates when the asset inherits them.","properties":{"day":{"type":"number","nullable":true},"week":{"type":"number","nullable":true},"4week":{"type":"number","nullable":true}}},"hour_meter":{"type":"number","nullable":true},"asset_checklist_status":{"type":"string","nullable":true},"has_telematics":{"type":"boolean","description":"Whether the asset has a telematics device (a current Samsara row), so the client can grey out its telematics icon. ASSET lines only, when the org has the SAMSARA_GPS module; absent otherwise (DOZ-8147).\n"}}},"total":{"type":"number","description":"Line price before discount."},"discount_value":{"type":"number"},"sub_total":{"type":"number","description":"total less discount_value."},"rates_object":{"type":"object","description":"How the rental window was priced; the rate fields are on ASSET lines only.","properties":{"days":{"type":"integer"},"weeks":{"type":"integer"},"months":{"type":"integer"},"day_rate":{"type":"number"},"week_rate":{"type":"number"},"month_rate":{"type":"number"}}},"most_accurate_start_date":{"type":"string","nullable":true,"description":"GET /lotus/rentals/{id} ASSET lines only. dispatched_date, else expected_dispatch_date, formatted in the server's locale (e.g. 8/12/2026), not ISO.","example":"8/12/2026"},"most_accurate_end_date":{"type":"string","nullable":true,"description":"GET /lotus/rentals/{id} ASSET lines only. off_rent_date, else expected_return_date, server-locale format."},"start_date_is_actual":{"type":"boolean","description":"GET /lotus/rentals/{id} ASSET lines only."},"end_date_is_actual":{"type":"boolean","description":"GET /lotus/rentals/{id} ASSET lines only."},"meter_out":{"type":"number","description":"GET /lotus/rentals/{id} ASSET lines only. The dispatch hour-meter reading; -1 when there is none.\n"},"meter_in":{"type":"number","description":"GET /lotus/rentals/{id} ASSET lines only. The off-rent hour-meter reading; -1 when there is none.\n"},"hours_used":{"type":"number","description":"GET /lotus/rentals/{id} ASSET lines only. meter_in minus meter_out; 0 unless both readings exist.\n"},"has_hour_meter":{"type":"boolean","description":"GET /lotus/rentals/{id} ASSET lines only."},"category_name":{"type":"string","description":"GET /lotus/rentals/{id} ASSET lines only; \"Uncategorized\" when unknown."}}},"rental_search_row":{"description":"A rental order with its details, as listed by search.","allOf":[{"$ref":"#/components/schemas/rental_order_row"},{"type":"object","properties":{"rental_details":{"$ref":"#/components/schemas/rental_details"},"customer_details":{"$ref":"#/components/schemas/lotus_order_customer_details"},"site_details":{"$ref":"#/components/schemas/lotus_order_site_details"},"contact_details":{"$ref":"#/components/schemas/lotus_order_contact_details"},"assignee_details":{"$ref":"#/components/schemas/lotus_order_assignee_details"},"is_continuation":{"type":"boolean","description":"True for a contract renewed from another one."},"parent_order_number":{"type":"string","nullable":true,"description":"The order number the parent contract has now: a parent renamed -A when it was renewed shows the -A number.\n"},"order_adjustments":{"type":"array","items":{"$ref":"#/components/schemas/lotus_order_adjustment"}},"fulfillment_tickets":{"type":"array","items":{"$ref":"#/components/schemas/lotus_order_fulfillment_ticket"}},"invoice_details":{"type":"array","items":{"$ref":"#/components/schemas/lotus_order_invoice_summary"}},"signed_artifact":{"$ref":"#/components/schemas/lotus_order_signed_artifact"},"line_items":{"type":"array","items":{"$ref":"#/components/schemas/rental_line_item"}}}},{"$ref":"#/components/schemas/lotus_order_financials"}]},"rental_order_detail":{"description":"A rental order with everything the order screen shows.","allOf":[{"$ref":"#/components/schemas/rental_search_row"},{"type":"object","properties":{"ancestry":{"type":"array","description":"The chain of parent orders, oldest first; empty for most orders.","items":{"$ref":"#/components/schemas/rental_order_ancestor"}}}}]},"rental_order_ancestor":{"description":"One contract up the continuation chain, with its date range. Each asset line item's actual dispatched / off-rent date wins over its expected one; start_date is the earliest, end_date the latest across the order.\n","allOf":[{"$ref":"#/components/schemas/lotus_order_ancestor"},{"type":"object","properties":{"start_date":{"type":"string","format":"date-time","nullable":true,"description":"Null when the order has no asset line item with a start date."},"end_date":{"type":"string","format":"date-time","nullable":true,"description":"Null when the order has no asset line item with an end date."},"deleted":{"type":"boolean","description":"True for a voided order. It stays in the chain and GET /lotus/rentals/{id} still opens it, as a voided order.\n"}}}]},"rental_search_body":{"type":"object","description":"Body of POST /lotus/rentals/search. Unknown keys are stripped. search may be left out (an empty one is filled in).","required":["paginate"],"properties":{"search":{"type":"object","description":"Every filter is optional; omitted means no filter. order_by sorts by order_number, status, company_name, valid_until or created_at; the default order is priority, then newest first.\n","properties":{"status":{"type":"array","nullable":true,"items":{"type":"string","enum":["QUOTE","RESERVATION","OPEN_CONTRACT","CLOSED_CONTRACT","COMPLETED_CONTRACT"]}},"dispatch_status":{"type":"array","nullable":true,"description":"Orders with a line item at one of these stages (lifecycle_status).","items":{"type":"string","enum":["QUOTED","RESERVED","PENDING","DISPATCH_INSPECTION","IN_TRANSIT","DISPATCHED","OFF_RENT","OFF_RENT_INSPECTION","RELEASED","REPLACED","DAMAGED","LOST"]}},"asset_number":{"type":"string","nullable":true,"description":"Partial match on an asset on the order."},"asset_name":{"type":"string","nullable":true,"description":"Partial match on an asset's or a line item's name."},"date_from":{"type":"string","format":"date-time","description":"Applied only together with date_to - orders with a line item whose window (actual dates, else expected) overlaps date_from..date_to."},"date_to":{"type":"string","format":"date-time","description":"Not before date_from."},"customer":{"type":"string","format":"uuid","nullable":true,"description":"Customer id. Forced to the session's customer for a customer-portal session."},"customer_number":{"type":"number","nullable":true},"company_name":{"type":"string","nullable":true,"description":"Partial match."},"order_number":{"type":"string","nullable":true,"description":"Partial match, so a contract's number also finds its lettered renewals (-A, -B …)."},"has_open_invoices":{"type":"boolean","nullable":true,"description":"true - only orders with a DRAFT, UNPAID or PARTIALLY_PAID invoice; false - only orders without one."},"is_continuation":{"type":"boolean","nullable":true,"description":"true - only renewed contracts; false - only the others."},"deleted":{"type":"boolean","description":"true lists only voided orders (expired quotes included)."},"format":{"type":"string","enum":["json","csv"],"default":"json","description":"csv streams every match as a CSV file, unpaginated."},"order_by":{"$ref":"#/components/schemas/order_by"}},"example":{}},"paginate":{"type":"object","description":"Required unless search.format is csv.","required":["currentPage","perPage"],"properties":{"currentPage":{"type":"integer","minimum":1,"example":1},"perPage":{"type":"integer","minimum":1,"maximum":100,"example":25}}}}},"rental_search_result":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/rental_search_row"}},"pagination":{"$ref":"#/components/schemas/lotus_order_pagination"},"orderByColumnNamesList":{"$ref":"#/components/schemas/orderByColumnNamesList"}}},"rental_item":{"type":"object","description":"A line item of a create or update body. ASSET items need item_id and both expected dates (return after dispatch); unit_price is ignored for them, and an optional quantity rents that many units of the asset (DOZ-8243). CUSTOM items need quantity (above 0) and unit_price (any amount, negative for a credit line). Unknown keys are stripped.\n","required":["item_type"],"properties":{"item_type":{"type":"string","enum":["ASSET","CUSTOM"]},"item_id":{"type":"string","format":"uuid","nullable":true,"description":"The asset id. Required for ASSET; optional (or null) for CUSTOM."},"id":{"type":"string","format":"uuid","description":"The existing line item's id, on update - keeps it (and its dispatch) instead of replacing it."},"name":{"type":"string","nullable":true,"description":"The CUSTOM line's text."},"expected_dispatch_date":{"type":"string","format":"date-time","description":"Required for ASSET."},"expected_return_date":{"type":"string","format":"date-time","description":"Required for ASSET, after expected_dispatch_date."},"discount":{"type":"number","minimum":0,"maximum":100,"default":0,"description":"Percent off this line."},"quantity":{"type":"number","example":1,"description":"CUSTOM - required, above 0. ASSET - optional whole number, at least 1: the units of the asset this line rents, billed quantity × the period rate. A null or omitted value is 1 on create and on a quote / reservation update (so echo the stored value back); on a contract it can't change once the line exists (400). More units than the asset owns is a 400.\n"},"unit_price":{"type":"number","description":"CUSTOM only (required, 2 decimals)."},"quoted_cost":{"type":"number","nullable":true,"minimum":0,"description":"CUSTOM only - internal per-unit cost, never billed."},"is_taxable":{"type":"boolean","default":true},"parent_item_id":{"type":"string","format":"uuid","nullable":true,"description":"The item_id of the asset this one is attached to (not its own item_id). ASSET items only - on a CUSTOM item it is a 400.\n"}}},"rental_order_fields":{"type":"object","description":"The fields shared by create, update and update-contract. Unknown keys are stripped. A tax or fee flag left out is filled in by the route (create - the customer and site default; update and update-contract - the order's current value), and UPDATE_FEES_ON_RENTAL_ORDERS decides whether the caller may change them (see the route).\n","properties":{"customer_id":{"type":"string","format":"uuid","description":"Ignored by update-contract (the stored customer stays).","example":"3f2a1b4c-5d6e-4f70-8a9b-0c1d2e3f4a5b"},"contact_id":{"type":"string","format":"uuid","nullable":true,"description":"Ignored by update-contract."},"site_id":{"type":"string","format":"uuid","nullable":true,"description":"Required for status RESERVATION on create. Not checked by the validator on update, but a RESERVATION updated without it answers 404 (\"Site doesn't exist\"). Ignored by update-contract.\n","example":"5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d"},"assignee_id":{"type":"string","format":"uuid","nullable":true,"description":"Must be a user allowed to own orders (ALLOW_ORDER_OWNERSHIP)."},"valid_until":{"type":"string","format":"date-time","nullable":true,"description":"The quote's expiry. Required for status QUOTE on create. Optional on update, but update stores what is sent - left out, it is cleared and a QUOTE no longer shows in search. Ignored by update-contract.\n","example":"2026-12-31T18:00:00.000Z"},"items":{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/rental_item"},"example":[{"item_type":"ASSET","item_id":"9b8a7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d","expected_dispatch_date":"2026-12-01T15:00:00.000Z","expected_return_date":"2026-12-08T15:00:00.000Z","discount":0}]},"delivery_freight_company":{"type":"string","format":"uuid","nullable":true},"return_freight_company":{"type":"string","format":"uuid","nullable":true},"additional_freight_company":{"type":"string","format":"uuid","nullable":true},"quoted_delivery_fee":{"type":"number","nullable":true,"minimum":0,"default":0,"description":"Delivery freight charged to the customer."},"quoted_return_fee":{"type":"number","nullable":true,"minimum":0,"default":0,"description":"Pickup freight charged to the customer."},"quoted_additional_freight_fee":{"type":"number","nullable":true,"minimum":0,"default":0},"delivery_fee":{"type":"number","minimum":0,"default":0,"description":"What the freight company charges for the delivery (internal)."},"return_fee":{"type":"number","minimum":0,"default":0},"additional_freight_fee":{"type":"number","minimum":0,"default":0},"state_sales_tax_status":{"type":"boolean"},"county_sales_tax_status":{"type":"boolean"},"city_sales_tax_status":{"type":"boolean"},"special_sales_tax_status":{"type":"boolean"},"damage_waiver_status":{"type":"boolean"},"env_fee_status":{"type":"boolean"},"discount":{"type":"number","minimum":0,"maximum":100,"default":0,"description":"Order discount in percent."},"customer_discount":{"type":"number","minimum":0,"maximum":100,"default":0,"description":"Customer discount in percent."},"flat_discount_value":{"type":"number","minimum":0,"default":0,"description":"Dollar discount; above 0 needs TRANSACTION_FLAT_CASH_DISCOUNT."},"external_notes":{"type":"string","nullable":true},"internal_notes":{"type":"string","nullable":true},"customer_po":{"type":"string","nullable":true},"org":{"type":"string","format":"uuid","nullable":true,"description":"Accepted and ignored - the org is the session's."},"parent_id":{"type":"string","nullable":true,"enum":[null],"description":"Only null is accepted."},"acknowledge_credit_warning":{"type":"boolean","description":"Resend with true after a credit-limit 409 to go ahead."}}},"rental_create_body":{"description":"Body of POST /lotus/rentals/create.","allOf":[{"$ref":"#/components/schemas/rental_order_fields"},{"type":"object","required":["customer_id","status","items"],"properties":{"status":{"type":"string","enum":["QUOTE","RESERVATION"],"example":"QUOTE","description":"RESERVATION saves a QUOTE and converts it (needs FF_TO_RESERVATION)."}}}]},"rental_update_body":{"description":"Body of PUT /lotus/rentals/update. status is refused (400).","allOf":[{"$ref":"#/components/schemas/rental_order_fields"},{"type":"object","required":["id","customer_id","items"],"properties":{"id":{"type":"string","format":"uuid","description":"The rental order id.","example":"7c1e2d3a-4b5c-4d6e-8f70-1a2b3c4d5e6f"}}}]},"rental_update_contract_body":{"description":"Body of PUT /lotus/rentals/update-contract (and update-open). status is refused (400).","allOf":[{"$ref":"#/components/schemas/rental_update_body"},{"type":"object","properties":{"acknowledge_invoice_sent":{"type":"boolean","description":"Confirms editing a closed order whose invoice was already sent (needed then, else 409)."}}}]},"rental_create_result":{"type":"object","properties":{"message":{"type":"string","example":"Order Created"},"data":{"$ref":"#/components/schemas/rental_order_row"},"metadata":{"type":"object","nullable":true,"description":"Always null."}}},"rental_message_result":{"type":"object","properties":{"message":{"type":"string"},"metadata":{"type":"object","nullable":true,"description":"Always null."}}},"rental_update_contract_result":{"type":"object","properties":{"status":{"type":"string","example":"success"},"message":{"type":"string","description":"\"Open Contract updated locally in Lotus\", followed by \"; the automatic card charge was declined, please collect payment manually\" when data.auto_charge.status is DECLINED.\n","example":"Open Contract updated locally in Lotus"},"data":{"type":"object","properties":{"orderId":{"type":"string","format":"uuid"},"deltaAmount":{"type":"number","description":"The total of the invoice the re-bill produced - a new delta invoice's or credit note's total (negative for a credit), or, when the latest invoice was changed in place, that invoice's new full total. 0 when nothing was re-billed.\n"},"deltaInvoiceId":{"type":"string","format":"uuid","nullable":true,"description":"The invoice the re-bill made or changed in place; null when nothing was re-billed."},"auto_charge":{"$ref":"#/components/schemas/rental_auto_charge"}}}}},"rental_auto_charge":{"type":"object","nullable":true,"readOnly":true,"description":"What settling the invoice this call billed did (DOZ-7986): the customer's deposits and credit are applied first, then a customer on auto-payment with a saved card is charged what is left. null when nothing was settled - nothing was billed, the invoice is a credit, it is not approved yet (DRAFT, settled when it is approved), or the org's invoice approval setting is off.\n","properties":{"status":{"type":"string","enum":["PAID_FROM_BALANCE","NOTHING_DUE","NO_AUTO_PAY","CHARGE_STARTED","DECLINED"],"description":"PAID_FROM_BALANCE - deposits/credit covered the invoice. NOTHING_DUE - nothing was left to collect and no credit was used. NO_AUTO_PAY - amount is left UNPAID; the customer has no auto-payment or no saved card. CHARGE_STARTED - the card was charged amount plus the card fee and its tax; the payment is recorded when Stripe confirms it. DECLINED - Stripe refused the card; amount is left UNPAID, collect it manually.\n","example":"DECLINED"},"invoice_id":{"type":"string","format":"uuid"},"invoice_number":{"type":"string","example":"INV-R-20261007-003-01"},"applied_from_balance":{"type":"number","description":"Deposits and credit applied to the invoice by this call.","example":0},"amount":{"type":"number","description":"Left after deposits and credit - charged for CHARGE_STARTED, else unpaid.","example":1083.14},"reason":{"type":"string","nullable":true,"description":"Stripe's message for DECLINED; null otherwise.","example":"Your card was declined."}}},"rental_convert_body":{"type":"object","description":"Body of PUT /lotus/rentals/convert. Unknown keys are stripped.","required":["order_id","status"],"properties":{"order_id":{"type":"string","format":"uuid","example":"7c1e2d3a-4b5c-4d6e-8f70-1a2b3c4d5e6f"},"status":{"type":"string","description":"The status to move to. Case-insensitive.","enum":["RESERVATION","OPEN_CONTRACT","OPEN_CONTRACT_SKIP_PAYMENT","CLOSED_CONTRACT","COMPLETED_CONTRACT","BACKDATED_CLOSED_CONTRACT"],"example":"RESERVATION"},"dispatched_date":{"type":"string","format":"date-time","nullable":true,"description":"Stamped on every dispatch on RESERVATION to OPEN_CONTRACT and on BACKDATED_CLOSED_CONTRACT; refused (400) on the close of an open contract. A past date needs RENTAL_BACKDATED; on BACKDATED_CLOSED_CONTRACT any date does, and a future one is refused (400).\n"},"off_rent_date":{"type":"string","format":"date-time","nullable":true,"description":"Stamped on every dispatch being closed (both closes). A past date needs RENTAL_BACKDATED; on BACKDATED_CLOSED_CONTRACT any date does, and a future one is refused (400).\n"},"acknowledge_credit_warning":{"type":"boolean","description":"Resend with true after a credit-limit 409 (RESERVATION and BACKDATED_CLOSED_CONTRACT)."}}},"rental_convert_result":{"type":"object","properties":{"message":{"type":"string","description":"What happened. For OPEN_CONTRACT on ADVANCE billing it may be \"A charge has been initiated on the customer account...\", \"Auto payment is disabled on this customer...\" or \"Could not create a payment charge...\" - the order then stays RESERVATION. A close ends with \"; the automatic card charge was declined, please collect payment manually\" when auto_charge.status is DECLINED.\n","example":"Order reserved successfully"},"auto_charge":{"description":"CLOSED_CONTRACT and BACKDATED_CLOSED_CONTRACT only.","allOf":[{"$ref":"#/components/schemas/rental_auto_charge"}]}}},"rental_dates_body":{"type":"object","description":"Body of PUT /lotus/rentals/{orderId}/dates. At least one date (top level or in items) is required. Unknown keys are stripped.\n","properties":{"expected_dispatch_date":{"type":"string","format":"date-time"},"expected_return_date":{"type":"string","format":"date-time","example":"2026-12-15T15:00:00.000Z"},"dispatched_date":{"type":"string","format":"date-time"},"off_rent_date":{"type":"string","format":"date-time"},"items":{"type":"array","description":"Per line item dates; each needs line_item_id and at least one date.","items":{"type":"object","required":["line_item_id"],"properties":{"line_item_id":{"type":"string","format":"uuid","example":"4e5f6a7b-8c9d-4e0f-9a1b-2c3d4e5f6a7b"},"dispatch_id":{"type":"string","format":"uuid","description":"Must be the line item's active dispatch when given."},"expected_dispatch_date":{"type":"string","format":"date-time"},"expected_return_date":{"type":"string","format":"date-time","example":"2026-12-15T15:00:00.000Z"},"dispatched_date":{"type":"string","format":"date-time"},"off_rent_date":{"type":"string","format":"date-time"}}}},"acknowledge_credit_warning":{"type":"boolean","description":"Resend with true after a credit-limit 409. Does not count as a date."}}},"rental_dates_result":{"type":"object","properties":{"message":{"type":"string","description":"\"Rental dates updated successfully\" (followed by \"; the automatic card charge was declined, please collect payment manually\" when auto_charge.status is DECLINED), or \"No date changes applied (submitted values match current state)\".\n","example":"Rental dates updated successfully"},"backdated_status":{"type":"string","enum":["NO","PARTIAL","FULL"]},"delta_amount":{"type":"number","description":"The total of the invoice the re-bill produced - a new delta invoice's or credit note's total (negative for a credit), or, when the latest invoice was changed in place, that invoice's new full total. 0 when nothing was re-billed.\n"},"delta_invoice":{"description":"The full stored invoice row the re-bill made or changed in place, or null when nothing was re-billed.\n","nullable":true,"allOf":[{"$ref":"#/components/schemas/invoice"},{"type":"object","properties":{"org_snapshot":{"type":"object","nullable":true,"description":"Currently always null."},"adjustments_snapshot":{"type":"object","nullable":true,"description":"Currently always null."},"deletion_information":{"type":"object","nullable":true,"description":"Who voided the invoice and when; null on a live invoice."},"is_credit_note":{"type":"boolean","nullable":true},"last_emailed_at":{"type":"string","format":"date-time","nullable":true},"last_emailed_status":{"type":"string","nullable":true,"description":"The invoice status when it was last emailed.","example":"UNPAID"}}}]},"auto_charge":{"$ref":"#/components/schemas/rental_auto_charge"}}},"rental_dispatch_status_body":{"type":"object","description":"Body of PUT /lotus/rentals/dispatch/status. Exactly one of dispatch_id and dispatch_ids. Unknown keys are stripped.\n","required":["status"],"properties":{"dispatch_id":{"type":"string","format":"uuid","example":"2d3e4f5a-6b7c-4d8e-9f0a-1b2c3d4e5f6a"},"dispatch_ids":{"type":"array","minItems":1,"maxItems":50,"uniqueItems":true,"description":"OFF_RENT_INSPECTION only (any other status takes one id); all on one order.","items":{"type":"string","format":"uuid"}},"status":{"type":"string","description":"Case-insensitive.","enum":["DISPATCHED","OFF_RENT","OFF_RENT_INSPECTION","RELEASED"],"example":"DISPATCHED"},"timestamp":{"type":"string","format":"date-time","description":"When it happened; default now. Past and future are both accepted."}}},"rental_dispatch_status_result":{"type":"object","properties":{"message":{"type":"string","description":"\"Dispatch status updated to <status>\", \"Dispatch off-rented; contract auto-closed\", \"Dispatch released; contract auto-completed\", or for a batch \"<n> dispatches moved to Off-Rent Inspection\" / \"<n> of <m> dispatches moved to Off-Rent Inspection\". The first two end with \"; the automatic card charge was declined, please collect payment manually\" when auto_charge.status is DECLINED.\n","example":"Dispatch status updated to DISPATCHED"},"status":{"type":"string","description":"The status asked for; results[].to_status is what was stored (IN_TRANSIT for a future DISPATCHED)."},"order_id":{"type":"string","format":"uuid"},"updated":{"type":"integer"},"skipped":{"type":"integer"},"auto_closed":{"type":"boolean","description":"The order was closed because the last asset went off rent."},"auto_completed":{"type":"boolean","description":"The order was completed because the last asset was released."},"auto_charge":{"description":"The OFF_RENT re-bill's settlement on an ADVANCE contract, or the auto-close's; null otherwise (always null for a batch).\n","allOf":[{"$ref":"#/components/schemas/rental_auto_charge"}]},"results":{"type":"array","items":{"type":"object","properties":{"dispatch_id":{"type":"string","format":"uuid"},"asset_id":{"type":"string","format":"uuid","nullable":true},"asset_number":{"type":"string","nullable":true},"asset_name":{"type":"string","nullable":true},"from_status":{"type":"string","nullable":true},"to_status":{"type":"string","nullable":true},"result":{"type":"string","enum":["UPDATED","SKIPPED","NOT_FOUND"]},"message":{"type":"string","description":"Why it was skipped (SKIPPED and NOT_FOUND only)."}}}}}},"rental_replace_asset_body":{"type":"object","description":"Body of PUT /lotus/rentals/dispatch/replace-asset. Unknown keys are stripped.","required":["dispatch_id","new_asset_id"],"properties":{"dispatch_id":{"type":"string","format":"uuid","example":"2d3e4f5a-6b7c-4d8e-9f0a-1b2c3d4e5f6a"},"new_asset_id":{"type":"string","format":"uuid","example":"8a9b0c1d-2e3f-4a4b-8c5d-6e7f8a9b0c1d"},"notes":{"type":"string","nullable":true,"maxLength":500}}},"rental_replace_asset_result":{"type":"object","properties":{"message":{"type":"string","example":"Asset replaced successfully"},"old_dispatch_id":{"type":"string","format":"uuid"},"new_dispatch_id":{"type":"string","format":"uuid"}}},"rental_credit_check_result":{"allOf":[{"type":"object","properties":{"order_id":{"type":"string","format":"uuid"},"customer_id":{"type":"string","format":"uuid"}}},{"$ref":"#/components/schemas/lotus_credit_decision"}]},"rental_charges_summary_body":{"type":"object","description":"Body of POST /lotus/rentals/charges_summary - the order being built. Unknown keys are stripped.\n","required":["customer_id","items"],"properties":{"customer_id":{"type":"string","format":"uuid","example":"3f2a1b4c-5d6e-4f70-8a9b-0c1d2e3f4a5b"},"site_id":{"type":"string","format":"uuid","nullable":true},"items":{"type":"array","minItems":1,"description":"As in create; ASSET items also need rental_rates {day, week, 4week}.","items":{"allOf":[{"$ref":"#/components/schemas/rental_item"},{"type":"object","properties":{"rental_rates":{"type":"object","description":"Required for ASSET.","properties":{"day":{"type":"number","nullable":true},"week":{"type":"number","nullable":true},"4week":{"type":"number","nullable":true}}},"dispatched_date":{"type":"string","format":"date-time","nullable":true,"description":"ASSET only (stripped otherwise). A committed order's actual dispatch date; when sent it is priced instead of expected_dispatch_date, as billing does, so a backdated dispatch previews what billing will charge. Leave it out for a quote.\n"},"off_rent_date":{"type":"string","format":"date-time","nullable":true,"description":"ASSET only (stripped otherwise). The actual off-rent date; when sent it is priced instead of expected_return_date.\n"}}}]},"example":[{"item_type":"ASSET","item_id":"9b8a7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d","expected_dispatch_date":"2026-12-01T15:00:00.000Z","expected_return_date":"2026-12-08T15:00:00.000Z","rental_rates":{"day":395,"week":1185,"4week":3555}}]},"state_sales_tax_status":{"type":"boolean","description":"Left out - off for a tax-exempt site, else the customer's flag."},"county_sales_tax_status":{"type":"boolean","description":"Left out - off for a tax-exempt site, else the customer's flag."},"city_sales_tax_status":{"type":"boolean","description":"Left out - off for a tax-exempt site, else the customer's flag."},"special_sales_tax_status":{"type":"boolean","description":"Left out - off for a tax-exempt site, else the customer's flag."},"damage_waiver_status":{"type":"boolean","description":"Left out - the customer's flag."},"env_fee_status":{"type":"boolean","description":"Left out - the customer's flag."},"discount":{"type":"number","minimum":0,"maximum":100,"default":0},"customer_discount":{"type":"number","minimum":0,"maximum":100,"default":0},"flat_discount_value":{"type":"number","minimum":0,"default":0},"quoted_delivery_fee":{"type":"number","default":0},"quoted_return_fee":{"type":"number","default":0},"quoted_additional_freight_fee":{"type":"number","default":0},"delivery_fee":{"type":"number","default":0,"description":"Accepted; not part of the preview."},"return_fee":{"type":"number","default":0,"description":"Accepted; not part of the preview."},"additional_freight_fee":{"type":"number","default":0,"description":"Accepted; not part of the preview."}}},"rental_charges_summary_result":{"allOf":[{"$ref":"#/components/schemas/lotus_order_financials"},{"type":"object","properties":{"line_items":{"type":"array","description":"The items as priced - the body's items with total, discount_value, sub_total and rates_object added.","items":{"type":"object","additionalProperties":true}},"credit_check":{"$ref":"#/components/schemas/lotus_credit_decision"}},"required":["credit_check"]}]},"rental_check_overlaps_body":{"type":"object","description":"Body of POST /lotus/rentals/check-overlaps. Unknown keys are stripped.","required":["assets"],"properties":{"assets":{"type":"object","description":"Keyed by asset id. Give start_datetime and end_datetime together to check that window, or neither to match any booking of the asset.\n","additionalProperties":{"type":"object","required":["asset_id"],"properties":{"asset_id":{"type":"string","format":"uuid"},"start_datetime":{"type":"string","format":"date-time"},"end_datetime":{"type":"string","format":"date-time"}}},"example":{"9b8a7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d":{"asset_id":"9b8a7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d","start_datetime":"2026-12-01T15:00:00.000Z","end_datetime":"2026-12-08T15:00:00.000Z"}}},"order_id":{"type":"string","format":"uuid","nullable":true,"description":"The order being edited, left out of the check."}}},"rental_overlap":{"type":"object","properties":{"asset_id":{"type":"string","format":"uuid"},"quote_overlap":{"type":"boolean"},"reservation_overlap":{"type":"boolean"},"open_contract_overlap":{"type":"boolean","description":"Also true for a CLOSED_CONTRACT still holding the asset."},"status":{"type":"string","nullable":true,"description":"The clashing line item's lifecycle_status (of the most serious clash); null without one."},"order_status":{"type":"string","nullable":true,"description":"The most serious clashing order's status; null without one."},"out_of_service":{"type":"boolean","nullable":true},"matched_orders":{"type":"array","items":{"type":"object","properties":{"order_id":{"type":"string","format":"uuid"},"order_number":{"type":"string"},"order_status":{"type":"string"},"customer_id":{"type":"string","format":"uuid"},"customer_name":{"type":"string"},"start_datetime":{"type":"string","format":"date-time"},"end_datetime":{"type":"string","format":"date-time"}}}}}},"rental_asset_history_body":{"type":"object","description":"Body of POST /lotus/rentals/asset_history. Unknown keys are stripped.","required":["asset_id","paginate"],"properties":{"asset_id":{"type":"string","format":"uuid","example":"9b8a7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d"},"search":{"type":"object","default":{},"properties":{"query":{"type":"string","nullable":true,"description":"Partial match on order number or customer name."},"order_type":{"type":"string","nullable":true,"enum":["RENTAL","SALE",null]}}},"paginate":{"type":"object","required":["currentPage","perPage"],"properties":{"currentPage":{"type":"integer","minimum":1,"example":1},"perPage":{"type":"integer","minimum":1,"maximum":100,"example":25}}}}},"rental_asset_history_result":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","description":"One rental dispatch, or one sale (whose dispatch fields are null).","properties":{"dispatch_id":{"type":"string","format":"uuid","nullable":true},"dispatch_status":{"type":"string","nullable":true},"dispatched_date":{"type":"string","format":"date-time","nullable":true},"off_rent_date":{"type":"string","format":"date-time","nullable":true},"released_date":{"type":"string","format":"date-time","nullable":true},"replacement_for_dispatch_id":{"type":"string","format":"uuid","nullable":true},"replaced_by_dispatch_id":{"type":"string","format":"uuid","nullable":true},"line_item_id":{"type":"string","format":"uuid"},"line_item_name":{"type":"string","nullable":true},"dispatch_at":{"type":"string","format":"date-time","nullable":true,"description":"The line item's expected dispatch date."},"return_at":{"type":"string","format":"date-time","nullable":true,"description":"The line item's expected return date."},"order_id":{"type":"string","format":"uuid"},"order_number":{"type":"string"},"order_status":{"type":"string"},"order_type":{"type":"string","enum":["RENTAL","SALE"]},"created_at":{"type":"string","format":"date-time"},"parent_id":{"type":"string","format":"uuid","nullable":true},"parent_order_number":{"type":"string","nullable":true},"customer_id":{"type":"string","format":"uuid","nullable":true},"customer_name":{"type":"string","nullable":true},"assignee_name":{"type":"string","nullable":true},"assignee_email":{"type":"string","nullable":true},"inspection_records":{"type":"array","description":"The dispatch's inspections; empty for a sale.","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"type":{"type":"string","enum":["DISPATCH","OFF_RENT"]},"status":{"type":"string"},"passed":{"type":"boolean","nullable":true},"assignee":{"type":"string","format":"uuid","nullable":true},"completed_at":{"type":"string","format":"date-time","nullable":true},"dispatch_inspection_id":{"type":"string","format":"uuid","nullable":true},"off_rent_inspection_id":{"type":"string","format":"uuid","nullable":true}}}}}}},"pagination":{"$ref":"#/components/schemas/lotus_order_pagination"}}},"rental_signnow_body":{"type":"object","description":"Body of POST /lotus/rentals/signnow/invite. Unknown keys are stripped.","required":["order_id","signer_email"],"properties":{"order_id":{"type":"string","format":"uuid","example":"7c1e2d3a-4b5c-4d6e-8f70-1a2b3c4d5e6f"},"signer_email":{"type":"string","format":"email","example":"signer@example.com"}}},"rental_email_body":{"type":"object","description":"Body of POST /lotus/rentals/email. Unknown keys are stripped.","required":["order_id","receiver_type"],"properties":{"order_id":{"type":"string","format":"uuid","example":"7c1e2d3a-4b5c-4d6e-8f70-1a2b3c4d5e6f"},"receiver_type":{"type":"string","enum":["customer","self","custom"],"example":"self"},"contact_id":{"type":"string","format":"uuid","description":"With customer - send to this contact only."},"custom_email":{"type":"array","minItems":1,"description":"Required with custom (singular name, but a list of addresses).","items":{"type":"string","format":"email"}}}},"rental_delete_body":{"type":"object","description":"Body of DELETE /lotus/rentals/{id}; optional. Unknown keys are stripped.","properties":{"reason":{"type":"string","nullable":true,"example":"Customer cancelled"}}},"sale_order_row":{"type":"object","description":"The stored sales order (a lotus_orders row).","properties":{"id":{"type":"string","format":"uuid","readOnly":true},"org":{"type":"string","format":"uuid","readOnly":true},"order_number":{"type":"string","readOnly":true,"example":"S-20260914-002"},"type":{"type":"string","enum":["SALE"],"readOnly":true},"status":{"type":"string","enum":["QUOTE","OPEN_SALES_ORDER","CLOSED_SALES_ORDER"],"readOnly":true,"description":"A voided order keeps the status it had (deleted is true)."},"customer_id":{"type":"string","format":"uuid","nullable":true,"readOnly":true,"description":"Null on a quote saved without a customer."},"contact_id":{"type":"string","format":"uuid","nullable":true,"readOnly":true},"site_id":{"type":"string","format":"uuid","nullable":true,"readOnly":true,"description":"Always null - sales have no site."},"assignee_id":{"type":"string","format":"uuid","nullable":true,"readOnly":true,"description":"The sales person (commission attribution)."},"parent_id":{"type":"string","format":"uuid","nullable":true,"readOnly":true},"priority":{"type":"integer","nullable":true,"readOnly":true},"customer_po":{"type":"string","nullable":true,"readOnly":true},"external_notes":{"type":"string","nullable":true,"readOnly":true},"internal_notes":{"type":"string","nullable":true,"readOnly":true},"metadata":{"type":"object","nullable":true,"readOnly":true,"description":"Where each tax rate came from - keys STATE_TAX_SOURCE, COUNTY_TAX_SOURCE, CITY_TAX_SOURCE, SPECIAL_TAX_SOURCE, SALES_TAX_SOURCE (string) and SALES_TAX_SOURCE_ZIP (string or null); ENV_FEE_SOURCE and DAMAGE_WAIVER_SOURCE are present too although sales carry neither.\n","additionalProperties":{"type":"string","nullable":true},"example":{"SALES_TAX_SOURCE":"Organization Defaults","STATE_TAX_SOURCE":"CONTRACT","SALES_TAX_SOURCE_ZIP":null}},"deleted":{"type":"boolean","readOnly":true,"description":"True for a deleted quote or a voided order."},"deletion_information":{"type":"object","nullable":true,"readOnly":true,"description":"Who deleted or voided the order, when and why; null on a live order.","properties":{"date":{"type":"string","format":"date-time"},"reason":{"type":"string","nullable":true},"user_id":{"type":"string","format":"uuid","nullable":true},"username":{"type":"string","nullable":true}}},"imported":{"type":"boolean","readOnly":true,"description":"True for history imported from the previous system."},"created_at":{"type":"string","format":"date-time","readOnly":true},"updated_at":{"type":"string","format":"date-time","readOnly":true},"status_updated_at":{"type":"string","format":"date-time","nullable":true,"readOnly":true},"created_by":{"type":"string","format":"uuid","nullable":true,"readOnly":true},"updated_by":{"type":"string","format":"uuid","nullable":true,"readOnly":true},"status_updated_by":{"type":"string","format":"uuid","nullable":true,"readOnly":true}}},"sale_details":{"type":"object","nullable":true,"description":"The order's lotus_sale_details row - quote approval, e-signature and commission.","properties":{"order_id":{"type":"string","format":"uuid"},"org":{"type":"string","format":"uuid"},"quote_status":{"type":"string","enum":["PENDING","APPROVED","REJECTED"],"description":"Uppercase here (PUT /lotus/sales/update takes it lowercase as sales_quote_status)."},"rejection_reason":{"type":"string","nullable":true},"valid_until":{"type":"string","format":"date-time","nullable":true,"description":"The quote's expiry; an expired QUOTE drops out of search."},"tax_exemption_expiration_date":{"type":"string","format":"date-time","nullable":true},"signnow_document_id":{"type":"string","nullable":true,"description":"Set by the first POST /lotus/sales/signnow/invite."},"signed_status":{"type":"string","nullable":true,"enum":["SENT_TO_SIGNER","SIGNED","SIGNER_DECLINED","SIGNER_INVITE_DELETED","INVITE_EMAIL_DELIVERY_FAILED","INVITE_EXPIRED",null],"description":"Null until the order is sent for signing; the SignNow webhook sets the later values."},"signnow_invite_sent_date":{"type":"string","format":"date-time","nullable":true},"signed_date":{"type":"string","format":"date-time","nullable":true},"commission_rate":{"type":"number","nullable":true,"description":"Per-sale commission percent; null uses the org default."}}},"sale_warranty_option":{"type":"object","description":"One SPP warranty tier offered on an asset's authorization form (a lotus_asset_warranty_options row). Coverage ends at whichever of the months or machine hours is reached first.\n","properties":{"id":{"type":"string","format":"uuid"},"org":{"type":"string","format":"uuid"},"line_item_id":{"type":"string","format":"uuid"},"order_id":{"type":"string","format":"uuid"},"weight":{"type":"integer","description":"Display order (10, 20, 30 - \"Option 1..3\" on the form)."},"coverage_months":{"type":"integer","nullable":true},"coverage_hours":{"type":"integer","nullable":true},"retail_price":{"type":"number","nullable":true,"description":"The price to the customer."},"internal_cost":{"type":"number","nullable":true,"description":"The underwriter's price to the org; never billed or printed on the form."},"deleted":{"type":"boolean"},"created_at":{"type":"string","format":"date-time","nullable":true},"updated_at":{"type":"string","format":"date-time","nullable":true},"created_by":{"type":"string","format":"uuid","nullable":true},"updated_by":{"type":"string","format":"uuid","nullable":true}}},"sale_line_item":{"type":"object","description":"A line item with its latest sale inspection, sale work order, current asset, SPP warranty state and price. The order of line_items is not guaranteed.\n","properties":{"id":{"type":"string","format":"uuid"},"org":{"type":"string","format":"uuid"},"order_id":{"type":"string","format":"uuid"},"parent_id":{"type":"string","format":"uuid","nullable":true,"description":"The parent line item when this is an attachment."},"parent_item_id":{"type":"string","format":"uuid","nullable":true,"description":"The parent line item's asset id."},"item_type":{"type":"string","enum":["ASSET","CUSTOM","WARRANTY"]},"item_id":{"type":"string","format":"uuid","nullable":true,"description":"The asset id; null for CUSTOM and WARRANTY lines."},"name":{"type":"string","nullable":true,"description":"CUSTOM and WARRANTY lines' text; a sold SPP warranty reads \"SPP — N months / H hrs\"."},"quantity":{"type":"number"},"unit_price":{"type":"number","nullable":true,"description":"The price per unit (an ASSET's sell price, or the customer_price sent)."},"quoted_cost":{"type":"number","nullable":true,"description":"Internal per-unit cost (a CUSTOM fee's or a warranty's); never billed."},"percentage_discount":{"type":"number","nullable":true},"is_taxable":{"type":"boolean","nullable":true},"bill_to":{"type":"string","nullable":true,"example":"CUSTOMER"},"capitalized":{"type":"boolean","nullable":true},"approval_status":{"type":"string","nullable":true,"enum":["QUOTED","APPROVED","REJECTED",null],"description":"Follows the quote's approval."},"lifecycle_status":{"type":"string","enum":["QUOTED","OPEN_SALES_ORDER","CLOSED_SALES_ORDER"]},"item_details":{"type":"object","nullable":true,"additionalProperties":true,"description":"ASSET lines - a copy of the asset row taken when it was added (asset_number, serial_number, hour_meter, sell_price, rental_rates, ...) plus add_on and category_linage; null (or an empty object) on CUSTOM and WARRANTY lines.\n"},"asset_ref":{"type":"string","format":"uuid","nullable":true,"description":"WARRANTY lines - the asset the warranty covers."},"sold_at":{"type":"string","format":"date-time","nullable":true,"description":"Set when the order is closed."},"sale_snapshot":{"type":"object","nullable":true,"additionalProperties":true,"description":"ASSET lines of a closed order - the profit figures frozen at the sale (book_value, book_value_source, sale_price, list_sell_price, purchase_price, initial_investment, total_cost, gross_profit, margin, commission_rate, commission_cost, depreciation_margin, accumulated_depreciation, months_in_inventory, rental_income_lifetime, warranty, order_discounts, category details, snapshot_version, ...); some values can be null. Null until closed.\n"},"warranty_source_option_id":{"type":"string","format":"uuid","nullable":true,"description":"WARRANTY lines added by a recorded SPP choice - the option sold."},"warranty_coverage_months":{"type":"integer","nullable":true},"warranty_coverage_hours":{"type":"integer","nullable":true},"purchase_order":{"type":"string","format":"uuid","nullable":true},"rental_rates":{"type":"object","nullable":true,"description":"Always null on sales."},"rental_charge_details":{"type":"object","nullable":true,"description":"Always null on sales."},"expected_dispatch_date":{"type":"string","format":"date-time","nullable":true,"description":"Always null on sales."},"expected_return_date":{"type":"string","format":"date-time","nullable":true,"description":"Always null on sales."},"billing_cycle_anchor":{"type":"string","format":"date-time","nullable":true,"description":"Always null on sales."},"deleted":{"type":"boolean"},"imported":{"type":"boolean","nullable":true},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time","nullable":true},"created_by":{"type":"string","format":"uuid","nullable":true},"updated_by":{"type":"string","format":"uuid","nullable":true},"sale_inspection":{"type":"object","nullable":true,"description":"The asset's latest SALE inspection; null when none exists.","properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string","example":"DONE"},"passed":{"type":"boolean","nullable":true},"assigned":{"type":"boolean"}}},"sale_task":{"type":"object","nullable":true,"description":"The asset's sale work order (an unfinished one first); null when none exists.","properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string","example":"PENDING"},"task_type":{"type":"string","example":"SALE"},"due_date":{"type":"string","format":"date-time","nullable":true},"assigned":{"type":"boolean"}}},"current_item_details":{"type":"object","nullable":true,"description":"The asset as it is now; null for CUSTOM and WARRANTY lines.","properties":{"id":{"type":"string","format":"uuid"},"asset_number":{"type":"string","nullable":true},"name":{"type":"string","nullable":true},"checklist_status":{"type":"string","nullable":true,"description":"The asset's sale stage - onboarded, selling, sale_approved, sold, ..."},"quantity":{"type":"number","nullable":true},"validated":{"type":"boolean","nullable":true},"category":{"type":"string","format":"uuid","nullable":true},"category_name":{"type":"string","nullable":true}}},"warranty_eligibility":{"type":"string","nullable":true,"enum":["QUALIFIES","DOES_NOT_QUALIFY",null],"description":"The asset's SPP warranty decision; null when the line carries none (CUSTOM, WARRANTY, attachments)."},"warranty_signnow_document_id":{"type":"string","nullable":true},"warranty_signed_status":{"type":"string","nullable":true,"enum":["UNSIGNED","SENT_TO_SIGNER","SIGNED","SIGNER_DECLINED","SIGNER_INVITE_DELETED","INVITE_EMAIL_DELIVERY_FAILED","INVITE_EXPIRED",null],"description":"UNSIGNED until the form is sent; null when the line carries no warranty decision."},"warranty_invite_sent_date":{"type":"string","format":"date-time","nullable":true},"warranty_signer_email":{"type":"string","nullable":true},"warranty_signed_date":{"type":"string","format":"date-time","nullable":true},"warranty_recorded_outcome":{"type":"string","nullable":true,"enum":["OPTION_SELECTED","REFUSED",null],"description":"The customer's choice as recorded by PUT /lotus/sales/update."},"warranty_selected_option_id":{"type":"string","format":"uuid","nullable":true},"warranty_recorded_at":{"type":"string","format":"date-time","nullable":true},"warranty_recorded_by":{"type":"string","format":"uuid","nullable":true},"warranty_options":{"type":"array","description":"The offered tiers, in display order; empty when none.","items":{"allOf":[{"$ref":"#/components/schemas/sale_warranty_option"},{"type":"object","properties":{"gross_profit":{"type":"number","description":"retail_price - internal_cost."},"margin":{"type":"number","nullable":true,"description":"Percent of retail_price; null when retail_price is 0."}}}]}},"warranty_signed_artifact":{"$ref":"#/components/schemas/lotus_order_signed_artifact"},"sub_total":{"type":"number","description":"quantity x unit_price."},"discount_value":{"type":"number"},"total":{"type":"number","description":"sub_total less the line discount."},"original_unit_price":{"type":"number","nullable":true,"description":"ASSET lines only - the asset's catalog sell price at quote time."},"is_discounted_from_sale_price":{"type":"boolean","description":"ASSET lines only - true when unit_price is below original_unit_price."}}},"sale_search_row":{"description":"A sales order with its details, as listed by search.","allOf":[{"$ref":"#/components/schemas/sale_order_row"},{"type":"object","properties":{"sale_details":{"$ref":"#/components/schemas/sale_details"},"customer_details":{"$ref":"#/components/schemas/lotus_order_customer_details"},"contact_details":{"$ref":"#/components/schemas/lotus_order_contact_details"},"order_adjustments":{"type":"array","description":"The four sales taxes and the discounts (no environmental fee or damage waiver).","items":{"$ref":"#/components/schemas/lotus_order_adjustment"}},"fulfillment_tickets":{"type":"array","items":{"$ref":"#/components/schemas/lotus_order_fulfillment_ticket"}},"invoice_details":{"type":"array","items":{"$ref":"#/components/schemas/lotus_order_invoice_summary"}},"signed_artifact":{"$ref":"#/components/schemas/lotus_order_signed_artifact"},"line_items":{"type":"array","items":{"$ref":"#/components/schemas/sale_line_item"}}}},{"$ref":"#/components/schemas/lotus_order_financials"}]},"sale_order_detail":{"description":"A sales order with everything the order screen shows.","allOf":[{"$ref":"#/components/schemas/sale_search_row"},{"type":"object","properties":{"assignee_details":{"$ref":"#/components/schemas/lotus_order_assignee_details"},"ancestry":{"type":"array","description":"The chain of parent orders, oldest first; empty for most orders.","items":{"$ref":"#/components/schemas/lotus_order_ancestor"}},"can_close":{"type":"object","nullable":true,"description":"For a live OPEN_SALES_ORDER - whether PUT /lotus/sales/convert to CLOSED_SALES_ORDER would pass; null otherwise.","properties":{"status":{"type":"boolean"},"reasons":{"type":"array","items":{"type":"string","enum":["Payment has not been received","One or more assets are not sale approved yet","Sale inspections are incomplete","Sale tasks are incomplete"]}}}}}}]},"sale_search_body":{"type":"object","description":"Body of POST /lotus/sales/search. Unknown keys are stripped.","required":["search","paginate"],"properties":{"search":{"type":"object","description":"Every filter is optional; omitted means no filter. order_by sorts by order_number, status, company_name, valid_until or created_at; the default order is priority, then newest first.\n","properties":{"status":{"type":"array","nullable":true,"items":{"type":"string","enum":["QUOTE","OPEN_SALES_ORDER","CLOSED_SALES_ORDER"]}},"sales_quote_status":{"type":"array","nullable":true,"description":"Uppercase here.","items":{"type":"string","enum":["PENDING","APPROVED","REJECTED"]}},"date_from":{"type":"string","format":"date-time","description":"Applied only together with date_to - orders created in date_from..date_to."},"date_to":{"type":"string","format":"date-time","description":"Not before date_from; sending it alone is a 400."},"customer":{"type":"string","format":"uuid","nullable":true,"description":"Customer id. Forced to the session's customer for a customer-portal session."},"company_name":{"type":"string","nullable":true,"description":"Partial match."},"order_number":{"type":"string","nullable":true,"description":"Partial match."},"deleted":{"type":"boolean","description":"true lists only deleted and voided orders (expired quotes included)."},"order_by":{"$ref":"#/components/schemas/order_by"}},"example":{}},"paginate":{"type":"object","required":["currentPage","perPage"],"properties":{"currentPage":{"type":"integer","minimum":1,"example":1},"perPage":{"type":"integer","minimum":1,"maximum":100,"example":25}}}}},"sale_search_result":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/sale_search_row"}},"pagination":{"$ref":"#/components/schemas/lotus_order_pagination"},"orderByColumnNamesList":{"$ref":"#/components/schemas/orderByColumnNamesList"}}},"sale_warranty_option_input":{"type":"object","description":"A tier offered on the asset's SPP form. Send the existing id to keep a tier (a recorded choice points at it); a tier without id is new and the ones not sent are removed. On update an id must be one of this line's own live tiers, else 400. The array order is the order on the form.\n","required":["coverage_months","coverage_hours","retail_price","internal_cost"],"properties":{"id":{"type":"string","format":"uuid"},"coverage_months":{"type":"integer","minimum":1,"example":12},"coverage_hours":{"type":"integer","minimum":1,"description":"Machine hours.","example":1000},"retail_price":{"type":"number","minimum":0,"description":"Price to the customer.","example":1500},"internal_cost":{"type":"number","minimum":0,"description":"The underwriter's price to the org; never billed.","example":900}}},"sale_item":{"type":"object","description":"A line item of a create or update body. ASSET items need item_id and are priced at the asset's sell price unless customer_price is sent. CUSTOM and WARRANTY items need name and unit_price (any amount, negative for a credit line); a WARRANTY item also needs asset_ref and is always one unit. The warranty fields apply to main ASSET lines only and are stripped elsewhere. Unknown keys are stripped.\n","required":["item_type"],"properties":{"item_type":{"type":"string","enum":["ASSET","CUSTOM","WARRANTY"]},"item_id":{"type":"string","format":"uuid","nullable":true,"description":"The asset id. Required for ASSET."},"id":{"type":"string","format":"uuid","description":"The existing line item's id, on update - keeps the line instead of replacing it."},"name":{"type":"string","nullable":true,"description":"Required for CUSTOM and WARRANTY."},"customer_price":{"type":"number","minimum":0,"description":"ASSET only - overrides the asset's sell price (0 sells it free)."},"unit_price":{"type":"number","description":"CUSTOM and WARRANTY only (required, 2 decimals)."},"asset_ref":{"type":"string","format":"uuid","description":"WARRANTY only (required) - the asset on this order the warranty covers."},"quantity":{"type":"number","default":1,"description":"Above 0; always 1 for WARRANTY."},"discount":{"type":"number","minimum":0,"maximum":100,"default":0,"description":"Percent off this line."},"quoted_cost":{"type":"number","nullable":true,"minimum":0,"description":"Internal per-unit cost; never billed."},"is_taxable":{"type":"boolean","default":true},"parent_item_id":{"type":"string","format":"uuid","nullable":true,"description":"The asset id this one is attached to (not its own item_id)."},"warranty_eligibility":{"type":"string","enum":["QUALIFIES","DOES_NOT_QUALIFY"],"description":"Left out on a main asset - QUALIFIES when warranty_options are sent, otherwise DOES_NOT_QUALIFY."},"warranty_options":{"type":"array","maxItems":3,"default":[],"description":"Not allowed with DOES_NOT_QUALIFY; cannot change once the form was sent.","items":{"$ref":"#/components/schemas/sale_warranty_option_input"}},"warranty_recorded_outcome":{"type":"string","nullable":true,"enum":["OPTION_SELECTED","REFUSED",null],"description":"Update only, after the form is SIGNED - the customer's choice; null clears it."},"warranty_selected_option_id":{"type":"string","format":"uuid","nullable":true,"description":"Required with OPTION_SELECTED and allowed only with it - one of this asset's warranty_options ids."},"warranty_void":{"type":"boolean","description":"Update only - throws the asset's warranty away while the asset stays; not together with warranty_recorded_outcome."}}},"sale_order_fields":{"type":"object","description":"The fields shared by create and update. Unknown keys are stripped. The tax flags have no default; sending any of them needs UPDATE_FEES_ON_SALE_ORDERS.\n","required":["valid_until","items"],"properties":{"customer_id":{"type":"string","format":"uuid","nullable":true,"description":"Optional on a quote; needed to approve it.","example":"3f2a1b4c-5d6e-4f70-8a9b-0c1d2e3f4a5b"},"contact_id":{"type":"string","format":"uuid","nullable":true,"description":"A contact of customer_id; not allowed without a customer."},"assignee_id":{"type":"string","format":"uuid","nullable":true,"description":"The sales person - a user allowed to own orders (ALLOW_ORDER_OWNERSHIP). Left out on create, the customer's sales person."},"valid_until":{"type":"string","format":"date-time","description":"The quote's expiry.","example":"2026-12-31T18:00:00.000Z"},"tax_exemption_expiration_date":{"type":"string","format":"date-time","nullable":true,"description":"Cleared when left out on update."},"items":{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/sale_item"},"example":[{"item_type":"ASSET","item_id":"9b8a7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d","customer_price":45000,"discount":0,"warranty_eligibility":"QUALIFIES","warranty_options":[{"coverage_months":12,"coverage_hours":1000,"retail_price":1500,"internal_cost":900}]},{"item_type":"CUSTOM","name":"Pre-delivery service","unit_price":250,"quantity":1}]},"state_sales_tax_status":{"type":"boolean"},"county_sales_tax_status":{"type":"boolean"},"city_sales_tax_status":{"type":"boolean"},"special_sales_tax_status":{"type":"boolean"},"discount":{"type":"number","minimum":0,"maximum":100,"default":0,"description":"Order discount in percent."},"customer_discount":{"type":"number","minimum":0,"maximum":100,"default":0,"description":"Customer discount in percent; at most the customer's own."},"flat_discount_value":{"type":"number","minimum":0,"default":0,"description":"Dollar discount; above 0 needs TRANSACTION_FLAT_CASH_DISCOUNT."},"delivery_freight_company":{"type":"string","format":"uuid","nullable":true},"return_freight_company":{"type":"string","format":"uuid","nullable":true},"additional_freight_company":{"type":"string","format":"uuid","nullable":true},"quoted_delivery_fee":{"type":"number","nullable":true,"minimum":0,"default":0,"description":"Delivery freight charged to the customer. A leg with no fee and no company makes no freight ticket."},"quoted_return_fee":{"type":"number","nullable":true,"minimum":0,"default":0},"quoted_additional_freight_fee":{"type":"number","nullable":true,"minimum":0,"default":0},"delivery_fee":{"type":"number","nullable":true,"minimum":0,"default":0,"description":"What the freight company charges for the delivery (internal)."},"return_fee":{"type":"number","nullable":true,"minimum":0,"default":0},"additional_freight_fee":{"type":"number","nullable":true,"minimum":0,"default":0},"commission_rate":{"type":"number","nullable":true,"minimum":0,"maximum":100,"description":"Per-sale commission percent, at most the org default; null clears it."},"customer_po":{"type":"string","nullable":true},"external_notes":{"type":"string","nullable":true},"notes":{"type":"string","nullable":true,"description":"Used as external_notes when external_notes is not sent."},"internal_notes":{"type":"string","nullable":true}}},"sale_create_body":{"description":"Body of POST /lotus/sales/create. The update-only top-level fields are stripped. The line-level warranty_recorded_outcome, warranty_selected_option_id and warranty_void are validated but ignored.\n","allOf":[{"$ref":"#/components/schemas/sale_order_fields"}]},"sale_update_body":{"description":"Body of PUT /lotus/sales/update - the whole order.","allOf":[{"$ref":"#/components/schemas/sale_order_fields"},{"type":"object","required":["id"],"properties":{"id":{"type":"string","format":"uuid","example":"6d5c4b3a-2f1e-4d0c-9b8a-7f6e5d4c3b2a"},"sales_quote_status":{"type":"string","enum":["pending","approved","rejected"],"description":"Lowercase. Left out keeps the stored value; ignored on an open order."},"sales_quote_rejection_reason":{"type":"string","nullable":true},"provision_subwork_now":{"type":"boolean","default":false,"description":"Open order only - create added assets' sale checklists, inspections and work orders now, unless that type is set to \"never\" (work orders also need a default task level)."},"acknowledge_credit_warning":{"type":"boolean","description":"Open order only - resend with true after a credit-limit 409 to go ahead."},"acknowledge_warranty_warning":{"type":"boolean","description":"Approving only - resend with true after the WARRANTY_UNRESOLVED 409 to approve anyway."}}}]},"sale_create_result":{"type":"object","properties":{"message":{"type":"string","example":"Order Created"},"data":{"description":"The order row as saved (status QUOTE) with its sale details.","allOf":[{"$ref":"#/components/schemas/sale_order_row"},{"type":"object","properties":{"sale_details":{"$ref":"#/components/schemas/sale_details"}}}]},"metadata":{"type":"object","nullable":true,"description":"Always null."}}},"sale_update_result":{"type":"object","properties":{"message":{"type":"string","example":"Order Updated"},"data":{"description":"The order row as saved (no sale_details).","allOf":[{"$ref":"#/components/schemas/sale_order_row"},{"type":"object","properties":{"billing_message":{"type":"string","description":"Open-order edits only - what happened to billing.","enum":["Open sales order updated","Open sales order updated. An invoice was created — please review and approve it","Open sales order updated. An invoice was created — auto-payment is disabled on this customer, please collect payment manually","Open sales order updated. An invoice was created but the customer has no default payment method, so it could not be charged automatically — please set one and collect payment manually","Open sales order updated. The invoice is already covered by an in-flight payment — no new charge issued","Open sales order updated and an invoice was created, but the automatic charge failed — please collect payment manually","Open sales order updated. The invoice was updated — please review and approve it","Open sales order updated. The invoice was updated — auto-payment is disabled on this customer, please collect payment manually","Open sales order updated. The invoice was updated but the customer has no default payment method, so it could not be charged automatically — please set one and collect payment manually","Open sales order updated and the invoice was updated, but the automatic charge failed — please collect payment manually","Open sales order updated and a charge on the sales order invoice has been created","Open sales order updated and the sales order invoice has been adjusted"]}}}]},"metadata":{"type":"object","nullable":true,"description":"Always null."}}},"sale_convert_body":{"type":"object","description":"Body of PUT /lotus/sales/convert. Unknown keys are stripped.","required":["order_id","status"],"properties":{"order_id":{"type":"string","format":"uuid","example":"6d5c4b3a-2f1e-4d0c-9b8a-7f6e5d4c3b2a"},"status":{"type":"string","enum":["OPEN_SALES_ORDER","CLOSED_SALES_ORDER"],"description":"Case-insensitive.","example":"OPEN_SALES_ORDER"},"provision_subwork_now":{"type":"boolean","default":false,"description":"OPEN_SALES_ORDER only - create the sale checklists, inspections and work orders now, unless that type is set to \"never\" (work orders also need a default task level)."},"acknowledge_credit_warning":{"type":"boolean","description":"Resend with true after a credit-limit 409 to go ahead."}}},"sale_convert_result":{"type":"object","properties":{"message":{"type":"string","enum":["Sales quote converted to open sales order","Sales quote converted to open sales order. An invoice was created — please review and approve it","Sales quote converted to open sales order. An invoice was created — auto-payment is disabled on this customer, please collect payment manually","Sales quote converted to open sales order. An invoice was created but the customer has no default payment method, so it could not be charged automatically — please set one and collect payment manually","Sales quote converted to open sales order. The invoice is already covered by an in-flight payment — no new charge issued","Sales quote converted to open sales order and an invoice was created, but the automatic charge failed — please collect payment manually","Sales quote converted to open sales order and a charge on the sales order invoice has been created","Sales quote converted to open sales order and the sales order invoice has been updated","Open sales order closed"]},"data":{"type":"object","description":"The request echoed back.","properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["OPEN_SALES_ORDER","CLOSED_SALES_ORDER"]}}},"metadata":{"type":"object","nullable":true,"description":"Always null."}}},"sale_credit_check_result":{"allOf":[{"type":"object","properties":{"order_id":{"type":"string","format":"uuid"},"customer_id":{"type":"string","format":"uuid","nullable":true}}},{"$ref":"#/components/schemas/lotus_credit_decision"}]},"sale_charges_summary_body":{"type":"object","description":"Body of POST /lotus/sales/charges_summary - the order being built. Prices are taken as sent. Unknown keys are stripped.\n","required":["items"],"properties":{"customer_id":{"type":"string","format":"uuid","nullable":true,"description":"Adds credit_check; its address can set the tax rates.","example":"3f2a1b4c-5d6e-4f70-8a9b-0c1d2e3f4a5b"},"items":{"type":"array","minItems":1,"items":{"type":"object","required":["item_type","unit_price"],"properties":{"item_type":{"type":"string","enum":["ASSET","CUSTOM","WARRANTY"]},"item_id":{"type":"string","format":"uuid","nullable":true},"name":{"type":"string","nullable":true,"description":"Required for CUSTOM and WARRANTY."},"unit_price":{"type":"number","description":"At least 0 for ASSET; any amount otherwise."},"quantity":{"type":"number","default":1},"discount":{"type":"number","minimum":0,"maximum":100,"default":0},"is_taxable":{"type":"boolean","default":true},"parent_item_id":{"type":"string","format":"uuid","nullable":true}}},"example":[{"item_type":"ASSET","item_id":"9b8a7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d","name":"JCB 3CX Backhoe","unit_price":45000},{"item_type":"CUSTOM","name":"Pre-delivery service","unit_price":250}]},"state_sales_tax_status":{"type":"boolean"},"county_sales_tax_status":{"type":"boolean"},"city_sales_tax_status":{"type":"boolean"},"special_sales_tax_status":{"type":"boolean"},"discount":{"type":"number","minimum":0,"maximum":100,"default":0},"customer_discount":{"type":"number","minimum":0,"maximum":100,"default":0},"flat_discount_value":{"type":"number","minimum":0,"default":0},"delivery_freight_company":{"type":"string","format":"uuid","nullable":true},"return_freight_company":{"type":"string","format":"uuid","nullable":true},"additional_freight_company":{"type":"string","format":"uuid","nullable":true},"quoted_delivery_fee":{"type":"number","nullable":true,"minimum":0,"default":0},"quoted_return_fee":{"type":"number","nullable":true,"minimum":0,"default":0},"quoted_additional_freight_fee":{"type":"number","nullable":true,"minimum":0,"default":0},"delivery_fee":{"type":"number","nullable":true,"minimum":0,"default":0},"return_fee":{"type":"number","nullable":true,"minimum":0,"default":0},"additional_freight_fee":{"type":"number","nullable":true,"minimum":0,"default":0}}},"sale_charges_summary_result":{"allOf":[{"$ref":"#/components/schemas/lotus_order_financials"},{"type":"object","properties":{"line_items":{"type":"array","description":"The items as priced.","items":{"type":"object","properties":{"name":{"type":"string","nullable":true},"item_type":{"type":"string","enum":["ASSET","CUSTOM","WARRANTY"]},"item_id":{"type":"string","format":"uuid","nullable":true},"quantity":{"type":"number"},"unit_price":{"type":"number"},"is_taxable":{"type":"boolean"},"percentage_discount":{"type":"number","description":"A priced attachment's 100% free discount is reset to 0."},"parent_id":{"type":"string","format":"uuid","nullable":true,"description":"The parent line's asset id (its item_id), as sent - not a line item id."},"sub_total":{"type":"number"},"discount_value":{"type":"number"},"total":{"type":"number"},"original_unit_price":{"type":"number","nullable":true,"description":"ASSET lines only; always null here."},"is_discounted_from_sale_price":{"type":"boolean","description":"ASSET lines only; always false here."}}}},"sales_tax_source":{"type":"string","enum":["Customer","Organization","Organization Defaults"],"description":"Where the tax rates came from."},"sales_tax_source_zip":{"type":"string","nullable":true},"credit_check":{"$ref":"#/components/schemas/lotus_credit_decision"}}}]},"sale_analysis_body":{"type":"object","description":"Body of POST /lotus/sales/sales_analysis/{id}; optional. Unknown keys are stripped.","properties":{"soldDate":{"type":"string","format":"date-time","nullable":true,"description":"The projected sale date; default now."}}},"sale_analysis_row":{"type":"object","description":"One figure on an analysis card.","properties":{"key":{"type":"string"},"label":{"type":"string"},"value":{"nullable":true,"description":"A number, a date string, or null when missing."},"format":{"type":"string","enum":["currency","date","number","percent","months"]},"status":{"type":"string","enum":["sourced","computed","input","missing"],"description":"sourced - read from the data; computed - derived; input - from soldDate; missing - no data source yet (value null)."}}},"sale_analysis_result":{"type":"object","properties":{"meta":{"type":"object","properties":{"order_id":{"type":"string","format":"uuid"},"order_number":{"type":"string","nullable":true},"quote_status":{"type":"string","nullable":true,"enum":["PENDING","APPROVED","REJECTED",null]},"sale_date":{"type":"string","format":"date-time","nullable":true},"customer":{"type":"object","nullable":true,"description":"Its id and name are null for a sale without a customer.","properties":{"id":{"type":"string","format":"uuid","nullable":true},"name":{"type":"string","nullable":true}}},"currency":{"type":"string","example":"USD"}}},"deal":{"type":"object","properties":{"revenue":{"type":"number","description":"items_revenue less the order discounts."},"items_revenue":{"type":"number"},"discounts":{"type":"object","properties":{"customer_discount":{"type":"object","properties":{"rate":{"type":"number","nullable":true},"amount":{"type":"number"}}},"order_percent_discount":{"type":"object","properties":{"rate":{"type":"number","nullable":true},"amount":{"type":"number"}}},"order_flat_discount":{"type":"object","properties":{"amount":{"type":"number"}}},"total":{"type":"number"}}},"cost":{"type":"number"},"fee_service_cost":{"type":"number","description":"Custom fees and freight."},"gross_profit":{"type":"number"},"margin":{"type":"number","nullable":true,"description":"Percent of revenue; null when revenue is 0."},"target_gross_margin":{"type":"number","nullable":true,"description":"Always null for now."},"net_cash_at_close":{"type":"number","nullable":true,"description":"Always null for now."},"total_loan_payoff":{"type":"number","nullable":true,"description":"Always null for now."},"line_item_count":{"type":"integer"},"cost_incomplete":{"type":"boolean","description":"True when some item's cost is unknown."}}},"revenue_split":{"type":"array","description":"Where the revenue goes - initial_investment, interest_paid, fee_service_costs, repairs, commission, gross_profit.","items":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"amount":{"type":"number","nullable":true},"pct":{"type":"number","nullable":true},"status":{"type":"string","enum":["computed","missing"]}}}},"profit_distribution":{"type":"array","description":"The items with a known gross profit, largest first.","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"kind":{"type":"string","enum":["ASSET","ATTACHMENT","FEE","FREIGHT"]},"gross_profit":{"type":"number"},"profit_share":{"type":"number","nullable":true}}}},"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"ref":{"type":"object","description":"type LINE_ITEM with line_item_id, item_id, asset_id and asset_line_item_id (the asset an attachment or warranty belongs to), or type FULFILLMENT_TICKET with fulfillment_ticket_id.\n","additionalProperties":true},"kind":{"type":"string","enum":["ASSET","ATTACHMENT","FEE","FREIGHT"]},"name":{"type":"string"},"subtitle":{"type":"string","nullable":true},"asset_number":{"type":"string","nullable":true},"has_telematics":{"type":"boolean","description":"Whether the asset has a telematics device (a current Samsara row). ASSET rows only, when the org has the SAMSARA_GPS module; absent otherwise (DOZ-8147).\n"},"badges":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string"},"label":{"type":"string"},"tone":{"type":"string"}}}},"revenue":{"type":"number"},"cost":{"type":"number","nullable":true},"gross_profit":{"type":"number","nullable":true},"margin":{"type":"number","nullable":true},"profit_share":{"type":"number","nullable":true},"cards":{"type":"array","description":"Assets have sale_inventory, cost_breakdown, financing and purchase_depreciation; the others one economics card.","items":{"type":"object","properties":{"key":{"type":"string"},"title":{"type":"string"},"rows":{"type":"array","items":{"$ref":"#/components/schemas/sale_analysis_row"}}}}},"missing":{"type":"array","description":"The \"card.row\" keys whose value is missing.","items":{"type":"string"}}}}},"flags":{"type":"array","items":{"type":"object","properties":{"level":{"type":"string","enum":["ok","warn","info"]},"code":{"type":"string","enum":["all_items_above_cost","items_below_cost","above_book_value"]},"message":{"type":"string"},"items":{"type":"array","items":{"type":"string"}}}}}}},"sale_email_body":{"type":"object","description":"Body of POST /lotus/sales/email. Unknown keys are stripped.","required":["order_id","receiver_type"],"properties":{"order_id":{"type":"string","format":"uuid","example":"6d5c4b3a-2f1e-4d0c-9b8a-7f6e5d4c3b2a"},"receiver_type":{"type":"string","enum":["customer","self","custom"],"example":"self"},"contact_id":{"type":"string","format":"uuid","description":"With customer - send to this contact only."},"custom_email":{"type":"array","minItems":1,"description":"Required with custom (singular name, but a list of addresses).","items":{"type":"string","format":"email"},"example":["buyer@example.com"]}}},"sale_delete_body":{"type":"object","description":"Body of DELETE /lotus/sales/{id}; optional. Unknown keys are stripped.","properties":{"reason":{"type":"string","nullable":true,"description":"Saved in deletion_information.","example":"Customer cancelled"}}},"sale_signnow_body":{"type":"object","description":"Body of POST /lotus/sales/signnow/invite. Unknown keys are stripped.","required":["order_id","signer_email"],"properties":{"order_id":{"type":"string","format":"uuid","example":"6d5c4b3a-2f1e-4d0c-9b8a-7f6e5d4c3b2a"},"signer_email":{"type":"string","format":"email","example":"signer@example.com"}}},"sale_warranty_invite_body":{"type":"object","description":"Body of POST /lotus/sales/warranty/signnow/invite. Unknown keys are stripped.","required":["order_id","signer_email"],"properties":{"order_id":{"type":"string","format":"uuid","example":"6d5c4b3a-2f1e-4d0c-9b8a-7f6e5d4c3b2a"},"signer_email":{"type":"string","format":"email","example":"signer@example.com"},"line_item_ids":{"type":"array","minItems":1,"description":"Only these assets' line items; ids not on the order are ignored. Left out - every asset with a warranty decision.","items":{"type":"string","format":"uuid"}}}},"sale_warranty_invite_result":{"type":"object","properties":{"order_id":{"type":"string","format":"uuid"},"sent":{"type":"integer","description":"How many forms went out (results with status SENT)."},"results":{"type":"array","items":{"type":"object","properties":{"line_item_id":{"type":"string","format":"uuid"},"asset":{"type":"string","description":"The asset number, else its name, else the line item id."},"status":{"type":"string","enum":["SENT","ALREADY_SENT","SKIPPED_NO_OPTIONS","FAILED"]},"signnow_document_id":{"type":"string","description":"SENT only."},"message":{"type":"string","description":"SKIPPED_NO_OPTIONS and FAILED only - why."}}}}}},"sale_signed_selection_result":{"type":"object","properties":{"status":{"type":"string","enum":["READ","NOT_APPLICABLE","NO_CHOICE_ON_FORM","NOT_SENT","NOT_SIGNED","UNREADABLE"]},"message":{"type":"string","description":"Sent with NOT_APPLICABLE, NO_CHOICE_ON_FORM and UNREADABLE."},"line_item_id":{"type":"string","format":"uuid"},"order_id":{"type":"string","format":"uuid"},"asset_id":{"type":"string","format":"uuid","nullable":true},"asset":{"type":"string","description":"The asset number, else its name, else the line item id."},"signnow_document_id":{"type":"string","nullable":true},"warranty_signed_status":{"type":"string","nullable":true},"document_value":{"type":"string","nullable":true,"description":"The raw value on the form (OPTION_1..3 or REFUSED); READ, and UNREADABLE when it matches no option.","example":"OPTION_1"},"outcome":{"type":"string","nullable":true,"enum":["OPTION_SELECTED","REFUSED",null],"description":"READ only - pass to PUT /lotus/sales/update as warranty_recorded_outcome."},"selected_option_id":{"type":"string","format":"uuid","nullable":true,"description":"READ with OPTION_SELECTED - pass as warranty_selected_option_id."},"selected_option":{"nullable":true,"description":"READ with OPTION_SELECTED - the chosen option row; null otherwise.","allOf":[{"$ref":"#/components/schemas/sale_warranty_option"}]},"recorded_outcome":{"type":"string","nullable":true,"enum":["OPTION_SELECTED","REFUSED",null],"description":"What is already recorded on the line."},"recorded_option_id":{"type":"string","format":"uuid","nullable":true},"matches_recorded":{"type":"boolean","description":"True when the form's choice is already the recorded one."}}},"generic_search_result":{"type":"object","description":"Keys come in this order; clients list the buckets in key order","properties":{"assets":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/search_asset_row"}},"length":{"type":"integer"}}},"parts":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/search_part_row"}},"length":{"type":"integer"}}},"purchase_orders":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/search_purchase_order_row"}},"length":{"type":"integer"}}},"customers":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/search_customer_row"}},"length":{"type":"integer"}}},"invoices":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/search_invoice_row"}},"length":{"type":"integer"}}},"orders":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/search_order_row"}},"length":{"type":"integer"}}},"tasks":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/search_task_row"}},"length":{"type":"integer"}}},"sites":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/search_site_row"}},"length":{"type":"integer"}}},"reports":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/search_report_row"}},"length":{"type":"integer"}}},"settings":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/search_setting_row"}},"length":{"type":"integer"}}}}},"search_asset_row":{"type":"object","description":"Matches name, asset number, make, model, serial number or description (all words in one of them)\n","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","nullable":true},"asset_number":{"type":"string"},"validated":{"type":"boolean"}}},"search_part_row":{"type":"object","description":"Every word must appear somewhere in the part's item number, part number, name, description, make, model or serial number; the words may sit in different fields, and `%` and `_` are literal. The 10 rows come in relevance order, highest item number within a tier: the item number, part number or name equals the whole text; then starts with it; then every word is found within those three fields; then the matches that need the description, make, model or serial number.\n","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","nullable":true},"item_number":{"type":"integer","nullable":true},"part_number":{"type":"string","nullable":true},"validated":{"type":"boolean","nullable":true}}},"search_purchase_order_row":{"type":"object","description":"A purchase order that is not voided and matches in one of two ways: it is not closed and has a line for a part matching the text, by the parts bucket's rule (CUSTOM lines never match); or its PO number, vendor name or vendor number holds every word of the text, closed or not. Ranked: the PO whose number is exactly the text, then POs found by a part, then the other open POs, then closed ones; newest first within each.\n","properties":{"id":{"type":"string","format":"uuid"},"po_number":{"type":"string","nullable":true},"state":{"type":"string","enum":["new","released","partially_received","received","closed"]},"created_at":{"type":"string","format":"date-time","nullable":true},"vendor_name":{"type":"string","nullable":true},"parts":{"type":"array","description":"The PO's lines whose part matched, by item number. Empty when the PO matched only by its PO number, vendor name or vendor number.\n","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"The part's id"},"item_number":{"type":"integer","nullable":true},"name":{"type":"string","nullable":true},"part_number":{"type":"string","nullable":true},"quantity_ordered":{"type":"integer","nullable":true}}}}}},"search_customer_row":{"type":"object","description":"Matches customer number (as digits, substring; a single all-digit query puts the customer with exactly that number first), company name, street, city, key or insurance policy number, or a contact's phone (digits only) or email\n","properties":{"id":{"type":"string","format":"uuid"},"company_name":{"type":"string"},"customer_number":{"type":"integer"},"validated":{"type":"boolean"},"banned":{"type":"boolean"}}},"search_site_row":{"type":"object","description":"Matches the site name","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"customer":{"type":"string","format":"uuid","nullable":true}}},"search_order_row":{"type":"object","description":"A rental or sale order, matched on its number; imported orders are hidden","properties":{"id":{"type":"string","format":"uuid"},"order_number":{"type":"string"},"type":{"type":"string","enum":["rental","sale"]}}},"search_invoice_row":{"type":"object","description":"Matched on invoice number or total amount","properties":{"id":{"type":"string","format":"uuid"},"invoice_number":{"type":"string"}}},"search_task_row":{"type":"object","description":"A work order or an inspection, matched on the asset's name or number, the order number, the customer, the assignee or (work orders only) the task level. Imported work orders and hidden inspections are left out.\n","properties":{"id":{"type":"string","format":"uuid"},"type":{"type":"string","description":"WORK_ORDER for a work order, the inspection type for an inspection","enum":["WORK_ORDER","OFF_RENT","INDEPENDENT","DISPATCH","SALE"]},"order_number":{"type":"string","nullable":true},"asset_details":{"type":"object","properties":{"asset_number":{"type":"string","nullable":true},"name":{"type":"string","nullable":true}}},"record_type":{"type":"string","enum":["work_order","inspection"]}}},"search_report_row":{"type":"object","description":"An enabled, working, non-single-item report from the registry, matched on name or URL. The whole registry row.\n","properties":{"id":{"type":"string","format":"uuid"},"report_name":{"type":"string","nullable":true},"report_url":{"type":"string","nullable":true},"report_group":{"type":"string","nullable":true},"sec_group":{"type":"number","nullable":true},"enabled":{"type":"boolean","nullable":true},"created_at":{"type":"string","format":"date-time","nullable":true},"org":{"type":"string","format":"uuid","nullable":true},"date_range_searchable":{"type":"boolean","nullable":true},"single_item_report":{"type":"boolean","nullable":true},"available_online":{"type":"boolean","nullable":true},"available_offline":{"type":"boolean","nullable":true},"supported_formats":{"nullable":true,"description":"The formats the report offers (JSON)"},"permissions":{"type":"array","nullable":true,"items":{"type":"string"}}}},"search_setting_row":{"description":"A settings-page group or setting the caller's settings page would show, as that page returns it, plus `type` and `breadcrumb`. Matches on the label, key, description, option labels or breadcrumb; a label hit ranks first.\n","anyOf":[{"allOf":[{"$ref":"#/components/schemas/platform_settings_group"},{"type":"object","properties":{"type":{"type":"string","enum":["GROUP"]},"breadcrumb":{"type":"array","description":"The group labels from the top tab down to this node","items":{"type":"string"}}}}]},{"allOf":[{"$ref":"#/components/schemas/platform_setting"},{"type":"object","properties":{"type":{"type":"string","enum":["SETTING"]},"breadcrumb":{"type":"array","description":"The group labels from the top tab down to the setting's group","items":{"type":"string"}}}}]}]},"inspection_create_body":{"type":"object","description":"Unknown keys are stripped.","required":["assets"],"properties":{"assets":{"type":"array","minItems":1,"description":"Asset ids. One inspection (or rental pair) per asset.","items":{"type":"string","format":"uuid"},"example":["3f1a2b4c-5d6e-4f70-8a9b-0c1d2e3f4a5b"]},"independent_inspection":{"type":"boolean","default":false,"description":"true for one INDEPENDENT inspection per asset; false for a DISPATCH inspection with a hidden OFF_RENT pair."},"due_date":{"type":"string","format":"date-time","nullable":true,"description":"Due date of the DISPATCH or INDEPENDENT inspection; the OFF_RENT pair gets its own when it is revealed."},"customer":{"type":"string","format":"uuid","nullable":true,"description":"Customer to attach. Refused (400) when the org's work-order settings have customer assignment switched off."},"notes":{"type":"string","nullable":true,"description":"External notes, shown on the inspection as `notes`."},"internal_notes":{"type":"string","nullable":true,"description":"Internal notes, visible to ACCESS_ALL_INSPECTIONS holders."},"is_recurring":{"type":"boolean","default":false,"description":"Marks the inspection as created by the recurring-inspection schedule."}}},"inspection_create_result":{"type":"object","properties":{"message":{"type":"string","example":"Inspections created against the assets"},"id":{"type":"array","description":"One id per asset - the DISPATCH or INDEPENDENT inspection. The hidden OFF_RENT pair's id is not returned.","items":{"type":"string","format":"uuid"}}}},"inspection_quick_pass_body":{"type":"object","description":"Unknown keys are stripped.","required":["inspection_ids","quick_pass_note"],"properties":{"inspection_ids":{"type":"array","minItems":1,"items":{"type":"string","format":"uuid"},"example":["9e8d7c6b-5a49-4f38-8e2d-1c0b9a8f7e6d"]},"quick_pass_note":{"type":"string","description":"Stored as the internal notes of every inspection completed.","example":"Quick passed at the yard, unit looked fine"}}},"inspection_email_body":{"type":"object","description":"Unknown keys are stripped.","required":["inspection_id","receiver_type"],"properties":{"inspection_id":{"type":"string","format":"uuid","example":"9e8d7c6b-5a49-4f38-8e2d-1c0b9a8f7e6d"},"receiver_type":{"type":"string","enum":["self","custom"],"description":"self emails the caller's own address; custom emails custom_email.","example":"custom"},"custom_email":{"type":"array","minItems":1,"description":"Required for custom. Each entry must be an email address.","items":{"type":"string","format":"email"},"example":["ops@example.com"]}}},"inspection_delete_body":{"type":"object","description":"Unknown keys are stripped.","required":["reason"],"properties":{"reason":{"type":"string","description":"Kept in the inspection's deletion information.","example":"Created on the wrong asset"}}},"inspection_delete_result":{"type":"object","properties":{"message":{"type":"string","example":"Inspection(s) deleted successfully"},"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"The inspection deleted"},"deleted_ids":{"type":"array","description":"That inspection plus its paired dispatch or off-rent inspection, when it has one","items":{"type":"string","format":"uuid"}}}}}},"inspection_item_answer":{"type":"object","required":["id"],"properties":{"id":{"type":"string","format":"uuid","description":"The item's id from template.items","example":"fda4002e-8156-48b8-8e14-8bf8d5eb1d3e"},"value":{"$ref":"#/components/schemas/inspection_item_value"}}},"inspection_item_value":{"description":"An item's answer, by item_type. TEXT and DROPDOWN - a non-empty string (DROPDOWN one of the item's elements). NUMBER - a number. PASS_FAIL and YES_NO - a boolean (only a PASS_FAIL false fails the inspection). YES_NO_AND_GAUGE - {active, reading}, the reading required when active. CHECK_AND_GAUGE - {active, text}, text the amount added (a number, '' or null); not checked, or an amount above 0, fails. DAMAGE_AND_PHOTO - {damage, description, images, lazy_upload}, damage \"repair\" or \"replace\" fails. GAUGE_AND_PHOTO - {reading, images, lazy_upload}, a reading above 0 fails. images holds the storage paths uploaded through POST /media/inspection/inspection_item; on this route only lazy_upload, description and damage (or lazy_upload and reading) are taken from the body - the photos stay as uploaded.\n","oneOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"object","description":"YES_NO_AND_GAUGE","properties":{"active":{"type":"boolean"},"reading":{"type":"number","nullable":true}}},{"type":"object","description":"CHECK_AND_GAUGE","properties":{"active":{"type":"boolean"},"text":{"type":"string","nullable":true}}},{"type":"object","description":"DAMAGE_AND_PHOTO","properties":{"images":{"type":"array","items":{"type":"string"}},"lazy_upload":{"type":"boolean"},"description":{"type":"string","nullable":true},"damage":{"type":"string","nullable":true,"description":"no, repair or replace"}}},{"type":"object","description":"GAUGE_AND_PHOTO","properties":{"images":{"type":"array","items":{"type":"string"}},"lazy_upload":{"type":"boolean"},"reading":{"type":"number","nullable":true}}}]},"inspection_hour_meter":{"type":"object","nullable":true,"properties":{"reading":{"type":"number","nullable":true,"description":"Hours, no coercion from a string; rounded to one decimal when saved. Cannot be below the asset's current reading.","example":1234.5},"verified":{"type":"boolean","description":"Must be true to complete the inspection.","example":true}}},"inspection_update_body":{"type":"object","description":"Unknown keys are refused (400). Every key but id is optional; an omitted key leaves the stored value as it is, except notes, which is written on every save (omitted = cleared).","required":["id"],"properties":{"id":{"type":"string","format":"uuid","example":"9e8d7c6b-5a49-4f38-8e2d-1c0b9a8f7e6d"},"assignee":{"type":"string","format":"uuid","nullable":true,"description":"A mechanic (a user who may be assigned). A change sends them a push notification."},"done":{"type":"boolean","description":"true completes the inspection (see the route description for what that needs).","example":false},"all_assignees_access":{"type":"boolean","description":"Open to every assignee. Refused when the org assigns inspections to one assignee."},"priority":{"type":"integer","enum":[0,1]},"due_date":{"type":"string","format":"date-time","nullable":true},"status":{"type":"string","enum":["PENDING","READY_FOR_INSPECTION"],"description":"The statuses a client may set directly; DONE comes from done and PARTIALLY_INSPECTED from answering items."},"hour_meter":{"$ref":"#/components/schemas/inspection_hour_meter"},"items":{"type":"array","description":"Answers. Only while the inspection is READY_FOR_INSPECTION or PARTIALLY_INSPECTED.","items":{"$ref":"#/components/schemas/inspection_item_answer"}},"set_for_siblings":{"type":"array","description":"Which of the fields sent to copy to the same-type, not-DONE inspections on the rental's other dispatches.","items":{"type":"string","enum":["status","assignee","due_date","priority"]}},"asset_out_of_service":{"type":"boolean","nullable":true,"description":"Applied only when the inspection completes as failed."},"notes":{"type":"string","nullable":true,"description":"External notes. Written on every save - omit it and the stored notes are cleared."},"internal_notes":{"type":"string","nullable":true},"customer":{"type":"string","format":"uuid","nullable":true,"description":"Stand-alone inspections only, and only when the org allows customer assignment."}}},"inspection_template_item":{"type":"object","description":"A form item on the inspection, as stored from the category's form templates.","properties":{"id":{"type":"string","format":"uuid"},"description":{"type":"string","description":"The item's text"},"item_type":{"type":"string","enum":["TEXT","NUMBER","DROPDOWN","PASS_FAIL","YES_NO","YES_NO_AND_GAUGE","CHECK_AND_GAUGE","DAMAGE_AND_PHOTO","GAUGE_AND_PHOTO"]},"required":{"type":"boolean"},"weight":{"type":"integer","description":"Display order"},"elements":{"type":"array","nullable":true,"description":"DROPDOWN choices","items":{"type":"string"}},"value":{"$ref":"#/components/schemas/inspection_item_value"},"passed":{"type":"boolean","nullable":true,"description":"Set when the item is answered; false for a failing answer."},"template_details":{"type":"array","description":"The form templates the item came from","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"description":{"type":"string"}}}}}},"inspection_template":{"type":"object","properties":{"item_count":{"type":"integer"},"items":{"type":"array","items":{"$ref":"#/components/schemas/inspection_template_item"}}}},"inspection_record":{"type":"object","description":"A lotus_inspections row as the update returns it (every column).","properties":{"id":{"type":"string","format":"uuid","readOnly":true},"org":{"type":"string","format":"uuid","readOnly":true},"line_item_id":{"type":"string","format":"uuid","nullable":true,"readOnly":true,"description":"The rental line item, for a dispatch-linked inspection"},"asset_id":{"type":"string","format":"uuid","nullable":true,"readOnly":true},"customer_id":{"type":"string","format":"uuid","nullable":true},"dispatch_id":{"type":"string","format":"uuid","nullable":true,"readOnly":true,"description":"null for a stand-alone inspection"},"dispatch_inspection_id":{"type":"string","format":"uuid","nullable":true,"readOnly":true,"description":"On an OFF_RENT inspection, its paired DISPATCH inspection"},"off_rent_inspection_id":{"type":"string","format":"uuid","nullable":true,"readOnly":true,"description":"On a DISPATCH inspection, its paired OFF_RENT inspection"},"type":{"type":"string","enum":["DISPATCH","OFF_RENT","INDEPENDENT","SALE"],"readOnly":true},"status":{"type":"string","enum":["PENDING","READY_FOR_INSPECTION","PARTIALLY_INSPECTED","DONE","HIDDEN"],"description":"HIDDEN rows are never returned."},"assignee":{"type":"string","format":"uuid","nullable":true},"all_assignees_access":{"type":"boolean","nullable":true},"template":{"$ref":"#/components/schemas/inspection_template"},"passed":{"type":"boolean","nullable":true,"description":"Recomputed on every save - null while a required item is unanswered, false when an answer fails, else true"},"quick_pass":{"type":"boolean","nullable":true,"readOnly":true},"hour_meter":{"$ref":"#/components/schemas/inspection_hour_meter"},"hour_meter_image":{"type":"object","nullable":true,"readOnly":true,"description":"Set by POST /media/inspection/hour_meter_image","additionalProperties":true},"asset_out_of_service":{"type":"boolean","nullable":true},"priority":{"type":"integer","nullable":true},"due_date":{"type":"string","format":"date-time","nullable":true},"completed_at":{"type":"string","format":"date-time","nullable":true,"readOnly":true},"external_notes":{"type":"string","nullable":true},"internal_notes":{"type":"string","nullable":true},"is_recurring":{"type":"boolean","nullable":true},"deleted":{"type":"boolean","readOnly":true},"deletion_information":{"type":"object","nullable":true,"readOnly":true,"additionalProperties":true},"created_at":{"type":"string","format":"date-time","readOnly":true},"updated_at":{"type":"string","format":"date-time","readOnly":true},"created_by":{"type":"string","format":"uuid","nullable":true,"readOnly":true},"updated_by":{"type":"string","format":"uuid","nullable":true,"readOnly":true}}},"inspection_update_result":{"allOf":[{"$ref":"#/components/schemas/inspection_record"},{"type":"object","properties":{"reasonable_hours":{"type":"number","nullable":true,"readOnly":true,"description":"The hours the org considers reasonable for the rental period; set when a reading was sent"},"actual_hours_used":{"type":"number","nullable":true,"readOnly":true,"description":"This reading minus the dispatch reading (off-rent inspections)"}}}]},"inspection_detail":{"type":"object","description":"The GET /lotus/inspections/{id} response.","properties":{"id":{"type":"string","format":"uuid"},"dispatch_id":{"type":"string","format":"uuid","nullable":true},"line_item_id":{"type":"string","format":"uuid","nullable":true},"asset_id":{"type":"string","format":"uuid","nullable":true},"assignee":{"type":"string","format":"uuid","nullable":true},"type":{"type":"string","enum":["DISPATCH","OFF_RENT","INDEPENDENT","SALE"]},"created_at":{"type":"string","description":"Formatted in the org's timezone","example":"Tue, Oct 14, 2026 1:05 PM"},"updated_at":{"type":"string","format":"date-time"},"status":{"type":"string","enum":["PENDING","READY_FOR_INSPECTION","PARTIALLY_INSPECTED","DONE"]},"completed_at":{"type":"string","format":"date-time","nullable":true},"off_rent_inspection_id":{"type":"string","format":"uuid","nullable":true},"dispatch_inspection_id":{"type":"string","format":"uuid","nullable":true},"due_date":{"type":"string","format":"date-time","nullable":true},"org":{"type":"string","format":"uuid"},"hour_meter":{"$ref":"#/components/schemas/inspection_hour_meter"},"all_assignees_access":{"type":"boolean","nullable":true},"priority":{"type":"integer","nullable":true},"passed":{"type":"boolean","nullable":true},"hour_meter_image":{"type":"object","nullable":true,"additionalProperties":true},"asset_out_of_service":{"type":"boolean","nullable":true},"notes":{"type":"string","nullable":true,"description":"The external notes"},"deleted":{"type":"boolean"},"quick_pass":{"type":"boolean","nullable":true},"is_recurring":{"type":"boolean","nullable":true},"asset_name":{"type":"string","nullable":true},"asset_number":{"type":"string","nullable":true},"serial_number":{"type":"string","nullable":true},"manufacturer":{"type":"string","nullable":true},"model_number":{"type":"string","nullable":true},"model_year":{"type":"string","nullable":true},"dispatched_date":{"type":"string","format":"date-time","nullable":true},"off_rent_date":{"type":"string","format":"date-time","nullable":true},"dispatch_inspection_date":{"type":"string","format":"date-time","nullable":true},"asset_out_date":{"type":"string","nullable":true,"description":"The dispatched date, formatted in the org's timezone"},"asset_in_date":{"type":"string","nullable":true,"description":"The off-rent inspection date, formatted in the org's timezone"},"asset_hour_meter":{"type":"number","nullable":true,"description":"The asset's current reading, one decimal"},"has_hour_meter":{"type":"boolean","nullable":true},"category":{"type":"string","format":"uuid","nullable":true},"category_name":{"type":"string","nullable":true},"order_id":{"type":"string","format":"uuid","nullable":true,"description":"The rental or sale the dispatch belongs to"},"order_number":{"type":"string","nullable":true},"order_status":{"type":"string","nullable":true},"order_type":{"type":"string","nullable":true},"customer_name":{"type":"string","nullable":true},"customer_id":{"type":"string","format":"uuid","nullable":true},"customer_number":{"type":"string","nullable":true},"internal_notes":{"type":"string","nullable":true,"description":"null unless the caller holds ACCESS_ALL_INSPECTIONS"},"dispatch_inspection_details":{"type":"object","nullable":true,"description":"On an OFF_RENT inspection, a summary of the paired dispatch inspection","properties":{"id":{"type":"string","format":"uuid"},"assignee":{"type":"string","format":"uuid","nullable":true},"completed_at":{"type":"string","format":"date-time","nullable":true},"passed":{"type":"boolean","nullable":true},"hour_meter":{"$ref":"#/components/schemas/inspection_hour_meter"},"hour_meter_image":{"type":"object","nullable":true,"additionalProperties":true},"template":{"$ref":"#/components/schemas/inspection_template"}}},"template":{"$ref":"#/components/schemas/inspection_template"},"order_dispatch_count":{"type":"integer","description":"Dispatches on the parent order (0 for a stand-alone inspection)"},"assignee_details":{"type":"object","nullable":true,"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"username":{"type":"string"}}},"analysis":{"type":"object","nullable":true,"additionalProperties":true,"description":"Present only for an org with the Inspection Analysis add-on - the latest analysis run's result for this inspection, or null."},"reasonable_hours":{"type":"number","nullable":true,"description":"The hours the org considers reasonable for the rental period (off-rent inspections)"},"actual_hours_used":{"type":"number","nullable":true},"orgDetails":{"type":"object","properties":{"name":{"type":"string"},"timezone":{"type":"string","example":"America/Chicago"},"flag_unreasonable_hours":{"type":"boolean","nullable":true}}}}},"inspection_anonymous_data":{"type":"object","properties":{"inspectionRecord":{"$ref":"#/components/schemas/inspection_detail"},"linkExpiresAt":{"type":"string","format":"date-time","description":"When the link stops working"}}},"work_order_row":{"type":"object","description":"The stored work order (a lotus_orders row).","properties":{"id":{"type":"string","format":"uuid","readOnly":true},"org":{"type":"string","format":"uuid","readOnly":true},"order_number":{"type":"string","readOnly":true,"example":"WO-20261001-001"},"type":{"type":"string","enum":["WORK_ORDER"],"readOnly":true},"status":{"type":"string","enum":["PENDING","READY","IN_PROGRESS","PAUSED","WORK_COMPLETED","DONE"],"readOnly":true},"customer_id":{"type":"string","format":"uuid","nullable":true,"readOnly":true,"description":"Null on an internal work order; the parent rental's customer when there is a parent."},"contact_id":{"type":"string","format":"uuid","nullable":true,"readOnly":true},"site_id":{"type":"string","format":"uuid","nullable":true,"readOnly":true},"parent_id":{"type":"string","format":"uuid","nullable":true,"readOnly":true,"description":"The parent rental contract, for a repair on a rented asset."},"assignee_id":{"type":"string","format":"uuid","nullable":true,"readOnly":true,"description":"The mechanic."},"priority":{"type":"integer","readOnly":true,"description":"0 normal, 1 high."},"customer_po":{"type":"string","nullable":true,"readOnly":true},"external_notes":{"type":"string","nullable":true,"readOnly":true,"description":"The notes sent as notes (an off-site job's site details are appended)."},"internal_notes":{"type":"string","nullable":true,"readOnly":true},"metadata":{"type":"object","nullable":true,"readOnly":true,"description":"Unused on work orders - an empty object or null."},"deleted":{"type":"boolean","readOnly":true},"deletion_information":{"type":"object","nullable":true,"readOnly":true,"description":"Who deleted the work order, when and why; null on a live one.","properties":{"date":{"type":"string","format":"date-time"},"reason":{"type":"string","nullable":true},"user_id":{"type":"string","format":"uuid","nullable":true},"username":{"type":"string","nullable":true}}},"imported":{"type":"boolean","readOnly":true,"description":"True for history imported from the previous system."},"created_at":{"type":"string","format":"date-time","readOnly":true},"updated_at":{"type":"string","format":"date-time","readOnly":true},"status_updated_at":{"type":"string","format":"date-time","nullable":true,"readOnly":true},"created_by":{"type":"string","format":"uuid","nullable":true,"readOnly":true},"updated_by":{"type":"string","format":"uuid","nullable":true,"readOnly":true},"status_updated_by":{"type":"string","format":"uuid","nullable":true,"readOnly":true}}},"work_order_task_details":{"type":"object","nullable":true,"description":"The work order's lotus_task_details row (null on an old work order without one).","properties":{"order_id":{"type":"string","format":"uuid"},"org":{"type":"string","format":"uuid"},"asset_id":{"type":"string","format":"uuid"},"task_level":{"type":"string","format":"uuid","nullable":true},"task_type":{"type":"string","nullable":true,"enum":["SERVICE","ONBOARDING","MAINTENANCE","SALE",null]},"inspection_id":{"type":"string","format":"uuid","nullable":true,"description":"The inspection the work order was raised from."},"dispatch_id":{"type":"string","format":"uuid","nullable":true,"description":"The parent rental's dispatch of the asset."},"sale_order_id":{"type":"string","format":"uuid","nullable":true,"description":"The sales order a SALE work order prepares the asset for."},"due_date":{"type":"string","format":"date-time","nullable":true},"expected_start_date":{"type":"string","format":"date-time","nullable":true},"in_progress_date":{"type":"string","format":"date-time","nullable":true,"description":"First moved to IN_PROGRESS."},"done_date":{"type":"string","format":"date-time","nullable":true,"description":"Set on WORK_COMPLETED (each time) or on DONE when not set yet."},"off_site":{"type":"boolean","nullable":true},"asset_out_of_service":{"type":"boolean","nullable":true},"all_assignees_access":{"type":"boolean","nullable":true,"description":"Every mechanic may see and work it."},"is_recurring":{"type":"boolean","nullable":true},"maintenance_trigger":{"type":"string","nullable":true,"enum":["HOUR_METER","CALENDAR_CADENCE",null]},"parts_allocation_status":{"type":"string","nullable":true,"enum":["ALLOCATED","PARTIALLY_RECEIVED","RECEIVED","FAILED",null],"description":"Null without purchase-order parts. Anything but null or RECEIVED blocks WORK_COMPLETED and DONE."}}},"work_order_line_item":{"type":"object","description":"A part or custom line of a work order, with its purchase-order reservation and price.","properties":{"id":{"type":"string","format":"uuid"},"org":{"type":"string","format":"uuid"},"order_id":{"type":"string","format":"uuid"},"parent_id":{"type":"string","format":"uuid","nullable":true},"item_type":{"type":"string","enum":["PART","CUSTOM"]},"item_id":{"type":"string","format":"uuid","nullable":true,"description":"The part; null on CUSTOM lines."},"name":{"type":"string","nullable":true},"quantity":{"type":"number"},"unit_price":{"type":"number","nullable":true,"description":"The price per unit after the markup shelf."},"quoted_cost":{"type":"number","nullable":true,"description":"CUSTOM - internal per-unit cost; never billed."},"percentage_discount":{"type":"number","nullable":true,"description":"Negative for a markup."},"bill_to":{"type":"string","enum":["CUSTOMER","ASSET","PARENT_ORDER"],"description":"CUSTOMER - the work order's invoice; ASSET - an internal cost of the asset; PARENT_ORDER - the parent rental's invoice."},"is_taxable":{"type":"boolean","nullable":true},"capitalized":{"type":"boolean","nullable":true},"purchase_order":{"type":"string","format":"uuid","nullable":true},"approval_status":{"type":"string","nullable":true,"example":"APPROVED"},"lifecycle_status":{"type":"string","example":"NONE"},"item_details":{"type":"object","nullable":true,"additionalProperties":true,"description":"The pricing behind the line - markup_percentage, markup_value, price_adjustment_type (none, discount, markup), line_item_price, line_item_quantity, purchase_order, bill_to_customer, capitalized and line_item_discount_value; on a PART also a copy of the part row (part_number, bin_location, sell_price, type, ...); on a CUSTOM base_unit_price; on a purchase-order freight or misc line po_charge, purchase_order_id, po_number and charge_key.\n"},"asset_ref":{"type":"string","format":"uuid","nullable":true},"sold_at":{"type":"string","format":"date-time","nullable":true},"sale_snapshot":{"type":"object","nullable":true,"description":"Always null on work orders."},"warranty_source_option_id":{"type":"string","format":"uuid","nullable":true},"warranty_coverage_months":{"type":"integer","nullable":true},"warranty_coverage_hours":{"type":"integer","nullable":true},"rental_rates":{"type":"object","nullable":true,"description":"Always null on work orders."},"rental_charge_details":{"type":"object","nullable":true,"description":"Always null on work orders."},"expected_dispatch_date":{"type":"string","format":"date-time","nullable":true},"expected_return_date":{"type":"string","format":"date-time","nullable":true},"billing_cycle_anchor":{"type":"string","format":"date-time","nullable":true},"deleted":{"type":"boolean"},"imported":{"type":"boolean","nullable":true},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time","nullable":true},"created_by":{"type":"string","format":"uuid","nullable":true},"updated_by":{"type":"string","format":"uuid","nullable":true},"po_details":{"type":"object","nullable":true,"description":"The line's live purchase-order reservation; null when it comes from stock.","properties":{"id":{"type":"string","format":"uuid","description":"The purchase order."},"po_number":{"type":"string"},"purchase_order_item_id":{"type":"string","format":"uuid","nullable":true},"quantity_allocated":{"type":"number"},"quantity_received":{"type":"number"},"fulfillment_status":{"type":"string","enum":["ALLOCATED","PARTIALLY_RECEIVED","RECEIVED","FAILED"]}}},"sub_total":{"type":"number"},"discount_value":{"type":"number"},"total":{"type":"number"}}},"work_order_detail":{"description":"A work order with everything the work order screen shows.","allOf":[{"$ref":"#/components/schemas/work_order_row"},{"type":"object","properties":{"task_details":{"$ref":"#/components/schemas/work_order_task_details"},"customer_details":{"type":"object","description":"The customer; an object with every field null on a work order without one.","additionalProperties":true,"properties":{"id":{"type":"string","format":"uuid","nullable":true},"company_name":{"type":"string","nullable":true},"customer_number":{"type":"integer","nullable":true},"address":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/invoice_postal_address"}]},"billing_address":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/invoice_billing_address"}]},"default_contact":{"type":"string","format":"uuid","nullable":true},"default_site":{"type":"string","format":"uuid","nullable":true},"default_payment_method":{"type":"string","nullable":true},"stripe_id":{"type":"string","nullable":true},"auto_payment":{"type":"boolean","nullable":true},"pay_on_open":{"type":"boolean","nullable":true},"terms":{"type":"integer","nullable":true},"validated":{"type":"boolean","nullable":true},"env_fee_status":{"type":"boolean","nullable":true},"damage_waiver_status":{"type":"boolean","nullable":true},"state_sales_tax_status":{"type":"boolean","nullable":true},"county_sales_tax_status":{"type":"boolean","nullable":true},"city_sales_tax_status":{"type":"boolean","nullable":true},"special_sales_tax_status":{"type":"boolean","nullable":true},"deleted":{"type":"boolean","nullable":true}}},"site_details":{"type":"object","description":"The site; every field null when none.","properties":{"id":{"type":"string","format":"uuid","nullable":true},"name":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"address":{"type":"string","nullable":true},"city":{"type":"string","nullable":true},"state":{"type":"string","nullable":true},"zip":{"type":"string","nullable":true},"tax_exemption_status":{"type":"boolean","nullable":true,"description":"An exempt site drops every tax."},"tax_exemption_number":{"type":"string","nullable":true},"tax_exemption_expiry":{"type":"string","nullable":true},"deleted":{"type":"boolean","nullable":true}}},"contact_details":{"type":"object","description":"The contact; every field null when none.","properties":{"id":{"type":"string","format":"uuid","nullable":true},"name":{"type":"string","nullable":true},"title":{"type":"string","nullable":true},"email":{"type":"string","nullable":true},"cell_phone":{"type":"string","nullable":true}}},"assignee_details":{"type":"object","description":"The mechanic; every field null when unassigned.","properties":{"id":{"type":"string","format":"uuid","nullable":true},"name":{"type":"string","nullable":true},"email":{"type":"string","nullable":true}}},"task_level_details":{"type":"object","properties":{"id":{"type":"string","format":"uuid","nullable":true},"name":{"type":"string","nullable":true},"duration":{"type":"integer","nullable":true},"color":{"type":"string","nullable":true}}},"asset_details":{"type":"object","properties":{"id":{"type":"string","format":"uuid","nullable":true},"name":{"type":"string","nullable":true},"asset_number":{"type":"string","nullable":true},"manufacturer":{"type":"string","nullable":true},"category":{"type":"string","format":"uuid","nullable":true},"category_linage":{"type":"string","nullable":true,"example":"Equipment -> Lifts -> Scissor Lifts"}}},"ancestry":{"type":"array","description":"The parent orders, oldest first; empty without a parent.","items":{"$ref":"#/components/schemas/lotus_order_ancestor"}},"line_items":{"type":"array","items":{"$ref":"#/components/schemas/work_order_line_item"}},"adjustments":{"type":"array","description":"The stored taxes, fees and discount (lotus_order_adjustments rows), by calculation priority.","items":{"$ref":"#/components/schemas/work_order_adjustment"}},"invoice_details":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"invoice_number":{"type":"string","example":"INV-WO-20261001-001-01"},"payment_status":{"type":"string","enum":["DRAFT","UNPAID","PARTIALLY_PAID","PAID","VOIDED"]},"amount":{"type":"number"}}}},"time_logs":{"type":"array","description":"WORK minutes logged per person (commute legs not counted).","items":{"type":"object","properties":{"assignee_id":{"type":"string","format":"uuid"},"assignee_name":{"type":"string","nullable":true},"assignee_username":{"type":"string","nullable":true},"minutes_logged":{"type":"string","description":"A whole number sent as a string.","example":"90"}}}},"total_minutes_logged":{"type":"integer"},"inspection_details":{"type":"object","nullable":true,"description":"The inspection the work order was raised from; null when none.","properties":{"id":{"type":"string","format":"uuid"},"type":{"type":"string"},"status":{"type":"string"},"passed":{"type":"boolean","nullable":true},"asset_out_of_service":{"type":"boolean","nullable":true},"completed_at":{"type":"string","format":"date-time","nullable":true},"deleted":{"type":"boolean"}}},"purchase_order_costs":{"type":"object","nullable":true,"additionalProperties":true,"description":"Purchase order and vendor bill figures for the work order's purchase-order parts - purchase_orders (each with its lines, invoices and charges), allocated_charges (freight, sales tax and misc by who pays) and charge_suggestions. Null unless the caller has PURCHASE_ORDERS_READ.\n"},"parent_tax_types":{"type":"array","nullable":true,"description":"The taxes the parent rental carries - the only ones this work order may keep; null without a parent.","items":{"type":"string","example":"STATE_TAX"}},"projected_financials":{"type":"object","description":"Totals computed on read; nothing is stored.","properties":{"subtotal":{"type":"number","description":"The CUSTOMER lines."},"taxable_subtotal":{"type":"number"},"total_billed_to_asset":{"type":"number"},"total_billed_to_order":{"type":"number","description":"The PARENT_ORDER lines."},"grand_total":{"type":"number","description":"subtotal plus taxes and fees, less the discount."},"applied_adjustments":{"type":"array","items":{"$ref":"#/components/schemas/work_order_applied_adjustment"}}}}}}]},"work_order_applied_adjustment":{"description":"A stored adjustment with the amount it comes to on this work order.","allOf":[{"$ref":"#/components/schemas/work_order_adjustment"},{"type":"object","properties":{"calculated_dollar_value":{"type":"number","description":"Rounded to 2 decimals; subtracted for a DISCOUNT."},"calculated_percentage_value":{"type":"number","nullable":true},"taxable_base":{"type":"number"},"calculation_basis_description":{"type":"string"}}}]},"work_order_adjustment":{"type":"object","description":"A stored tax, fee or discount of the work order.","properties":{"id":{"type":"string","format":"uuid"},"org":{"type":"string","format":"uuid"},"order_id":{"type":"string","format":"uuid"},"line_item_id":{"type":"string","format":"uuid","nullable":true},"type":{"type":"string","example":"STATE_TAX"},"category":{"type":"string","enum":["TAX","FEE","DISCOUNT"]},"is_percentage":{"type":"boolean","nullable":true},"value":{"type":"number","nullable":true},"description":{"type":"string","nullable":true},"calculation_priority":{"type":"integer"},"tax_state_code":{"type":"string","nullable":true},"tax_county_name":{"type":"string","nullable":true},"tax_city_name":{"type":"string","nullable":true},"tax_source_zip":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time","nullable":true}}},"work_order_search_row":{"type":"object","description":"One row of the service list - a work order, an inspection or an NADC checklist (record_type). Keys that do not apply to a kind are null, false or 0 as noted. The *_details objects leave out null-valued keys.\n","properties":{"id":{"type":"string","format":"uuid"},"record_type":{"type":"string","enum":["work_order","inspection","nadc"]},"type":{"type":"string","description":"WORK_ORDER, the inspection type (DISPATCH, OFF_RENT, INDEPENDENT, SALE) or NADC."},"status":{"type":"string","description":"A work order status, or an inspection or checklist status (PENDING, READY_FOR_INSPECTION, PARTIALLY_INSPECTED, READY, IN_PROGRESS, DONE, ...)."},"priority":{"type":"integer","nullable":true},"order_number":{"type":"string","nullable":true,"description":"The work order's number, or the order an inspection belongs to; null for checklists."},"due_date":{"type":"string","format":"date-time","nullable":true},"created_at":{"type":"string","format":"date-time"},"deleted":{"type":"boolean","nullable":true},"deletion_information":{"type":"object","nullable":true,"additionalProperties":true},"notes":{"type":"string","nullable":true},"internal_notes":{"type":"string","nullable":true,"description":"Null without the matching ACCESS_ALL permission."},"off_site_wo":{"type":"boolean","nullable":true},"passed":{"type":"boolean","nullable":true,"description":"Inspections only."},"quick_pass":{"type":"boolean","nullable":true,"description":"Inspections only."},"has_analysis":{"type":"boolean","description":"Inspections only, when the org has inspection analysis."},"inspection_id":{"type":"string","format":"uuid","nullable":true,"description":"Work orders - the inspection it came from."},"all_assignees_access":{"type":"boolean","nullable":true},"is_recurring":{"type":"boolean","nullable":true},"maintenance_trigger":{"type":"string","nullable":true},"task_type":{"type":"string","nullable":true,"description":"Work orders only."},"task_expected_start_date":{"type":"string","format":"date-time","nullable":true,"description":"Work orders - expected start; others - the due date."},"task_in_progress_date":{"type":"string","format":"date-time","nullable":true},"task_done_date":{"type":"string","format":"date-time","nullable":true,"description":"Work orders and checklists - when completed; null for inspections."},"total_minutes_logged":{"type":"integer"},"total":{"type":"number","description":"The grand total (the Due Amount column): the customer subtotal plus taxes and fees, less the order discount; 0 on inspection and NADC rows. Computed after the page is fetched, so none of the money fields can be sorted.\n"},"subtotal":{"type":"number","description":"Customer Total (DOZ-7929): the lines billed to the customer before taxes, fees and the discount; equals charges_summary.subtotal. 0 on inspection and NADC rows.\n"},"total_billed_to_asset":{"type":"number","description":"Asset Total, i.e. Internal (DOZ-7929): the lines billed to the asset; equals charges_summary.total_billed_to_asset. A PO's allocated freight, tax and misc are never line items and are not included. 0 on inspection and NADC rows.\n"},"total_billed_to_order":{"type":"number","description":"Parent Order Total (DOZ-7929): the lines billed to the parent rental; equals charges_summary.total_billed_to_order. 0 without a parent and on inspection and NADC rows.\n"},"parts_allocation_status":{"type":"string","nullable":true},"parts_fulfillment_status":{"type":"string","nullable":true,"enum":["AWAITING_PARTS","ALL_PARTS_RECEIVED",null],"description":"DOZ-8104. Derived from the work order's live purchase-order allocations on every read: AWAITING_PARTS while any ordered part is not fully received (including one on a purchase order closed short), ALL_PARTS_RECEIVED once every one is. Null when the work order has no ordered parts (stock parts only, or none), and on inspection and NADC rows. Not the same as parts_allocation_status, the stored flag the completion guard reads.\n"},"asset_out_of_service":{"type":"boolean","nullable":true},"blocks_order_closure":{"type":"boolean","description":"True for an open (not DONE, not deleted) work order that keeps its parent rental from completing or its parent sales order from closing.\n"},"asset_name":{"type":"string","nullable":true},"assignee_name":{"type":"string","nullable":true},"customer_name":{"type":"string","nullable":true},"parent_order_number":{"type":"string","nullable":true},"site_id":{"type":"string","format":"uuid","nullable":true},"asset_details":{"type":"object","nullable":true,"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"asset_number":{"type":"string"},"manufacturer":{"type":"string"},"model_number":{"type":"string"},"has_telematics":{"type":"boolean","description":"Whether the asset has a telematics device (a current Samsara row). Only when the org has the SAMSARA_GPS module; absent otherwise (DOZ-8147).\n"}}},"assignee_details":{"type":"object","nullable":true,"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string"}}},"task_level_details":{"type":"object","nullable":true,"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"duration":{"type":"integer"},"color":{"type":"string"}}},"customer_details":{"type":"object","nullable":true,"properties":{"id":{"type":"string","format":"uuid"},"company_name":{"type":"string"},"customer_number":{"type":"integer","nullable":true},"name":{"type":"string","nullable":true},"deleted":{"type":"boolean","nullable":true},"contacts":{"type":"array","nullable":true,"items":{"$ref":"#/components/schemas/lotus_order_customer_contact"}}}},"inspection_details":{"type":"object","nullable":true,"additionalProperties":true,"description":"Work orders - the inspection it came from (id, type, deleted, completed_at, asset_out_of_service)."},"parent_order_details":{"type":"object","nullable":true,"additionalProperties":true,"description":"The parent order (id, order_number, type, status)."},"site_details":{"type":"object","nullable":true,"additionalProperties":true,"description":"Work orders - the site (id, name, description, address, city, state, zip, deleted, tax_exemption_*)."}}},"work_order_search_body":{"type":"object","description":"Body of POST /lotus/work-orders/search. Unknown keys are stripped.","required":["search"],"properties":{"search":{"type":"object","description":"Every filter is optional. logical_operation (default and) joins them. Joined with and, a task_level filter, off_site true, tasks_with_inspections true or a parts_fulfillment_status filter returns work orders only (false keeps inspections and checklists); a customer or order filter leaves out checklists.\n","properties":{"type":{"type":"array","items":{"type":"string","enum":["WORK_ORDER","DISPATCH","OFF_RENT","INDEPENDENT","SALE","NADC"]},"description":"PURCHASE is still accepted as NADC."},"status":{"type":"array","items":{"type":"string","enum":["PENDING","READY_FOR_INSPECTION","PARTIALLY_INSPECTED","READY","IN_PROGRESS","PAUSED","WORK_COMPLETED","DONE"]},"description":"DONE rows are listed only when DONE is asked for."},"priority":{"type":"integer","nullable":true,"enum":[0,1,null],"description":"0 lists only priority-0 rows and 1 only priority-1 rows. null or left out = no priority filter.\n"},"category":{"type":"array","description":"Asset category ids.","items":{"type":"string","format":"uuid"}},"due_date":{"type":"string","nullable":true,"description":"A date (due on or before it), \"null\" (no due date) or \"not_null\"."},"assignee":{"type":"string","nullable":true,"description":"A user id, \"unassigned\" or \"anyone\"."},"asset_id":{"type":"string","format":"uuid","nullable":true},"customer_id":{"type":"string","format":"uuid","nullable":true},"site_id":{"type":"string","format":"uuid","nullable":true,"description":"Work orders only."},"task_level":{"type":"array","items":{"type":"string"},"description":"Level ids, \"null\" or \"not_null\"."},"asset_number":{"type":"string","nullable":true,"description":"Partial match."},"manufacturer":{"type":"string","nullable":true},"model_number":{"type":"string","nullable":true},"customer_name":{"type":"string","nullable":true},"customer_number":{"type":"string","nullable":true},"order_number":{"type":"string","nullable":true},"skip_days_limit":{"type":"boolean","nullable":true,"description":"Include dispatch inspections due beyond the caller's \"show tasks within\" days."},"off_site":{"type":"boolean","nullable":true},"out_of_service":{"type":"boolean","nullable":true},"deleted":{"type":"boolean","nullable":true,"description":"true lists only deleted rows."},"tasks_with_inspections":{"type":"boolean","nullable":true,"description":"Work orders raised from an inspection."},"is_part_of_order":{"type":"boolean","nullable":true},"parts_fulfillment_status":{"type":"array","nullable":true,"uniqueItems":true,"items":{"type":"string","enum":["AWAITING_PARTS","ALL_PARTS_RECEIVED"]},"example":["AWAITING_PARTS"],"description":"Work orders in any of the given parts-fulfillment states (DOZ-8104, see parts_fulfillment_status on the row). Both values list every work order with ordered parts; empty or null applies no filter. A work order with no ordered parts matches neither, and inspections and checklists are left out whenever the filter is set.\n"},"is_recurring":{"type":"boolean"},"maintenance_trigger":{"type":"string","nullable":true,"enum":["HOUR_METER","CALENDAR_CADENCE",null]},"completed_from":{"type":"string","format":"date-time","nullable":true,"description":"Inspections and checklists only (completed_at on or after); work orders are not filtered by it."},"completed_till":{"type":"string","format":"date-time","nullable":true,"description":"Inspections and checklists only (completed_at on or before); work orders are not filtered by it."},"date_from":{"type":"string","format":"date-time","nullable":true,"description":"Completed on or after."},"date_to":{"type":"string","format":"date-time","nullable":true,"description":"Not before date_from."},"logical_operation":{"type":"string","enum":["and","or"]},"format":{"type":"string","enum":["json","csv"],"description":"csv streams every match as a CSV file, unpaginated."},"order_by":{"$ref":"#/components/schemas/order_by"}},"example":{}},"paginate":{"type":"object","description":"Optional - left out, every match is returned.","required":["currentPage","perPage"],"properties":{"currentPage":{"type":"integer","minimum":1,"example":1},"perPage":{"type":"integer","minimum":1,"maximum":100,"example":25}}}}},"work_order_search_result":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/work_order_search_row"}},"pagination":{"type":"object","description":"knex-paginate - total, lastPage, prevPage and nextPage are sent on page 1 only. Without paginate in the body - currentPage 1, total, and every other key null.\n","properties":{"perPage":{"type":"integer","nullable":true},"currentPage":{"type":"integer"},"from":{"type":"integer","nullable":true},"to":{"type":"integer","nullable":true},"total":{"type":"integer"},"lastPage":{"type":"integer","nullable":true},"prevPage":{"type":"integer","nullable":true},"nextPage":{"type":"integer","nullable":true}}},"orderByColumnNamesList":{"$ref":"#/components/schemas/orderByColumnNamesList"}}},"work_order_item":{"type":"object","description":"A line of a create or update body. PART lines need item_id and are priced from inventory; CUSTOM lines need unit_price (or base_unit_price). Unknown keys are stripped.\n","required":["item_type","quantity","bill_to"],"properties":{"id":{"type":"string","format":"uuid","nullable":true,"description":"Update only - the saved line's id, to keep it (and its frozen price)."},"item_type":{"type":"string","enum":["PART","CUSTOM"]},"item_id":{"type":"string","format":"uuid","nullable":true,"description":"The part; required for PART."},"name":{"type":"string","nullable":true},"quantity":{"type":"number","minimum":0,"exclusiveMinimum":true,"description":"Above 0. Whole numbers for parts that are not supplies."},"unit_price":{"type":"number","description":"CUSTOM only - required unless base_unit_price is sent; any amount (negative for a credit). Without base_unit_price it is read as the base price and the custom-items markup shelf is added on top (stored unit_price is the marked-up price). On a saved line, a value equal to the stored unit_price means unchanged and keeps the stored base. Not marked up: purchase-order charge lines (po_charge), prices of 0 or less, and lines saved before DOZ-7737.\n"},"base_unit_price":{"type":"number","nullable":true,"description":"CUSTOM only - the price before the custom-items markup."},"quoted_cost":{"type":"number","nullable":true,"minimum":0,"description":"CUSTOM only - internal cost per unit; never billed."},"discount":{"type":"number","maximum":100,"default":0,"description":"Percent off the line; negative for a markup."},"bill_to":{"type":"string","enum":["CUSTOMER","ASSET","PARENT_ORDER"],"description":"PARENT_ORDER needs a parent rental."},"is_taxable":{"type":"boolean","description":"Left out - a new PART takes the part's flag, a new CUSTOM is taxed, a saved line keeps its value."},"capitalized":{"type":"boolean","default":false},"purchase_order":{"type":"string","format":"uuid","nullable":true,"description":"Reserve the line against this released purchase order instead of stock."},"purchase_order_item_id":{"type":"string","format":"uuid","nullable":true,"description":"CUSTOM lines with purchase_order - the purchase order line used (a saved line may leave it out)."},"po_charge":{"type":"object","nullable":true,"description":"CUSTOM only - marks the line as a purchase order's freight or misc charge (billed at cost).","properties":{"purchase_order_id":{"type":"string","format":"uuid"},"charge_key":{"type":"string","enum":["freight","miscellaneous"]}}}}},"work_order_fields":{"type":"object","description":"The fields shared by create and update. Unknown keys are stripped. Update is a full replace - an omitted boolean is saved as false.\n","required":["asset","task_level"],"properties":{"asset":{"type":"string","format":"uuid","description":"The asset worked on.","example":"9b8a7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d"},"task_level":{"type":"string","format":"uuid","description":"The level (GET /lotus/work-orders/levels).","example":"4c5d6e7f-8a9b-4c0d-9e1f-2a3b4c5d6e7f"},"task_type":{"type":"string","nullable":true,"enum":["SERVICE","ONBOARDING","MAINTENANCE","SALE",null],"description":"Default SERVICE.","example":"SERVICE"},"customer_id":{"type":"string","format":"uuid","nullable":true,"description":"Replaced by the parent rental's customer whenever parent (or parent_id) is sent. On update without parent it is saved as sent - null when left out - unless the org has customer assignment switched off (then the stored value is kept).\n"},"site_id":{"type":"string","format":"uuid","nullable":true,"description":"Left out while parent (or parent_id) is sent, the parent rental's site is used. On update without parent it is saved as sent - null when left out - unless the org has customer assignment switched off (then the stored value is kept).\n"},"contact_id":{"type":"string","format":"uuid","nullable":true},"assignee_id":{"type":"string","format":"uuid","nullable":true,"description":"A mechanic (ALLOW_ASSIGNEE_OWNERSHIP)."},"parent_id":{"type":"string","format":"uuid","nullable":true,"description":"The parent rental contract; cannot change once set."},"parent":{"type":"string","format":"uuid","nullable":true,"description":"Same as parent_id."},"dispatch_id":{"type":"string","format":"uuid","nullable":true},"priority":{"type":"integer","nullable":true,"enum":[0,1,null]},"expected_start_date":{"type":"string","format":"date-time","nullable":true,"description":"Scheduled from the assignee's queue when left out."},"due_date":{"type":"string","format":"date-time","nullable":true},"off_site_wo":{"type":"boolean","default":false},"asset_out_of_service":{"type":"boolean","default":false},"all_assignees_access":{"type":"boolean","default":false},"is_recurring":{"type":"boolean","default":false},"maintenance_trigger":{"type":"string","nullable":true,"enum":["HOUR_METER","CALENDAR_CADENCE",null]},"items":{"type":"array","default":[],"items":{"$ref":"#/components/schemas/work_order_item"},"example":[{"item_type":"CUSTOM","name":"Labour","unit_price":95,"quantity":2,"bill_to":"CUSTOMER"}]},"state_sales_tax_status":{"type":"boolean"},"county_sales_tax_status":{"type":"boolean"},"city_sales_tax_status":{"type":"boolean"},"special_sales_tax_status":{"type":"boolean"},"damage_waiver_status":{"type":"boolean"},"env_fee_status":{"type":"boolean"},"discount":{"type":"number","minimum":0,"maximum":100,"default":0,"description":"Order discount in percent, on the customer's lines."},"notes":{"type":"string","nullable":true,"description":"Saved as external_notes."},"internal_notes":{"type":"string","nullable":true},"customer_po":{"type":"string","nullable":true}}},"work_order_create_body":{"description":"Body of POST /lotus/work-orders/create.","allOf":[{"$ref":"#/components/schemas/work_order_fields"},{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["PENDING"],"example":"PENDING"}}}]},"work_order_update_body":{"description":"Body of PUT /lotus/work-orders/update/{id} - the whole work order. status is ignored.","allOf":[{"$ref":"#/components/schemas/work_order_fields"}]},"work_order_create_result":{"type":"object","properties":{"message":{"type":"string","example":"Work order created successfully"},"data":{"$ref":"#/components/schemas/work_order_row"}}},"work_order_id_result":{"type":"object","properties":{"message":{"type":"string","example":"Work order updated successfully"},"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid"}}}}},"work_order_delete_result":{"type":"object","properties":{"message":{"type":"string","example":"Work order deleted successfully"},"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid"}}}}},"work_order_convert_body":{"type":"object","description":"Body of POST /lotus/work-orders/convert/{id}. Unknown keys are stripped.","required":["status"],"properties":{"status":{"type":"string","enum":["PENDING","READY","IN_PROGRESS","PAUSED","WORK_COMPLETED","DONE"],"example":"READY"},"asset_out_of_service":{"type":"boolean","nullable":true,"description":"Required when leaving PENDING, going to PENDING and going to DONE.","example":false}}},"work_order_convert_result":{"type":"object","properties":{"message":{"type":"string","description":"\"Work order converted to '<status>' status\" for any status but DONE; for DONE one of the enum values.\n","enum":["Task converted to 'DONE' status","Task converted to 'DONE' status. An invoice was created — please review and approve it","Task converted to 'DONE' status. An invoice was created — auto-payment is disabled on this customer, please collect payment manually","Task converted to 'DONE' status. An invoice was created but the customer has no default payment method, so it couldn't be charged automatically — please set one and collect payment manually","Task converted to 'DONE' status. The invoice is already covered by an in-flight payment — no new charge issued","Task converted to 'DONE' status. Parts billed to the parent order were billed to the parent rental's invoice","Work order converted to 'DONE' status and an invoice was created, but the automatic charge failed — please collect payment manually","Work order converted to 'DONE' status and a charge on the work-order invoice has been created","Work order converted to 'DONE' status and the work-order invoice has been updated","Work order converted to 'DONE' status. The work-order invoice was updated and parts billed to the parent order were billed to the parent rental's invoice","Work order converted to 'PENDING' status","Work order converted to 'READY' status","Work order converted to 'IN_PROGRESS' status","Work order converted to 'PAUSED' status","Work order converted to 'WORK_COMPLETED' status"]},"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["PENDING","READY","IN_PROGRESS","PAUSED","WORK_COMPLETED","DONE"]}}}}},"work_order_delete_body":{"type":"object","description":"Body of DELETE /lotus/work-orders/{id}. Unknown keys are stripped.","required":["reason"],"properties":{"reason":{"type":"string","description":"Saved in deletion_information.","example":"Created by mistake"}}},"work_order_charges_summary_body":{"type":"object","description":"Body of POST /lotus/work-orders/charges_summary - the work order being built. Unknown keys are stripped.","required":["items"],"properties":{"customer_id":{"type":"string","format":"uuid","nullable":true},"site_id":{"type":"string","format":"uuid","nullable":true},"parent_id":{"type":"string","format":"uuid","nullable":true,"description":"The parent rental - only its taxes apply, its customer is used, and its site unless site_id is sent."},"state_sales_tax_status":{"type":"boolean","default":false},"county_sales_tax_status":{"type":"boolean","default":false},"city_sales_tax_status":{"type":"boolean","default":false},"special_sales_tax_status":{"type":"boolean","default":false},"damage_waiver_status":{"type":"boolean","default":false},"env_fee_status":{"type":"boolean","default":false},"discount":{"type":"number","minimum":0,"maximum":100,"default":0},"items":{"type":"array","minItems":1,"items":{"type":"object","required":["quantity","bill_to"],"properties":{"id":{"type":"string","format":"uuid","nullable":true,"description":"PART - the part (inventory) id; the line is priced from that part and takes its taxable flag (type and unit_price are used only when it is not found). Echoed back as line_items[].id.\n","example":"3c9a1f2e-7b4d-4e8a-9f61-2d5b8c0e4a17"},"item_type":{"type":"string","enum":["PART","CUSTOM"],"default":"PART"},"type":{"type":"string","enum":["supplies","inventoried","non_inventoried"],"description":"PART - the inventory type, used only when the part is not found."},"unit_price":{"type":"number","nullable":true,"description":"CUSTOM - required unless base_unit_price; PART - only used when the part is not found."},"base_unit_price":{"type":"number","nullable":true},"line_item_id":{"type":"string","format":"uuid","nullable":true,"description":"A saved line - priced at its saved price."},"quantity":{"type":"number","minimum":0},"discount":{"type":"number","maximum":100,"default":0},"bill_to":{"type":"string","enum":["CUSTOMER","ASSET","PARENT_ORDER"]},"capitalized":{"type":"boolean","default":false,"description":"Accepted; not used by the preview."},"is_taxable":{"type":"boolean"},"po_charge":{"type":"object","nullable":true,"properties":{"purchase_order_id":{"type":"string","format":"uuid"},"charge_key":{"type":"string","enum":["freight","miscellaneous"]}}}}},"example":[{"item_type":"CUSTOM","unit_price":95,"quantity":2,"bill_to":"CUSTOMER"}]}}},"work_order_charges_summary_result":{"type":"object","properties":{"subtotal":{"type":"number","description":"The CUSTOMER lines."},"taxable_subtotal":{"type":"number"},"total_billed_to_asset":{"type":"number"},"total_billed_to_order":{"type":"number"},"grand_total":{"type":"number"},"applied_adjustments":{"type":"array","items":{"$ref":"#/components/schemas/lotus_order_applied_adjustment"}},"line_items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","nullable":true},"bill_to":{"type":"string","enum":["CUSTOMER","ASSET","PARENT_ORDER"]},"is_taxable":{"type":"boolean"},"quantity":{"type":"number"},"unit_price":{"type":"number"},"markup_percentage":{"type":"number","description":"A fraction (0.25 is 25%)."},"markup_value":{"type":"number"},"discount":{"type":"number"},"discount_value":{"type":"number"},"price_adjustment_type":{"type":"string","enum":["none","discount","markup"]},"sub_total":{"type":"number"},"total":{"type":"number"}}}},"sales_tax_source":{"type":"string","enum":["Organization Defaults","Site","Customer","Organization"]},"sales_tax_source_zip":{"type":"string","nullable":true},"tax_exempt_reason":{"type":"string","nullable":true,"enum":["PARENT_ORDER","SITE","CUSTOMER",null],"description":"Why taxes were dropped."},"parent_tax_types":{"type":"array","nullable":true,"items":{"type":"string"}}}},"work_order_email_body":{"type":"object","description":"Body of POST /lotus/work-orders/email. Unknown keys are stripped.","required":["order_id","receiver_type"],"properties":{"order_id":{"type":"string","format":"uuid","example":"1f2e3d4c-5b6a-4978-8695-a4b3c2d1e0f9"},"receiver_type":{"type":"string","enum":["customer","self","custom"],"example":"self"},"contact_id":{"type":"string","format":"uuid","description":"With customer - send to this contact only."},"custom_email":{"type":"array","minItems":1,"description":"Required with custom (singular name, but a list of addresses).","items":{"type":"string","format":"email"}}}},"work_order_note":{"type":"object","description":"A work order note (a lotus_order_notes row).","properties":{"note_id":{"type":"string","format":"uuid"},"org":{"type":"string","format":"uuid"},"order_id":{"type":"string","format":"uuid"},"order_type":{"type":"string","example":"WORK_ORDER"},"user_id":{"type":"string","format":"uuid","description":"Who wrote it."},"log_type":{"type":"string","enum":["WORK","COMMUTE_TO_WORK","COMMUTE_FROM_WORK"],"description":"Commute values are for off-site work orders only."},"minutes_logged":{"type":"integer"},"status_update":{"type":"string","nullable":true,"description":"The status the note moved the work order to."},"notes":{"type":"string","nullable":true},"images":{"type":"array","nullable":true,"description":"Storage paths of the attached photos.","items":{"type":"string"}},"deleted":{"type":"boolean"},"created_by":{"type":"string","format":"uuid","nullable":true},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time","nullable":true}}},"work_order_note_create_body":{"type":"object","description":"Body of POST /lotus/work-orders/notes. Unknown keys are stripped.","required":["order_id","notes"],"properties":{"order_id":{"type":"string","format":"uuid","description":"The work order.","example":"1f2e3d4c-5b6a-4978-8695-a4b3c2d1e0f9"},"notes":{"type":"string","example":"Replaced hydraulic hose"},"minutes_logged":{"type":"integer","minimum":0,"default":0},"log_type":{"type":"string","enum":["WORK","COMMUTE_TO_WORK","COMMUTE_FROM_WORK"],"default":"WORK","description":"Case-insensitive. Commute values are for off-site work orders only."},"images":{"type":"array","nullable":true,"maxItems":0,"description":"Only empty or null - photos are attached afterwards with POST /v1/media/orders/note.","items":{"type":"string"}},"status_update":{"type":"string","enum":["PENDING","READY","IN_PROGRESS","PAUSED","WORK_COMPLETED","DONE"],"description":"Also move the work order to this status (as convert does)."},"asset_out_of_service":{"type":"boolean","nullable":true,"description":"With status_update - as on convert."}}},"work_order_note_update_body":{"type":"object","description":"Body of PUT /lotus/work-orders/notes/{id} - at least one field. Unknown keys (status_update included) are stripped.","minProperties":1,"properties":{"notes":{"type":"string","example":"Replaced hydraulic hose and filter"},"minutes_logged":{"type":"integer","minimum":0},"log_type":{"type":"string","enum":["WORK","COMMUTE_TO_WORK","COMMUTE_FROM_WORK"],"description":"Case-insensitive. Commute values are for off-site work orders only."},"images":{"type":"array","nullable":true,"description":"Paths already on the note, to reorder or remove; null or empty clears them.","items":{"type":"string"}}}},"work_order_note_search_body":{"type":"object","description":"Body of POST /lotus/work-orders/notes/search. Unknown keys are stripped.","required":["search","paginate"],"properties":{"search":{"type":"object","required":["id"],"properties":{"id":{"type":"string","format":"uuid","description":"The work order."},"log_type":{"type":"string","nullable":true,"enum":["WORK","COMMUTE_TO_WORK","COMMUTE_FROM_WORK",null]}},"example":{"id":"1f2e3d4c-5b6a-4978-8695-a4b3c2d1e0f9"}},"paginate":{"type":"object","required":["currentPage","perPage"],"properties":{"currentPage":{"type":"integer","minimum":1,"example":1},"perPage":{"type":"integer","minimum":1,"maximum":100,"example":25}}}}},"work_order_note_search_result":{"type":"object","properties":{"data":{"type":"array","description":"Newest first.","items":{"allOf":[{"$ref":"#/components/schemas/work_order_note"},{"type":"object","properties":{"username":{"type":"string","nullable":true,"description":"The writer's email."},"user_name":{"type":"string","nullable":true}}}]}},"pagination":{"$ref":"#/components/schemas/lotus_order_pagination"},"total_minutes_logged":{"type":"integer","description":"All WORK minutes on the work order."}}},"work_order_type":{"type":"object","properties":{"desc":{"type":"string","example":"Service"},"value":{"type":"string","enum":["SERVICE","ONBOARDING","MAINTENANCE","SALE"]}}},"work_order_level":{"type":"object","description":"A work order level (task_levels row).","properties":{"id":{"type":"string","format":"uuid"},"org":{"type":"string","format":"uuid"},"name":{"type":"string","example":"Quick Fix"},"description":{"type":"string","nullable":true},"weight":{"type":"integer","nullable":true,"description":"Display order."},"color":{"type":"string","nullable":true,"example":"#3f8cff"},"duration":{"type":"integer","nullable":true,"description":"Expected days."},"deleted":{"type":"boolean","nullable":true}}},"work_order_level_update_body":{"type":"object","description":"Body of POST /lotus/work-orders/levels/update - id and at least one field to change. Unknown keys are stripped.","required":["id"],"properties":{"id":{"type":"string","format":"uuid","example":"4c5d6e7f-8a9b-4c0d-9e1f-2a3b4c5d6e7f"},"name":{"type":"string","example":"Quick Fix"},"description":{"type":"string","nullable":true},"weight":{"type":"integer"},"color":{"type":"string","nullable":true,"description":"#rgb or #rrggbb."},"duration":{"type":"integer","minimum":1,"description":"Days."}}},"asset_checklist":{"type":"object","required":["asset_id","checklist_id","items"],"properties":{"asset_id":{"type":"string","format":"uuid"},"checklist_id":{"type":"string","format":"uuid"},"valid":{"type":"boolean","description":"Send true to validate the checklist. Refused with a 409 while a required item is unanswered."},"name":{"type":"string","readOnly":true},"type":{"type":"string","description":"Checklist type, e.g. Buy or Sale.","readOnly":true},"validated_at":{"type":"string","format":"date-time","nullable":true,"readOnly":true},"validated_by":{"type":"object","nullable":true,"readOnly":true,"properties":{"id":{"type":"string","format":"uuid"},"username":{"type":"string"}}},"sign_off_permission":{"type":"string","nullable":true,"description":"Name of the permission required to sign off this section. Null means ungated. Ignored on write.","example":"CHECKLIST_BUY_ACCOUNTING_SIGN_OFF"},"items":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/checklist_item"}],"properties":{"value":{"type":"boolean","description":"Answer for checkbox items. Null for text and select items."},"value_text":{"type":"string","nullable":true,"description":"Answer for text items, or the chosen option for select items. Null for checkbox items."},"updated_at":{"type":"string","format":"date-time"},"updated_by":{"type":"string","format":"uuid"}}}}}},"asset_fields":{"type":"object","description":"The columns a client may write. asset_create and asset_update add their own required lists on top; the stored record (asset) adds the server-set columns.\n","properties":{"asset_number":{"type":"string","pattern":"^[a-zA-Z0-9-_.]+$","description":"Letters, digits, dash, underscore and dot. On update, sending it at all (changed or not) needs ASSET_NUMBER_UPDATE, else 403.\n"},"name_override":{"type":"string","nullable":true,"description":"Replaces the auto-generated name when set."},"category":{"type":"string","format":"uuid","description":"A category of the caller's org. Its add_on flag decides the asset's type."},"category_import":{"type":"string","nullable":true},"type_import":{"type":"string","nullable":true},"part_number":{"type":"string","nullable":true},"manufacturer":{"type":"string","nullable":true},"model_number":{"type":"string","nullable":true},"model_year":{"type":"string","nullable":true},"serial_number":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"bulk_item":{"type":"string","nullable":true},"samt":{"type":"number","nullable":true},"bought_used":{"type":"boolean","nullable":true},"po_number":{"type":"string","nullable":true,"description":"Manually entered PO number. Ignored while the asset is linked to a purchase order; reads then return the linked PO's number.\n"},"purchase_order":{"type":"string","format":"uuid","nullable":true,"description":"Links the new asset to a purchase order (adds a PO line). Create only - the update accepts it, because the web echoes the record back, and drops it.\n"},"rental_rates":{"type":"object","nullable":true,"properties":{"day":{"type":"number","minimum":0},"week":{"type":"number","minimum":0},"4week":{"type":"number","minimum":0}}},"inc":{"type":"object","nullable":true,"description":"Free-form. The web stores inc1, inc2 and inc3 as numbers."},"repc":{"type":"object","nullable":true,"description":"Free-form. The web stores repc1 and repc2 as numbers."},"tmot":{"type":"object","nullable":true,"description":"Free-form. The web stores tmot1, tmot2 and tmot3 as numbers."},"hrot":{"type":"object","nullable":true,"description":"Free-form. The web stores hrot1, hrot2 and hrot3 as numbers."},"vendor":{"type":"object","nullable":true,"description":"Free-form. The web stores vendor1, vendor2 and vendor3."},"order_no":{"type":"object","nullable":true,"description":"Free-form. The web stores order1, order2 and order3."},"hour_meter":{"type":"number","minimum":0,"description":"Must be a JSON number (a numeric string is refused). Stored rounded to one decimal. Cannot change while the asset is dispatched (400).\n"},"purchase_price":{"type":"number","nullable":true,"description":"Derived from purchase_details whenever that array has items."},"extra_charges":{"type":"number","nullable":true},"sell_price":{"type":"number","nullable":true},"purchase_date":{"type":"string","format":"date-time","nullable":true},"sold_date":{"type":"string","format":"date-time","nullable":true},"last_date":{"type":"string","format":"date-time","nullable":true},"in_service_date":{"type":"string","format":"date-time","nullable":true,"description":"Stamped by the first rental when unset. Changing a set value on update needs ASSET_IN_SERVICE_DATE_UPDATE (400)."},"latest_maintenance_date":{"type":"string","format":"date-time","nullable":true},"latest_maintenance_hour_meter":{"type":"number","minimum":0,"nullable":true,"description":"Cannot exceed hour_meter, and cannot go down on update (400)."},"latest_inspection_date":{"type":"string","format":"date-time","nullable":true},"facility":{"type":"string","format":"uuid","nullable":true},"capitalization":{"type":"boolean"},"has_hour_meter":{"type":"boolean"},"transaction_group":{"type":"array","minItems":1,"items":{"type":"string","enum":["sale","rental"]},"description":"Adding rental to a sale-only asset on update needs ASSET_SALE_ONLY_TO_RENTAL (400)."},"depreciation_schedules":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Depreciation schedules to attach. Needs SCHEDULE_ASSET_ASSIGNMENT and depreciation enabled for the org, else 403."},"warranty_months":{"type":"number","minimum":0,"nullable":true},"warranty_miles":{"type":"number","minimum":0,"nullable":true}}},"asset_purchase_detail_input":{"type":"object","description":"One purchase-price component, as sent on create.","required":["price","type"],"properties":{"description":{"type":"string","nullable":true},"price":{"type":"number","minimum":0},"weight":{"type":"integer","minimum":0,"nullable":true,"description":"Display order"},"type":{"type":"string","enum":["UNIT_PRICE","SHIPPING","OTHER"]}}},"asset_purchase_detail":{"description":"A stored purchase-price component. On update an item with an id edits that row and an item without one inserts.","allOf":[{"$ref":"#/components/schemas/asset_purchase_detail_input"},{"type":"object","properties":{"id":{"type":"string","format":"uuid"}}}]},"asset_create":{"description":"Body of POST /assets. Unknown keys are refused with 400 (\"x\" is not allowed), so send only these fields and none of the readOnly columns of asset.\n","allOf":[{"$ref":"#/components/schemas/asset_fields"},{"type":"object","required":["asset_number","category","transaction_group"],"properties":{"purchase_details":{"type":"array","description":"Optional. When it has items their prices are summed into purchase_price and a purchase_price sent alongside is ignored. Whether details are mandatory comes from the org's purchase_price field setting.\n","items":{"$ref":"#/components/schemas/asset_purchase_detail_input"}},"create_onboarding_task":{"type":"boolean","nullable":true},"create_nadc":{"type":"boolean","nullable":true,"description":"When the org setting ASSET.CREATE_NADC is prompt_user, whether to create a New Asset Detail Checklist for the new asset."},"out_of_service":{"type":"boolean","nullable":true}}}]},"asset_update":{"description":"Body of POST /assets/{id}. Unknown keys are dropped silently, not refused. The update is otherwise partial, but category must always be sent - the server re-reads it on every update and answers 400 without it. The web echoes the whole stored record back, so media_count, rental_hours, rental_hour_history, org and utilization pass validation too; echo them unchanged (org is never written from the body, the other four are stored as sent).\n","allOf":[{"$ref":"#/components/schemas/asset_fields"},{"type":"object","required":["category"],"properties":{"purchase_details":{"type":"array","description":"The full desired set. Items with an id update that row, items without one insert, stored rows missing from the array are soft-deleted, and [] clears them. Editing while purchase_details_locked is true needs ASSET_PURCHASE_DETAILS_UPDATE (400).\n","items":{"$ref":"#/components/schemas/asset_purchase_detail"}},"purchase_details_locked":{"type":"boolean","description":"Lock toggle. Unlocking needs ASSET_PURCHASE_DETAILS_UPDATE (400)."}}}]},"asset":{"description":"The stored asset record, as every read returns it. readOnly columns are set by the server - refused by the create, ignored by the update.\n","allOf":[{"$ref":"#/components/schemas/asset_fields"},{"type":"object","properties":{"id":{"type":"string","format":"uuid","readOnly":true},"name":{"type":"string","readOnly":true,"description":"Auto-generated from the org's configured name fields (default manufacturer, model_number, model_year) unless name_override is set."},"type":{"type":"string","enum":["NORMAL","ADD_ON"],"readOnly":true,"description":"Derived from the category's add_on flag."},"org":{"type":"string","format":"uuid","readOnly":true},"deleted":{"type":"boolean","readOnly":true},"quantity":{"type":"integer","readOnly":true},"media_count":{"type":"integer","readOnly":true},"rental_hours":{"type":"number","nullable":true,"readOnly":true},"rental_hour_history":{"type":"string","nullable":true,"readOnly":true},"utilization":{"type":"number","readOnly":true},"rental_history":{"type":"object","readOnly":true,"properties":{"hours":{"type":"object","properties":{"m3":{"type":"number"},"m6":{"type":"number"},"m12":{"type":"number"},"all":{"type":"number"}}},"income":{"type":"object","properties":{"m3":{"type":"number"},"m6":{"type":"number"},"m12":{"type":"number"},"all":{"type":"number"}}}}},"cost_history":{"type":"object","nullable":true,"readOnly":true,"description":"Service cost totals by period (m3, m6, m12, all)."},"inherited_rental_rates":{"type":"object","nullable":true,"readOnly":true,"description":"The category's rates, used when the asset has none of its own.","properties":{"day":{"type":"number"},"week":{"type":"number"},"4week":{"type":"number"}}},"checklist_status":{"type":"string","enum":["onboarding","onboarded","selling","sale_approved"],"readOnly":true},"validated":{"type":"boolean","readOnly":true},"validation_failures":{"type":"object","nullable":true,"readOnly":true,"description":"Result of validating the record against the org's field settings."},"out_of_service":{"type":"boolean","description":"Set on create or through POST /assets/toggle/out_of_service; the update ignores it."},"current_rental_status":{"type":"string","nullable":true,"readOnly":true,"enum":["quote","reservation","dispatch_inspection","in_transit","dispatched","off_rent","off_rent_inspection"],"description":"Legacy-lowercase rental status mirror. GET /assets/{id} derives it from the live rental line items when the stored value is empty."},"routine_maintenance":{"type":"object","nullable":true,"readOnly":true,"properties":{"hour_meter_reading":{"type":"number"},"timestamp":{"type":"string","format":"date-time"}}},"purchase_details_locked":{"type":"boolean","description":"Writable on update only; see asset_update."},"purchase_details_locked_at":{"type":"string","format":"date-time","nullable":true,"readOnly":true},"purchase_details_locked_by":{"type":"string","format":"uuid","nullable":true,"readOnly":true},"created_at":{"type":"string","format":"date-time","readOnly":true},"created_by":{"type":"string","format":"uuid","nullable":true,"readOnly":true}}}]},"samsara_details":{"type":"object","description":"The asset's current Samsara telematics reading, i.e. the newest module_samsara_assets row with is_current true (DOZ-7349, DOZ-7352).\n","properties":{"id":{"type":"string","format":"uuid"},"org":{"type":"string","format":"uuid"},"asset_id":{"type":"string","format":"uuid"},"created_at":{"type":"string","format":"date-time"},"is_current":{"type":"boolean"},"hour_meter":{"type":"number","nullable":true},"gps_lat":{"type":"number","nullable":true},"gps_long":{"type":"number","nullable":true},"data_source":{"type":"string","nullable":true},"data_ref_id":{"type":"string","nullable":true},"data_blob":{"type":"object","nullable":true,"description":"Raw payload as received from the provider"},"confidence":{"type":"number","nullable":true},"gps_reported_at":{"type":"string","format":"date-time","nullable":true,"description":"Device-side timestamp of the GPS fix, read out of data_blob (the gps entry of readings, else the trailerStats or vehicle gps summary). Null when the payload carries no device timestamp. Not the same as created_at, which is when we ingested the reading.\n"},"data_health":{"type":"string","nullable":true,"enum":["GREEN","YELLOW","RED"],"description":"Staleness of gps_reported_at against server time. GREEN within 48 hours, YELLOW from 48 hours to 7 days, RED beyond 7 days. Null when gps_reported_at is null, which the frontend renders as no icon.\n"}}},"asset_depreciation_schedule_link":{"type":"object","description":"A depreciation schedule attached to the asset, with the state of its calculation.","properties":{"schedule_id":{"type":"string","format":"uuid"},"asset_depreciation_schedule_id":{"type":"string","format":"uuid"},"source_entity":{"type":"string","nullable":true},"calculation_status":{"type":"string","nullable":true},"error_message":{"type":"string","nullable":true},"starting_value":{"type":"number","nullable":true},"starting_date":{"type":"string","format":"date-time","nullable":true},"name":{"type":"string"},"type":{"type":"string"},"method":{"type":"string"},"term":{"type":"number","nullable":true}}},"extra_asset_information":{"type":"object","description":"Computed on every asset read - GET /assets/{id} (and the create/update echo) and each POST /assets/search row.","properties":{"dispatched":{"type":"boolean","description":"True while a live Lotus dispatch holds the asset (DISPATCHED, OFF_RENT or OFF_RENT_INSPECTION)."},"order_status":{"type":"string","nullable":true,"description":"Order-level status (lotus_orders.status - QUOTE / RESERVATION / OPEN_CONTRACT ...) of the most-forward active rental order the asset appears on. Populated for quotes/reservations too, unlike current_rental_status."},"inspections":{"type":"array","description":"Lotus inspections currently in flight against the asset, newest first. Excludes completed (DONE) inspections and the HIDDEN OFF_RENT placeholders minted alongside each DISPATCH inspection - this is not an inspection history. Always an array, never null.","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"type":{"type":"string","enum":["DISPATCH","OFF_RENT","SALE","INDEPENDENT"]},"status":{"type":"string","description":"Any non-DONE inspection status"},"due_date":{"type":"string","format":"date-time","nullable":true}}}},"blocks_new_standalone_inspection":{"type":"boolean","description":"True when the asset already carries an in-flight stand-alone inspection (type INDEPENDENT, status not DONE), so POST /v1/lotus/inspections/create would reject it. Mirrors that gate exactly - the FE should bind its disabled state to this rather than inferring one from the inspections array."},"can_delete":{"type":"boolean","description":"False when the asset is still attached to at least one open artifact, in which case DELETE /v1/assets/{id} will reject it. Bind the delete control's disabled state to this rather than inferring one from deletion_blockers."},"delete_blocked_reasons":{"type":"array","description":"Ready-to-render sentences explaining why can_delete is false, one per non-empty class of deletion_blockers. Empty when can_delete is true. The same array is returned under details on the delete 400.","items":{"type":"string"}},"deletion_blockers":{"type":"object","description":"Every open artifact currently holding this asset, keyed by class. Each value is always an array, never null. Open means - rental up to and including CLOSED_CONTRACT (only COMPLETED_CONTRACT releases), sale QUOTE or OPEN_SALES_ORDER, work order not yet WORK_COMPLETED/DONE, inspection not yet DONE, New Asset Detail Checklist not yet DONE. Deliberately excluded - expired quotes (past valid_until) and ETL-imported historical orders, since neither is navigable in the UI. Use the ids to deep-link. Inspections have no order_number and are linked by id.","properties":{"rental":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"order_number":{"type":"string"},"status":{"type":"string","enum":["QUOTE","RESERVATION","OPEN_CONTRACT","CLOSED_CONTRACT"]},"dispatch_status":{"type":"string","nullable":true,"description":"Status of the live dispatch tying this asset to the order. Null on a QUOTE, which has no dispatch layer yet."}}}},"sale":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"order_number":{"type":"string"},"status":{"type":"string","enum":["QUOTE","OPEN_SALES_ORDER"]}}}},"work_order":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"order_number":{"type":"string"},"status":{"type":"string"},"due_date":{"type":"string","format":"date-time","nullable":true}}}},"inspection":{"type":"array","description":"Subset of the inspections array above - inspections whose parent order is an imported historical one are omitted here.","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"type":{"type":"string","enum":["DISPATCH","OFF_RENT","SALE","INDEPENDENT"]},"status":{"type":"string"},"due_date":{"type":"string","format":"date-time","nullable":true}}}},"nadc":{"type":"array","description":"Open New Asset Detail Checklists (DOZ-6923) - any status but DONE. Deep-link with GET /v1/lotus/nadc/{id}.","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["PENDING","READY","IN_PROGRESS","WORK_COMPLETED"]},"due_date":{"type":"string","format":"date-time","nullable":true}}}}}},"category_details":{"type":"object","description":"The asset's category. maintenance_meter_hours, maintenance_task_level and inherited are present on GET /assets/{id} only.","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"category_id":{"type":"string","format":"uuid","nullable":true},"super_category_id":{"type":"string","format":"uuid","nullable":true},"add_on":{"type":"boolean"},"child_attachments":{"type":"array","items":{"type":"string","format":"uuid"}},"parent_categories":{"type":"array","items":{"type":"string","format":"uuid"}},"maintenance_meter_hours":{"type":"number","nullable":true},"maintenance_task_level":{"type":"string","nullable":true},"inherited":{"type":"object","nullable":true,"description":"What the category inherits from its parents (form templates, rates)."}}},"active_tasks":{"type":"array","description":"Open work orders on the asset (status not WORK_COMPLETED, DONE or CANCELLED).","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"order_number":{"type":"string"},"status":{"type":"string"},"due_date":{"type":"string","format":"date-time","nullable":true},"expected_start_date":{"type":"string","format":"date-time","nullable":true},"start_date":{"type":"string","format":"date-time","nullable":true}}}},"samsara_details":{"nullable":true,"description":"Present only when the SAMSARA_GPS module is enabled for the org - the key is omitted entirely otherwise. Null when the module is on but the asset has no current reading.\n","allOf":[{"$ref":"#/components/schemas/samsara_details"}]}}},"asset_detail":{"description":"What GET /assets/{id} returns, and what POST /assets and POST /assets/{id} echo back after writing.","allOf":[{"$ref":"#/components/schemas/asset"},{"$ref":"#/components/schemas/extra_asset_information"},{"type":"object","properties":{"computed_name":{"type":"string","description":"Preview of the auto-generated name from the org's configured fields, ignoring name_override."},"depreciation_schedules":{"type":"array","items":{"$ref":"#/components/schemas/asset_depreciation_schedule_link"}},"open_nadc":{"type":"object","nullable":true,"description":"The asset's open New Asset Detail Checklist (DOZ-6923), or null. At most one can be open per asset; DELETE /v1/assets/{id} is blocked while it exists. Link to GET /v1/lotus/nadc/{id}.","properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["PENDING","READY","IN_PROGRESS","WORK_COMPLETED"]},"assignee":{"type":"string","format":"uuid","nullable":true},"due_date":{"type":"string","format":"date-time","nullable":true}}},"has_active_inspection":{"type":"boolean","description":"Any non-DONE Lotus inspection on the asset, hidden placeholders included. Looser than inspections and not the create gate - bind that to blocks_new_standalone_inspection."},"created_by_details":{"type":"object","nullable":true,"properties":{"id":{"type":"string","format":"uuid"},"username":{"type":"string"}}},"po_details":{"type":"object","nullable":true,"description":"The linked purchase order, when purchase_order is set.","properties":{"id":{"type":"string","format":"uuid"},"po_number":{"type":"string"}}},"purchase_details":{"type":"array","description":"Ordered by weight.","items":{"$ref":"#/components/schemas/asset_purchase_detail"}},"sale_inspection_details":{"type":"object","nullable":true,"description":"The newest SALE inspection on the asset.","properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string"},"passed":{"type":"boolean","nullable":true},"quick_pass":{"type":"boolean","nullable":true}}},"facility_details":{"type":"object","nullable":true,"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"}}}}}]},"asset_search_row":{"description":"One row of POST /assets/search.","allOf":[{"$ref":"#/components/schemas/asset"},{"$ref":"#/components/schemas/extra_asset_information"},{"type":"object","properties":{"has_inspection_template":{"type":"boolean","description":"True when an inspection form template resolves for the asset's category (org default, category link or inherited)."},"sales_order_details":{"type":"object","nullable":true,"description":"The newest Lotus sale order the asset is on.","properties":{"order_number":{"type":"string"},"status":{"type":"string"},"customer_name":{"type":"string"},"customer_number":{"type":"string"}}},"pending_tasks_count":{"type":"integer","description":"Open work orders on the asset still in PENDING."}}}]},"asset_search_body":{"type":"object","required":["search","paginate"],"properties":{"search":{"type":"object","description":"Every filter is optional; null or empty means no filter.","properties":{"name":{"type":"string","nullable":true},"asset_number":{"type":"string","nullable":true},"serial_number":{"nullable":true,"description":"Substring match. Send false to match only assets with no serial number.","oneOf":[{"type":"string"},{"type":"boolean"}]},"manufacturer":{"type":"string","nullable":true},"category":{"type":"array","items":{"type":"string","format":"uuid"}},"facility":{"type":"string","format":"uuid","nullable":true},"type":{"type":"array","items":{"type":"string","enum":["ADD_ON","NORMAL"]}},"add_on":{"type":"boolean","nullable":true},"transaction_group":{"type":"array","items":{"type":"string","enum":["sale","rental"]}},"transaction_group_exact_match":{"type":"boolean","nullable":true},"checklist_status":{"type":"array","items":{"type":"string","enum":["onboarding","onboarded","selling","sale_approved"]}},"validation_failure":{"type":"array","items":{"type":"string"}},"on_rent":{"type":"boolean","nullable":true},"out_of_service":{"type":"boolean","nullable":true},"stock":{"type":"boolean","nullable":true},"show_sold":{"type":"boolean","nullable":true},"show_sibling_assets":{"type":"boolean","nullable":true},"zero_cost_rentals":{"type":"boolean","nullable":true},"deleted":{"type":"boolean","nullable":true,"description":"Search deleted assets instead of live ones"},"purchase_order":{"description":"A PO id. true / false pass validation but currently filter nothing - the handler's length guard returns before its boolean branches.","nullable":true,"oneOf":[{"type":"string","format":"uuid"},{"type":"boolean"}]},"sales_customer":{"type":"string","format":"uuid","nullable":true},"order_by":{"description":"Sortable columns: asset_number (by its leading number, then the text), name, serial_number, model_year (text; blank sorts as empty), sell_price, hour_meter. Default order without it: the rental-history metric, then id. With a highlighted asset_number search the matched asset still comes first.\n","allOf":[{"$ref":"#/components/schemas/order_by"}]}}},"paginate":{"$ref":"#/components/schemas/paginate"}}},"asset_search_result":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/asset_search_row"}},"pagination":{"$ref":"#/components/schemas/pagination"},"orderByColumnNamesList":{"$ref":"#/components/schemas/orderByColumnNamesList"}}},"asset_details_template":{"type":"object","description":"An asset's Asset Details answers, one row per asset (asset_details_form_templates).","required":["asset","template"],"properties":{"id":{"type":"string","format":"uuid"},"asset":{"type":"string","format":"uuid"},"template":{"type":"object","description":"The filled-in form. Its items array holds one entry per question.","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"description":{"type":"string"},"item_type":{"type":"string","description":"Item type name, e.g. TEXT. An old integer is refused (400)."},"value":{"description":"The answer, typed by item_type"},"required":{"type":"boolean"},"weight":{"type":"number","nullable":true},"elements":{"nullable":true,"description":"Options for a choice item"},"template_details":{"type":"array","description":"The form templates (and their category level/scope) that contributed the item","items":{"type":"object"}}}}}}},"archived":{"type":"boolean"},"dispatch_id":{"type":"string","format":"uuid"},"created_at":{"type":"string","format":"date-time"},"org":{"type":"string","format":"uuid","nullable":true,"description":"Ignored on write - the caller's org is always stored."}}},"auth_session":{"type":"object","description":"The signed-in session, built the same way by POST /login, POST /otp/login and POST /login/refresh. Send `jwt` as `Authorization: Bearer <jwt>`.\n","required":["jwt","data","roles","permissions","customer_id","user_id","company_name","user_discounts","max_discount","userPreferences","orgDetails","userDetails"],"properties":{"jwt":{"type":"string","description":"The login token (HS256). Valid until `data`, or until logout or refresh."},"data":{"type":"integer","format":"int64","description":"When the token expires, in epoch milliseconds.","example":1761480000000},"roles":{"type":"array","description":"The user's roles; empty when the user has none.","items":{"$ref":"#/components/schemas/auth_named_ref"}},"permissions":{"type":"array","description":"Every permission of the user's roles, minus those belonging to a module the org has not enabled. Not de-duplicated: a permission two roles grant appears twice. Empty when the user has no roles.\n","items":{"$ref":"#/components/schemas/auth_named_ref"}},"customer_id":{"type":"string","format":"uuid","nullable":true,"description":"Set for a customer-portal user; null for staff."},"company_name":{"type":"string","nullable":true,"description":"The portal user's customer company name; null for staff."},"user_id":{"type":"string","format":"uuid"},"user_discounts":{"type":"array","description":"The discounts the org's FEES.ROLES_DISCOUNTS setting gives the user's roles.","items":{"type":"number"}},"max_discount":{"type":"number","description":"The largest of user_discounts; 0 when there is none."},"userPreferences":{"allOf":[{"$ref":"#/components/schemas/user_preference_settings"}],"description":"The stored preferences, with any missing or null key filled from the shipped defaults. A row saved by an older client can also hold retired top-level keys (activeAssets, unpaidInvoices, dashBoardWidgets, ...); they are returned as stored, and saving the preferences back with them is refused ('\"x\" is not allowed').\n"},"orgDetails":{"$ref":"#/components/schemas/auth_org_details"},"userDetails":{"$ref":"#/components/schemas/auth_user_details"}}},"auth_named_ref":{"type":"object","required":["id","name"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","nullable":true}}},"auth_user_details":{"type":"object","description":"These three keys only; no other users column (password, override_pin, phone, ...) is sent.\n","required":["username","name","mobile_log_2xx"],"properties":{"username":{"type":"string","format":"email","nullable":true},"name":{"type":"string","nullable":true},"mobile_log_2xx":{"type":"boolean","nullable":true,"description":"Tells the mobile app to upload its successful-request logs too, not only failures."}}},"auth_org_details":{"description":"The user's organizations row as platform_org describes it (stripe_private_key and stripe_webhook_secret are never sent; rental_app_document is a setting and is not on the row), with `modules` and two login-only extras. Org settings are not here: read them with GET /lotus/platform-settings.\n","allOf":[{"$ref":"#/components/schemas/platform_org"},{"type":"object","required":["facilities_count","modules","depreciation_schedules"],"properties":{"facilities_count":{"type":"integer","description":"The org's facilities that are not deleted."},"depreciation_schedules":{"type":"array","description":"Every depreciation schedule of the org, deleted ones included, ordered by method, type and term. Empty when the org's ASSET.ENABLE_DEPRECIATION setting is false.\n","items":{"$ref":"#/components/schemas/auth_depreciation_schedule"}}}}]},"auth_depreciation_schedule":{"type":"object","description":"A depreciation_schedules row.","required":["id","org","name","description","method","type","term","calculation_window_days","convention","deleted","created_at","updated_at"],"properties":{"id":{"type":"string","format":"uuid"},"org":{"type":"string","format":"uuid"},"name":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"method":{"type":"string","example":"STRAIGHT_LINE"},"type":{"type":"string","example":"INTERNAL_VALUE"},"term":{"type":"number","example":5},"calculation_window_days":{"type":"integer","example":30},"convention":{"type":"string","example":"FULL_MONTH"},"deleted":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"auth_reset_token_verdict":{"type":"object","description":"The `{ value, message }` answer of the reset-link checks.","required":["value","message"],"properties":{"value":{"type":"boolean","description":"True only when the link is valid (and, on reset, the password was written)."},"message":{"type":"string"}}},"category_rental_rates":{"type":"object","nullable":true,"description":"Rates per period; each above -1 (so 0 passes). Any other key is refused. Null on a category means it takes its parents' rates (`inherited.rental_rates`).\n","properties":{"day":{"type":"number","minimum":-1,"exclusiveMinimum":true},"week":{"type":"number","minimum":-1,"exclusiveMinimum":true},"4week":{"type":"number","minimum":-1,"exclusiveMinimum":true}}},"category_form_template_ref":{"type":"object","description":"One form template to attach. Any other key is refused.\n","required":["id","scope"],"properties":{"id":{"type":"string","format":"uuid","description":"A form template of the org; never its New Asset Detail Checklist template."},"scope":{"type":"string","enum":["GENERIC","RENTAL","SALE","INDEPENDENT","PURCHASE"],"description":"Which inspections the template applies to. The retired PURCHASE is still accepted, and the entry is dropped before anything is saved.\n"},"weight":{"type":"integer","minimum":0,"nullable":true,"description":"The template's place on the category's list, 0 at the top; the top template wins where two carry the same question. Send it on every entry or on none.\n"},"type":{"type":"string","description":"Accepted and ignored (the type is read from the template)."},"description":{"type":"string","description":"Accepted and ignored."}}},"category_fields":{"type":"object","description":"The writable columns. Level: no parent ids makes a super category, `super_category_id` alone a category, both a sub-category (`category_id` is the category). `gl_number` and `cogs_number` are text (DOZ-8180): stored as typed, trimmed; a JSON number is accepted and stored as its digits; a blank string or null clears the field. The integer columns `weight`, `maintenance_meter_hours` and `maintenance_days` pass Joi as any number, and a fraction is then a 400 \"Invalid data type provided\"; `inspection_days` is an integer to Joi, so a fraction there is the Joi 400 \"inspection_days\" must be an integer.\n","properties":{"name":{"type":"string","minLength":1,"example":"Scissor Lifts"},"gl_number":{"type":"string","nullable":true,"maxLength":50,"example":"4700.10","description":"General ledger account number. Text, stored as typed (trimmed): \"4700.10\" keeps its trailing zero and \"12-4700\" is valid. A JSON number is accepted and stored as its digits; a blank string or null clears it. Over 50 characters is a 400 (\"gl_number\" length must be less than or equal to 50 characters long); any other type is a 400 (\"gl_number\" must be one of [string, number]).\n"},"cogs_number":{"type":"string","nullable":true,"maxLength":50,"example":"5000","description":"Cost of goods sold account number, same rules as gl_number."},"category_id":{"type":"string","format":"uuid","nullable":true,"description":"The parent category, on a sub-category."},"super_category_id":{"type":"string","format":"uuid","nullable":true,"description":"The super category, on a category or sub-category."},"org":{"type":"string","format":"uuid","nullable":true,"description":"Accepted and ignored; the session's org is used."},"rental_rates":{"$ref":"#/components/schemas/category_rental_rates"},"hidden":{"type":"boolean","description":"Recomputed for every category of the org by each batch, merge and delete: hidden when no active, in-stock asset is under the category, shown otherwise. A value set here lasts until the next one.\n"},"weight":{"type":"integer","nullable":true,"description":"Sort order among its siblings, ascending."},"maintenance_meter_hours":{"type":"integer","minimum":0,"nullable":true},"maintenance_days":{"type":"integer","minimum":0,"nullable":true},"maintenance_task_level":{"type":"string","format":"uuid","nullable":true,"description":"A work order level id; not checked."},"inspection_days":{"type":"integer","minimum":0,"nullable":true},"financial_utilization_benchmark_percent":{"type":"number","minimum":0,"maximum":1000,"nullable":true,"description":"The node's own yearly dollar-utilization target in percent of OEC (DOZ-7813). Null = inherit from the parent node, else the org's financial-utilization benchmark setting.\n"}}},"category_write_fields":{"type":"object","description":"The fields a write takes in another shape than the stored record returns: the link lists (ids in, objects out for schedules) and `base_tree` (`null` is refused, though stored rows hold it). Each list replaces the stored one when sent and is left alone when omitted. Attachment links (`child_attachments`, `parent_categories`) are sub-category only, and are refused with \"Attachments and parent categories can only be assigned to a sub-category.\", \"Non Attachment categories cannot have parent categories.\", \"Attachment categories cannot have child attachments.\", \"All child attachments must be attachment sub categories.\" or \"All parent categories must be non-attachments sub categories.\".\n","properties":{"base_tree":{"type":"boolean"},"form_templates":{"type":"array","uniqueItems":true,"description":"Template ids must be unique. Templates no longer listed are detached.\n","items":{"$ref":"#/components/schemas/category_form_template_ref"}},"depreciation_schedules":{"type":"array","description":"Depreciation schedule ids. Not checked: an id that is not one of the org's schedules is stored and never shown.\n","items":{"type":"string","format":"uuid"}},"child_attachments":{"type":"array","description":"On a non-attachment sub-category, the attachment sub-categories offered with it.\n","items":{"type":"string","format":"uuid"}},"parent_categories":{"type":"array","description":"On an attachment sub-category, the non-attachment sub-categories it is offered with.\n","items":{"type":"string","format":"uuid"}}}},"category_create":{"description":"Create body. Any key not listed is refused (\"<key>\" is not allowed).\n","allOf":[{"$ref":"#/components/schemas/category_fields"},{"$ref":"#/components/schemas/category_write_fields"},{"type":"object","required":["name","add_on"],"properties":{"add_on":{"type":"boolean","description":"Whether this is an attachment category. On a category or sub-category it is overwritten with its super category's.\n"}}}],"example":{"name":"Mini Excavators","add_on":false,"super_category_id":"bbe71148-9d59-44ae-ac9c-b7f9829d2901","category_id":"090029f7-fbe5-4640-8f99-7deac339dfc3","gl_number":"4100"}},"category_update":{"description":"Update body. `add_on` is refused (\"add_on\" is not allowed), and so is any key not listed. `category_id`, `super_category_id` and `org` are accepted and dropped.\n","allOf":[{"$ref":"#/components/schemas/category_fields"},{"$ref":"#/components/schemas/category_write_fields"},{"type":"object","required":["id","name"],"properties":{"id":{"type":"string","format":"uuid"}}}],"example":{"id":"49359a5e-30ba-4a01-9908-5ff75c88e9af","name":"M-Category"}},"category_merge":{"description":"Merge body: the new category's settings plus the two categories to merge. Any key not listed is refused. `org` is accepted and ignored; the parent ids are read as described on the operation.\n","allOf":[{"$ref":"#/components/schemas/category_fields"},{"$ref":"#/components/schemas/category_write_fields"},{"type":"object","required":["name","add_on","categories_to_be_merged"],"properties":{"add_on":{"type":"boolean","description":"Must equal the merged categories' add_on."},"categories_to_be_merged":{"type":"array","minItems":2,"maxItems":2,"uniqueItems":true,"items":{"type":"string","format":"uuid"}}}}],"example":{"name":"Compact Equipment","add_on":false,"categories_to_be_merged":["0180737d-0c27-456b-92ec-9a42418aabf2","c254a782-6a6f-46f4-bd91-99a6c9b39dbc"],"category_id":"b42bcbf9-54fc-49a5-9977-7ecfac71e4a6"}},"category_batch_item":{"type":"object","description":"One category to change. Any other key is refused. `id` is optional to Joi, but an item without one fails the whole batch with a 400 (a known issue, see the operation).\n","properties":{"id":{"type":"string","format":"uuid"},"parent_id":{"type":"string","format":"uuid","nullable":true,"description":"The new parent: a category for a sub-category, a super category for a category. Null, or the current parent, moves nothing.\n"},"weight":{"type":"integer","nullable":true,"description":"Omitted keeps the stored value; null clears it."},"gl_number":{"type":"string","nullable":true,"maxLength":50,"example":"4700.10","description":"Omitted keeps the stored value; null or a blank string clears it. Text, stored as typed (trimmed); a JSON number is accepted and stored as its digits.\n"}}},"category_inherited_template":{"type":"object","description":"A template a parent attached, as cached on the child.","properties":{"id":{"type":"string","format":"uuid"},"type":{"type":"string","enum":["inspection","asset_details"]},"scope":{"type":"string"},"description":{"type":"string"},"weight":{"type":"integer","description":"Its place on the parent's list."},"source_id":{"type":"string","format":"uuid","description":"The parent that attached it."},"source_level":{"type":"string","enum":["SUPER_CAT","CAT"]}}},"category_inherited":{"type":"object","nullable":true,"readOnly":true,"description":"What a category takes from its parents: for each field, the nearest parent's non-empty value (category first, then super category), null (or [] for a list) when none sets one; `form_templates` is the union of the parents' templates. A cache, written when the category is created, moved (batch) or merged, and when one of its parents is saved; saving the category itself does not rewrite it. So a row not touched since an older release can be null or carry fewer or older keys (`parent_assets`). `add_on` is never inherited. On a super category it is null, or every value is null or [].\n","properties":{"gl_number":{"type":"string","nullable":true,"description":"The parent's gl_number as text (older copies were converted)."},"cogs_number":{"type":"string","nullable":true},"hidden":{"type":"boolean","nullable":true},"child_attachments":{"type":"array","nullable":true,"items":{"type":"string","format":"uuid"}},"parent_categories":{"type":"array","nullable":true,"items":{"type":"string","format":"uuid"}},"rental_rates":{"$ref":"#/components/schemas/category_rental_rates"},"maintenance_meter_hours":{"type":"integer","nullable":true},"maintenance_days":{"type":"integer","nullable":true},"maintenance_task_level":{"type":"string","format":"uuid","nullable":true},"price_per_asset":{"type":"number","nullable":true},"inspection_days":{"type":"integer","nullable":true},"financial_utilization_benchmark_percent":{"type":"number","nullable":true,"description":"The nearest ancestor's own benchmark, or null when no ancestor sets one"},"depreciation_schedules":{"type":"array","nullable":true,"items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"}}}},"form_templates":{"type":"array","items":{"$ref":"#/components/schemas/category_inherited_template"}},"parent_assets":{"type":"array","deprecated":true,"description":"Only on caches written before `parent_categories` replaced it; always empty.\n","items":{"type":"string","format":"uuid"}}}},"category":{"description":"A stored category: the columns of the `categories` row, server-set ones readOnly. Every reader returns it with its links (`category_detail`).\n","allOf":[{"$ref":"#/components/schemas/category_fields"},{"type":"object","required":["id","name","org","category_id","super_category_id","add_on","hidden","use_count","assignment_count","inherited"],"properties":{"id":{"type":"string","format":"uuid","readOnly":true},"name":{"type":"string"},"org":{"type":"string","format":"uuid","readOnly":true},"add_on":{"type":"boolean","description":"Whether this is an attachment category. Set on create, or on the category a merge creates; an update can't change it.\n"},"base_tree":{"type":"boolean","nullable":true},"use_count":{"type":"integer","readOnly":true,"description":"Active, in-stock assets in this category and below. Recomputed for the whole org by every batch, merge and delete, and stepped by asset writes.\n"},"assignment_count":{"type":"integer","readOnly":true,"description":"Every asset in this category and below, deleted ones included."},"price_per_asset":{"type":"number","nullable":true,"readOnly":true,"description":"Not written by these routes."},"inherited":{"$ref":"#/components/schemas/category_inherited"}}}]},"category_detail":{"description":"A category as the readers return it: the row plus the templates, depreciation schedules and attachment links attached to it. Never carries the legacy `parent_assets` column.\n","allOf":[{"$ref":"#/components/schemas/category"},{"type":"object","required":["form_templates","depreciation_schedules","child_attachments","parent_categories"],"properties":{"form_templates":{"type":"array","description":"The templates attached to this category, top of the list first (by `weight`, then the highest id).\n","items":{"type":"object","required":["id","type","scope","description","weight"],"properties":{"id":{"type":"string","format":"uuid"},"type":{"type":"string","enum":["inspection","asset_details"]},"scope":{"type":"string","enum":["GENERIC","RENTAL","SALE","INDEPENDENT"]},"description":{"type":"string"},"weight":{"type":"integer"}}}},"depreciation_schedules":{"type":"array","items":{"type":"object","required":["id","name"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"}}}},"child_attachments":{"type":"array","description":"The attachment sub-categories offered with this one (from the attachment links; empty unless it is a non-attachment sub-category).\n","items":{"type":"string","format":"uuid"}},"parent_categories":{"type":"array","description":"The sub-categories this attachment sub-category is offered with (empty unless it is an attachment sub-category).\n","items":{"type":"string","format":"uuid"}}}}]},"category_tree_node":{"description":"A category in the tree. Super categories and categories always carry `children` (possibly empty); sub-categories never do.\n","allOf":[{"$ref":"#/components/schemas/category_detail"},{"type":"object","properties":{"children":{"type":"array","items":{"$ref":"#/components/schemas/category_tree_node"}}}}]},"category_template_source":{"type":"object","required":["id","description","type"],"description":"A template a merged question came from.","properties":{"id":{"type":"string","format":"uuid"},"description":{"type":"string"},"type":{"type":"string","enum":["inspection","asset_details"]},"scope":{"type":"string","description":"The template's scope on this category. Present only when the template is attached to this category itself (not for the org default or an inherited one).\n"},"level":{"type":"string","enum":["SUPER_CAT","CAT","SUB_CAT"],"description":"As `scope`, the level of this category's own attachment."}}},"category_template_item":{"type":"object","description":"One merged question. `weight`, `required` and `block` come from the link on the nearest template that carries it; `template_details` lists every template it appeared on, in walk order.\n","required":["id","description","item_type","elements","required","weight","block","template_details"],"properties":{"id":{"type":"string","format":"uuid"},"description":{"type":"string"},"item_type":{"type":"string","enum":["TEXT","NUMBER","DROPDOWN","PASS_FAIL","YES_NO","YES_NO_AND_GAUGE","CHECK_AND_GAUGE","DAMAGE_AND_PHOTO","GAUGE_AND_PHOTO"]},"elements":{"type":"array","nullable":true,"description":"The options of a DROPDOWN question; null or [] on other types.","items":{"type":"string"}},"required":{"type":"boolean","nullable":true},"weight":{"type":"integer","nullable":true,"description":"Order within its template; null sorts after the weighted ones."},"block":{"type":"string","description":"The question's block (DOZ-7591), one of the keys from GET /v1/form_template/blocks.\n"},"template_details":{"type":"array","items":{"$ref":"#/components/schemas/category_template_source"}}}},"category_template_disagreement":{"type":"object","required":["template_type","item","differs_on","templates"],"properties":{"template_type":{"type":"string","enum":["inspection","asset_details"]},"item":{"type":"object","required":["id","description","item_type"],"properties":{"id":{"type":"string","format":"uuid"},"description":{"type":"string"},"item_type":{"type":"string","description":"The item's type name, e.g. PASS_FAIL"}}},"differs_on":{"type":"array","items":{"type":"string","enum":["block","required"]}},"templates":{"type":"array","items":{"type":"object","required":["id","description","scope","weight","block","required"],"properties":{"id":{"type":"string","format":"uuid"},"description":{"type":"string"},"scope":{"type":"string"},"weight":{"type":"integer"},"block":{"type":"string"},"required":{"type":"boolean"}}}}}},"checklist":{"type":"object","description":"A stored checklist (one sign-off section). Read only through the API - checklists are provisioned outside it.\n","properties":{"id":{"type":"string","format":"uuid","readOnly":true},"name":{"type":"string","readOnly":true,"example":"Accounting"},"type":{"type":"string","enum":["Buy","Sale"],"readOnly":true,"description":"Buy - signed off when an asset is onboarded. Sale - signed off when it is sold."},"deleted":{"type":"boolean","readOnly":true,"description":"Always false on GET /platform/checklists, which lists live checklists only."},"org":{"type":"string","format":"uuid","readOnly":true},"sign_off_permission":{"type":"string","nullable":true,"readOnly":true,"description":"Name of the permission a user must hold to sign off this checklist section. Null means no permission is required. Written outside the API.\n","example":"CHECKLIST_BUY_ACCOUNTING_SIGN_OFF"}}},"checklist_detail":{"description":"A checklist as GET /platform/checklists returns it, with its item tree.","allOf":[{"$ref":"#/components/schemas/checklist"},{"type":"object","properties":{"items":{"type":"array","description":"The checklist's live top-level items (parent_id null). Each carries its live children in `items`; the children carry no `items` of their own.\n","items":{"$ref":"#/components/schemas/checklist_item"}}}}]},"checklist_item_fields":{"type":"object","description":"The item columns both the create and the update accept. checklist_item_create and checklist_item_update add their own keys and required lists on top.\n","properties":{"name":{"type":"string","minLength":1,"description":"The question text. An empty string is refused.","example":"Paint condition"},"setting":{"type":"string","enum":["required","use","omit"],"description":"required - must be answered before the asset's checklist can be validated. use - shown, but optional. omit - not shown on asset checklists by the web app. Defaults to required on create.\n","example":"required"},"input_type":{"type":"string","enum":["text","checkbox","select"],"description":"The kind of question - text (free text answer), checkbox (yes/no), select (one of options). Defaults to text on create.\n","example":"select"},"options":{"type":"array","nullable":true,"uniqueItems":true,"description":"The choices of a select item - required, non-empty and without duplicates when input_type select is sent. Stored as null for every other type: on create always, on update when input_type is sent too. On an update that does not send input_type, the value must still fit the stored type (see the update route).\n","items":{"type":"string"},"example":["Good","Fair","Poor"]},"data_reference":{"type":"string","nullable":true,"enum":["name_override","part_number","manufacturer","model_number","model_year","serial_number","description","category","transaction_group","facility","bulk_item","bought_used","samt","rental_rates","sell_price","inc","repc","tmot","hrot","purchase_price","extra_charges","purchase_date","vendor","order_no","po_number","capitalization","warranty_months","warranty_miles","purchase_details","depreciation_schedules","has_hour_meter","hour_meter","latest_maintenance_date","latest_maintenance_hour_meter","latest_inspection_date","in_service_date","media","",null],"description":"The asset field this item's answer is meant to fill in - a key of GET /platform/checklists/data_references (every key of the full list is accepted, including the ones that list hides for the org). null or '' clears it; '' is stored as sent.\n","example":null}}},"checklist_item_create":{"description":"Body of POST /platform/checklists/items/create. Unknown keys (org included) are refused with 400 (\"x\" is not allowed).\n","allOf":[{"$ref":"#/components/schemas/checklist_item_fields"},{"type":"object","required":["name","checklist_id"],"properties":{"checklist_id":{"type":"string","format":"uuid","description":"The checklist the item belongs to. Stored as sent.","example":"6b1e3f0a-2c4d-4e8f-9a7b-1c2d3e4f5a6b"},"parent_id":{"type":"string","format":"uuid","nullable":true,"description":"The parent item for a child item; null or absent for a top-level item. Stored as sent."},"id":{"type":"string","format":"uuid","description":"Optional client-chosen id; generated when absent. An id already in use answers 409."},"deleted":{"type":"boolean","description":"Optional; defaults to false. true stores the item already deleted."}}}]},"checklist_item_update":{"description":"One element of the POST /platform/checklists/items/update array - the item's id and only the fields to change. Unknown keys (checklist_id, parent_id, deleted, org, items...) are refused with 400.\n","allOf":[{"$ref":"#/components/schemas/checklist_item_fields"},{"type":"object","required":["id"],"properties":{"id":{"type":"string","format":"uuid","description":"The item to update. The validator does not require it, but an element without it fails the batch with 400 (\"Column 'id' is required for batch updates\").\n","example":"3f9c2b7e-8d41-4c6a-9e2f-1b5d7a0c4e88"}}}]},"checklist_item":{"description":"A stored checklist item. The create and update routes return the row without `items`. Asset checklist reads (GET /assets/checklists/{id}) reuse this shape for their items, without checklist_id and org.\n","allOf":[{"$ref":"#/components/schemas/checklist_item_fields"},{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"deleted":{"type":"boolean"},"checklist_id":{"type":"string","format":"uuid"},"parent_id":{"type":"string","format":"uuid","nullable":true,"description":"Null for a top-level item."},"org":{"type":"string","format":"uuid","readOnly":true},"items":{"type":"array","description":"Child items, on the top-level items of GET /platform/checklists (one level, live children only) and on asset checklist items.\n","items":{"$ref":"#/components/schemas/checklist_item"}}}}]},"checklist_data_reference":{"type":"object","properties":{"key":{"type":"string","description":"The value to store in a checklist item's data_reference, and also the key to send on POST /assets/{id} unless storage is endpoint.\n","example":"serial_number"},"name":{"type":"string","description":"Display label.","example":"Serial Number"},"group":{"type":"string","enum":["Identification","Classification","Rates & Pricing","Purchase","Meters & Maintenance","Media"],"description":"Grouping for the picker."},"storage":{"type":"string","enum":["column","related","endpoint"],"description":"column - a real assets column. related - an asset concept kept in another table, still written through the asset update as an array (purchase_details, depreciation_schedules). endpoint - NOT writable through the asset update; media is the only such entry (attachments go through the media upload).\n"}}},"customer_list_pagination":{"description":"The page block of the customer lists (knex-paginate without isLengthAware). total, lastPage, prevPage and nextPage come back on page 1 only and are missing, not null, from page 2 on. prevPage is null on page 1 and nextPage on the last page; with no rows lastPage is 0 and nextPage null. from is the row offset (0 on page 1) and to is from plus this page's row count; perPage and currentPage echo the request. A currentPage sent as a numeric string (\"1\") passes validation but is not the number 1, so even page 1 then comes back without the four totals.\n","allOf":[{"$ref":"#/components/schemas/pagination"}]},"customer_address":{"type":"object","description":"Every key is required when the object is sent; '' is allowed.","required":["street1","street2","city","state","zip"],"properties":{"street1":{"type":"string"},"street2":{"type":"string"},"city":{"type":"string"},"state":{"type":"string"},"zip":{"type":"string"}}},"customer_billing_address":{"type":"object","description":"Every key is required when the object is sent; '' is allowed.","required":["billing_name","street1","street2","city","state","email","phone","zip"],"properties":{"billing_name":{"type":"string"},"street1":{"type":"string"},"street2":{"type":"string"},"city":{"type":"string"},"state":{"type":"string"},"email":{"type":"string","description":"An email address, or ''."},"phone":{"type":"string"},"zip":{"type":"string"}}},"customer_broker_information":{"type":"object","nullable":true,"description":"Every key is required when the object is sent; '' is allowed.","required":["broker_name","agent_name","address","phone","city","state","email","zip"],"properties":{"broker_name":{"type":"string"},"agent_name":{"type":"string"},"address":{"type":"string"},"phone":{"type":"string"},"city":{"type":"string"},"state":{"type":"string"},"email":{"type":"string","description":"An email address, or ''."},"zip":{"type":"string"}}},"customer_fields":{"type":"object","description":"The customer columns a client may write. customer_create and customer_update add their required lists and their differences on top; the stored record (customer) adds the server-set columns. Restricted fields (env_fee_status, damage_waiver_status, the four *_sales_tax_status, discount, terms, cash_discount_percentage, cash_discount_days, credit_limit, credit_limit_action, banned_reason, balance_application_setting) are refused (\"x\" is not allowed) without CUSTOMER_RESTRICTED_UPDATE. banned is not in that list, so staff without CUSTOMER_RESTRICTED_UPDATE can set banned but not banned_reason; a customer-portal session can't change banned (see POST /customers/{id}). The six fee and tax status fields are refused earlier, at any value, without CUSTOMER_UPDATE_FEES (\"Not permitted to update fees & taxes\"). Date fields pass the schema as a number or a digits-only string too (read as milliseconds), but the body is written as sent: a 6- or 8-digit string is stored as a YYMMDD / YYYYMMDD date, and the column refuses anything else of that kind (a 500).\n","properties":{"company_name":{"type":"string","example":"Acme Rentals LLC"},"key":{"type":"string","nullable":true,"description":"Free-text customer key (e.g. an accounting code)."},"address":{"$ref":"#/components/schemas/customer_address"},"billing_address":{"$ref":"#/components/schemas/customer_billing_address"},"status":{"type":"string","nullable":true},"auto_payment":{"type":"boolean"},"org":{"type":"string","format":"uuid","nullable":true,"description":"Set by the server to the caller's org. The create and the update accept it (a uuid or null; anything else is a 400) for the web's round-trip and never write it.\n"},"env_fee_status":{"type":"boolean"},"damage_waiver_status":{"type":"boolean"},"state_sales_tax_status":{"type":"boolean"},"county_sales_tax_status":{"type":"boolean"},"city_sales_tax_status":{"type":"boolean"},"special_sales_tax_status":{"type":"boolean"},"discount":{"type":"number"},"default_site":{"type":"string","format":"uuid","nullable":true,"description":"Not checked on create or update: any uuid is stored, also one that is not a site of this customer. On update, sites: [] sets it to null; a non-empty sites list that leaves the default site out soft-deletes that site and leaves default_site pointing at it.\n"},"default_contact":{"type":"string","format":"uuid","nullable":true,"description":"Not checked on create. On update it is looked up by id alone, so an unknown id, a deleted contact, or another customer's or another org's contact is accepted. The only refusal is a stored contact that fails the contact schema (400 \"Can not set an invalid contact as default contact\"): no name, an email that is null, empty or not a valid address, or a *_email switch stored as null. Neither contacts: [] nor a contacts list that leaves the default contact out clears it.\n"},"terms":{"type":"integer","description":"Payment terms in days."},"cash_discount_percentage":{"type":"number","minimum":0,"maximum":100},"cash_discount_days":{"type":"integer","minimum":0},"credit_limit":{"type":"number","minimum":0,"nullable":true},"credit_limit_action":{"type":"string","enum":["WARN","BLOCK"]},"pay_on_open":{"type":"boolean","description":"True for a pay-on-open (cash) customer, false for an on-account one."},"insurance_liability_option":{"type":"string","enum":["liability_only","liability_&_physical_damage"]},"insurance_policy_option":{"type":"string","enum":["blanket_policy","individual_asset_policy"]},"insurance_carrier":{"type":"string"},"insurance_policy_number":{"type":"string"},"insurance_expiry":{"type":"string","format":"date-time"},"gallagher_verified":{"type":"boolean"},"broker_information":{"$ref":"#/components/schemas/customer_broker_information"},"banned":{"type":"boolean"},"banned_reason":{"type":"string","nullable":true},"balance_application_setting":{"type":"string","enum":["ORDER_ONLY","ORDER_AND_FLOATING","ALL"]},"sales_person":{"type":"string","format":"uuid","nullable":true,"description":"A user of the org whose roles grant ALLOW_ORDER_OWNERSHIP. The lookup does not skip deleted users or deleted roles, and a user whose roles grant no permission at all is answered \"Sales person does not exist\". '' or null means unassigned (stored as null).\n"},"facility":{"type":"string","format":"uuid","nullable":true}}},"customer_create":{"description":"Body of POST /customers. Unknown keys are refused with 400 (\"x\" is not allowed), including blacklisted / blacklist_reason (set those with an update) and every readOnly column of customer; org is accepted and ignored. The org's customer field settings can make more fields required (\"Field '<key>' is required to create a customer\"; see the operation for which keys can meet a requirement).\n","allOf":[{"$ref":"#/components/schemas/customer_fields"},{"type":"object","required":["company_name","pay_on_open"],"properties":{"pay_on_open":{"type":"boolean","example":true},"contacts":{"type":"array","description":"Contacts to create with the customer. An id is refused.","items":{"$ref":"#/components/schemas/customer_contact_fields"}},"sites":{"type":"array","description":"Sites to create with the customer. A site id sent here is inserted as the new site's id (see customer_site_item).\n","items":{"$ref":"#/components/schemas/customer_site_item"}}}}]},"customer_update":{"description":"Body of POST /customers/{id}. Every field is optional and unknown keys are refused with 400 (\"x\" is not allowed). payment_methods and org are accepted and ignored. Every other server-set column (id, customer_number, created_at, ...) is still refused, so the stored record can't be sent back as it is. pay_on_open needs CUSTOMER_CASH_VS_ACCOUNT_UPDATE (400 otherwise); blacklisted / blacklist_reason need CUSTOMER_BLACKLIST_UPDATE (\"x\" is not allowed otherwise). The insurance fields pass the schema with null or '' as well, but the body is written as sent, so the column decides: insurance_liability_option, insurance_policy_option, insurance_carrier and insurance_policy_number store null or '' as sent; insurance_expiry null clears it and '' is a 500; gallagher_verified '' is a 400 \"Invalid data type provided\" and null a 500 (the column is NOT NULL).\n","allOf":[{"$ref":"#/components/schemas/customer_fields"},{"type":"object","properties":{"blacklisted":{"type":"boolean"},"blacklist_reason":{"type":"string","nullable":true,"description":"Required, non-empty, when blacklisted is true."},"contacts":{"type":"array","description":"The full desired list. Items with an id (one of this customer's contacts, else 400 \"Unknown contact id: <id>\") update it, items without one are inserted, every other live contact is soft-deleted, and [] soft-deletes them all. Omit the key to leave contacts alone. A deleted contact of this customer passes the id check: its columns are rewritten from the item, it stays deleted, and no journal row is written. default_contact is not touched here, so [] or a list that leaves the default contact out leaves it pointing at a deleted contact.\n","items":{"allOf":[{"$ref":"#/components/schemas/customer_contact_fields"},{"type":"object","properties":{"id":{"type":"string","format":"uuid"}}}]}},"sites":{"type":"array","description":"The full desired list. Items with an id (one of this customer's sites, see customer_site_item.id) update it, items without one are inserted, and every other live site is soft-deleted. [] soft-deletes them all and sets default_site to null; a non-empty list that leaves the default site out soft-deletes it but leaves default_site pointing at it. Omit the key to leave sites alone.\n","items":{"$ref":"#/components/schemas/customer_site_item"}}}}]},"customer":{"description":"The stored customer record. readOnly columns are set by the server, and the create and the update refuse them (\"x\" is not allowed). org is set by the server too, but both accept and ignore it (see customer_fields). blacklisted and blacklist_reason are not readOnly: the update writes them with CUSTOMER_BLACKLIST_UPDATE.\n","allOf":[{"$ref":"#/components/schemas/customer_fields"},{"type":"object","properties":{"id":{"type":"string","format":"uuid","readOnly":true},"customer_number":{"type":"integer","readOnly":true,"description":"The org's highest customer number plus one, at creation."},"created_at":{"type":"string","format":"date-time","readOnly":true},"updated_at":{"type":"string","format":"date-time","readOnly":true},"deleted":{"type":"boolean","readOnly":true},"stripe_id":{"type":"string","nullable":true,"readOnly":true,"description":"The Stripe customer id, created on the first card setup."},"default_payment_method":{"type":"string","nullable":true,"readOnly":true,"description":"The default card's Stripe payment method id. Set by POST /customers/set_default_payment_method, and by the Stripe webhook on the customer's first card; cleared by POST /customers/detach_payment_method when the detached card is the default.\n"},"validated":{"type":"boolean","readOnly":true,"description":"Result of the last validation against the org's field settings."},"validation_failures":{"allOf":[{"$ref":"#/components/schemas/customer_validation_failures"}],"nullable":true,"readOnly":true},"blacklisted":{"type":"boolean","description":"Refused by the create. Written by POST /customers/{id} (with CUSTOMER_BLACKLIST_UPDATE), set true by the auto-renewal job when a renewal goes ahead while the customer is over its credit limit, and written by the ETL customer import.\n"},"blacklist_reason":{"type":"string","nullable":true,"description":"Refused by the create. Written by POST /customers/{id} (with CUSTOMER_BLACKLIST_UPDATE), by the auto-renewal job when a renewal goes ahead while the customer is over its credit limit (a \"[System] Auto-renewal ...\" reason, appended to the reason of a customer already blacklisted, or written in place of the old one otherwise), and by the ETL customer import.\n"},"media_count":{"type":"integer","readOnly":true},"balance":{"type":"number","readOnly":true},"address":{"allOf":[{"$ref":"#/components/schemas/customer_address"}],"nullable":true},"billing_address":{"allOf":[{"$ref":"#/components/schemas/customer_billing_address"}],"nullable":true},"insurance_liability_option":{"type":"string","nullable":true},"insurance_policy_option":{"type":"string","nullable":true},"insurance_carrier":{"type":"string","nullable":true},"insurance_policy_number":{"type":"string","nullable":true},"insurance_expiry":{"type":"string","format":"date-time","nullable":true},"county_sales_tax_status":{"type":"boolean","nullable":true},"city_sales_tax_status":{"type":"boolean","nullable":true},"special_sales_tax_status":{"type":"boolean","nullable":true}}}]},"customer_detail":{"description":"What GET /customers/{id} and POST /customers return. With CUSTOMER_RESTRICTED_VIEW (GET) or CUSTOMER_RESTRICTED_UPDATE (POST) every customer column; without it only these 28: id, company_name, address, billing_address, created_at, updated_at, pay_on_open, status, deleted, org, key, customer_number, stripe_id, default_payment_method, auto_payment, default_site, default_contact, the five insurance_* fields, gallagher_verified, broker_information, discount, banned, validated, validation_failures. The extras below are always there. POST /customers returns validated and validation_failures as they stood before its own validation (see its 200).\n","allOf":[{"$ref":"#/components/schemas/customer"},{"type":"object","properties":{"sites":{"type":"array","description":"Live sites ([] when none).","items":{"$ref":"#/components/schemas/customer_site"}},"payment_methods":{"type":"array","description":"Live cards ([] when none).","items":{"$ref":"#/components/schemas/customer_payment_method"}},"contacts":{"type":"array","description":"Live contacts ([] when none).","items":{"$ref":"#/components/schemas/customer_contact_summary"}},"active_orders_count":{"type":"integer","description":"Live Lotus orders of the customer not in COMPLETED_CONTRACT, DONE or CLOSED_SALES_ORDER."},"sales_person_details":{"$ref":"#/components/schemas/customer_sales_person_details"},"facility_details":{"type":"object","nullable":true,"description":"The live facility the customer belongs to; null when none.","required":["id","name"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","nullable":true}}}}}]},"customer_search_row":{"description":"One row of POST /customers/search. With CUSTOMER_RESTRICTED_VIEW these 21 customer columns: id, company_name, customer_number, address, validated, env_fee_status, sales_person, damage_waiver_status, the four *_sales_tax_status, terms, default_contact, validation_failures, discount, banned, deleted, blacklisted, blacklist_reason, pay_on_open. Without it these 12: id, company_name, customer_number, address, validated, validation_failures, default_contact, discount, banned, sales_person, deleted, pay_on_open. The extras below are always there.\n","allOf":[{"$ref":"#/components/schemas/customer"},{"type":"object","properties":{"contacts":{"type":"array","description":"Live contacts ([] when none).","items":{"$ref":"#/components/schemas/customer_contact_summary"}},"active_orders_count":{"type":"integer","description":"Live Lotus orders of the customer not in COMPLETED_CONTRACT, DONE or CLOSED_SALES_ORDER."},"sales_person_details":{"$ref":"#/components/schemas/customer_sales_person_details"}}}]},"customer_sales_person_details":{"type":"object","nullable":true,"description":"The assigned sales person; null when none or the user is deleted.","required":["id","name","email","phone"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","nullable":true},"email":{"type":"string","description":"The user's username."},"phone":{"type":"string","nullable":true}}},"customer_validation_entry":{"type":"object","required":["valid","reason"],"properties":{"valid":{"type":"boolean"},"reason":{"type":"string","nullable":true,"description":"Why the check failed, e.g. \"A primary address is required\"; null when valid."},"name":{"type":"string","description":"Display label, e.g. \"Primary Address\". Most failed entries (all but site) carry no name."}}},"customer_validation_failures":{"type":"object","description":"One entry per check. A check runs only when the org's field setting for it is a validation or requirement for the customer's type; the others stay valid.\n","required":["address","billing_address","primary_contact","site","file_rental_app","credit_card","credit_application","insurance_information"],"properties":{"address":{"$ref":"#/components/schemas/customer_validation_entry"},"billing_address":{"$ref":"#/components/schemas/customer_validation_entry"},"primary_contact":{"$ref":"#/components/schemas/customer_validation_entry"},"site":{"$ref":"#/components/schemas/customer_validation_entry"},"file_rental_app":{"$ref":"#/components/schemas/customer_validation_entry"},"credit_card":{"$ref":"#/components/schemas/customer_validation_entry"},"credit_application":{"$ref":"#/components/schemas/customer_validation_entry"},"insurance_information":{"$ref":"#/components/schemas/customer_validation_entry"}}},"customer_site_fields":{"type":"object","properties":{"name":{"type":"string","example":"Main yard"},"description":{"type":"string","nullable":true},"address":{"type":"string","nullable":true},"city":{"type":"string","nullable":true},"state":{"type":"string","nullable":true},"zip":{"type":"string","nullable":true},"tax_exemption_status":{"type":"boolean","example":false},"tax_exemption_number":{"type":"string","nullable":true},"tax_exemption_expiry":{"type":"string","format":"date-time","nullable":true,"description":"Required unless the same body sends tax_exemption_status false. A body without tax_exemption_status needs it too (\"tax_exemption_expiry\" is required). A number or a digits-only string passes the schema (read as milliseconds) but is written as sent: a 6- or 8-digit string is stored as a YYMMDD / YYYYMMDD date, and the column refuses anything else of that kind (a 500).\n"}}},"customer_site_item":{"description":"A site inside a customer create or update body.","allOf":[{"$ref":"#/components/schemas/customer_site_fields"},{"type":"object","required":["name","tax_exemption_status"],"properties":{"id":{"type":"string","format":"uuid","description":"On create, inserted as the new site's id; an id already in use is a 409 (\"Duplicate field value entered for: id. Please use another value.\"). On update, one of this customer's sites (400 \"Unknown site id: <id>\" otherwise). A deleted site of this customer passes that check: its columns are rewritten from the item, it stays deleted, and no journal row is written. Leave it out for a new site.\n"}}}]},"customer_site_update":{"description":"Body of POST /customers/sites/{id}. A partial update, but every body must carry tax_exemption_status false or a tax_exemption_expiry. Unknown keys are refused with 400.\n","allOf":[{"$ref":"#/components/schemas/customer_site_fields"},{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Accepted, but leave it out - any other value is written as the site's own id."}}}]},"customer_site":{"description":"A live site as the customer reads return it.","allOf":[{"$ref":"#/components/schemas/customer_site_fields"},{"type":"object","required":["id","name","description","address","city","state","zip","tax_exemption_status","tax_exemption_number","tax_exemption_expiry"],"properties":{"id":{"type":"string","format":"uuid","readOnly":true},"tax_exemption_status":{"type":"boolean","nullable":true}}}]},"customer_contact_fields":{"type":"object","required":["name"],"description":"The contact columns a client may write. Unknown keys are refused with 400. Each *_email switch can be true only when the same body sends email. created_at and updated_at pass the schema as a number or a digits-only string too (read as milliseconds), but are written as sent: a 6- or 8-digit string is stored as a YYMMDD / YYYYMMDD date, and the column refuses anything else of that kind (a 500).\n","properties":{"name":{"type":"string","example":"Jane Doe"},"title":{"type":"string","nullable":true},"cell_phone":{"type":"string","nullable":true},"office_phone":{"type":"string","nullable":true},"key":{"type":"integer","nullable":true,"description":"The schema takes any number, but the column is an integer: a fractional key such as 1.5 is a 400 \"Invalid data type provided\".\n"},"department":{"type":"string","nullable":true},"fax":{"type":"string","nullable":true},"email":{"type":"string","format":"email","description":"A valid address; null and '' are refused."},"note":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time","nullable":true},"updated_at":{"type":"string","format":"date-time","nullable":true},"new_quotes_email":{"type":"boolean"},"new_reservations_email":{"type":"boolean"},"open_contracts_email":{"type":"boolean"},"closed_contracts_email":{"type":"boolean"},"completed_contracts_email":{"type":"boolean"},"new_invoices_email":{"type":"boolean"},"invoice_payments_email":{"type":"boolean"},"asset_offrents_email":{"type":"boolean"},"statements_email":{"type":"boolean"}}},"customer_contact_update":{"description":"Body of POST /customers/contacts/{id}. name is required; every other field is optional.","allOf":[{"$ref":"#/components/schemas/customer_contact_fields"},{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Accepted, but leave it out - any other value is written as the contact's own id."}}}]},"customer_contact_summary":{"type":"object","description":"A live contact as the customer reads return it (key, department, fax, created_at and updated_at are not included).\n","required":["id","name","title","cell_phone","office_phone","email","note","new_quotes_email","new_reservations_email","open_contracts_email","closed_contracts_email","completed_contracts_email","new_invoices_email","invoice_payments_email","asset_offrents_email","statements_email"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","nullable":true},"title":{"type":"string","nullable":true},"cell_phone":{"type":"string","nullable":true},"office_phone":{"type":"string","nullable":true},"email":{"type":"string","nullable":true},"note":{"type":"string","nullable":true},"new_quotes_email":{"type":"boolean","nullable":true},"new_reservations_email":{"type":"boolean","nullable":true},"open_contracts_email":{"type":"boolean","nullable":true},"closed_contracts_email":{"type":"boolean","nullable":true},"completed_contracts_email":{"type":"boolean","nullable":true},"new_invoices_email":{"type":"boolean","nullable":true},"invoice_payments_email":{"type":"boolean","nullable":true},"asset_offrents_email":{"type":"boolean","nullable":true},"statements_email":{"type":"boolean","nullable":true}}},"customer_payment_method":{"type":"object","description":"A stored card (customer_payment_methods).","required":["id","customer","stripe_id","type","details"],"properties":{"id":{"type":"string","description":"The Stripe payment method id."},"customer":{"type":"string","format":"uuid"},"stripe_id":{"type":"string","description":"The Stripe customer id."},"type":{"type":"string","example":"card"},"details":{"type":"object","description":"Lean card fields for a card (cardBrand, cardExpMonth, cardExpYear, cardFunding, cardLast4, label) plus extraDetails, the rest of Stripe's payment method object.\n"}}},"customer_payment_method_body":{"type":"object","required":["customer_id","payment_method"],"description":"Unknown keys are ignored.","properties":{"customer_id":{"type":"string","format":"uuid","example":"00000000-0000-0000-0000-000000000000"},"payment_method":{"type":"string","description":"The Stripe payment method id.","example":"pm_1Example"}}},"customer_note_fields":{"type":"object","description":"The note fields a client writes, on create and on edit.","properties":{"note_content":{"type":"string","example":"Called about the overdue invoice."},"type":{"type":"string","enum":["service","sales","rental","collection"],"example":"sales"}}},"customer_note_create":{"description":"Body of POST /customers/notes. Unknown keys are refused with 400.","allOf":[{"$ref":"#/components/schemas/customer_note_fields"},{"type":"object","required":["customer_id","note_content","type"],"properties":{"customer_id":{"type":"string","format":"uuid","example":"00000000-0000-0000-0000-000000000000"},"order":{"type":"string","format":"uuid","nullable":true,"description":"A Lotus order the note is about."}}}]},"customer_note_update":{"description":"Body of POST /customers/notes/{id}. Both fields on every edit; unknown keys (order included) are refused with 400.\n","allOf":[{"$ref":"#/components/schemas/customer_note_fields"},{"type":"object","required":["note_content","type"]}]},"customer_note_row":{"type":"object","description":"One row of POST /customers/notes/search. Lists its own fields rather than reusing customer_note_fields: note_content and type can be null on older notes.\n","required":["note_id","customer_id","note_content","type","order","user_id","org","created_at","updated_at","deleted","username","order_number"],"properties":{"note_id":{"type":"string","format":"uuid"},"customer_id":{"type":"string","format":"uuid"},"note_content":{"type":"string","nullable":true,"description":"Null on some older notes (the column allows null; the API always sets it)."},"type":{"type":"string","nullable":true,"description":"service, sales, rental or collection; null on older notes."},"order":{"type":"string","format":"uuid","nullable":true},"user_id":{"type":"string","format":"uuid","nullable":true,"description":"The author."},"org":{"type":"string","format":"uuid"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"deleted":{"type":"boolean"},"username":{"type":"string","nullable":true,"description":"The author's username; null when the note has no author."},"order_number":{"type":"string","nullable":true,"description":"The linked Lotus order's number; null when none."}}},"form_template_item_type":{"type":"string","enum":["TEXT","NUMBER","DROPDOWN","PASS_FAIL","YES_NO","YES_NO_AND_GAUGE","CHECK_AND_GAUGE","DAMAGE_AND_PHOTO","GAUGE_AND_PHOTO"],"description":"The item type, sent and returned as one of these names. The old integers (0-6) are refused with a 400 since DOZ-7740, like any other unknown type. VERIFIED_TEXT (DOZ-7561) and VERIFIED_READING (DOZ-7596) are never form-template items — they exist only on the NADC identity section — and item create and update refuse them too. (GET /form_template/items accepts both as a `type` filter and answers 200 `[]`.)\n","example":"DROPDOWN"},"form_template_item_fields":{"type":"object","properties":{"description":{"type":"string","minLength":1,"description":"The question text.","example":"Trailer Hitch Type"},"item_type":{"$ref":"#/components/schemas/form_template_item_type"},"elements":{"type":"array","nullable":true,"items":{"type":"string"},"description":"The choices of a DROPDOWN: required for one, with at least one entry. Optional and nullable for every other type, and stored as sent (the web sends `[]`). On update, leaving it out keeps the stored value.\n","example":["2 & 5/26\" Ball","2\" Ball","Fifth wheel","Pintle Hitch"]}}},"form_template_item_create":{"description":"Unknown keys are refused. `allowed_form_templates` and `template_id` are accepted and ignored (older clients send them). An `id` is accepted and written as the new item's id (known issue); one that is already a live item's id in the org is a 409 with the duplicate-item message. A deleted item's id is accepted, and the new item takes over that item's template links.\n","allOf":[{"$ref":"#/components/schemas/form_template_item_fields"},{"type":"object","required":["description","item_type"],"properties":{"id":{"type":"string","format":"uuid","description":"Don't send it; when sent it becomes the new item's id, and a live item's id in the org is a 409.\n"},"allowed_form_templates":{"description":"Ignored; derived from item_type."},"template_id":{"description":"Ignored; the column was dropped in DOZ-4903."}}}]},"form_template_item_update":{"description":"The whole item, as on create, plus its id. Unknown keys are refused; `allowed_form_templates` and `template_id` are ignored.\n","allOf":[{"$ref":"#/components/schemas/form_template_item_fields"},{"type":"object","required":["id","description","item_type"],"properties":{"id":{"type":"string","format":"uuid","example":"6f7187b4-d728-4107-a54b-45f2c2a6bb7c"},"allowed_form_templates":{"description":"Ignored; derived from item_type."},"template_id":{"description":"Ignored; the column was dropped in DOZ-4903."}}}]},"form_template_item":{"description":"A stored item (form_template_items.*).","allOf":[{"$ref":"#/components/schemas/form_template_item_fields"},{"type":"object","required":["id","description","item_type","elements","org","deleted","allowed_form_templates"],"properties":{"id":{"type":"string","format":"uuid","readOnly":true},"org":{"type":"string","format":"uuid","readOnly":true},"deleted":{"type":"boolean","readOnly":true,"description":"Always false here; deleted items are never returned."},"allowed_form_templates":{"type":"array","readOnly":true,"items":{"type":"string","enum":["asset_details","inspection","nadc"]},"description":"The template types the item may be linked into, derived from item_type: all three for every type but DAMAGE_AND_PHOTO and GAUGE_AND_PHOTO, which are inspection-only.\n"}}}]},"form_template_item_search_row":{"description":"A row of GET /form_template/items.","allOf":[{"$ref":"#/components/schemas/form_template_item"},{"type":"object","required":["templates"],"properties":{"templates":{"type":"array","description":"The live templates the item is linked into, in no set order.","items":{"$ref":"#/components/schemas/form_template_ref"}}}}]},"form_template_ref":{"type":"object","required":["id","description","type"],"properties":{"id":{"type":"string","format":"uuid"},"description":{"type":"string"},"type":{"type":"string","enum":["asset_details","inspection","nadc"]}}},"form_template_block":{"type":"object","required":["key","label","weight"],"properties":{"key":{"type":"string","example":"GENERAL"},"label":{"type":"string","example":"General"},"weight":{"type":"integer","description":"Render order, ascending.","example":0}}},"form_template_item_link":{"type":"object","description":"One item linked into a template. Unknown keys are refused.","required":["item_id"],"properties":{"item_id":{"type":"string","format":"uuid","description":"A live item of the org whose allowed_form_templates holds the template's type.","example":"8a1a7c38-edee-41b6-84ff-d878f2473706"},"weight":{"type":"integer","minimum":0,"nullable":true,"description":"The item's place in the template. Null sorts last.","example":0},"required":{"type":"boolean","description":"True when left out, on update too: unlike `block`, a link saved without it does not keep its stored value.\n","example":true},"block":{"type":"string","nullable":true,"minLength":1,"description":"A key from GET /form_template/blocks (anything else is a 400). Absent or null keeps the block the item already has on this template, GENERAL on a new link.\n","example":"GENERAL"}}},"form_template_fields":{"type":"object","properties":{"description":{"type":"string","minLength":1,"example":"Truck Hydraulics"},"type":{"type":"string","enum":["asset_details","inspection","nadc"],"description":"nadc is refused on create; on update it can be neither added nor removed.","example":"inspection"}}},"form_template_create":{"description":"Unknown keys are refused. `items` needs at least one entry. Joi accepts an `id` (it becomes the new template's id; a live template's id in the org is a 409 with the duplicate message, while a deleted template's id is accepted and the new template takes over its category attachments) and a nullable `template_id` (the write then 500s); send neither.\n","allOf":[{"$ref":"#/components/schemas/form_template_fields"},{"type":"object","required":["description","type","items"],"properties":{"id":{"type":"string","format":"uuid","description":"Don't send it (known issue); when sent it becomes the new template's id.\n"},"template_id":{"type":"string","format":"uuid","nullable":true,"description":"Don't send it; Joi accepts it but the write then 500s (the column was dropped in DOZ-4903)."},"items":{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/form_template_item_link"}}}}]},"form_template_update":{"description":"The whole template, as on create, plus its id; `items` replaces the stored list. Only the nadc template may send an empty `items`.\n","allOf":[{"$ref":"#/components/schemas/form_template_fields"},{"type":"object","required":["id","description","type","items"],"properties":{"id":{"type":"string","format":"uuid","example":"1cae4a1a-1da7-4518-826a-46c3baeabf1e"},"template_id":{"type":"string","format":"uuid","nullable":true,"description":"Don't send it; Joi accepts it but the write then 500s (the column was dropped in DOZ-4903)."},"items":{"type":"array","items":{"$ref":"#/components/schemas/form_template_item_link"}}}}]},"form_template":{"description":"A stored template (form_templates.*).","allOf":[{"$ref":"#/components/schemas/form_template_fields"},{"type":"object","required":["id","description","type","org","deleted"],"properties":{"id":{"type":"string","format":"uuid","readOnly":true},"org":{"type":"string","format":"uuid","readOnly":true},"deleted":{"type":"boolean","readOnly":true,"description":"Always false here; deleted templates are never returned."}}}]},"form_template_category":{"type":"object","description":"A live category attachment of the template.","required":["id","name","use_count","scope","level"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"use_count":{"type":"integer"},"scope":{"type":"string","enum":["GENERIC","RENTAL","SALE","INDEPENDENT"]},"level":{"type":"string","enum":["SUPER_CAT","CAT","SUB_CAT"]}}},"form_template_question":{"type":"object","description":"One item on the template: the item's own fields plus the link's weight, required flag and block. Sorted by weight, nulls last. Deleted items are left out.\n","required":["id","description","item_type","elements","required","weight","block"],"properties":{"id":{"type":"string","format":"uuid","description":"The item's id."},"description":{"type":"string"},"item_type":{"$ref":"#/components/schemas/form_template_item_type"},"elements":{"type":"array","nullable":true,"items":{"type":"string"}},"required":{"type":"boolean"},"weight":{"type":"integer","nullable":true},"block":{"type":"string"}}},"form_template_search_row":{"description":"A template with its categories and items, as search, create and update return it.","allOf":[{"$ref":"#/components/schemas/form_template"},{"type":"object","required":["categories","items"],"properties":{"categories":{"type":"array","items":{"$ref":"#/components/schemas/form_template_category"}},"items":{"type":"array","items":{"$ref":"#/components/schemas/form_template_question"}}}}]},"form_template_detail":{"description":"GET /form_template/{id}. As a search row, with `used_by` on each item.","allOf":[{"$ref":"#/components/schemas/form_template"},{"type":"object","required":["categories","items"],"properties":{"categories":{"type":"array","items":{"$ref":"#/components/schemas/form_template_category"}},"items":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/form_template_question"},{"type":"object","required":["used_by"],"properties":{"used_by":{"type":"array","description":"The other live templates the item is linked into, sorted by type then description.\n","items":{"$ref":"#/components/schemas/form_template_ref"}}}}]}}}}]},"form_template_search_body":{"type":"object","required":["paginate"],"description":"Only `search` and `paginate` are read; any other top-level key is ignored. Unknown keys inside `search` or `paginate` are refused.\n","properties":{"search":{"type":"object","description":"Optional. Unknown keys are refused.","properties":{"type":{"type":"string","nullable":true,"enum":["asset_details","inspection","nadc","",null],"description":"Only templates of this type. Empty or null means every type but nadc; nadc is returned only when named.\n"},"description":{"type":"string","nullable":true,"description":"Case-insensitive substring of the description (ILIKE `%description%`; a `%` or `_` in it acts as a wildcard). Empty or null means no filter.\n"},"categories":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Templates with a live attachment to any of these categories, or to a category or sub-category under one of them. Empty means no filter.\n"},"order_by":{"$ref":"#/components/schemas/order_by"}}},"paginate":{"$ref":"#/components/schemas/paginate"}},"example":{"search":{"type":"inspection","description":"truck","categories":[]},"paginate":{"perPage":25,"currentPage":1}}},"form_template_search_result":{"type":"object","required":["data","pagination","orderByColumnNamesList"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/form_template_search_row"}},"pagination":{"$ref":"#/components/schemas/pagination"},"orderByColumnNamesList":{"allOf":[{"$ref":"#/components/schemas/orderByColumnNamesList"}],"example":["description","type"]}}},"freight_company_fields":{"type":"object","description":"The writable freight company columns.","properties":{"name":{"type":"string","minLength":1,"example":"Swift Logistics Co."},"no_fee":{"type":"boolean","description":"Marks a carrier whose haul carries no freight fee. A label only: the API never enforces it.\n","example":false}}},"freight_company_create":{"description":"The create body. Any other key is refused (`\"<key>\" is not allowed`).","allOf":[{"$ref":"#/components/schemas/freight_company_fields"},{"type":"object","required":["name"]}],"example":{"name":"Swift Logistics Co.","no_fee":false}},"freight_company_update":{"description":"The update body: the create schema again, so `name` is required and any other key is refused. An omitted `no_fee` keeps its value.\n","allOf":[{"$ref":"#/components/schemas/freight_company_fields"},{"type":"object","required":["name"]}],"example":{"name":"Swift Logistics Co.","no_fee":true}},"freight_company":{"type":"object","description":"A stored freight_companies row.","required":["id","name","org","created_at","updated_at","deleted","no_fee"],"properties":{"id":{"type":"string","format":"uuid","readOnly":true,"example":"e4ddffd5-f67c-462b-9654-fe57df9043c0"},"name":{"type":"string","example":"Swift Logistics Co."},"org":{"type":"string","format":"uuid","readOnly":true,"example":"b67aba8b-9819-4179-b2a1-384968cfa963"},"created_at":{"type":"string","format":"date-time","nullable":true,"readOnly":true},"updated_at":{"type":"string","format":"date-time","nullable":true,"readOnly":true,"description":"Set on create and on every update."},"deleted":{"type":"boolean","nullable":true,"readOnly":true},"no_fee":{"type":"boolean","description":"Marks a no-fee carrier (default false); a label only, never enforced."}}},"freight_company_search_body":{"type":"object","required":["paginate"],"description":"`search` and `paginate` are read; any other top-level key is ignored. Keys inside `search` other than these are refused.\n","properties":{"search":{"type":"object","additionalProperties":false,"properties":{"name":{"type":"string","nullable":true,"description":"Case-insensitive substring of the name (ILIKE `%name%`; a `%` or `_` in it acts as a wildcard). '' or null applies no filter.\n","example":"Logistics"},"no_fee":{"type":"boolean","nullable":true,"description":"Only companies with this no_fee value. Known issue: null is accepted but filters on `no_fee IS NULL`, a NOT NULL column, so it returns no rows; leave the key out for no filter.\n","example":true},"order_by":{"allOf":[{"$ref":"#/components/schemas/order_by"}],"description":"Column name only."}}},"paginate":{"$ref":"#/components/schemas/freight_company_paginate"}},"example":{"search":{"name":"Logistics"},"paginate":{"currentPage":1,"perPage":20}}},"freight_company_paginate":{"type":"object","description":"Which page to return (validPaginateBody). Known issue: numeric strings (\"1\") pass, but are passed on unconverted; see the search 200.\n","required":["currentPage","perPage"],"properties":{"currentPage":{"type":"integer","minimum":1,"example":1},"perPage":{"type":"integer","minimum":1,"maximum":100,"example":20}}},"freight_company_search_result":{"type":"object","required":["data","pagination","orderByColumnNamesList"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/freight_company"}},"pagination":{"$ref":"#/components/schemas/pagination"},"orderByColumnNamesList":{"allOf":[{"$ref":"#/components/schemas/orderByColumnNamesList"}],"example":["name"]}}},"journal_history_search":{"type":"object","description":"Every field is optional; unknown keys are refused.","example":{"order_id":"fdd45a8c-7dfc-4edb-bcab-1990655452e8","types":["order","email_delivery_contracts","signnow"],"start":"2026-09-01T05:00:00.000Z","end":"2026-10-01T04:59:59.999Z"},"properties":{"types":{"type":"array","maxItems":100,"description":"Components to read (journal.component). Omitted - every component the caller may see. A name no row carries simply matches nothing. A customer-portal session's names outside its portal's components are dropped.\n","items":{"type":"string","maxLength":100},"example":["order","email_delivery_contracts","signnow"]},"entity_type":{"type":"string","description":"What the row is about.","enum":["organization","role","user","category","form_template","form_template_question","checklist","work_order_level","depreciation_schedule","facility","part","vendor","freight_company","part_bundle","saved_filter","cash_drawer","customer","customer_site","customer_contact","customer_payment_method","customer_note","asset","order","invoice","inspection","nadc","work_order","purchase_order"]},"entity_id":{"type":"string","format":"uuid","description":"The changed record's id."},"customer_id":{"type":"string","format":"uuid","description":"Rows in this customer's history. Ignored for a customer-portal session, which is always pinned to its own customer."},"order_id":{"type":"string","format":"uuid","description":"Rows in this order's history - a rental, a sale, or a work order."},"asset_id":{"type":"string","format":"uuid","description":"Rows in this asset's history."},"invoice_id":{"type":"string","format":"uuid","description":"Rows about this invoice."},"inspection_id":{"type":"string","format":"uuid","description":"Rows about this inspection."},"actor_user_id":{"type":"string","format":"uuid","description":"Rows written by this user."},"action":{"type":"string","enum":["CREATE","UPDATE","DELETE","RESTORE","MERGE","CLONE","EMAIL_SENT","ESIGN_EVENT","SNAPSHOT","BASELINE"],"description":"SNAPSHOT marks a legacy row holding the record as it was before a change. BASELINE marks the record, in that legacy shape, where its legacy rows hand over to rows carrying changes (DOZ-7836).\n"},"start":{"type":"string","format":"date-time","description":"Inclusive. Defaults to 30 days before end. ISO 8601 recommended; any value Joi.date() reads (e.g. epoch milliseconds) is accepted.\n"},"end":{"type":"string","format":"date-time","description":"Inclusive. Defaults to now. ISO 8601 recommended; any value Joi.date() reads (e.g. epoch milliseconds) is accepted.\n"},"paginate":{"type":"object","description":"Opt-in. Omitted - every matching row, and no pagination block.","required":["currentPage","perPage"],"properties":{"currentPage":{"type":"integer","minimum":1,"example":1},"perPage":{"type":"integer","minimum":1,"maximum":100,"example":50}}}}},"journal_history_result":{"type":"object","required":["count","data"],"properties":{"count":{"type":"integer","description":"The number of rows in data (this page's, when paginated)."},"data":{"type":"array","items":{"$ref":"#/components/schemas/journal_row"}},"pagination":{"description":"Only when paginate was sent. total and lastPage count every matching row; prevPage is null on page 1 and nextPage on the last page.\n","allOf":[{"$ref":"#/components/schemas/pagination"}]}}},"journal_row":{"type":"object","description":"One journal row. Rows written since DOZ-7260 carry their diff in changes; legacy rows (action SNAPSHOT) carry the record as it was before the change in snapshot, and event rows (EMAIL_SENT, ESIGN_EVENT) the event's detail. A BASELINE row (DOZ-7836) carries the record, in its legacy snapshot shape and limited to the newest legacy row's keys, as it stood before its first row with changes - the image the newest legacy row is compared with.\n","properties":{"id":{"type":"string","format":"uuid","readOnly":true},"timestamp":{"type":"string","format":"date-time","readOnly":true},"component":{"type":"string","readOnly":true,"description":"The grouping key - for a retired name asked for in types, the name that was asked for rather than the stored one.\n"},"entity_type":{"type":"string","nullable":true,"readOnly":true},"entity_id":{"type":"string","format":"uuid","nullable":true,"readOnly":true},"entity_label":{"type":"string","nullable":true,"readOnly":true,"description":"The record's name or number at the time of the change."},"action":{"type":"string","nullable":true,"readOnly":true,"description":"Null only on a row the DOZ-7260 backfill has not reached yet."},"source":{"type":"string","nullable":true,"readOnly":true,"description":"USER, SYSTEM, WEBHOOK, CRON, ETL or ADMIN; null on legacy rows."},"user_id":{"type":"string","nullable":true,"readOnly":true,"description":"The actor's user id, or a job's label."},"actor_username":{"type":"string","nullable":true,"readOnly":true},"request_id":{"type":"string","format":"uuid","nullable":true,"readOnly":true},"batch_id":{"type":"string","format":"uuid","nullable":true,"readOnly":true,"description":"Shared by the rows of one business action."},"customer_id":{"type":"string","format":"uuid","nullable":true,"readOnly":true},"order_id":{"type":"string","format":"uuid","nullable":true,"readOnly":true},"asset_id":{"type":"string","format":"uuid","nullable":true,"readOnly":true},"changes":{"type":"array","nullable":true,"readOnly":true,"items":{"type":"object","properties":{"field":{"type":"string"},"from":{"nullable":true},"to":{"nullable":true}}}},"snapshot":{"type":"object","nullable":true,"readOnly":true,"additionalProperties":true},"user":{"type":"object","nullable":true,"readOnly":true,"description":"id is user_id and username is actor_username, each falling back to the row's stored user json. name comes only from that json: the legacy writers (snapshot, email and e-sign rows) still store it, rows written by the journal module do not, so it is null on those. Null when the row names no actor.\n","properties":{"id":{"type":"string","nullable":true},"username":{"type":"string","nullable":true},"name":{"type":"string","nullable":true}}}}},"media_type_option":{"type":"object","description":"One entry of a fixed media-type list","properties":{"desc":{"type":"string","description":"Display label","example":"Marketing Photo"},"value":{"type":"string","description":"The value to send as a media type","example":"marketing_photo"}}},"media_validation_result":{"type":"object","description":"The owning record's validation after the change (the same check the asset or customer screens show). Both keys are absent when the asset was not validated - it is deleted, or the validation failed (logged, not raised).\n","properties":{"validated":{"type":"boolean"},"validation_failures":{"type":"object","nullable":true,"description":"What failed, keyed by check"}}},"media_update":{"type":"object","description":"The body of POST /media/assets/resource/{media_id} and POST /media/parts/resource/{media_id}. Unknown keys are refused.\n","required":["media_type"],"additionalProperties":false,"properties":{"media_type":{"type":"string","description":"One of the entity's media types","example":"marketing_photo"},"description":{"type":"string","example":"Front view"}}},"customer_media_update":{"type":"object","description":"The body of POST /media/customers/resource/{media_id}. Unknown keys are refused.","required":["media_type","sensitive","expiration_date","description"],"additionalProperties":false,"properties":{"media_type":{"type":"string","description":"One of the customer media types","example":"insurance"},"sensitive":{"type":"boolean","example":false},"expiration_date":{"type":"string","format":"date-time","description":"Must be later than now","example":"2027-12-31T00:00:00.000Z"},"description":{"type":"string","example":"Liability insurance certificate"}}},"media_row_fields":{"type":"object","description":"The columns every media table shares","properties":{"media_id":{"type":"string","format":"uuid","readOnly":true,"description":"Also the last segment of full_path"},"created_at":{"type":"string","format":"date-time","readOnly":true},"file_name":{"type":"string","readOnly":true,"description":"The uploaded file's name"},"file_size":{"type":"number","readOnly":true,"description":"Bytes. For an image, the size of the 1024px copy."},"file_type":{"type":"string","readOnly":true,"description":"The uploaded file's mimetype","example":"image/jpeg"},"full_path":{"type":"string","readOnly":true,"description":"The object path; pass it to GET /media/platform/resource/{path}"},"valid":{"type":"boolean","readOnly":true,"description":"False until the upload has finished"},"description":{"type":"string","nullable":true},"media_type":{"type":"string","nullable":true},"deleted":{"type":"boolean","readOnly":true},"org":{"type":"string","format":"uuid","readOnly":true}}},"asset_media":{"description":"An asset2media row","allOf":[{"$ref":"#/components/schemas/media_row_fields"},{"type":"object","properties":{"asset_id":{"type":"string","format":"uuid","readOnly":true},"asset_number":{"type":"string","nullable":true,"readOnly":true,"description":"Not written by these routes"},"thumbnail":{"type":"boolean","readOnly":true,"description":"True for an image (it has .thumbnail and .original copies)"}}}]},"part_media":{"description":"A parts_media row","allOf":[{"$ref":"#/components/schemas/media_row_fields"},{"type":"object","properties":{"part_id":{"type":"string","format":"uuid","readOnly":true},"expiration_date":{"type":"string","format":"date-time","nullable":true,"readOnly":true,"description":"Not written by these routes"}}}]},"customer_media":{"description":"A customer2media row","allOf":[{"$ref":"#/components/schemas/media_row_fields"},{"type":"object","properties":{"customer_id":{"type":"string","format":"uuid","readOnly":true},"expiration_date":{"type":"string","format":"date-time","description":"The upload time when none was sent"},"sensitive":{"type":"boolean","description":"Listed and streamed only for callers holding CUSTOMER_MEDIA_READ_SENSITIVE"}}}]},"order_media":{"description":"An orders_media row","allOf":[{"$ref":"#/components/schemas/media_row_fields"},{"type":"object","properties":{"order_id":{"type":"string","format":"uuid","readOnly":true,"description":"A lotus_orders id"},"valid":{"type":"boolean","readOnly":true,"description":"True once the objects are stored (the row is inserted after the upload). False marks a replaced, declined or expired SignNow artifact; GET /media/orders/{order_id} still returns those rows.\n"},"expiration_date":{"type":"string","format":"date-time","nullable":true},"line_item_id":{"type":"string","format":"uuid","nullable":true,"readOnly":true,"description":"Set only on a per-line-item signed form; null for uploads"}}}]},"po_invoice_media":{"description":"A po_invoice_media row","allOf":[{"$ref":"#/components/schemas/media_row_fields"},{"type":"object","properties":{"invoice_id":{"type":"string","format":"uuid","readOnly":true,"description":"A lotus_po_vendor_invoices id"},"expiration_date":{"type":"string","format":"date-time","nullable":true,"readOnly":true,"description":"Not written by these routes"}}}]},"po_invoice_media_uploaded":{"type":"object","description":"One stored file, as POST /media/po_invoices returns it","properties":{"media_id":{"type":"string","format":"uuid"},"file_name":{"type":"string"},"file_size":{"type":"integer"},"file_type":{"type":"string"},"description":{"type":"string","nullable":true}}},"customer_signed_document":{"type":"object","description":"A signed contract of one of the customer's orders","properties":{"media_id":{"type":"string","format":"uuid"},"full_path":{"type":"string"},"file_name":{"type":"string"},"file_type":{"type":"string"},"file_size":{"type":"integer"},"media_type":{"type":"string","example":"signed_artifact"},"created_at":{"type":"string","format":"date-time"},"expiration_date":{"type":"string","format":"date-time","nullable":true},"order_id":{"type":"string","format":"uuid"},"order_number":{"type":"string"},"order_type":{"type":"string","enum":["RENTAL","SALE"]},"order_status":{"type":"string"},"signed_status":{"type":"string","nullable":true},"signed_date":{"type":"string","format":"date-time","nullable":true}}},"asset_media_upload":{"type":"object","description":"multipart/form-data. Array fields are sent as repeated keys with the `[]` suffix; index i of each array belongs to the i-th file.\n","required":["files[]","media_types[]","asset_id"],"properties":{"files[]":{"type":"array","items":{"type":"string","format":"binary"},"description":"The files. A file sent under any other field name is refused by the upload middleware with a 500.\n"},"media_types[]":{"type":"array","items":{"type":"string"},"description":"An asset media type per file; a file with any other value is skipped","example":["marketing_photo"]},"descriptions[]":{"type":"array","items":{"type":"string"},"example":["Front view"]},"asset_id":{"type":"string","format":"uuid","example":"08861081-b4b8-4209-95b6-70b26dba7c76"}}},"part_media_upload":{"type":"object","description":"multipart/form-data. Array fields are sent as repeated keys with the `[]` suffix; index i of each array belongs to the i-th file.\n","required":["files[]","media_types[]","part_id"],"properties":{"files[]":{"type":"array","items":{"type":"string","format":"binary"},"description":"The files. A file sent under any other field name is refused by the upload middleware with a 500.\n"},"media_types[]":{"type":"array","items":{"type":"string"},"description":"A part media type per file; a file with any other value is skipped","example":["marketing_photo"]},"descriptions[]":{"type":"array","items":{"type":"string"},"example":["Side view"]},"part_id":{"type":"string","format":"uuid","example":"004ff693-f643-484d-a3e8-d59a66cf5a62"}}},"po_invoice_media_upload":{"type":"object","description":"multipart/form-data. Array fields are sent as repeated keys with the `[]` suffix; index i of each array belongs to the i-th file.\n","required":["files[]","invoice_id"],"properties":{"files[]":{"type":"array","items":{"type":"string","format":"binary"},"description":"The files, any type. A file sent under any other field name is refused by the upload middleware with a 500.\n"},"descriptions[]":{"type":"array","items":{"type":"string"},"example":["Vendor invoice scan"]},"invoice_id":{"type":"string","format":"uuid","description":"A lotus_po_vendor_invoices id","example":"6e1f0c8a-2b3d-4e5f-8a9b-0c1d2e3f4a5b"}}},"customer_media_upload":{"type":"object","description":"multipart/form-data. Array fields are sent as repeated keys with the `[]` suffix; index i of each array belongs to the i-th file.\n","required":["files[]","media_types[]","customer_id"],"properties":{"files[]":{"type":"array","items":{"type":"string","format":"binary"},"description":"The files. A file sent under any other field name is refused by the upload middleware with a 500.\n"},"media_types[]":{"type":"array","items":{"type":"string"},"description":"A customer media type per file; a file with any other value is skipped","example":["insurance"]},"descriptions[]":{"type":"array","items":{"type":"string"},"example":["Liability insurance certificate"]},"expiration_dates[]":{"type":"array","items":{"type":"string","format":"date-time"},"description":"Stored as the upload time when missing","example":["2027-12-31T00:00:00.000Z"]},"sensitive[]":{"type":"array","items":{"type":"boolean"},"description":"Stored as false when missing","example":[false]},"customer_id":{"type":"string","format":"uuid","example":"2a5828b1-7e4c-497f-a11a-4a7f67137a95"}}},"order_media_upload":{"type":"object","description":"multipart/form-data. Array fields are sent as repeated keys with the `[]` suffix; index i of each array belongs to the i-th file.\n","required":["files[]","media_types[]","order_id"],"properties":{"files[]":{"type":"array","items":{"type":"string","format":"binary"},"description":"The files. A file sent under any other field name is refused by the upload middleware with a 500.\n"},"media_types[]":{"type":"array","items":{"type":"string","enum":["sales_tax_exemption_document"]},"example":["sales_tax_exemption_document"]},"expiration_dates[]":{"type":"array","items":{"type":"string","format":"date-time"},"example":["2027-12-31T00:00:00.000Z"]},"order_id":{"type":"string","format":"uuid","description":"A lotus_orders id","example":"a9f83fed-a287-430b-8b9b-68dc848898c1"}}},"inspection_item_media_upload":{"type":"object","description":"multipart/form-data","required":["files[]","inspection_record","inspection_item"],"properties":{"files[]":{"type":"array","items":{"type":"string","format":"binary"},"description":"The images (any image/* type). A file sent under any other field name is refused by the upload middleware with a 500.\n"},"inspection_record":{"type":"string","format":"uuid","description":"The inspection (lotus_inspections) id","example":"3f6c1d2e-8a4b-4c5d-9e7f-1a2b3c4d5e6f"},"inspection_item":{"type":"string","format":"uuid","description":"The item's id in the inspection template","example":"7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d"},"description":{"type":"string","nullable":true,"description":"Damage items only"},"damage":{"type":"string","nullable":true,"enum":["no","repair","replace"],"description":"Damage items only"},"reading":{"type":"number","nullable":true,"description":"Gauge items only"},"lazy_upload":{"type":"boolean","description":"Accepted but not used; the item's stored flag decides"},"captured_at_dates[]":{"type":"array","items":{"type":"string","format":"date-time"},"description":"When each image was taken, by index; drawn onto the image, not stored"}}},"oauth_grant":{"type":"object","description":"One live AI-assistant connection.","properties":{"id":{"type":"string","pattern":"^[A-Za-z0-9_-]{16,128}$","description":"The connection id, used to revoke it.","example":"pX3k9vQ2LmN8rT5wYz1aB"},"client_id":{"type":"string","description":"The OAuth client (AI assistant) the connection was approved for."},"client_name":{"type":"string","description":"The client's display name at approval time.","example":"Claude"},"scope":{"type":"string","description":"The scope granted. Always mcp:read today.","example":"mcp:read"},"created_at":{"type":"string","format":"date-time","description":"When the connection was approved."},"expires_at":{"type":"string","format":"date-time","description":"Absolute end of the connection, 90 days after approval."},"last_used_at":{"type":"string","format":"date-time","nullable":true,"description":"Last API call made through the connection (refreshed at most once a minute); null until first use."},"user_id":{"type":"string","format":"uuid","description":"The user who approved the connection."},"user_name":{"type":"string","nullable":true,"description":"That user's name.","example":"Jane Doe"}}},"oauth_grant_revoked":{"type":"object","properties":{"id":{"type":"string","description":"The id from the path.","example":"pX3k9vQ2LmN8rT5wYz1aB"},"revoked":{"type":"boolean","example":true}}},"part_type":{"type":"string","enum":["supplies","inventoried","non_inventoried","default"],"description":"Picks the field-settings column the requirement checks use. `default` is accepted and stored, but the search's `type` filter can't select it.\n"},"part_fields":{"type":"object","description":"The writable part columns as create takes them (validPart). Both bodies are checked with Joi's conversion on but the raw body is stored, so number fields also take numeric strings (\"12.5\") and boolean fields the strings \"true\" / \"false\" (any case); Postgres casts them. Dates and `model_year` are text columns: the value is stored as sent.\n","properties":{"name":{"type":"string","minLength":1,"example":"Hydraulic fitting"},"type":{"$ref":"#/components/schemas/part_type"},"category_import":{"type":"string","nullable":true},"type_import":{"type":"string","minLength":1},"part_number":{"type":"string","minLength":1,"example":"HF-1216"},"manufacture":{"type":"string","minLength":1,"description":"The make.","example":"Parker"},"model_number":{"type":"string","minLength":1},"samt":{"type":"number"},"serial_number":{"type":"string","minLength":1},"model_year":{"type":"string","minLength":1,"example":"2024"},"description":{"type":"string","nullable":true},"bulk_item":{"type":"string","minLength":1},"bought_used":{"oneOf":[{"type":"boolean"},{"type":"string","enum":["true","false"]}],"description":"Stored in a text column, so it reads back as \"true\" / \"false\" (the string as sent). Joi takes the strings in any case (\"TRUE\"), and they are stored as sent.\n"},"rental_rates":{"type":"object","additionalProperties":true,"description":"Any object; stored as sent."},"purchase_price":{"type":"number","example":12.5},"extra_charges":{"type":"number"},"quantity":{"type":"number","minimum":-1,"exclusiveMinimum":true,"description":"On-hand stock. The schema takes any number above -1, but the column is an integer: while PART.PARTS_MANUAL_INVENTORY is on and no `purchase_order` is sent, a fraction or a value between -1 and 0 is a 400. Stored as 0 when manual inventory is off or `purchase_order` is sent; with `purchase_order` it becomes the line's quantity_ordered (a positive fraction is a 400, 0 or less orders 1; see the operation).\n"},"sell_price":{"type":"number","example":18},"time_out":{"type":"number"},"time_in":{"type":"number"},"purchase_date":{"type":"string","description":"An ISO 8601 date, stored as sent.","example":"2026-09-30"},"sold_date":{"type":"string","nullable":true,"description":"An ISO 8601 date, stored as sent."},"last_date":{"type":"string","nullable":true,"description":"An ISO 8601 date, stored as sent."},"bin_location":{"type":"string","nullable":true,"example":"A-12"},"purchase_order":{"type":"string","format":"uuid","nullable":true,"description":"A live 'new' purchase order of the org to add the part to as a line (see the operation). Create only.\n"},"capitalization":{"type":"boolean"},"is_taxable":{"type":"boolean","description":"Omitted on create, the column default (true) applies. Null is refused. Stored only.\n"}}},"part_create":{"description":"The create body. `name` and `type` are required, plus whatever the org's field settings require for that type. Unknown keys are refused (`\"<key>\" is not allowed`), so the server-set columns (id, item_number, org, validated, validation_failures, deleted) can't be sent.\n","allOf":[{"$ref":"#/components/schemas/part_fields"},{"type":"object","required":["name","type"]}],"example":{"name":"Hydraulic fitting","type":"inventoried","part_number":"HF-1216","manufacture":"Parker","purchase_price":12.5,"sell_price":18,"purchase_date":"2026-09-30","bin_location":"A-12"}},"part_update":{"type":"object","description":"The update body (validPartUpdate). Every key is optional and unknown keys are refused. Unlike create, most fields take null (and the text ones '', except `purchase_date`, `sold_date` and `last_date`, which must be ISO 8601 dates), which clears them; `name`, `type`, `type_import`, `quantity`, `capitalization` and `is_taxable` refuse null. `bought_used` also takes '' (create doesn't), so a value read back from GET can be sent again. Numeric and boolean strings are taken as on create (see part_fields).\n","additionalProperties":false,"properties":{"name":{"type":"string","minLength":1},"type":{"$ref":"#/components/schemas/part_type"},"category_import":{"type":"string","nullable":true},"type_import":{"type":"string","minLength":1},"part_number":{"type":"string","nullable":true},"manufacture":{"type":"string","nullable":true},"model_number":{"type":"string","nullable":true},"samt":{"type":"number","nullable":true},"serial_number":{"type":"string","nullable":true},"model_year":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"bulk_item":{"type":"string","nullable":true},"bought_used":{"oneOf":[{"type":"boolean","nullable":true},{"type":"string","enum":["true","false",""]}]},"rental_rates":{"type":"object","additionalProperties":true,"nullable":true},"purchase_price":{"type":"number","nullable":true},"extra_charges":{"type":"number","nullable":true},"quantity":{"type":"number","minimum":-1,"exclusiveMinimum":true,"description":"Dropped while PART.PARTS_MANUAL_INVENTORY is off. While it is on the column is an integer, so a fraction or a value between -1 and 0 is a 400.\n"},"sell_price":{"type":"number","nullable":true},"time_out":{"type":"number","nullable":true},"time_in":{"type":"number","nullable":true},"purchase_date":{"type":"string","nullable":true,"description":"An ISO 8601 date, stored as sent."},"sold_date":{"type":"string","nullable":true},"last_date":{"type":"string","nullable":true},"bin_location":{"type":"string","nullable":true},"capitalization":{"type":"boolean"},"is_taxable":{"type":"boolean"},"purchase_order":{"type":"string","format":"uuid","nullable":true,"description":"Accepted and dropped; the purchase-order link is create-only."},"org":{"type":"string","format":"uuid","description":"Accepted and dropped; the session's org is used."}},"example":{"name":"Hydraulic fitting 3/4in","type":"inventoried","bin_location":"A-14"}},"part_validation_entry":{"type":"object","required":["valid","reason","name"],"properties":{"valid":{"type":"boolean"},"reason":{"type":"string","nullable":true,"example":"A valid 'bin location' field is required"},"name":{"type":"string","description":"The field's label: a fixed one (e.g. \"Make\") while it passes, the field setting's own name when it fails.\n"}}},"part_validation_failures":{"type":"object","description":"Every field the part checks cover, keyed by field. Only fields at \"validation\" or \"requirement\" for the part's type are checked; the others always read valid, and `purchase_order` is never checked. A write through this API stores all eleven keys; older rows can hold `{}` or an extra legacy key (`quick_descriptor`).\n","additionalProperties":{"$ref":"#/components/schemas/part_validation_entry"},"properties":{"manufacture":{"$ref":"#/components/schemas/part_validation_entry"},"model_number":{"$ref":"#/components/schemas/part_validation_entry"},"model_year":{"$ref":"#/components/schemas/part_validation_entry"},"serial_number":{"$ref":"#/components/schemas/part_validation_entry"},"part_number":{"$ref":"#/components/schemas/part_validation_entry"},"bin_location":{"$ref":"#/components/schemas/part_validation_entry"},"sell_price":{"$ref":"#/components/schemas/part_validation_entry"},"quantity":{"$ref":"#/components/schemas/part_validation_entry"},"purchase_price":{"$ref":"#/components/schemas/part_validation_entry"},"purchase_date":{"$ref":"#/components/schemas/part_validation_entry"},"purchase_order":{"$ref":"#/components/schemas/part_validation_entry"}}},"part":{"type":"object","description":"A stored parts row.","required":["id","name","category_import","type","type_import","part_number","item_number","manufacture","model_number","samt","serial_number","model_year","description","bulk_item","bought_used","rental_rates","purchase_price","extra_charges","quantity","sell_price","time_out","time_in","purchase_date","sold_date","last_date","org","deleted","validated","validation_failures","bin_location","capitalization","purchase_order","is_taxable"],"properties":{"id":{"type":"string","format":"uuid","readOnly":true},"name":{"type":"string","nullable":true},"category_import":{"type":"string","nullable":true},"type":{"type":"string","nullable":true,"description":"One of the part_type values on anything written through this API."},"type_import":{"type":"string","nullable":true},"part_number":{"type":"string","nullable":true},"item_number":{"type":"integer","nullable":true,"readOnly":true,"description":"Assigned on create as the org's highest item_number + 1. Not unique in the database (two creates at once can get the same number); null on some imported rows.\n"},"manufacture":{"type":"string","nullable":true},"model_number":{"type":"string","nullable":true},"samt":{"type":"number","nullable":true},"serial_number":{"type":"string","nullable":true},"model_year":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"bulk_item":{"type":"string","nullable":true},"bought_used":{"type":"string","nullable":true,"description":"A text column: \"true\" / \"false\" when a boolean was sent, otherwise the string as sent.\n","example":"false"},"rental_rates":{"type":"object","additionalProperties":true,"nullable":true},"purchase_price":{"type":"number","nullable":true},"extra_charges":{"type":"number","nullable":true},"quantity":{"type":"integer","nullable":true,"description":"On-hand stock (search and GET answer 0 for null)."},"sell_price":{"type":"number","nullable":true},"time_out":{"type":"number","nullable":true},"time_in":{"type":"number","nullable":true},"purchase_date":{"type":"string","nullable":true,"description":"Text, as it was sent."},"sold_date":{"type":"string","nullable":true},"last_date":{"type":"string","nullable":true},"org":{"type":"string","format":"uuid","nullable":true,"readOnly":true},"deleted":{"type":"boolean","nullable":true,"readOnly":true},"validated":{"type":"boolean","nullable":true,"readOnly":true,"description":"False when any checked field fails (see validation_failures)."},"validation_failures":{"allOf":[{"$ref":"#/components/schemas/part_validation_failures"}],"nullable":true,"readOnly":true},"bin_location":{"type":"string","nullable":true},"capitalization":{"type":"boolean","nullable":true},"purchase_order":{"type":"string","format":"uuid","nullable":true,"description":"The purchase order the part was created against. Written on create only; update accepts and drops it.\n"},"is_taxable":{"type":"boolean"}}},"part_search_row":{"description":"A search row, the stored part with `quantity` 0 instead of null.","allOf":[{"$ref":"#/components/schemas/part"},{"type":"object","properties":{"quantity":{"type":"integer"}}}]},"part_detail":{"description":"GET /parts/{id}, the search row plus its purchase order.","allOf":[{"$ref":"#/components/schemas/part_search_row"},{"type":"object","required":["po_details"],"properties":{"po_details":{"type":"object","nullable":true,"description":"The purchase order of `purchase_order`; null when the part has none. A deleted purchase order still shows.\n","properties":{"id":{"type":"string","format":"uuid"},"po_number":{"type":"string"}}}}}]},"part_write_result":{"type":"object","description":"The create / update answer.","required":["message","data"],"properties":{"message":{"type":"string","enum":["Part created","Part updated"]},"data":{"$ref":"#/components/schemas/part"}}},"part_search_body":{"type":"object","required":["paginate"],"description":"`search` and `paginate` are read; any other top-level key is ignored. Keys inside `search` other than these are refused.\n","properties":{"search":{"type":"object","nullable":true,"additionalProperties":false,"description":"Null or omitted applies no filter.","properties":{"query":{"type":"string","nullable":true,"maxLength":200,"example":"hyd fitting","description":"One search box across item_number, part_number, name, description, manufacture, model_number and serial_number (DOZ-7907). The text is split on whitespace and every word must appear, as a case-insensitive substring, somewhere in those fields - not necessarily the same one. % and _ match literally. Blank or null means no filter. Without order_by the rows come in relevance order, then item_number descending - first the parts whose item_number, part_number or name equals the whole query, then those where one of them starts with it, then those matched by those three fields alone, then the matches that need the description, make, model or serial.\n"},"name":{"type":"string","description":"Case-insensitive substring of the name (ILIKE `%name%`; a `%` or `_` in it acts as a wildcard). '' applies no filter; null is refused.\n"},"description":{"type":"string","nullable":true,"description":"Case-insensitive substring of the description (a `%` or `_` acts as a wildcard). '' or null applies no filter.\n"},"part_number":{"type":"string","nullable":true,"minLength":1,"description":"Case-insensitive substring of the part number (a `%` or `_` acts as a wildcard). Null applies no filter; '' is refused.\n"},"item_number":{"type":"number","nullable":true,"description":"Substring of the item number's digits, so 12 matches 120 and 312. 0 or null applies no filter.\n"},"type":{"type":"array","description":"Any of these types; [] applies no filter, null is refused. A part stored as `default` can't be selected.\n","items":{"type":"string","enum":["supplies","inventoried","non_inventoried"]}},"stock":{"type":"boolean","nullable":true,"description":"true matches quantity above 0 or type supplies; false matches a non-supplies part with quantity exactly 0 (a non-supplies part with a null quantity matches neither). Null applies no filter.\n"},"purchase_order":{"nullable":true,"description":"A purchase-order id (the parts created against it), or true / false for parts created against any purchase order / none. Null applies no filter.\n","oneOf":[{"type":"string","format":"uuid"},{"type":"boolean"}]},"order_by":{"description":"Sortable columns: item_number, part_number, type, name, sell_price, quantity. Without it: item_number descending, led by relevance to search.query (see query), with no id tiebreaker.\n","allOf":[{"$ref":"#/components/schemas/order_by"}]}}},"paginate":{"$ref":"#/components/schemas/part_paginate"}},"example":{"search":{"query":"hyd fitting","type":["inventoried"],"stock":true},"paginate":{"currentPage":1,"perPage":20}}},"part_paginate":{"type":"object","description":"Which page to return (validPaginateBody). Known issue: numeric strings (\"1\") pass, but are passed on unconverted; see the search 200.\n","required":["currentPage","perPage"],"properties":{"currentPage":{"type":"integer","minimum":1,"example":1},"perPage":{"type":"integer","minimum":1,"maximum":100,"example":20}}},"part_search_result":{"type":"object","required":["data","pagination","orderByColumnNamesList"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/part_search_row"}},"pagination":{"$ref":"#/components/schemas/pagination"},"orderByColumnNamesList":{"allOf":[{"$ref":"#/components/schemas/orderByColumnNamesList"}],"example":["item_number","part_number","type","name","sell_price","quantity"]}}},"platform_org_recipients":{"type":"object","description":"An internal notification email bag - who gets this kind of org email and whether it is on. Used by new_quotes_email, new_reservations_email, open_contracts_email, closed_contracts_email, completed_contracts_email, work_orders_email, inspections_email, failed_inspections_email, new_invoices_email, invoice_payments_email, system_notifications and daily_summary_report_email (where an empty string is accepted as a list entry), and by dispatch_inspection_completion_email and off_rent_inspection_completion_email (where it is not).\n","properties":{"email":{"type":"array","items":{"anyOf":[{"type":"string","format":"email"},{"type":"string","maxLength":0}]},"example":["jane.doe@example.com"]},"active":{"type":"boolean","example":true}}},"platform_org_slack_url":{"type":"object","description":"A Slack incoming-webhook channel for one kind of event. `active` may be true only when `endpoint` is a non-empty string.\n","properties":{"endpoint":{"type":"string","nullable":true,"example":"https://hooks.slack.com/services/T000/B000/XXXX"},"active":{"type":"boolean","example":true}}},"platform_org_accounts":{"type":"object","description":"The org's general-ledger account mapping. Once sent, all five groups and every key in them are required; each value is an account string or null.\n","required":["asset_accounts","expense_accounts","purchase_orders","income_accounts","liability_accounts"],"properties":{"asset_accounts":{"type":"object","description":"Keys - bank_deposits, account_payments, credit_cards, american_express, debit_cards, added_cash, petty_cash, accounts_receivable, equipment_default, accum_deprc_default, sales_inventory_default, inventory_suspense, equipment_suspense.\n","additionalProperties":{"type":"string","nullable":true},"example":{"bank_deposits":"1000","accounts_receivable":"1200"}},"expense_accounts":{"type":"object","description":"Keys - over_under, disbursements_default, miscellaneous, depreciation, cost_of_asset_sales, cost_of_goods_sold, repair_cost, inventory_adjustments, inter_store_balance.\n","additionalProperties":{"type":"string","nullable":true}},"purchase_orders":{"type":"object","description":"Keys - shipping, tax, miscellaneous, expense_default, accounts_payable, subrent_po_liability, other_liability, subrent_expense.\n","additionalProperties":{"type":"string","nullable":true}},"income_accounts":{"type":"object","description":"Keys - rentals, sales, asset_sales, dmg_waiver, other_income, discounts_taken.","additionalProperties":{"type":"string","nullable":true}},"liability_accounts":{"type":"object","description":"Keys - state_sales_tax_payable, county_sales_tax_payable, city_sales_tax_payable, special_sales_tax_payable, enviro_fee, customer_deposits, suspense_fund.\n","additionalProperties":{"type":"string","nullable":true}}}},"platform_org_fields":{"type":"object","description":"The organization profile columns POST /platform can write (validOrgPostBody), plus rental_app_document, which goes to the settings store. Every key is optional; nested objects are described on their own schema. Org settings are not columns any more (DOZ-7318) - see the platform settings store.\n","properties":{"name":{"type":"string","example":"Acme Equipment Rentals"},"logo":{"type":"string","description":"Storage path of the logo (set by POST /platform/logo); '' allowed."},"address":{"type":"object","nullable":true,"description":"The org's address. Once sent, all five keys are required (each may be '' or null).","required":["street1","street2","city","state","zip"],"properties":{"street1":{"type":"string","nullable":true,"example":"100 Main St"},"street2":{"type":"string","nullable":true,"example":""},"city":{"type":"string","nullable":true,"example":"Springfield"},"state":{"type":"string","nullable":true,"example":"IL"},"zip":{"type":"string","nullable":true,"example":"62701"}}},"rental_app_document":{"type":"string","nullable":true,"description":"Storage path of the rental application document. Not a column: POST /platform saves it to the CUSTOMER.RENTAL_APP_DOCUMENT setting (empty or blank clears it). GET /platform/general and POST /platform never return it - read it from GET /lotus/platform-settings; only the upload POST /media/platform/resource/rental_app_document answers with the new path.\n"},"stripe_public_key":{"type":"string","nullable":true,"description":"The org's Stripe publishable key; '' allowed."},"default_facility":{"type":"string","format":"uuid","nullable":true,"description":"The default facility. Stored as sent (not checked against the org's facilities)."},"accounts":{"$ref":"#/components/schemas/platform_org_accounts"},"new_quotes_email":{"$ref":"#/components/schemas/platform_org_recipients"},"new_reservations_email":{"$ref":"#/components/schemas/platform_org_recipients"},"open_contracts_email":{"$ref":"#/components/schemas/platform_org_recipients"},"closed_contracts_email":{"$ref":"#/components/schemas/platform_org_recipients"},"completed_contracts_email":{"$ref":"#/components/schemas/platform_org_recipients"},"work_orders_email":{"$ref":"#/components/schemas/platform_org_recipients"},"inspections_email":{"$ref":"#/components/schemas/platform_org_recipients"},"failed_inspections_email":{"$ref":"#/components/schemas/platform_org_recipients"},"new_invoices_email":{"$ref":"#/components/schemas/platform_org_recipients"},"invoice_payments_email":{"$ref":"#/components/schemas/platform_org_recipients"},"system_notifications":{"$ref":"#/components/schemas/platform_org_recipients"},"daily_summary_report_email":{"$ref":"#/components/schemas/platform_org_recipients"},"dispatch_inspection_completion_email":{"$ref":"#/components/schemas/platform_org_recipients"},"off_rent_inspection_completion_email":{"$ref":"#/components/schemas/platform_org_recipients"},"daily_summary_report_preferences":{"type":"object","description":"Which parts of the daily summary report are hidden - hideEmptySections, highLevelSummary.hidden, and for salesDesk (sections newQuotes, newOpenSalesOrders, newClosedSalesOrders), rentalDesk (sections newQuotes, newReservations, newOpenContracts, newClosedContracts) and serviceDesk (sections tasks, dispatchInspections, completedDispatchInspections, offRentInspections, completedOffRentInspections; plus the retired showDueNull) a `hidden` flag on the desk and on each section. Every key is an optional boolean.\n","properties":{"hideEmptySections":{"type":"boolean"},"highLevelSummary":{"type":"object","properties":{"hidden":{"type":"boolean"}}},"salesDesk":{"type":"object","additionalProperties":true},"rentalDesk":{"type":"object","additionalProperties":true},"serviceDesk":{"type":"object","additionalProperties":true}}},"slack_webhook_url":{"type":"string","nullable":true,"description":"Legacy single Slack webhook URL."},"slack_urls":{"type":"object","description":"Slack channels per kind of event, each optional.","properties":{"new_quotes":{"$ref":"#/components/schemas/platform_org_slack_url"},"new_reservations":{"$ref":"#/components/schemas/platform_org_slack_url"},"open_contracts":{"$ref":"#/components/schemas/platform_org_slack_url"},"closed_contracts":{"$ref":"#/components/schemas/platform_org_slack_url"},"completed_contracts":{"$ref":"#/components/schemas/platform_org_slack_url"},"new_invoices":{"$ref":"#/components/schemas/platform_org_slack_url"},"invoice_payments":{"$ref":"#/components/schemas/platform_org_slack_url"},"work_orders":{"$ref":"#/components/schemas/platform_org_slack_url"},"inspections":{"$ref":"#/components/schemas/platform_org_slack_url"},"failed_inspections":{"$ref":"#/components/schemas/platform_org_slack_url"},"system_notifications":{"$ref":"#/components/schemas/platform_org_slack_url"}}},"allow_fake_login":{"type":"boolean","description":"When true, phone (OTP) sign-in for the org's users sends no text message and accepts a fixed test code instead (a user's own override PIN still takes precedence).\n"}}},"platform_org_update":{"description":"The POST /platform body - any subset of platform_org_fields. Unknown keys are stripped (not refused) at every level, and former settings columns are dropped with a server-side warning. required_customer_fields is still validated but never written.\n","allOf":[{"$ref":"#/components/schemas/platform_org_fields"},{"type":"object","properties":{"required_customer_fields":{"type":"array","minItems":1,"description":"Accepted and ignored.","items":{"type":"object","required":["name","pay_on_open","on_account"],"properties":{"name":{"type":"string"},"pay_on_open":{"type":"boolean"},"on_account":{"type":"boolean"}}}}}}],"example":{"name":"Acme Equipment Rentals","address":{"street1":"100 Main St","street2":"","city":"Springfield","state":"IL","zip":"62701"},"slack_webhook_url":null}},"platform_org_module":{"type":"object","description":"One module of the catalog, flagged for this org.","properties":{"key":{"type":"string","example":"CUSTOMER_PORTAL"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"type":{"type":"string","enum":["TIER","ADD_ON"]},"supported_tiers":{"type":"array","nullable":true,"description":"For an add-on, the tiers it can be added to.","items":{"type":"string"},"example":["CLOUDERM"]},"enabled":{"type":"boolean","description":"Whether this org has the module."}}},"platform_org":{"description":"An organizations row as the API returns it - every column except stripe_private_key and stripe_webhook_secret (which are never returned). Since DOZ-7318 the row holds the profile only; org settings are read with GET /lotus/platform-settings. The writable columns are those of platform_org_fields (rental_app_document excepted, which is a setting); the others are read-only. Columns are nullable in the table except id, allow_fake_login and demo_org; a bag never saved may be null.\n","allOf":[{"$ref":"#/components/schemas/platform_org_fields"},{"type":"object","properties":{"id":{"type":"string","format":"uuid","readOnly":true},"created_at":{"type":"string","format":"date-time","readOnly":true},"deleted":{"type":"boolean","readOnly":true},"template":{"type":"boolean","readOnly":true},"demo_org":{"type":"boolean","readOnly":true},"enable_e_sign":{"type":"boolean","readOnly":true},"stripe_id":{"type":"string","nullable":true,"readOnly":true,"description":"The org's own Stripe customer id (set by GET /platform/payment_method_client_secret).","example":"cus_PqR2sT3uV4wX5y"},"default_payment_method":{"type":"string","nullable":true,"readOnly":true,"description":"Set by POST /platform/set_default_payment_method.","example":"pm_1PqR2sT3uV4wX5yZ"},"stripe_webhook_identifier":{"type":"string","format":"uuid","nullable":true,"readOnly":true,"description":"The id in the org's Stripe webhook URL."},"required_customer_fields":{"type":"array","readOnly":true,"description":"Legacy list (superseded by the field settings).","items":{"type":"object","additionalProperties":true}},"modules":{"type":"array","readOnly":true,"description":"The module catalog flagged for this org, ordered by weight. Present on GET /platform/general, the POST /platform response and the login session's orgDetails (POST /login, /otp/login and /login/refresh) only.\n","items":{"$ref":"#/components/schemas/platform_org_module"}}}}]},"platform_org_general":{"description":"The GET /platform/general response.","allOf":[{"$ref":"#/components/schemas/platform_org"},{"type":"object","properties":{"cards":{"type":"array","maxItems":0,"readOnly":true,"description":"Always an empty array (legacy).","items":{}},"motorized_assets_count":{"type":"integer","nullable":true,"readOnly":true,"description":"From the latest org statistics row; null when that row is not this org's, which is the case for most orgs.\n"},"non_motorized_assets_count":{"type":"integer","nullable":true,"readOnly":true,"description":"As motorized_assets_count."}}}]},"platform_payment_method_body":{"type":"object","required":["payment_method"],"properties":{"payment_method":{"type":"string","description":"A Stripe payment method id saved to the org's Stripe customer.","example":"pm_1PqR2sT3uV4wX5yZ"}}},"field_settings_schema":{"type":"object","description":"One per-org field setting. Rows are created by the platform's seeding, never by the API; the API only changes `setting`. `entity` selects both the record type and the shape of `setting` - for asset, part, customer, customer_site and vendor each context maps to requirement / validation / optional / omit; for asset_cloning each context is a boolean (whether the field is copied when a user clones an asset, used by the web client only).\n","properties":{"name":{"type":"string","description":"Display label (never written by the API).","example":"Serial Number"},"key":{"type":"string","description":"The field.","example":"serial_number"},"entity":{"type":"string","enum":["asset","part","customer","customer_site","vendor","asset_cloning"]},"setting":{"type":"object","description":"Contexts - asset and asset_cloning default, normal, add_on (asset type); part default, supplies, inventoried, non_inventoried (part type); customer and customer_site default, pay_on_open, on_account; vendor default.\n","additionalProperties":true,"example":{"default":"requirement","normal":"requirement","add_on":"optional"}},"org":{"type":"string","format":"uuid"}}},"field_settings_update_item":{"type":"object","required":["key","entity","org"],"description":"Unknown keys (including name) are refused.","properties":{"key":{"type":"string","example":"serial_number"},"entity":{"type":"string","enum":["asset","part","customer","customer_site","vendor","asset_cloning"],"example":"asset"},"org":{"type":"string","format":"uuid","description":"Required, but replaced by the session org.","example":"b67aba8b-9819-4179-b2a1-384968cfa963"},"setting":{"type":"object","description":"Not required by validation, but an item without it fails the whole batch (see the operation). `default` is required inside it. Values - for asset_cloning booleans (keys default, normal, add_on); otherwise one of requirement, validation, optional, omit, with keys asset default/normal/add_on, part default/supplies/inventoried/non_inventoried, customer and customer_site default/pay_on_open/on_account, vendor default only. Other keys are refused.\n","additionalProperties":true,"example":{"default":"requirement","normal":"validation","add_on":"optional"}}}},"entity_validate_body":{"type":"object","required":["entity","data"],"properties":{"entity":{"type":"string","enum":["asset","part","customer","vendor"],"example":"asset"},"data":{"type":"object","additionalProperties":true,"description":"The record as the form holds it (the asset, part, customer or vendor fields, with its id).\n","example":{"id":"1f0e2d3c-4b5a-6978-8a9b-0c1d2e3f4a5b","category":"2a1b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d","manufacturer":"CAT","model_number":"320","model_year":2019,"serial_number":"X1234","purchase_price":1000,"rental_rates":{"day":100,"week":400,"4week":1200},"media":[],"depreciation_schedules":[]}}}},"entity_validation_result":{"type":"object","properties":{"validationStatus":{"type":"boolean","description":"True when no required or validated field fails."},"validationFailureObject":{"type":"object","description":"One entry per failing field, keyed by the field-settings key (e.g. manufacturer, serial_number, rental_rates for assets; part_number, bin_location for parts; address, primary_contact, credit_card for customers; name, website_url for vendors). Empty when everything passes.\n","additionalProperties":{"type":"object","properties":{"valid":{"type":"boolean","example":false},"reason":{"type":"string","nullable":true,"description":"Why the field fails; null when it passes.","example":"A valid asset 'make' field is required"},"name":{"type":"string","description":"The field's display name (absent on some entries).","example":"Make"}}}}}},"NotificationSetting":{"type":"object","description":"One notification of the catalog with this org's delivery settings.","properties":{"notification_key":{"type":"string","example":"off_rent_inspection"},"notification_name":{"type":"string","example":"New Off-Rent Inspections"},"notification_group":{"type":"string","example":"Service"},"weight":{"type":"integer","description":"Display order (not unique).","example":8},"emails":{"type":"array","description":"Recipients; empty when none.","items":{"type":"string","format":"email"},"example":[]},"slack_webhook":{"type":"string","nullable":true,"example":"https://hooks.slack.com/services/T000/B000/XXXX"},"emails_active":{"type":"boolean","example":false},"slack_active":{"type":"boolean","example":false}}},"NotificationSettingsResponse":{"type":"array","items":{"$ref":"#/components/schemas/NotificationSetting"}},"notification_setting_update":{"type":"object","required":["notification_key"],"description":"Unknown keys are refused; null is refused for emails, emails_active and slack_active.","properties":{"notification_key":{"type":"string","description":"Must be a key of the notification catalog.","example":"off_rent_inspection"},"emails":{"type":"array","items":{"type":"string","format":"email"},"example":["jane.doe@example.com"]},"slack_webhook":{"type":"string","format":"uri","nullable":true,"description":"A URI, '' or null; at most 255 characters.","example":"https://hooks.slack.com/services/T000/B000/XXXX"},"emails_active":{"type":"boolean","example":true},"slack_active":{"type":"boolean","example":false},"org_id":{"type":"string","format":"uuid","description":"Accepted and ignored."},"notification_name":{"type":"string","description":"Accepted and ignored."},"notification_group":{"type":"string","description":"Accepted and ignored."},"weight":{"type":"integer","minimum":1,"nullable":true,"description":"Accepted and ignored."}}},"role":{"type":"object","description":"A role as the roles endpoints return it.","properties":{"id":{"type":"string","format":"uuid","readOnly":true},"name":{"type":"string","example":"Yard Manager"},"description":{"type":"string","nullable":true,"example":"Runs the yard"},"type":{"type":"string","enum":["org","cust"],"description":"org - for org users; cust - for customer-portal users."},"org_id":{"type":"string","format":"uuid","nullable":true,"readOnly":true,"description":"Null for a global default role."},"customer_id":{"type":"string","format":"uuid","nullable":true,"description":"The customer a cust role belongs to; null for org roles and shared cust roles."},"deleted":{"type":"boolean","readOnly":true,"example":false},"modules":{"type":"array","nullable":true,"readOnly":true,"description":"The modules the role belongs to; null = visible to every org.","items":{"type":"string"}},"permissions":{"type":"array","readOnly":true,"description":"The role's permissions within the org's enabled modules.","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","example":"ASSET_READ"},"modules":{"type":"array","nullable":true,"items":{"type":"string"}}}}},"assignment_count":{"type":"string","readOnly":true,"description":"How many users (service-credential users excluded) hold the role, counted over every user row - a number in a string.\n","example":"3"}}},"role_create_body":{"type":"object","required":["name","type"],"description":"Unknown keys are refused.","properties":{"name":{"type":"string","description":"Unique among the org's live roles; at most 255 characters.","example":"Yard Manager"},"description":{"type":"string","nullable":true,"description":"An empty string is allowed.","example":"Runs the yard"},"type":{"type":"string","enum":["org","cust"],"example":"org"},"customer_id":{"type":"string","format":"uuid","nullable":true,"description":"For a cust role of one customer. Ignored for a customer-portal session (its own customer is used).\n"}}},"role_clone_body":{"type":"object","required":["role_id","name","description"],"description":"Unknown keys are refused.","properties":{"role_id":{"type":"string","description":"The role to copy (a uuid).","example":"7c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f"},"name":{"type":"string","example":"Org Admin (Copy)"},"description":{"type":"string","description":"Required and not empty.","example":"Copy of the org admin role"},"customer_id":{"type":"string","format":"uuid","nullable":true,"description":"Ignored for a customer-portal session (its own customer is used)."}}},"role_update_body":{"type":"object","description":"At least one key. Neither may be empty or null; unknown keys (type, customer_id, ...) are refused.\n","properties":{"name":{"type":"string","example":"Yard Manager"},"description":{"type":"string","example":"Runs the yard and its drivers"}}},"role_permissions_body":{"type":"array","minItems":1,"description":"One entry per role to save.","items":{"type":"object","required":["id","permissions"],"properties":{"id":{"type":"string","format":"uuid","description":"The role id.","example":"7c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f"},"permissions":{"type":"array","description":"The complete list of permission ids the role should hold.","items":{"type":"string","format":"uuid"},"example":["0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d"]}}}},"permission":{"type":"object","description":"One permission of the global catalog.","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","example":"ASSET_READ"},"description":{"type":"string","nullable":true},"long_description":{"type":"string","nullable":true},"type":{"type":"string","enum":["org","cust"],"nullable":true},"permission_group":{"type":"string","nullable":true,"example":"Assets"},"modules":{"type":"array","nullable":true,"description":"The modules the permission belongs to; null = every org.","items":{"type":"string"},"example":["CLOUDERM"]}}},"facility":{"type":"object","description":"A facility (location / yard) of the org. Every value may be null in the table.","properties":{"id":{"type":"string","format":"uuid","readOnly":true},"name":{"type":"string","nullable":true,"example":"Main Yard"},"address":{"type":"string","nullable":true,"example":"100 Main St"},"state":{"type":"string","nullable":true,"example":"IL"},"city":{"type":"string","nullable":true,"example":"Springfield"},"zip":{"type":"string","nullable":true,"example":"62701"}}},"facility_record":{"description":"The stored facility row.","allOf":[{"$ref":"#/components/schemas/facility"},{"type":"object","properties":{"org":{"type":"string","format":"uuid","readOnly":true},"deleted":{"type":"boolean","readOnly":true,"example":false}}}]},"facility_body":{"type":"object","required":["name","address","state","city"],"description":"Unknown keys are refused. Each value at most 255 characters.","properties":{"id":{"type":"string","format":"uuid","description":"Omit to create; the facility's id to update it."},"name":{"type":"string","example":"Main Yard"},"address":{"type":"string","example":"100 Main St"},"state":{"type":"string","example":"IL"},"city":{"type":"string","example":"Springfield"},"zip":{"type":"string","nullable":true,"description":"An empty string is allowed.","example":"62701"}}},"part_bundle_item_input":{"type":"object","required":["part_id"],"description":"Unknown keys are refused. An omitted value is stored with the column default (capitalized false, quantity 1, bill_to_customer true, discount 0) - except that a missing quantity is refused for parts not of type supplies.\n","properties":{"part_id":{"type":"string","format":"uuid","description":"A live part of the org, once per bundle.","example":"5e6f7a8b-9c0d-4e1f-8a2b-3c4d5e6f7a8b"},"capitalized":{"type":"boolean","example":false},"quantity":{"type":"number","minimum":0,"description":"A whole number unless the part is of type supplies.","example":2},"bill_to_customer":{"type":"boolean","example":true},"discount":{"type":"number","maximum":100,"description":"Discount percentage.","example":0}}},"part_bundle_body":{"type":"object","required":["name","items"],"description":"The create and update body. Unknown keys are refused.","properties":{"name":{"type":"string","description":"Unique (exact match) among the org's bundles; at most 255 characters.","example":"Service Parts Bundle"},"task_type":{"type":"string","enum":["onboarding","service","generic","maintenance"],"description":"The kind of task the bundle is for. Omitted - generic on create, unchanged on update.\n","example":"service"},"items":{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/part_bundle_item_input"}}}},"part_bundle_item":{"type":"object","properties":{"part_id":{"type":"string","format":"uuid"},"quantity":{"type":"number","example":2},"capitalized":{"type":"boolean","nullable":true},"bill_to_customer":{"type":"boolean","nullable":true},"discount":{"type":"number","nullable":true,"example":0}}},"part_bundle_item_with_part":{"allOf":[{"$ref":"#/components/schemas/part_bundle_item"},{"type":"object","properties":{"item_details":{"type":"object","description":"The part as it is now (values null if the part row is gone).","properties":{"name":{"type":"string","nullable":true,"example":"Hydraulic Filter"},"sell_price":{"type":"number","nullable":true,"example":24.5},"type":{"type":"string","nullable":true,"enum":["supplies","inventoried","non_inventoried"]},"quantity":{"type":"integer","nullable":true,"description":"The part's stock quantity."},"item_number":{"type":"integer","nullable":true}}}}}]},"part_bundle_head":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","example":"Service Parts Bundle"},"task_type":{"type":"string","enum":["onboarding","service","generic","maintenance"]},"created_at":{"type":"string","format":"date-time","nullable":true},"updated_at":{"type":"string","format":"date-time","nullable":true},"updated_by":{"type":"string","format":"uuid","nullable":true,"description":"The user who last saved the bundle."}}},"part_bundle":{"description":"A bundle as GET /platform/part_bundles/{id} and the search return it.","allOf":[{"$ref":"#/components/schemas/part_bundle_head"},{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/part_bundle_item_with_part"}}}}]},"part_bundle_updated":{"description":"The bundle after an update - items without item_details.","allOf":[{"$ref":"#/components/schemas/part_bundle_head"},{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/part_bundle_item"}}}}]},"part_bundle_created":{"description":"The inserted rows - the bundle row (with org) and its item rows as stored.","allOf":[{"$ref":"#/components/schemas/part_bundle_head"},{"type":"object","properties":{"org":{"type":"string","format":"uuid"},"items":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/part_bundle_item"},{"type":"object","properties":{"part_bundle":{"type":"string","format":"uuid"},"org":{"type":"string","format":"uuid"}}}]}}}}]},"part_bundle_search_body":{"type":"object","required":["paginate","search"],"properties":{"paginate":{"type":"object","required":["currentPage","perPage"],"properties":{"currentPage":{"type":"integer","minimum":1,"example":1},"perPage":{"type":"integer","minimum":1,"maximum":100,"example":10}}},"search":{"type":"object","description":"Unknown keys are refused. {} lists every bundle.","properties":{"name":{"type":"string","nullable":true,"description":"Part of the bundle name, case-insensitive; '' or null for any.","example":""},"task_type":{"type":"array","description":"Only these task types; empty for any.","items":{"type":"string","enum":["onboarding","service","generic","maintenance"]},"example":[]},"order_by":{"$ref":"#/components/schemas/order_by"}}}}},"part_bundle_search_result":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/part_bundle"}},"pagination":{"$ref":"#/components/schemas/lotus_order_pagination"},"orderByColumnNamesList":{"type":"array","items":{"type":"string"},"example":["name","task_type","created_at"]}}},"service_credential":{"type":"object","description":"A credential as the list returns it. Never carries the secret.","properties":{"id":{"type":"string","format":"uuid"},"key_id":{"type":"string","description":"The public key id the service sends in x-clouderm-key-id.","example":"cerm_k_0123456789abcdef0123456789abcdef"},"name":{"type":"string","example":"Inspection analysis service"},"description":{"type":"string","nullable":true},"expires_at":{"type":"string","format":"date-time"},"last_used_at":{"type":"string","format":"date-time","nullable":true},"revoked_at":{"type":"string","format":"date-time","nullable":true},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"created_by":{"type":"string","format":"uuid","nullable":true},"service_user_id":{"type":"string","format":"uuid","description":"The dedicated service user the credential acts as."},"role_id":{"type":"string","format":"uuid","nullable":true},"role_name":{"type":"string","nullable":true,"example":"CloudERM Service Desk"},"status":{"type":"string","enum":["ACTIVE","EXPIRED","REVOKED"],"description":"REVOKED when revoked_at is set, else EXPIRED once expires_at has passed, else ACTIVE."}}},"service_credential_row":{"type":"object","description":"The stored credential row (PATCH response). No secret, role_id or status.","properties":{"id":{"type":"string","format":"uuid"},"org":{"type":"string","format":"uuid"},"key_id":{"type":"string","example":"cerm_k_0123456789abcdef0123456789abcdef"},"service_user_id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"expires_at":{"type":"string","format":"date-time"},"last_used_at":{"type":"string","format":"date-time","nullable":true},"revoked_at":{"type":"string","format":"date-time","nullable":true},"revoked_by":{"type":"string","format":"uuid","nullable":true},"deleted":{"type":"boolean","example":false},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"created_by":{"type":"string","format":"uuid","nullable":true},"updated_by":{"type":"string","format":"uuid","nullable":true}}},"service_credential_create_body":{"type":"object","required":["name","role_id","expires_at"],"description":"Unknown keys are refused. Strings are trimmed.","properties":{"name":{"type":"string","minLength":1,"maxLength":255,"description":"Also the service user's name.","example":"Inspection analysis service"},"description":{"type":"string","nullable":true,"description":"An empty string is allowed.","example":"Posts inspection analysis results"},"role_id":{"type":"string","format":"uuid","description":"The role the credential acts with - an org-type role whose modules the org has enabled.\n","example":"7c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f"},"expires_at":{"type":"string","format":"date-time","description":"ISO date in the future, at most 390 days from now.","example":"2027-06-30T00:00:00Z"}}},"service_credential_update_body":{"type":"object","minProperties":1,"description":"At least one key; unknown keys are refused. Strings are trimmed.","properties":{"name":{"type":"string","minLength":1,"maxLength":255,"example":"Inspection analysis service"},"description":{"type":"string","nullable":true},"role_id":{"type":"string","format":"uuid","description":"Checked as on the create."},"expires_at":{"type":"string","format":"date-time","description":"ISO date in the future, at most 390 days from now.","example":"2027-06-30T00:00:00Z"}}},"service_credential_created":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"key_id":{"type":"string","example":"cerm_k_0123456789abcdef0123456789abcdef"},"secret":{"type":"string","description":"The plaintext secret - returned here only, never again.","example":"cerm_s_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"expires_at":{"type":"string","format":"date-time"},"service_user_id":{"type":"string","format":"uuid"},"notice":{"type":"string","example":"Store this secret now. It cannot be retrieved again — rotate the credential if it is lost."}}},"service_credential_rotated":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"key_id":{"type":"string","description":"Unchanged.","example":"cerm_k_0123456789abcdef0123456789abcdef"},"secret":{"type":"string","description":"The new plaintext secret - returned here only, never again.","example":"cerm_s_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"},"notice":{"type":"string","example":"Store this secret now. It cannot be retrieved again — rotate the credential if it is lost."}}},"service_credential_revoked":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"key_id":{"type":"string","example":"cerm_k_0123456789abcdef0123456789abcdef"},"revoked_at":{"type":"string","format":"date-time"}}},"purchase_order_fields":{"type":"object","description":"The purchase order header columns a client may write. The create and update bodies add po_number (each with its own rule), their other keys and their required lists on top; the stored record (purchase_order) adds the server-set columns.\n","properties":{"description":{"type":"string","nullable":true},"vendor":{"type":"string","format":"uuid","nullable":true,"description":"Required on create and never cleared by the API, so null is not a value to send; a few old or imported stored rows have none.\n"},"freight":{"type":"number","minimum":0,"maximum":99999999.99,"nullable":true,"description":"Flat freight charge in dollars, rounded to 2 decimals. Not a rate, and not tied to freight_companies. null is accepted on write and stored as 0.\n"},"sales_tax":{"type":"number","minimum":0,"maximum":99999999.99,"nullable":true,"description":"Flat sales tax in dollars (a money value, not a rate), rounded to 2 decimals. null is accepted on write and stored as 0.\n"},"miscellaneous":{"type":"number","minimum":0,"maximum":99999999.99,"nullable":true,"description":"Flat miscellaneous charge in dollars, rounded to 2 decimals. null is accepted on write and stored as 0.\n"}}},"purchase_order":{"description":"The stored purchase order record (purchase_orders), as every read returns it. readOnly columns are set by the server. The three charges are never null once stored.\n","allOf":[{"$ref":"#/components/schemas/purchase_order_fields"},{"type":"object","properties":{"po_number":{"type":"string","description":"Unique in the org, voided purchase orders included; allocated by the API when the create sends none."},"id":{"type":"string","format":"uuid","readOnly":true},"org":{"type":"string","format":"uuid","readOnly":true},"state":{"type":"string","readOnly":true,"description":"new, released, partially_received, received or closed. Moved by the create (new or released), POST /purchase_orders/status/update (released, closed), receiving (partially_received, received) and the update's re-opening, which moves a partially_received, received or closed PO back to partially_received or released. The column has no CHECK, and a few old or imported rows carry other values (RECEIVED, open), so treat an unknown value as read-only history.\n"},"invoice_state":{"type":"string","enum":["not_invoiced","partially_invoiced","invoiced"],"readOnly":true,"description":"DOZ-7163. How much of the purchase order its vendor invoices cover, independent of state; recomputed by every invoice write, every update or receipt and every status change.\n"},"projected_cost":{"type":"number","nullable":true,"readOnly":true,"description":"Sum of (item_price x quantity_ordered) over the line items only."},"actual_cost":{"type":"number","nullable":true,"readOnly":true,"description":"Sum of (item_price x quantity_received) over the line items only."},"created_at":{"type":"string","format":"date-time","readOnly":true},"created_by":{"type":"string","format":"uuid","nullable":true,"readOnly":true,"description":"DOZ-7488. The user who created the PO (the \"purchaser\" both segregation-of-duties settings refer to). NULL for ETL-imported POs and POs created before the audit log existed; the rules do not apply to those.\n"},"closed_at":{"type":"string","format":"date-time","nullable":true,"readOnly":true},"closing_information":{"description":"Who closed the purchase order. null after the update re-opens a closed or received PO.\n","nullable":true,"allOf":[{"$ref":"#/components/schemas/purchase_order_action_information"}]},"deletion_information":{"$ref":"#/components/schemas/purchase_order_action_information"},"deleted":{"type":"boolean","readOnly":true},"imported":{"type":"boolean","readOnly":true,"description":"True for a purchase order the ETL imported"}}}]},"purchase_order_action_information":{"type":"object","readOnly":true,"description":"Who closed (closing_information) or voided (deletion_information) the purchase order, and why. A new record starts with every key null; a re-open sets closing_information itself to null.\n","properties":{"date":{"type":"string","format":"date-time","nullable":true},"user_id":{"type":"string","format":"uuid","nullable":true},"username":{"type":"string","nullable":true},"reason":{"type":"string","nullable":true,"description":"The closing_notes or deletion_notes; absent when a close sent none"},"reconciliation":{"description":"closing_information only, on a PO closed since DOZ-7163. The vendor-invoice reconciliation frozen at the close, plus note (from the close request), invoice_ids (the live invoices then) and closed_short_variances (what the closer signed, DOZ-7758). A later receipt or invoice edit never rewrites it, so it keeps the field set of its own close: one frozen before the DOZ-7758 rework carries accepted_variance_total and accepted_variance_count instead of the per-kind fields.\n","allOf":[{"$ref":"#/components/schemas/po_reconciliation"},{"type":"object","properties":{"note":{"type":"string","nullable":true},"invoice_ids":{"type":"array","items":{"type":"string","format":"uuid"}},"closed_short_variances":{"type":"object","properties":{"line_count":{"type":"integer"},"amount":{"type":"number"}}}}}]}}},"purchase_order_totals":{"type":"object","description":"The landed totals every purchase order read and write derives on top of the stored record.","properties":{"total_projected_cost":{"type":"number","readOnly":true,"description":"Derived, not stored. projected_cost + freight + sales_tax + miscellaneous, rounded to cents."},"total_actual_cost":{"type":"number","readOnly":true,"description":"Derived, not stored. actual_cost + freight + sales_tax + miscellaneous, rounded to cents."}}},"purchase_order_capabilities":{"type":"object","description":"What the calling user may do to the purchase order - on the search rows and GET /purchase_orders/{id}, not on the create or update answer.\n","properties":{"can_release":{"type":"boolean","readOnly":true,"description":"DOZ-7488, extended by DOZ-7489. Whether the CALLING user may move this PO to released. Folds in the state rule (only a new PO is releasable), PO.ALLOW_PURCHASER_EQUALS_APPROVER and the PURCHASE_ORDERS_APPROVE permission, so it is the single value to read.\n"},"can_receive":{"type":"boolean","readOnly":true,"description":"DOZ-7488, extended by DOZ-7489. Whether the CALLING user may receive against this PO. Folds in the state rule (released or partially_received), PO.ALLOW_PURCHASER_EQUALS_RECEIVER and the PURCHASE_ORDERS_RECEIVE permission.\n"},"can_close":{"type":"boolean","readOnly":true,"description":"DOZ-7489. Whether the CALLING user may close this PO. Folds in the state rule (received or partially_received) and the PURCHASE_ORDERS_CLOSE permission. It does NOT account for PURCHASE_ORDERS_CLOSE_DISCREPANCY, which depends on a full reconciliation summary per PO; a user holding CLOSE but not CLOSE_DISCREPANCY can see can_close true and still get a 403 when the invoices do not match the PO.\n"},"can_approve_price_variances":{"type":"boolean","readOnly":true,"description":"DOZ-7848. Whether the CALLING user may approve a vendor invoice's PRICE variances on this PO (billed above the PO's unit price): the PO's purchaser or a holder of PURCHASE_ORDERS_INVOICE_VARIANCE_APPROVE, while the PO is released, partially_received or received.\n"},"can_approve_quantity_variances":{"type":"boolean","readOnly":true,"description":"DOZ-7848. Whether the CALLING user may approve a vendor invoice's QUANTITY variances on this PO (units billed that no delivery can cover): holders of PURCHASE_ORDERS_INVOICE_QUANTITY_VARIANCE_APPROVE only, the purchaser included. Same states as the price flag.\n"},"can_approve_variances":{"type":"boolean","readOnly":true,"deprecated":true,"description":"DOZ-7758. Either of the two flags above. Kept for one release while the web moves to the per-kind flags; removed with DOZ-7850.\n"}}},"purchase_order_search_body":{"type":"object","required":["search","paginate"],"properties":{"search":{"type":"object","description":"Every filter is optional. Unknown keys are refused with 400.","properties":{"vendor":{"type":"string","description":"One vendor's purchase orders (a uuid); '' means no filter"},"state":{"type":"array","items":{"type":"string","enum":["new","released","partially_received","received","closed"]}},"invoice_state":{"type":"array","description":"DOZ-7163. Independent of state - a received purchase order can still be partially_invoiced.","items":{"type":"string","enum":["not_invoiced","partially_invoiced","invoiced"]}},"po_number":{"type":"string","minLength":1,"description":"Case-insensitive substring match; % and _ are not escaped, so they act as wildcards.\n"},"item_ids":{"type":"array","description":"Purchase orders with a live line for any of these catalog part/asset ids","items":{"type":"string","format":"uuid"}},"has_allocatable_items":{"type":"boolean","nullable":true,"description":"true keeps only purchase orders with a part or CUSTOM line not yet received and short of its ordered quantity (existing allocations are not subtracted).\n"},"order_by":{"$ref":"#/components/schemas/order_by"}}},"paginate":{"$ref":"#/components/schemas/paginate"}},"example":{"search":{"state":["released","partially_received"]},"paginate":{"currentPage":1,"perPage":10}}},"purchase_order_search_row":{"description":"One row of POST /purchase_orders/search.","allOf":[{"$ref":"#/components/schemas/purchase_order"},{"$ref":"#/components/schemas/purchase_order_totals"},{"$ref":"#/components/schemas/purchase_order_capabilities"},{"type":"object","properties":{"vendor_details":{"type":"object","properties":{"name":{"type":"string","nullable":true},"contacts":{"type":"array","nullable":true,"description":"The vendor's live contacts; null, not [], when it has none","items":{"$ref":"#/components/schemas/purchase_order_vendor_contact"}}}},"item_count":{"type":"string","nullable":true,"description":"Total quantity_ordered over the live lines, as a bigint string; null with no lines"},"items_received":{"type":"string","nullable":true,"description":"Total quantity_received over the live lines, as a bigint string; null with no lines"},"allocatable_items_count":{"type":"string","description":"How many part or CUSTOM lines are not yet received and short of their ordered quantity, as a bigint string.\n"},"latest_received_at":{"type":"string","format":"date-time","nullable":true,"description":"When the last line was received"}}}]},"purchase_order_search_result":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/purchase_order_search_row"}},"pagination":{"$ref":"#/components/schemas/pagination"},"orderByColumnNamesList":{"$ref":"#/components/schemas/orderByColumnNamesList"}}},"purchase_order_vendor_contact":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"email":{"type":"string","nullable":true},"title":{"type":"string","nullable":true}}},"purchase_order_detail":{"description":"The purchase order as GET /purchase_orders/{id} returns it.","allOf":[{"$ref":"#/components/schemas/purchase_order"},{"$ref":"#/components/schemas/purchase_order_totals"},{"$ref":"#/components/schemas/purchase_order_capabilities"},{"type":"object","properties":{"vendor_details":{"type":"object","nullable":true,"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","nullable":true},"primary_address":{"type":"string","nullable":true},"city":{"type":"string","nullable":true},"state":{"type":"string","nullable":true},"website_url":{"type":"string","nullable":true},"default_contact":{"type":"string","format":"uuid","nullable":true},"contacts":{"type":"array","description":"The vendor's live contacts; [] when it has none","items":{"$ref":"#/components/schemas/purchase_order_vendor_contact"}}}},"items":{"type":"array","description":"The live lines, in no guaranteed order","items":{"$ref":"#/components/schemas/po_item_detail"}},"audit_log":{"type":"array","description":"The 20 newest audit rows, newest first; GET /purchase_orders/{id}/audit_log has them all","items":{"$ref":"#/components/schemas/purchase_order_audit_log_entry"}},"invoices":{"type":"array","description":"DOZ-7163. The purchase order's live vendor invoices with their lines and attachments - the same list as data.invoices from GET /lotus/purchase-orders/{id}/invoices.\n","items":{"$ref":"#/components/schemas/po_vendor_invoice"}},"invoice_positions":{"type":"array","description":"Per line - ordered, received, already invoiced, still invoiceable.","items":{"$ref":"#/components/schemas/po_invoice_line_position"}},"reconciliation":{"$ref":"#/components/schemas/po_reconciliation"}}}]},"po_item":{"type":"object","description":"A stored purchase order line (purchase_order_items). The columns not marked readOnly come from the line the client sent to the create or update; received is also cleared by the update's re-opening. item_total and actual_item_total are computed from the price, received_at by the receipt.\n","properties":{"id":{"type":"string","format":"uuid","readOnly":true},"purchase_order":{"type":"string","format":"uuid","readOnly":true},"po_number":{"type":"string","readOnly":true},"item_type":{"type":"string","enum":["part","asset","CUSTOM"]},"item_id":{"type":"string","format":"uuid","nullable":true,"description":"The catalog part or asset; null on a CUSTOM line"},"name":{"type":"string","nullable":true,"description":"A CUSTOM line's own name; a catalog line keeps one only if the create was sent one (the catalog record's name is in item_details)\n"},"quantity_ordered":{"type":"integer"},"quantity_received":{"type":"integer"},"item_price":{"type":"number","readOnly":true,"description":"The unit price - written from the line's `price` on create and update"},"item_total":{"type":"number","readOnly":true,"description":"item_price x quantity_ordered"},"actual_item_total":{"type":"number","nullable":true,"readOnly":true,"description":"item_price x quantity_received. Written by each receipt (0 when a receipt closes the line with nothing received), re-priced by an edit once something is received, otherwise kept; 0 until a receipt first writes it (the column defaults to 0); null only on a line written outside the API.\n"},"capitalized":{"type":"boolean"},"estimated_arrival_date":{"type":"string","format":"date-time","nullable":true},"part_asset_ref":{"type":"string","format":"uuid","nullable":true,"description":"The asset this line was bought for"},"received":{"type":"boolean","description":"Set by the receive call that sends it; true closes the line to further receiving, even short of quantity_ordered.\n"},"receiving_notes":{"type":"string","nullable":true},"received_at":{"type":"string","format":"date-time","nullable":true,"readOnly":true},"created_at":{"type":"string","format":"date-time","readOnly":true},"org":{"type":"string","format":"uuid","readOnly":true},"deleted":{"type":"boolean","readOnly":true},"imported":{"type":"boolean","readOnly":true}}},"po_item_detail":{"type":"object","description":"A purchase order line as GET /purchase_orders/{id} returns it - the po_item columns except purchase_order, po_number, actual_item_total, org, deleted and imported, plus the catalog details and the line's allocations.\n","properties":{"id":{"type":"string","format":"uuid"},"item_type":{"type":"string","enum":["part","asset","CUSTOM"]},"item_id":{"type":"string","format":"uuid","nullable":true},"name":{"type":"string","nullable":true},"quantity_ordered":{"type":"integer"},"quantity_received":{"type":"integer"},"item_price":{"type":"number"},"item_total":{"type":"number"},"capitalized":{"type":"boolean"},"estimated_arrival_date":{"type":"string","format":"date-time","nullable":true},"part_asset_ref":{"type":"string","format":"uuid","nullable":true},"part_asset_ref_details":{"type":"object","nullable":true,"description":"The asset named by part_asset_ref; null when it is not set","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","nullable":true},"asset_number":{"type":"string"}}},"received":{"type":"boolean"},"receiving_notes":{"type":"string","nullable":true},"received_at":{"type":"string","format":"date-time","nullable":true},"created_at":{"type":"string","format":"date-time"},"item_details":{"$ref":"#/components/schemas/po_item_details"},"consumed":{"type":"number","description":"Sum of quantity_RECEIVED over this line's allocations - NOT the allocated total. Use quantity_allocatable for remaining capacity. 0 on an asset line.\n"},"quantity_allocated":{"type":"number","description":"Units of this line allocated to work orders. 0 on an asset line."},"quantity_stocked":{"type":"number","description":"Units of this line received into stock (allocation rows with no work order, written by the receive flow). Count against capacity just like an allocation. 0 on an asset line.\n"},"quantity_allocatable":{"type":"number","description":"quantity_ordered - (quantity_allocated + quantity_stocked), floored at 0; 0 once the line is received.\n"},"allocatable":{"type":"boolean","description":"Whether DOZ-7310's allocation endpoint would accept this line: PO in an allocatable state (released or partially_received, DOZ-7308), not an asset line, not received, and quantity_allocatable above 0.\n"},"consumption_history":{"type":"array","description":"The line's live (not voided) lotus_parts_fulfillment rows, oldest first, each with entry_type and work_order added; [] on an asset line.\n","items":{"$ref":"#/components/schemas/po_item_allocation"}}}},"po_item_details":{"type":"object","description":"The catalog record behind a line. Keys that do not apply to the line's item_type are null; a CUSTOM line carries only its name.\n","properties":{"name":{"type":"string","nullable":true,"description":"The part's or asset's name, or the CUSTOM line's own name"},"item_number":{"type":"string","nullable":true,"description":"The part's item_number (as text) or the asset's asset_number"},"part_number":{"type":"string","nullable":true,"description":"Parts only"},"part_type":{"type":"string","nullable":true,"description":"Parts only"},"category_id":{"type":"string","format":"uuid","nullable":true,"description":"Assets only"},"category_name":{"type":"string","nullable":true,"description":"Assets only"},"sell_price":{"type":"number","nullable":true},"purchase_order":{"type":"string","format":"uuid","nullable":true,"description":"The purchase order recorded on the part or asset itself"}}},"po_item_allocation":{"type":"object","description":"One lotus_parts_fulfillment row (every column), plus entry_type and work_order.\n","properties":{"id":{"type":"string","format":"uuid","description":"The allocation id - what a receive's items[].allocation_id names"},"org":{"type":"string","format":"uuid"},"order_id":{"type":"string","format":"uuid","nullable":true,"description":"The work order; null for a stock entry"},"line_item_id":{"type":"string","format":"uuid","nullable":true},"purchase_order_id":{"type":"string","format":"uuid"},"purchase_order_item_id":{"type":"string","format":"uuid"},"purchase_order_item_ref":{"type":"string","format":"uuid","nullable":true},"quantity_allocated":{"type":"number"},"quantity_received":{"type":"number"},"status":{"type":"string","enum":["ALLOCATED","PARTIALLY_RECEIVED","RECEIVED","FAILED"]},"voided":{"type":"boolean"},"received_at":{"type":"string","format":"date-time","nullable":true},"received_by":{"type":"string","format":"uuid","nullable":true},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time","nullable":true},"entry_type":{"type":"string","enum":["WORK_ORDER","STOCK"]},"work_order":{"type":"object","nullable":true,"description":"null for a stock entry","properties":{"id":{"type":"string","format":"uuid"},"order_number":{"type":"string"},"status":{"type":"string"},"asset_id":{"type":"string","format":"uuid","nullable":true},"asset_name":{"type":"string","nullable":true},"asset_number":{"type":"string","nullable":true}}}}},"purchase_order_audit_log_entry":{"type":"object","description":"One audit row (purchase_order_audit_logs), as embedded in GET /purchase_orders/{id}.","properties":{"id":{"type":"string","format":"uuid"},"action":{"type":"string","description":"What happened - created, updated, received, status_changed, deleted, allocated, allocation_updated, allocation_removed, invoice_recorded, invoice_updated, invoice_removed or invoice_variance_approved.\n"},"actor_user_id":{"type":"string","format":"uuid","nullable":true},"actor_username":{"type":"string","nullable":true},"po_state_before":{"type":"string","nullable":true},"po_state_after":{"type":"string","nullable":true},"detail":{"type":"object","description":"What changed; its keys depend on the action"},"created_at":{"type":"string","format":"date-time"}}},"purchase_order_audit_log":{"description":"One stored audit row - the embedded entry plus the columns it leaves out.","allOf":[{"$ref":"#/components/schemas/purchase_order_audit_log_entry"},{"type":"object","properties":{"org":{"type":"string","format":"uuid"},"purchase_order":{"type":"string","format":"uuid"},"po_number":{"type":"string","nullable":true},"request_id":{"type":"string","format":"uuid","nullable":true,"readOnly":true,"description":"The X-Request-Id of the request that wrote the row, the same id its journal and financial audit rows and log lines carry. Null for scheduled jobs and for rows written before DOZ-7840. Not part of the entries embedded in GET /purchase_orders/{id}.\n"}}}]},"purchase_order_not_received_body":{"type":"object","required":["search","paginate"],"properties":{"search":{"type":"object","description":"Unknown keys are refused with 400.","properties":{"item_type":{"type":"string","nullable":true,"enum":["","part","asset",null],"description":"Only part or only asset lines; '' or null means every line, CUSTOM included"}}},"paginate":{"$ref":"#/components/schemas/paginate"}},"example":{"search":{"item_type":"part"},"paginate":{"currentPage":1,"perPage":10}}},"purchase_order_not_received_row":{"type":"object","properties":{"po_number":{"type":"string"},"po_id":{"type":"string","format":"uuid"},"vendor":{"type":"string","nullable":true,"description":"The vendor's name"},"quantity_ordered":{"type":"integer"},"quantity_received":{"type":"integer"},"item_price":{"type":"number"},"item_details":{"type":"object","description":"The catalog part or asset; both keys are null on a CUSTOM line","properties":{"name":{"type":"string","nullable":true},"item_number":{"type":"string","nullable":true,"description":"The part's item_number (as text) or the asset's asset_number"}}}}},"purchase_order_create":{"description":"Body of POST /purchase_orders. Unknown keys are refused with 400, so send none of the readOnly columns of purchase_order except state, which the create accepts.\n","allOf":[{"$ref":"#/components/schemas/purchase_order_fields"},{"type":"object","required":["vendor"],"properties":{"vendor":{"type":"string","format":"uuid","nullable":false},"po_number":{"type":"string","maxLength":255,"pattern":"^([a-zA-Z0-9-_.]+)?$","nullable":true,"description":"Optional (DOZ-7258). Omit, or send '' or null, to have the API allocate the org's next number, zero-padded to 6. A number made only of zeros answers 400.\n"},"state":{"type":"string","enum":["new","released"],"default":"new","description":"released creates it already released - see the operation for what that needs"},"booking":{"type":"boolean","description":"DOZ-7488. Set by the work-order parts-booking flow, which must create the PO already released for its lines to be allocatable. Exempts this create from the release permission and the purchaser-is-not-approver rule (PO.ALLOW_PURCHASER_EQUALS_APPROVER). Payload-only, never stored; it is recorded in the creation audit row instead.\n"},"items":{"type":"array","items":{"$ref":"#/components/schemas/po_item_create"}}}}],"example":{"vendor":"00000000-0000-4000-8000-000000000000","description":"Hydraulic filters","items":[{"item_type":"CUSTOM","name":"Freight-in pallet","quantity_ordered":1,"price":25}]}},"po_item_fields":{"type":"object","description":"The line fields a client may write on create and update.","properties":{"item_type":{"type":"string","enum":["part","asset","CUSTOM"],"description":"CUSTOM for a billing-only line with no catalog record. For a catalog line the stored item_type comes from the catalog record, whatever is sent.\n"},"item_id":{"type":"string","format":"uuid","description":"The catalog part or asset. Refused on a CUSTOM line."},"name":{"type":"string","description":"Required on a line sent with item_type CUSTOM - its display name (an update line with neither id nor item_id is also stored as a CUSTOM line, and without a name it is stored nameless). On a catalog line the create stores it; the update never writes it, but a name that differs from the stored one still counts as a structural edit (permission gate, \"updated\" audit row).\n"},"quantity_ordered":{"type":"integer","minimum":1,"maximum":32767,"description":"Whole units (a smallint column)"},"price":{"type":"number","exclusiveMinimum":true,"minimum":0,"description":"The unit price (stored as item_price). Defaults to the catalog record's purchase_price, or 0 on a CUSTOM line.\n"},"persist_price":{"type":"boolean","description":"Also write price onto the catalog part or asset as its purchase_price"},"capitalized":{"type":"boolean","description":"Defaults to false on a new line; omitted on an existing line keeps its stored value"},"estimated_arrival_date":{"type":"string","format":"date-time","nullable":true},"part_asset_ref":{"type":"string","format":"uuid","nullable":true,"description":"The asset this line is bought for. Only on a part or CUSTOM line; a live asset of the org."}}},"po_item_create":{"description":"A line of POST /purchase_orders. Unknown keys are refused with 400.","allOf":[{"$ref":"#/components/schemas/po_item_fields"},{"type":"object","required":["quantity_ordered"],"description":"item_id is required unless item_type is CUSTOM, and name is required when it is."}]},"purchase_order_update":{"description":"Body of POST /purchase_orders/{id}. Every key is optional; unknown keys are refused with 400. Only the keys sent are changed.\n","allOf":[{"$ref":"#/components/schemas/purchase_order_fields"},{"type":"object","properties":{"po_number":{"type":"string","maxLength":255,"pattern":"^[a-zA-Z0-9-_.]+$","description":"Renumbers the PO (DOZ-7258); '' and null are refused. On a PO past new it needs the state's PURCHASE_ORDERS_UPDATE_* permission like any header edit.\n"},"vendor":{"type":"string","format":"uuid","nullable":false},"reconciliation_notes":{"type":"string","nullable":true,"description":"A note on this save, recorded only in its \"updated\" audit row (detail.notes). Not stored on the PO, and needs no state permission on its own.\n"},"items":{"type":"array","description":"The full desired line set; a stored line left out is removed. Omit to keep the lines as they are.","items":{"$ref":"#/components/schemas/po_item_update"}}}}],"example":{"description":"Hydraulic filters, revised","reconciliation_notes":"Vendor confirmed the revised quantity"}},"po_item_update":{"description":"A line of POST /purchase_orders/{id}. Needs at least one of id, item_id or item_type. Unknown keys are refused with 400. Omitted fields keep their stored value.\n","allOf":[{"$ref":"#/components/schemas/po_item_fields"},{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"The stored line to edit or receive. A catalog line sent without it still edits the stored line when its item_id matches exactly one; otherwise it is a new line, and one with neither id nor item_id is a new CUSTOM line. A catalog line being received needs its item_id too - see the operation's Receiving notes.\n"},"quantity_received":{"type":"integer","minimum":0,"description":"The line's total received so far, not the increment. Required with received, receiving_notes or allocation_id. Can't go below the stored value or above quantity_ordered.\n"},"received":{"type":"boolean","description":"true closes the line to further receiving, even short of quantity_ordered"},"receiving_notes":{"type":"string","nullable":true},"allocation_id":{"type":"string","format":"uuid","description":"A work-order allocation on this line (the `id` of one of its `consumption_history` rows) to credit before the others. Requires `quantity_received`. It must be a live, not fully received work-order allocation of this line, or the request is a 400. Ignored when the line is already received or the line receives nothing new; closing an unreceived line short counts as receiving, so it is checked then.\n"}}}]},"purchase_order_write_result":{"type":"object","description":"The answer of the create and the update.","properties":{"message":{"type":"string","enum":["Purchase order created","Purchase order updated"]},"data":{"description":"The stored purchase order with its landed totals and every live line as stored. Unlike GET /purchase_orders/{id} it carries no vendor_details, catalog details, allocations, invoices, audit_log or can_* flags.\n","allOf":[{"$ref":"#/components/schemas/purchase_order"},{"$ref":"#/components/schemas/purchase_order_totals"},{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/po_item"}}}}]}}},"report_pagination":{"type":"object","description":"A page cut from the report's full result in memory (paginateRows, or the handler's identical block). Without paginate every row is returned and only currentPage (1) and total are set. from is 1-based.\n","required":["currentPage","perPage","total","from","to","lastPage","prevPage","nextPage"],"properties":{"currentPage":{"type":"integer","example":1},"perPage":{"type":"integer","nullable":true,"example":25},"total":{"type":"integer","description":"Rows in the whole result.","example":3},"from":{"type":"integer","nullable":true,"description":"1-based position of the page's first row; null when there are no rows.","example":1},"to":{"type":"integer","nullable":true,"example":3},"lastPage":{"type":"integer","nullable":true,"example":1},"prevPage":{"type":"integer","nullable":true},"nextPage":{"type":"integer","nullable":true}}},"ar_report_pagination":{"type":"object","description":"knex-paginate without isLengthAware when paginate is sent - from is the 0-based offset, and total, lastPage, prevPage and nextPage are only sent on page 1 (a currentPage sent as the string \"1\" passes validation but is not treated as page 1, so it gets no totals either). Without paginate every row is returned and the block is currentPage 1, the total and nulls - except the A/R transaction search, which then pages 50 by default and sends the page-1 block.\n","required":["currentPage","perPage","from","to"],"properties":{"perPage":{"type":"integer","nullable":true,"example":20},"currentPage":{"type":"integer","example":1},"from":{"type":"integer","nullable":true,"description":"The 0-based offset of the page.","example":0},"to":{"type":"integer","nullable":true,"example":2},"total":{"type":"integer","description":"Page 1 only (and without paginate).","example":2},"lastPage":{"type":"integer","nullable":true,"description":"Page 1 only; 0 when there are no rows.","example":1},"prevPage":{"type":"integer","nullable":true,"description":"Page 1 only (always null there)."},"nextPage":{"type":"integer","nullable":true,"description":"Page 1 only; null when there is no second page."}}},"report_catalog_entry":{"type":"object","description":"A row of the global reports registry.","required":["id","report_name","report_url","report_group","sec_group","date_range_searchable","single_item_report","available_online","available_offline","supported_formats","permissions"],"properties":{"id":{"type":"string","format":"uuid","example":"5c2e7a90-1b3d-4e8f-a6c4-9d0b2f7e1a35"},"report_name":{"type":"string","nullable":true,"example":"Customer Deposit Liability Report"},"report_url":{"type":"string","nullable":true,"description":"The report's route under /reports (POST /reports/<report_url>) and the key the web builds its page path from.\n","example":"accounting/liability_report"},"report_group":{"type":"string","nullable":true,"description":"The section it is listed under (accounting, assets, customers, ...).","example":"accounting"},"sec_group":{"type":"number","nullable":true,"example":1},"date_range_searchable":{"type":"boolean","nullable":true,"description":"The report filters by date_from / date_to.","example":false},"single_item_report":{"type":"boolean","nullable":true,"description":"The report covers one customer or asset and is opened from its record.","example":false},"available_online":{"type":"boolean","nullable":true,"description":"The web has a page for the report.","example":true},"available_offline":{"type":"boolean","nullable":true,"description":"The report can be downloaded from the list (a download-only report when available_online is false).","example":true},"supported_formats":{"type":"array","nullable":true,"items":{"type":"string","enum":["json","csv","excel","pdf","iif"]},"description":"The formats the report's route accepts in search.format.","example":["json","excel","csv","pdf"]},"permissions":{"type":"array","nullable":true,"items":{"type":"string"},"description":"The permissions that open the report (any one of them); null = no category permission.\n","example":["REPORTS_ACCOUNTING"]}}},"report_catalog_result":{"type":"object","required":["count","results"],"properties":{"count":{"type":"integer","example":1},"results":{"type":"array","items":{"$ref":"#/components/schemas/report_catalog_entry"}}}},"top_report_entry":{"allOf":[{"$ref":"#/components/schemas/report_catalog_entry"},{"type":"object","required":["report_count"],"properties":{"report_count":{"type":"string","description":"How many times the caller ran it - a count, sent as a string.","example":"12"}}}]},"liability_report_body":{"type":"object","example":{"search":{"format":"json","order_type":"RENTAL","linkage":"LINKED"},"paginate":{"currentPage":1,"perPage":25}},"properties":{"search":{"$ref":"#/components/schemas/liability_report_search"},"paginate":{"$ref":"#/components/schemas/paginate"}}},"liability_report_search":{"type":"object","description":"Every field is optional and '' or null means unset; unknown keys are refused (there is no date filter).\n","properties":{"format":{"type":"string","enum":["json","csv","excel","pdf"],"default":"json","example":"json"},"customer_id":{"type":"string","format":"uuid","nullable":true,"description":"Only this customer's deposits."},"order_type":{"type":"string","enum":["RENTAL","SALE",null],"nullable":true,"description":"Only deposits taken on an order of this type. Send it uppercase - a lowercase value passes validation but matches nothing.\n","example":"RENTAL"},"linkage":{"type":"string","enum":["LINKED","UNLINKED",null],"nullable":true,"description":"LINKED - deposits taken on an order; UNLINKED - deposits with no order.","example":"LINKED"}}},"liability_report_row":{"type":"object","required":["payment_id","customer_id","company_name","customer_number","order_id","order_number","order_type","order_status","amount","applied","unconsumed","payment_type","origin_type","source_invoice_id","reference_number","transaction_date","paid_at"],"properties":{"payment_id":{"type":"string","format":"uuid","example":"8e4b2c1d-6f7a-4b9e-8c3d-2a1f0e9d8c7b"},"customer_id":{"type":"string","format":"uuid","nullable":true,"example":"7d1c2f4e-9a3b-4c5d-8e6f-0a1b2c3d4e5f"},"company_name":{"type":"string","nullable":true,"description":"Null when the customer is deleted.","example":"ALAMON INC"},"customer_number":{"type":"integer","nullable":true,"example":5048},"order_id":{"type":"string","format":"uuid","nullable":true,"description":"The order the deposit was taken on; null for an unlinked deposit.","example":"2b7c9d1e-3f4a-4b5c-9d6e-7f8a9b0c1d2e"},"order_number":{"type":"string","nullable":true,"description":"Null for an unlinked deposit or a deleted order.","example":"R-1042"},"order_type":{"type":"string","nullable":true,"enum":["RENTAL","SALE",null],"example":"RENTAL"},"order_status":{"type":"string","nullable":true,"example":"OPEN_CONTRACT"},"amount":{"type":"number","description":"The deposit, 2 decimals.","example":1500},"applied":{"type":"number","description":"The part already applied to invoices.","example":400},"unconsumed":{"type":"number","description":"amount minus applied - what is still owed; always above 0.","example":1100},"payment_type":{"type":"string","example":"CHECK"},"origin_type":{"type":"string","example":"MANUAL"},"source_invoice_id":{"type":"string","format":"uuid","nullable":true},"reference_number":{"type":"string","nullable":true,"example":"10442"},"transaction_date":{"type":"string","format":"date-time","nullable":true},"paid_at":{"type":"string","format":"date-time","nullable":true,"example":"2026-10-02T15:04:11.000Z"}}},"liability_report_totals":{"type":"object","description":"Over the whole filtered set, not the page.","required":["by_order_type","linked","unlinked","grand_total"],"properties":{"by_order_type":{"type":"object","description":"unconsumed summed per order type. A key appears only when the set has such a deposit; UNLINKED holds the deposits with no order type (no order, or a deleted one).\n","properties":{"RENTAL":{"type":"number"},"SALE":{"type":"number"},"UNLINKED":{"type":"number"}},"example":{"RENTAL":1100,"UNLINKED":250}},"linked":{"type":"number","description":"Deposits with an order_id.","example":1100},"unlinked":{"type":"number","description":"Deposits without an order_id.","example":250},"grand_total":{"type":"number","example":1350}}},"liability_report_result":{"type":"object","required":["rows","totals","pagination","supported_formats"],"properties":{"rows":{"type":"array","items":{"$ref":"#/components/schemas/liability_report_row"}},"totals":{"$ref":"#/components/schemas/liability_report_totals"},"pagination":{"$ref":"#/components/schemas/report_pagination"},"supported_formats":{"type":"array","items":{"type":"string"},"description":"From the report's registry row.","example":["json","excel","csv","pdf"]}}},"ar_aging_body":{"type":"object","example":{"search":{"format":"json","contract_status":["open_contract"],"detailed":true,"approved":true},"paginate":{"currentPage":1,"perPage":20}},"properties":{"search":{"$ref":"#/components/schemas/ar_aging_search"},"paginate":{"$ref":"#/components/schemas/paginate"}}},"ar_aging_search":{"type":"object","description":"Every field is optional; null means unset for the fields that allow it. Unknown keys are refused. Send the flags as JSON booleans - the strings \"true\" and \"false\" pass validation, but either string acts as true for pay_on_open_contracts, detailed and assigned_to_me.\n","properties":{"format":{"type":"string","enum":["json","pdf","csv","excel"],"default":"json","description":"'' or null = json.","example":"json"},"contract_status":{"type":"array","description":"Only invoices on orders in these statuses (empty = any). reservation, open_contract (with its skip-payment variant), closed_contract (with backdated), completed_contract are rental statuses; quote, open_sales_order, closed_sales_order sale ones; task_* work-order ones. null is refused.\n","items":{"type":"string","enum":["reservation","open_contract","closed_contract","completed_contract","quote","open_sales_order","closed_sales_order","task_pending","task_ready","task_in_progress","task_paused","task_done"]},"example":["open_contract"]},"customer_id":{"type":"string","format":"uuid","nullable":true},"customer_name":{"type":"string","nullable":true,"description":"The exact company name (not a partial match). '' is refused."},"customer_number":{"type":"number","nullable":true,"description":"The exact customer number; a numeric string is read as a number.","example":5048},"pay_on_open_contracts":{"type":"boolean","nullable":true,"description":"true - only rental invoices billed in advance (ADVANCE); false - on account (ON_ACCOUNT). Either value drops sale and work-order invoices.\n"},"detailed":{"type":"boolean","nullable":true,"description":"Fill each row's orders with its invoices (and the per-invoice file lines).","example":true},"assigned_to_me":{"type":"boolean","nullable":true,"description":"Only customers whose sales person is the caller."},"interval_type":{"type":"integer","enum":[1,2],"description":"Accepted but ignored - the buckets are always 30/60/90/120. null is refused.","example":1},"sort_by":{"type":"string","enum":["site","order_sales_person","contract_month"],"description":"Accepted but ignored - rows are always ordered by customer id. null is refused."},"approved":{"type":"boolean","nullable":true,"description":"Only approved (true) or unapproved (false) invoices.","example":true}}},"ar_aging_customer":{"type":"object","required":["id","company_name","customer_number","sales_person_id","sales_person_name","deleted"],"properties":{"id":{"type":"string","format":"uuid","example":"7d1c2f4e-9a3b-4c5d-8e6f-0a1b2c3d4e5f"},"company_name":{"type":"string","example":"ALAMON INC"},"customer_number":{"type":"integer","example":5048},"sales_person_id":{"type":"string","format":"uuid","nullable":true},"sales_person_name":{"type":"string","nullable":true,"example":"Dana Ruiz"},"deleted":{"type":"boolean","description":"Always false - deleted customers are left out.","example":false}}},"ar_aging_interval":{"type":"object","required":["key","value"],"properties":{"key":{"type":"string","enum":["current","<=30","31-60","61-90","91-120",">120","total"],"example":"<=30"},"value":{"type":"number","description":"The balance due in the bucket (not rounded); total is past due only.","example":1250.5}}},"ar_aging_buckets":{"type":"object","description":"The same seven figures keyed by bucket.","required":["current","<=30","31-60","61-90","91-120",">120","total"],"properties":{"current":{"type":"number"},"<=30":{"type":"number"},"31-60":{"type":"number"},"61-90":{"type":"number"},"91-120":{"type":"number"},">120":{"type":"number"},"total":{"type":"number","description":"Past due only (current is left out)."}},"example":{"current":300,"<=30":1250.5,"31-60":0,"61-90":0,"91-120":0,">120":0,"total":1250.5}},"ar_aging_order":{"type":"object","description":"One open invoice (sent with detailed only).","required":["interval_index","invoice_id","invoice_number","order_number","status","amount","created_at","total","notes","internal_notes","customer_po","approved"],"properties":{"interval_index":{"type":"integer","description":"The bucket - 0 current, 1 <=30, 2 31-60, 3 61-90, 4 91-120, 5 >120.","example":1},"invoice_id":{"type":"string","format":"uuid","example":"3f9a8b7c-2d4e-4f61-9a0b-5c7d8e9f1a2b"},"invoice_number":{"type":"string","example":"INV-2087"},"order_number":{"type":"string","example":"R-1042"},"status":{"type":"string","description":"Always '' - the column is blanked on every surface (DOZ-6911).","example":""},"amount":{"type":"number","nullable":true,"description":"The invoice's balance due.","example":1250.5},"created_at":{"type":"string","description":"The ORDER's creation date as MM/DD/YYYY in the org's timezone.","example":"09/14/2026"},"total":{"type":"number","description":"The invoice total.","example":1250.5},"notes":{"type":"string","nullable":true,"description":"The order's external notes."},"internal_notes":{"type":"string","nullable":true},"customer_po":{"type":"string","nullable":true},"approved":{"type":"boolean","example":true}}},"ar_aging_row":{"type":"object","required":["customer_details","intervals","orders","aging_flat"],"properties":{"customer_details":{"$ref":"#/components/schemas/ar_aging_customer"},"intervals":{"type":"array","description":"The seven buckets in order - current, <=30, 31-60, 61-90, 91-120, >120, total.","items":{"$ref":"#/components/schemas/ar_aging_interval"}},"orders":{"type":"array","description":"The customer's open invoices with detailed; empty otherwise.","items":{"$ref":"#/components/schemas/ar_aging_order"}},"aging_flat":{"$ref":"#/components/schemas/ar_aging_buckets"}}},"ar_aging_result":{"type":"object","required":["data","pagination","supported_formats"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ar_aging_row"}},"pagination":{"$ref":"#/components/schemas/ar_report_pagination"},"supported_formats":{"type":"array","items":{"type":"string"},"description":"From the report's registry row.","example":["json","excel","csv","pdf"]}}},"ar_search_body":{"type":"object","example":{"search":{"customer_number":"504","date_from":"2026-10-01T05:00:00.000Z","date_to":"2026-10-28T04:59:59.999Z","action_types":["PAYMENT_RECEIVED","PAYMENT_APPLIED"]},"paginate":{"currentPage":1,"perPage":50}},"properties":{"search":{"$ref":"#/components/schemas/ar_search_search"},"paginate":{"$ref":"#/components/schemas/paginate"}}},"ar_search_search":{"type":"object","description":"Every field is optional; '' or null means unset (customer_id, the dates, the amounts and action_types take null only). Unknown keys are refused - format included. Without paginate the first 50 rows are returned.\n","properties":{"customer_id":{"type":"string","format":"uuid","nullable":true},"customer_name":{"type":"string","nullable":true,"description":"Part of the company name, any case."},"customer_number":{"type":"string","nullable":true,"description":"Part of the customer number, as a string - a JSON number is refused.","example":"504"},"invoice_number":{"type":"string","nullable":true,"description":"Part of the entity number (an invoice or payment number), any case."},"date_from":{"type":"string","format":"date-time","nullable":true,"description":"event_at at or after this ISO instant. A date alone is midnight in the database's timezone.\n","example":"2026-10-01T05:00:00.000Z"},"date_to":{"type":"string","format":"date-time","nullable":true,"description":"event_at at or before this ISO instant - send the end of the day; a date alone (midnight) leaves that day out.\n","example":"2026-10-28T04:59:59.999Z"},"amount_min":{"type":"number","nullable":true,"description":"Inclusive; 0 counts."},"amount_max":{"type":"number","nullable":true,"description":"Inclusive."},"action_types":{"type":"array","nullable":true,"description":"Only these actions (empty or null = any). Any value of the financial journal's vocabulary is accepted, but only invoice, payment and payment-application actions can match.\n","items":{"type":"string","enum":["DEPOSIT_RECEIVED","CC_DEPOSIT_INITIATED","CC_PAYMENT_INITIATED","CC_DEPOSIT_CONFIRMED","CC_PAYMENT_CONFIRMED","CC_FEE_CHARGES_RECORDED","CC_PAYMENT_FAILED","STRIPE_REFUND_FLAGGED","PAYMENT_RECEIVED","PAYMENT_APPLIED","DISCOUNT_GRANTED","DISCOUNT_CLAWED_BACK","REFUND_CREDIT_ABSORBED","OVERPAYMENT_CREDITED","INVOICE_CREATED","INVOICE_UPDATED","INVOICE_APPROVED","INVOICE_VOIDED","ORDER_VOIDED","INVOICE_EMAILED","STATEMENT_EMAILED","DRAWER_OPENED","DRAWER_CLOSED","CUSTOMER_CREDIT_UPDATED","GL_CONFIG_UPDATED","ORG_CONFIG_UPDATED","PAYMENT_METHOD_UPDATED","CATEGORY_CONFIG_UPDATED","DEPRECIATION_SCHEDULE_UPDATED"]},"example":["PAYMENT_RECEIVED","PAYMENT_APPLIED"]}}},"ar_search_row":{"type":"object","required":["id","org","action","entity_type","entity_id","entity_number","customer_id","customer_name","customer_number","order_id","invoice_id","amount","event_at","created_at"],"properties":{"id":{"type":"string","format":"uuid","example":"6a5b4c3d-2e1f-4a0b-9c8d-7e6f5a4b3c2d"},"org":{"type":"string","format":"uuid","example":"1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d"},"action":{"type":"string","example":"PAYMENT_RECEIVED"},"entity_type":{"type":"string","enum":["INVOICE","PAYMENT","PAYMENT_APPLICATION"],"example":"PAYMENT"},"entity_id":{"type":"string","format":"uuid","nullable":true},"entity_number":{"type":"string","nullable":true,"description":"The invoice or payment number when the action has one.","example":"INV-2087"},"customer_id":{"type":"string","format":"uuid","nullable":true},"customer_name":{"type":"string","nullable":true,"description":"The customer's company name; null when the row names no customer.","example":"ALAMON INC"},"customer_number":{"type":"integer","nullable":true,"example":5048},"order_id":{"type":"string","format":"uuid","nullable":true},"invoice_id":{"type":"string","format":"uuid","nullable":true},"amount":{"type":"number","nullable":true,"example":400},"event_at":{"type":"string","format":"date-time","description":"When the action happened (the date filter's column).","example":"2026-10-02T15:04:11.000Z"},"created_at":{"type":"string","format":"date-time","description":"When the row was written (later than event_at for a backfilled row)."}}},"ar_search_result":{"type":"object","required":["data","pagination"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ar_search_row"}},"pagination":{"$ref":"#/components/schemas/ar_report_pagination"}}},"ar_details_body":{"type":"object","required":["search"],"example":{"search":{"customer_id":"7d1c2f4e-9a3b-4c5d-8e6f-0a1b2c3d4e5f"},"paginate":{"currentPage":1,"perPage":10}},"properties":{"search":{"$ref":"#/components/schemas/ar_details_search"},"paginate":{"$ref":"#/components/schemas/paginate"}}},"ar_details_search":{"type":"object","description":"customer_id is required; unknown keys are refused.","required":["customer_id"],"properties":{"customer_id":{"type":"string","format":"uuid","example":"7d1c2f4e-9a3b-4c5d-8e6f-0a1b2c3d4e5f"},"order_id":{"type":"string","format":"uuid","description":"Notes route only - one order's notes. The details route ignores it."},"start":{"type":"string","format":"date-time","nullable":true,"description":"The LATEST due date counted (due_date <= start). Any date Joi can parse passes, and the value is compared as sent.\n"},"end":{"type":"string","format":"date-time","nullable":true,"description":"The EARLIEST due date counted (due_date >= end)."},"approved":{"type":"boolean","nullable":true,"description":"Details route only - only approved (true) or unapproved (false) invoices."},"format":{"type":"string","enum":["json"],"description":"json only; '' or null = json."}}},"ar_details_row":{"type":"object","required":["id","order_number","status","created_at","total","notes","internal_notes","customer_po","customer_id","approved","unpaid_balance","paid_balance"],"properties":{"id":{"type":"string","format":"uuid","description":"The order id.","example":"2b7c9d1e-3f4a-4b5c-9d6e-7f8a9b0c1d2e"},"order_number":{"type":"string","example":"R-1042"},"status":{"type":"string","description":"The order's status as stored.","example":"OPEN_CONTRACT"},"created_at":{"type":"string","format":"date-time","nullable":true},"total":{"type":"number","description":"The total of every live invoice of the order with this approved flag (paid ones included).\n","example":2500},"notes":{"type":"string","nullable":true,"description":"The order's external notes."},"internal_notes":{"type":"string","nullable":true},"customer_po":{"type":"string","nullable":true},"customer_id":{"type":"string","format":"uuid","example":"7d1c2f4e-9a3b-4c5d-8e6f-0a1b2c3d4e5f"},"approved":{"type":"boolean","description":"The approved flag the row's invoices share.","example":true},"unpaid_balance":{"type":"number","description":"Balance due of the open invoices in the window.","example":1250.5},"paid_balance":{"type":"number","nullable":true,"description":"amount_paid summed over the same invoices as total.","example":1249.5}}},"ar_details_result":{"type":"object","required":["data","pagination"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ar_details_row"}},"pagination":{"$ref":"#/components/schemas/ar_report_pagination"}}},"ar_note_row":{"type":"object","description":"The customer_notes row plus its author and order number.","required":["note_id","customer_id","note_content","user_id","org","created_at","updated_at","deleted","type","order","username","order_number"],"properties":{"note_id":{"type":"string","format":"uuid","example":"9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d"},"customer_id":{"type":"string","format":"uuid","example":"7d1c2f4e-9a3b-4c5d-8e6f-0a1b2c3d4e5f"},"note_content":{"type":"string","nullable":true,"example":"Called AP, check goes out Friday."},"user_id":{"type":"string","format":"uuid","nullable":true},"org":{"type":"string","format":"uuid"},"created_at":{"type":"string","format":"date-time","nullable":true},"updated_at":{"type":"string","format":"date-time","nullable":true},"deleted":{"type":"boolean","description":"Always false.","example":false},"type":{"type":"string","nullable":true,"example":"CALL"},"order":{"type":"string","format":"uuid","description":"The order the note is on.","example":"2b7c9d1e-3f4a-4b5c-9d6e-7f8a9b0c1d2e"},"username":{"type":"string","nullable":true,"description":"The author's username (email); null when the user is deleted.","example":"dana@example.com"},"order_number":{"type":"string","example":"R-1042"}}},"ar_notes_result":{"type":"object","required":["data","pagination"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ar_note_row"}},"pagination":{"$ref":"#/components/schemas/ar_report_pagination"}}},"ar_invoices_body":{"type":"object","required":["search"],"example":{"search":{"customer_id":"7d1c2f4e-9a3b-4c5d-8e6f-0a1b2c3d4e5f"}},"properties":{"search":{"type":"object","description":"customer_id only; any other key is refused.","required":["customer_id"],"properties":{"customer_id":{"type":"string","format":"uuid","example":"7d1c2f4e-9a3b-4c5d-8e6f-0a1b2c3d4e5f"}}}}},"ar_invoice_row":{"type":"object","required":["invoice_id","invoice_number","order_type","invoice_date","due_date","total_amount","balance_due","status","order_number","past_due"],"properties":{"invoice_id":{"type":"string","format":"uuid","example":"3f9a8b7c-2d4e-4f61-9a0b-5c7d8e9f1a2b"},"invoice_number":{"type":"string","example":"INV-2087"},"order_type":{"type":"string","example":"RENTAL"},"invoice_date":{"type":"string","format":"date-time"},"due_date":{"type":"string","format":"date-time","nullable":true},"total_amount":{"type":"number","example":1250.5},"balance_due":{"type":"number","nullable":true,"example":1250.5},"status":{"type":"string","enum":["UNPAID","PARTIALLY_PAID"],"example":"UNPAID"},"order_number":{"type":"string","nullable":true,"example":"R-1042"},"past_due":{"type":"boolean","description":"Due today or earlier in the org's timezone.","example":true}}},"ar_invoices_result":{"type":"object","required":["customer_id","summary","data"],"properties":{"customer_id":{"type":"string","format":"uuid","description":"As sent."},"summary":{"type":"object","required":["total","past_due","not_due"],"properties":{"total":{"type":"integer","example":3},"past_due":{"type":"integer","example":2},"not_due":{"type":"integer","example":1}}},"data":{"type":"array","items":{"$ref":"#/components/schemas/ar_invoice_row"}}}},"ar_invoice_selection":{"type":"object","description":"Which of the customer's open invoices to take. Unknown keys are refused.","required":["customer_id"],"properties":{"customer_id":{"type":"string","format":"uuid","example":"7d1c2f4e-9a3b-4c5d-8e6f-0a1b2c3d4e5f"},"mode":{"type":"string","enum":["past_due","not_due","all","ids"],"default":"all","description":"past_due - due today or earlier; not_due - due later or no due date; all - every open invoice; ids - the open invoices listed in invoice_ids.\n","example":"ids"},"invoice_ids":{"type":"array","description":"Required, with at least one id, when mode is ids; otherwise ignored. Ids outside the customer's open set are dropped.\n","items":{"type":"string","format":"uuid"},"example":["3f9a8b7c-2d4e-4f61-9a0b-5c7d8e9f1a2b"]}}},"ar_invoice_export_body":{"type":"object","required":["search"],"example":{"search":{"customer_id":"7d1c2f4e-9a3b-4c5d-8e6f-0a1b2c3d4e5f","mode":"ids","invoice_ids":["3f9a8b7c-2d4e-4f61-9a0b-5c7d8e9f1a2b"]}},"properties":{"search":{"$ref":"#/components/schemas/ar_invoice_selection"}}},"ar_invoice_email_body":{"type":"object","required":["search"],"example":{"search":{"customer_id":"7d1c2f4e-9a3b-4c5d-8e6f-0a1b2c3d4e5f","mode":"ids","invoice_ids":["3f9a8b7c-2d4e-4f61-9a0b-5c7d8e9f1a2b"],"receiver_type":"self"}},"properties":{"search":{"allOf":[{"$ref":"#/components/schemas/ar_invoice_selection"},{"type":"object","required":["receiver_type"],"properties":{"receiver_type":{"type":"string","enum":["customer","self","custom"],"description":"customer - the contacts opted in to new invoice emails; self - the caller; custom - custom_emails.\n","example":"self"},"custom_emails":{"type":"array","description":"Required, with at least one address, when receiver_type is custom; otherwise ignored (but still checked as email addresses).\n","items":{"type":"string","format":"email"},"example":["ap@example.com"]}}}]}}},"ar_invoice_email_result":{"type":"object","required":["message","status","recipients_count","recipients"],"properties":{"message":{"type":"string","enum":["Email sent","Email skipped (customer emails disabled)"],"example":"Email sent"},"status":{"type":"string","enum":["sent","skipped_customer_emails_disabled"],"example":"sent"},"recipients_count":{"type":"integer","example":1},"recipients":{"type":"array","items":{"type":"string"},"example":["dana@example.com"]}}},"payment_register_body":{"type":"object","example":{"search":{"format":"json","date_from":"2026-10-01T05:00:00.000Z","date_to":"2026-10-02T04:59:59.999Z","payment_type":"CASH"},"paginate":{"currentPage":1,"perPage":50}},"properties":{"search":{"$ref":"#/components/schemas/payment_register_search"},"paginate":{"$ref":"#/components/schemas/paginate"}}},"payment_register_search":{"type":"object","description":"Every field is optional; unknown keys are refused.","properties":{"format":{"type":"string","enum":["json","csv","excel","pdf"],"default":"json","example":"json"},"date_from":{"type":"string","format":"date-time","description":"First instant included (paid_at). Defaults to the start of today in the org's timezone.\n","example":"2026-10-01T05:00:00.000Z"},"date_to":{"type":"string","format":"date-time","description":"Last instant included. Defaults to the end of today in the org's timezone. Must not be before date_from when both are sent; sent alone, the window runs from the start of today to it, and an earlier value returns an empty report.\n","example":"2026-10-02T04:59:59.999Z"},"cash_drawer_id":{"type":"string","format":"uuid","description":"Only the payments taken into this cash drawer."},"payment_type":{"type":"string","enum":["CASH","CHECK","CREDIT_CARD","ACH","WIRE","OTHER"],"description":"Only this payment type (case-insensitive).","example":"CASH"}}},"payment_register_row":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"payment_type":{"type":"string","enum":["CASH","CHECK","CREDIT_CARD","ACH","WIRE","OTHER"]},"origin_type":{"type":"string","enum":["MANUAL","INVOICE_PAYMENT","CC_DEPOSIT"]},"payment_purpose":{"type":"string","nullable":true},"amount":{"type":"number","example":44.57},"currency":{"type":"string","nullable":true},"reference_number":{"type":"string","nullable":true},"transaction_date":{"type":"string","format":"date-time","nullable":true,"description":"The bank date of the payment, when recorded."},"card_brand":{"type":"string","nullable":true},"cc_last_four":{"type":"string","nullable":true},"paid_at":{"type":"string","format":"date-time"},"cash_drawer_id":{"type":"string","format":"uuid","nullable":true},"drawer_closing_id":{"type":"string","format":"uuid","nullable":true},"customer_id":{"type":"string","format":"uuid","nullable":true},"customer_name":{"type":"string","nullable":true},"customer_number":{"type":"string","nullable":true},"drawer_name":{"type":"string","nullable":true,"description":"Null for a payment in an unnamed (default) drawer or in no drawer."},"processed_by_name":{"type":"string","nullable":true},"register_group":{"type":"string","enum":["cash_and_checks","credit_cards","ach_and_wires","other"],"description":"The reconciliation group the payment type falls in."}}},"payment_register_subtotal":{"type":"object","properties":{"amount":{"type":"number"},"count":{"type":"integer"}}},"payment_register_result":{"type":"object","properties":{"rows":{"type":"array","items":{"$ref":"#/components/schemas/payment_register_row"}},"subtotals":{"type":"object","description":"Per group, over the whole window (not the page).","properties":{"cash_and_checks":{"$ref":"#/components/schemas/payment_register_subtotal"},"credit_cards":{"$ref":"#/components/schemas/payment_register_subtotal"},"ach_and_wires":{"$ref":"#/components/schemas/payment_register_subtotal"},"other":{"$ref":"#/components/schemas/payment_register_subtotal"}}},"grand_total":{"type":"number","example":1915.68},"payment_count":{"type":"integer","example":3},"date_from":{"type":"string","format":"date-time","description":"The window's start as used (the default resolved)."},"date_to":{"type":"string","format":"date-time","description":"The window's end as used (the default resolved)."},"pagination":{"$ref":"#/components/schemas/pagination"},"supported_formats":{"type":"array","items":{"type":"string"},"example":["json","csv","excel","pdf"]}}},"price_sheet_row":{"type":"object","properties":{"asset_number":{"type":"string"},"manufacturer":{"type":"string","nullable":true},"model_number":{"type":"string","nullable":true},"sell_price":{"type":"number","nullable":true},"day_rate":{"type":"string","description":"The asset's day rate, else its category's, as text; \"No Rates\" when neither has one.","example":"150"},"week_rate":{"type":"string","description":"As day_rate, for the week rate."},"month_rate":{"type":"string","description":"As day_rate, for the 4-week rate."},"category_name":{"type":"string","nullable":true,"description":"The name of the category the asset is assigned to, at whatever level."},"parent_category_name":{"type":"string","nullable":true},"super_category_name":{"type":"string","nullable":true}}},"price_sheet_result":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/price_sheet_row"}},"pagination":{"$ref":"#/components/schemas/asset_report_pagination"},"supported_formats":{"type":"array","items":{"type":"string"},"example":["json","csv","excel","pdf"]}}},"fleet_utilization_body":{"type":"object","example":{"search":{"format":"json","date_from":"2026-09-01","date_to":"2026-10-31","category":"dd1804a8-f726-453a-b1a0-825da1771c3b"},"paginate":{"currentPage":1,"perPage":50}},"properties":{"search":{"$ref":"#/components/schemas/fleet_utilization_search"},"paginate":{"$ref":"#/components/schemas/paginate"}}},"fleet_utilization_search":{"type":"object","description":"Every field is optional; unknown keys are refused. Shared by the Fleet Utilization and Org Fleet reports.\n","properties":{"format":{"type":"string","enum":["json","csv","excel","pdf"],"default":"json"},"date_from":{"type":"string","format":"date","description":"Snapped to the start of its month in the org's timezone. With no dates the window is the 12 complete months ending last month; with only date_to, the 12 months ending with its month; with only date_from, through last month (or the current month when date_from is in it). Never past the current month, at most 120 months.\n"},"date_to":{"type":"string","format":"date","description":"Snapped to the end of its month. Must not be before date_from."},"category":{"type":"string","format":"uuid","description":"Any category node - a super-category or category returns its whole branch."},"include_add_ons":{"type":"boolean","default":false,"description":"Include attachment (add-on) categories, left out by default."},"benchmark_percent":{"type":"number","nullable":true,"minimum":0,"maximum":1000,"description":"A yearly dollar-utilization target for this run only, in percent of OEC. It overrides the category tree and the org setting on every row and the totals.\n"},"order_by":{"$ref":"#/components/schemas/order_by"}},"example":{"format":"json","date_from":"2026-09-01","date_to":"2026-10-31","category":"dd1804a8-f726-453a-b1a0-825da1771c3b"}},"fleet_utilization_figures":{"type":"object","description":"The figures shared by a row and the totals. Percentages are 0-100 numbers rounded to 2 places; null = not calculable or no data for that month (never 0%).\n","properties":{"total_assets":{"type":"integer","description":"Units owned for any part of the period, in service or not - from the purchase date (else the record's creation, never after the unit entered service) until sold. Always >= fleet_count; the units without an In Service Date are the gap.\n"},"fleet_count":{"type":"integer","description":"Units in service on any day of the period."},"unpriced_fleet_count":{"type":"integer","description":"Of those, units with no purchase price (they add 0 to OEC)."},"days_measured":{"type":"integer","description":"Days counted - the calendar days, or in the current month the days completed so far (today counts once it is over).\n"},"available_days":{"type":"integer","description":"Whole days the fleet was in service, summed over its units. The day a unit goes into service counts and the day it is sold does not.\n"},"on_rent_days":{"type":"integer","nullable":true,"description":"Billed days on rent, summed over the units - whole 24-hour periods from pickup to return, one more when the leftover is over 4 hours, at least one; a rental still open counts its completed days.\n"},"peak_on_rent":{"type":"integer","nullable":true,"description":"Most units out on rent at one moment."},"average_on_rent":{"type":"number","nullable":true,"description":"on_rent_days / days_measured."},"time_utilization_percent":{"type":"number","nullable":true},"rental_income":{"type":"number","nullable":true},"fleet_oec":{"type":"number"},"dollar_utilization_percent":{"type":"number","nullable":true},"annualized_dollar_utilization_percent":{"type":"number","nullable":true},"trailing_12m_dollar_utilization_percent":{"type":"number","nullable":true,"description":"The 12 months ending with this one; null until all 12 have income data."},"benchmark_percent":{"type":"number","nullable":true,"description":"The yearly target this period is measured against."},"benchmark_gap_percent":{"type":"number","nullable":true,"description":"annualized_dollar_utilization_percent - benchmark_percent; positive = above target. Null when either side is null.\n"}}},"benchmark_source":{"type":"string","enum":["request","category","org_setting","default"],"description":"Where a benchmark_percent came from - the request, the category node (its own or an ancestor's), the org's ASSET.FINANCIAL_UTILIZATION_BENCHMARK_PERCENT setting (35 until an admin changes it), or the code default of 35 if the setting cannot be read.\n"},"fleet_utilization_row":{"allOf":[{"type":"object","properties":{"node_id":{"type":"string","format":"uuid","nullable":true,"description":"The category node the row is for (null = uncategorized). Key rows on it, not on a name."},"super_category_id":{"type":"string","format":"uuid","nullable":true},"super_category_name":{"type":"string","nullable":true},"category_id":{"type":"string","format":"uuid","nullable":true},"category_name":{"type":"string","nullable":true},"sub_category_id":{"type":"string","format":"uuid","nullable":true},"sub_category_name":{"type":"string","nullable":true},"category_path":{"type":"string","nullable":true,"description":"Null only on an Org Fleet org-level row.","example":"Construction / Compactors / Black Top Roller, Highway Class"},"month":{"type":"string","example":"2026-09"},"partial":{"type":"boolean","description":"The current month, counted to now."},"benchmark_source":{"$ref":"#/components/schemas/benchmark_source"}}},{"$ref":"#/components/schemas/fleet_utilization_figures"}]},"fleet_utilization_result":{"type":"object","properties":{"rows":{"type":"array","items":{"$ref":"#/components/schemas/fleet_utilization_row"}},"totals":{"nullable":true,"description":"The whole window by the same formulas - not an average of the rows.","allOf":[{"type":"object","properties":{"months":{"type":"integer"},"months_with_on_rent_data":{"type":"integer"},"months_with_income_data":{"type":"integer"}}},{"$ref":"#/components/schemas/fleet_utilization_figures"}]},"window":{"type":"object","properties":{"from_month":{"type":"string","example":"2026-09"},"to_month":{"type":"string","example":"2026-10"},"from":{"type":"string","format":"date-time","nullable":true},"to":{"type":"string","format":"date-time","nullable":true},"months":{"type":"integer"},"timezone":{"type":"string","example":"America/New_York"}}},"coverage":{"type":"object","properties":{"on_rent_from":{"type":"string","format":"date-time","nullable":true,"description":"The org's first contract on-rent start."},"income_from":{"type":"string","format":"date-time","nullable":true,"description":"The org's first issued rental invoice."}}},"benchmark_percent":{"type":"number","description":"The totals' benchmark - the request's, else the filtered category node's, else the org's.\n"},"benchmark_source":{"$ref":"#/components/schemas/benchmark_source"},"pagination":{"$ref":"#/components/schemas/pagination"},"supported_formats":{"type":"array","items":{"type":"string"},"example":["json","csv","excel"]}}},"complete_customer_body":{"type":"object","example":{"search":{"format":"json","banned":false,"columns":["customer_number","company_name","credit_limit","total_exposure"]},"paginate":{"currentPage":1,"perPage":20}},"properties":{"search":{"$ref":"#/components/schemas/complete_customer_search"},"paginate":{"$ref":"#/components/schemas/paginate"}}},"complete_customer_search":{"type":"object","description":"Every field is optional, and an absent one means all; unknown keys are refused.","properties":{"format":{"type":"string","enum":["json","csv","excel","pdf"],"default":"json","example":"json"},"company_name":{"type":"string","description":"Contains-match on the company name."},"customer_number":{"type":"number","minimum":0,"exclusiveMinimum":true},"banned":{"type":"boolean","example":false},"customer_type":{"type":"string","enum":["PAY_ON_OPEN","ON_ACCOUNT"],"nullable":true},"deleted":{"type":"boolean","description":"Deleted customers instead of live ones. Only with CUSTOMER_RESTORE; otherwise the report always lists live customers.\n"},"validation_failure":{"type":"array","description":"[\"None\"] = customers that pass validation; [\"All\"] = customers that fail any check; otherwise the field keys a customer must fail at least one of.\n","items":{"type":"string"}},"missing_fields":{"type":"array","description":"Customers missing at least one of these.","items":{"type":"string","enum":["primary_address","billing_address","terms","site","primary_contact","insurance_information","credit_application","file_rental_app","credit_card"]}},"exposure_basis":{"type":"string","enum":["BALANCE","BALANCE_PLUS_OPEN_ORDERS"],"default":"BALANCE_PLUS_OPEN_ORDERS","description":"BALANCE counts open A/R only; BALANCE_PLUS_OPEN_ORDERS also prices committed, uninvoiced orders. Affects the exposure columns.\n"},"columns":{"type":"array","minItems":1,"description":"The columns to emit, in this order, in every format. A credit-class column needs CUSTOMER_RESTRICTED_VIEW; naming one without it, or an unknown key, is a 400.\n","items":{"type":"string","enum":["customer_number","company_name","address","billing_address","banned","blacklisted","blacklist_reason","terms","site","primary_contact","customer_type","env_fee_status","damage_waiver_status","state_sales_tax_status","county_sales_tax_status","city_sales_tax_status","special_sales_tax_status","discount","cash_discount_percentage","cash_discount_days","credit_limit","current_balance","open_orders","total_exposure","auto_payment_status","insurance_information","credit_application","file_rental_app","credit_card","validated","deleted"]}}}},"complete_customer_row":{"type":"object","description":"Every column below, or id plus the picked ones with columns. The credit-class ones (marked) appear only with CUSTOMER_RESTRICTED_VIEW.\n","properties":{"id":{"type":"string","format":"uuid"},"customer_number":{"type":"integer","example":5048},"company_name":{"type":"string","example":"ALAMON INC"},"address":{"type":"string","description":"The primary address, comma-joined."},"billing_address":{"type":"string"},"banned":{"type":"boolean"},"blacklisted":{"type":"boolean","description":"Credit-class."},"blacklist_reason":{"type":"string","nullable":true,"description":"Credit-class."},"terms":{"type":"integer","nullable":true,"description":"Credit-class. Payment terms in days."},"site":{"type":"string","nullable":true,"description":"The default site's name."},"primary_contact":{"type":"string","nullable":true,"description":"The default contact's name."},"customer_type":{"type":"string","enum":["Pay On Open","On Account"]},"env_fee_status":{"description":"Credit-class."},"damage_waiver_status":{"description":"Credit-class."},"state_sales_tax_status":{"description":"Credit-class."},"county_sales_tax_status":{"description":"Credit-class."},"city_sales_tax_status":{"description":"Credit-class."},"special_sales_tax_status":{"description":"Credit-class."},"discount":{"type":"number","nullable":true,"description":"The general customer discount, percent."},"cash_discount_percentage":{"type":"number","nullable":true,"description":"Credit-class. The early-pay discount, percent."},"cash_discount_days":{"type":"integer","nullable":true,"description":"Credit-class. The early-pay window in days."},"credit_limit":{"type":"number","nullable":true,"description":"Credit-class."},"current_balance":{"type":"number","description":"Credit-class. Open A/R."},"open_orders":{"type":"number","description":"Credit-class. Committed, uninvoiced order value (0 under BALANCE)."},"total_exposure":{"type":"number","description":"Credit-class. Per exposure_basis."},"auto_payment_status":{"type":"string","enum":["Enabled","Disabled"]},"insurance_information":{"type":"object","description":"liability_option, policy_option, carrier, policy_number, insurance_status (Valid / Not Valid) and insurance_expiry (MM-DD-YYYY). The files print the status and expiry.\n"},"credit_application":{"type":"object","description":"Credit-class. has_credit_application (Yes / No) and expiration_date (MM-DD-YYYY).\n"},"file_rental_app":{"type":"object","description":"Credit-class. has_rental_application (Yes / No) and expiration_date (MM-DD-YYYY).\n"},"credit_card":{"type":"string","nullable":true,"description":"Credit-class. The latest card on file, brand and last four."},"validated":{"type":"boolean"},"validation_failures":{"type":"object","nullable":true},"deleted":{"type":"boolean"}}},"complete_customer_result":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/complete_customer_row"}},"pagination":{"type":"object","description":"knex-paginate's envelope: page fields when paginate is sent, else the total and nulls.\n"},"supported_formats":{"type":"array","items":{"type":"string"},"example":["json","csv","excel","pdf"]}}},"categories_checklists_body":{"type":"object","example":{"search":{"format":"json"},"paginate":{"currentPage":1,"perPage":50}},"properties":{"search":{"$ref":"#/components/schemas/categories_checklists_search"},"paginate":{"$ref":"#/components/schemas/paginate"}}},"categories_checklists_search":{"type":"object","description":"Every field is optional; unknown keys are refused.","properties":{"format":{"type":"string","enum":["json","csv","excel","pdf"],"default":"json","example":"json"},"date_from":{"type":"string","format":"date-time","description":"Accepted together with date_to; does not filter the rows."},"date_to":{"type":"string","format":"date-time","description":"Accepted together with date_from; does not filter the rows."}}},"categories_checklists_row":{"type":"object","description":"The template columns are empty strings on a category with no assigned template.","properties":{"lineage":{"type":"string","description":"Super category, category and base category names joined with \" -> \".","example":"Aerial -> Boom Lifts -> 60ft Boom"},"template_id":{"type":"string","example":"3f1c2a9e-5b7d-4e8a-9c21-6d0f4b8e7a13"},"template_description":{"type":"string","example":"Boom lift asset details"},"template_scope":{"type":"string","example":"GENERIC"},"template_source":{"type":"string","description":"DIRECT, INHERITED:CAT, INHERITED:SUPER_CAT or INHERITED.","example":"DIRECT"}}},"categories_checklists_result":{"type":"object","properties":{"rows":{"type":"array","items":{"$ref":"#/components/schemas/categories_checklists_row"}},"pagination":{"$ref":"#/components/schemas/pagination"},"supported_formats":{"type":"array","items":{"type":"string"},"example":["json","csv","excel","pdf"]}}},"maintenance_report_row":{"type":"object","properties":{"asset_number":{"type":"string"},"manufacturer":{"type":"string","nullable":true},"model_number":{"type":"string","nullable":true},"serial_number":{"type":"string","nullable":true},"category_name":{"type":"string","nullable":true,"description":"The name of the category the asset is assigned to, at whatever level."},"parent_category_name":{"type":"string","nullable":true},"super_category_name":{"type":"string","nullable":true},"hour_meter":{"type":"string","description":"The current hour meter as text; \"--\" when the asset has none.","example":"2218.9"},"latest_maintenance_date":{"type":"string","nullable":true,"description":"MM-DD-YYYY in the org's timezone.","example":"07-07-2026"},"latest_maintenance_hour_meter":{"type":"number","nullable":true},"maintenance_due_hours":{"type":"string","description":"The hour meter the next maintenance falls due at; \"--\" when it can't be worked out.","example":"2077.00"},"maintenance_due_date":{"type":"string","description":"MM-DD-YYYY in the org's timezone; \"--\" when it can't be worked out."},"latest_inspection_date":{"type":"string","nullable":true,"description":"MM-DD-YYYY in the org's timezone."},"inspection_due_date":{"type":"string","description":"MM-DD-YYYY in the org's timezone; \"--\" when it can't be worked out."},"maintenance_tasks":{"type":"array","description":"The active recurring maintenance work orders.","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"order_number":{"type":"string"},"task_level":{"type":"string","nullable":true},"status":{"type":"string","enum":["PENDING","READY","IN_PROGRESS","PAUSED"]},"created_at":{"type":"string","format":"date"}}}},"maintenance_inspections":{"type":"array","description":"The active recurring inspections (not dispatch inspections).","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"type":{"type":"string"},"status":{"type":"string"},"created_at":{"type":"string","format":"date"}}}}}},"maintenance_report_result":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/maintenance_report_row"}},"pagination":{"$ref":"#/components/schemas/asset_report_pagination"},"supported_formats":{"type":"array","items":{"type":"string"},"example":["json","csv","excel","pdf"]}}},"org_fleet_level":{"type":"string","enum":["org","super_category","category","sub_category"],"default":"org","description":"How the rows are grouped. org - the whole (filtered) fleet, one row per month. super_category / category - per node at that depth, with deeper names null; an asset sitting directly on a super-category gives that super-category its own row at the category level (category_id null). sub_category - per node the asset sits on, the Fleet Utilization report's rows. A null node_id below org is Uncategorized.\n"},"org_fleet_body":{"type":"object","example":{"search":{"format":"json","level":"org","date_from":"2025-10-01","date_to":"2026-09-30"},"paginate":{"currentPage":1,"perPage":50}},"properties":{"search":{"allOf":[{"$ref":"#/components/schemas/fleet_utilization_search"},{"type":"object","properties":{"level":{"$ref":"#/components/schemas/org_fleet_level"}},"example":{"level":"category","category":"e15725e5-2a3a-46f7-b190-9faf458c6fac","date_from":"2026-09-01","date_to":"2026-09-30"}}]},"paginate":{"$ref":"#/components/schemas/paginate"}}},"org_fleet_drill":{"type":"object","nullable":true,"description":"The next request's level and category for this row (keep the dates, or narrow them to the row's month). Null when there is nothing deeper - a sub_category row, Uncategorized, or a super-category's own row at the category level (its assets sit on the super-category itself). An org row drills one level below the category filter.\n","properties":{"level":{"$ref":"#/components/schemas/org_fleet_level"},"category":{"type":"string","format":"uuid","nullable":true,"description":"The node to filter on; null = no filter (the org row without one)."}},"example":{"level":"category","category":"e15725e5-2a3a-46f7-b190-9faf458c6fac"}},"org_fleet_row":{"allOf":[{"type":"object","properties":{"level":{"$ref":"#/components/schemas/org_fleet_level"}}},{"$ref":"#/components/schemas/fleet_utilization_row"},{"type":"object","properties":{"drill":{"$ref":"#/components/schemas/org_fleet_drill"}}}]},"org_fleet_result":{"allOf":[{"$ref":"#/components/schemas/fleet_utilization_result"},{"type":"object","properties":{"rows":{"type":"array","items":{"$ref":"#/components/schemas/org_fleet_row"}},"level":{"$ref":"#/components/schemas/org_fleet_level"}}}]},"asset_report_body":{"type":"object","description":"The request the asset-list reports share (validAssetsPriceSheetReportBody): Asset Inventory, Assets Price Sheet and Maintenance Report.\n","example":{"search":{"format":"json","stock":true},"paginate":{"currentPage":1,"perPage":50}},"properties":{"search":{"$ref":"#/components/schemas/asset_report_search"},"paginate":{"$ref":"#/components/schemas/paginate"}}},"asset_report_search":{"type":"object","description":"Every field is optional, and an absent one means all; unknown keys are refused.","properties":{"format":{"type":"string","enum":["json","csv","excel","pdf"],"default":"json","example":"json"},"category":{"type":"array","description":"Category ids; an asset matches when its own category is one of them or sits under one of them (as a category or sub category).\n","items":{"type":"string","format":"uuid"}},"stock":{"type":"boolean","description":"true = quantity above 0, false = quantity 0; absent = every asset. Send a JSON boolean - the strings \"true\" and \"false\" pass validation but filter nothing.\n","example":true},"asset_number":{"type":"string","description":"Contains-match on the asset number."},"name":{"type":"string","description":"Contains-match on the asset name."},"manufacturer":{"type":"string","description":"Contains-match on the make."}}},"asset_report_pagination":{"type":"object","description":"knex-paginate without isLengthAware when paginate is sent - from is the 0-based offset, and total, lastPage, prevPage and nextPage are only sent on page 1 (lastPage 0 when there are no rows; a currentPage sent as the string \"1\" passes validation but is not treated as page 1, so it gets no totals either). Without paginate, currentPage 1, the total and nulls.\n","required":["currentPage","perPage","from","to"],"properties":{"currentPage":{"type":"integer"},"total":{"type":"integer","description":"Page 1 only (and without paginate)."},"perPage":{"type":"integer","nullable":true},"from":{"type":"integer","nullable":true},"to":{"type":"integer","nullable":true},"lastPage":{"type":"integer","nullable":true},"prevPage":{"type":"integer","nullable":true},"nextPage":{"type":"integer","nullable":true}}},"inventory_report_row":{"type":"object","required":["asset_number","manufacturer","model_number","serial_number","model_year","purchase_date","purchase_price","category_name","category_gl_number","parent_category_name","parent_category_gl_number","super_category_name","super_category_gl_number"],"properties":{"asset_number":{"type":"string"},"manufacturer":{"type":"string","nullable":true},"model_number":{"type":"string","nullable":true},"serial_number":{"type":"string","nullable":true},"model_year":{"type":"string","nullable":true},"purchase_date":{"type":"string","format":"date-time","nullable":true},"purchase_price":{"type":"number","nullable":true,"description":"Rounded to cents.","example":1606.97},"category_name":{"type":"string","nullable":true,"description":"The name of the category the asset is assigned to, at whatever level."},"category_gl_number":{"type":"integer","nullable":true},"parent_category_name":{"type":"string","nullable":true},"parent_category_gl_number":{"type":"integer","nullable":true},"super_category_name":{"type":"string","nullable":true},"super_category_gl_number":{"type":"integer","nullable":true},"depreciated_value":{"type":"string","description":"Only when the org has depreciation on; a \"--\" placeholder."},"depreciation_schedules":{"type":"string","description":"Only when the org has depreciation on; a \"--\" placeholder."}}},"inventory_report_total":{"type":"object","required":["level","super_category_id","super_category_name","super_category_gl_number","parent_category_id","parent_category_name","parent_category_gl_number","category_id","category_name","category_gl_number","asset_count","priced_asset_count","total_purchase_price"],"properties":{"level":{"type":"string","enum":["GRAND_TOTAL","SUPER_CATEGORY","CATEGORY","SUB_CATEGORY"]},"super_category_id":{"type":"string","format":"uuid","nullable":true},"super_category_name":{"type":"string","nullable":true},"super_category_gl_number":{"type":"integer","nullable":true},"parent_category_id":{"type":"string","format":"uuid","nullable":true},"parent_category_name":{"type":"string","nullable":true},"parent_category_gl_number":{"type":"integer","nullable":true},"category_id":{"type":"string","format":"uuid","nullable":true},"category_name":{"type":"string","nullable":true},"category_gl_number":{"type":"integer","nullable":true},"asset_count":{"type":"integer"},"priced_asset_count":{"type":"integer","description":"The assets in the group that have a purchase price."},"total_purchase_price":{"type":"number","example":27369998.97}}},"inventory_report_result":{"type":"object","required":["data","pagination","totals","supported_formats"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/inventory_report_row"}},"pagination":{"$ref":"#/components/schemas/asset_report_pagination"},"totals":{"type":"array","description":"Ordered by the category tree, the GRAND_TOTAL row last.","items":{"$ref":"#/components/schemas/inventory_report_total"}},"supported_formats":{"type":"array","items":{"type":"string"},"example":["json","csv","excel","pdf"]}}},"delivery_subsidy_body":{"type":"object","example":{"search":{"format":"json","group_by":"customer","date_from":"2026-09-01","date_to":"2026-09-30"},"paginate":{"currentPage":1,"perPage":50}},"properties":{"search":{"type":"object","description":"Every field is optional; unknown keys are refused.","properties":{"format":{"type":"string","enum":["json","csv","excel","pdf"],"default":"json"},"group_by":{"type":"string","enum":["customer","salesperson","asset","leg"],"default":"customer","description":"Picks the row shape (see delivery_subsidy_result)."},"date_from":{"type":"string","format":"date","description":"A whole day in the org's timezone, included. With no dates the window is the 12 complete months ending last month; with only date_to, the 12 months ending on it; with only date_from, through today. Never past now.\n"},"date_to":{"type":"string","format":"date","description":"A whole day in the org's timezone, included. Must not be before date_from."},"customer_id":{"type":"string","format":"uuid","description":"The order's customer."},"salesperson_id":{"type":"string","description":"The order's assignee (a user uuid), or the literal unassigned for orders with no assignee.\n","example":"unassigned"},"asset_id":{"type":"string","format":"uuid","description":"Only legs that carried this machine, and only its share of each; the leg view still returns the whole leg in leg_* and the full split.\n"},"order_by":{"$ref":"#/components/schemas/order_by"}},"example":{"format":"json","group_by":"customer","date_from":"2026-09-01","date_to":"2026-09-30"}},"paginate":{"$ref":"#/components/schemas/paginate"}}},"delivery_subsidy_figures":{"type":"object","description":"The figures every grouped row and the totals carry. Money in dollars; the percent is a 0-100 number rounded to 2 places.\n","properties":{"legs":{"type":"integer","description":"Distinct legs - a leg that carried three of one customer's machines is one leg for that customer."},"charged":{"type":"number","description":"What we charged the customer (the group's share of each leg)."},"cost":{"type":"number","description":"What the carrier charged us."},"subsidy":{"type":"number","description":"cost - charged. Negative when we charged above cost."},"rental_income":{"type":"number","description":"The group's machines' gross rent on issued invoices dated in the window. For the totals, every machine invoiced in the window within the filters, including groups with no leg - so the rows need not add up to it.\n"},"subsidy_percent":{"type":"number","nullable":true,"description":"subsidy / rental_income x 100; null when rental_income is 0 or less."}}},"delivery_subsidy_customer_row":{"allOf":[{"type":"object","properties":{"customer_id":{"type":"string","format":"uuid","nullable":true},"customer_name":{"type":"string","nullable":true},"customer_number":{"type":"integer","nullable":true}}},{"$ref":"#/components/schemas/delivery_subsidy_figures"}]},"delivery_subsidy_salesperson_row":{"allOf":[{"type":"object","properties":{"salesperson_id":{"type":"string","format":"uuid","nullable":true,"description":"The order's assignee; null = orders with no assignee (sorted last)."},"salesperson_name":{"type":"string","nullable":true,"example":"Unassigned"}}},{"$ref":"#/components/schemas/delivery_subsidy_figures"}]},"delivery_subsidy_asset_row":{"allOf":[{"type":"object","properties":{"asset_id":{"type":"string","format":"uuid","nullable":true,"description":"null = legs that carried no machine (asset_name \"No asset\", sorted last)."},"asset_number":{"type":"string","nullable":true},"asset_name":{"type":"string","nullable":true},"serial_number":{"type":"string","nullable":true}}},{"$ref":"#/components/schemas/delivery_subsidy_figures"}]},"delivery_subsidy_split_piece":{"type":"object","description":"One machine's share of a leg. A leg's pieces add up to it exactly in every column.","properties":{"asset_id":{"type":"string","format":"uuid","nullable":true,"description":"null only for the single piece of a leg that carried no machine."},"asset_number":{"type":"string","nullable":true},"asset_name":{"type":"string","nullable":true},"serial_number":{"type":"string","nullable":true},"line_item_id":{"type":"string","format":"uuid","nullable":true},"rate_basis":{"type":"string","nullable":true,"description":"The rate the line was valued at - 4week, week (x 4) or day (x 28); null for an equal split or an unrated line.\n","example":"4week"},"weight":{"type":"number","nullable":true,"description":"The line's 4-week value; 0 for an unrated line on a rated leg, null for an equal split."},"share_percent":{"type":"number"},"charged":{"type":"number"},"cost":{"type":"number"},"subsidy":{"type":"number"}}},"delivery_subsidy_leg_row":{"type":"object","properties":{"leg_id":{"type":"string","format":"uuid"},"leg_date":{"type":"string","format":"date-time"},"leg_date_basis":{"type":"string","enum":["COMPLETED_DATE","FIRST_DISPATCH","CONTRACT_START","OFF_RENT","CONTRACT_CLOSE","SALE_CLOSE"],"description":"What the date was taken from. COMPLETED_DATE does not occur today."},"leg_type":{"type":"string","enum":["DELIVERY","PICKUP","ADDITIONAL"]},"order_id":{"type":"string","format":"uuid"},"order_number":{"type":"string"},"order_type":{"type":"string","enum":["RENTAL","SALE"]},"customer_id":{"type":"string","format":"uuid","nullable":true},"customer_name":{"type":"string","nullable":true},"customer_number":{"type":"integer","nullable":true},"salesperson_id":{"type":"string","format":"uuid","nullable":true},"salesperson_name":{"type":"string","nullable":true,"description":"Unassigned when salesperson_id is null."},"carrier_id":{"type":"string","format":"uuid","nullable":true},"carrier_name":{"type":"string","nullable":true},"carrier_no_fee":{"type":"boolean","description":"The \"customer hauls it\" carrier; its fees are reported as entered."},"charged":{"type":"number","description":"The leg's charge - only the filtered machine's share when asset_id is set."},"cost":{"type":"number"},"subsidy":{"type":"number"},"leg_charged":{"type":"number","description":"Always the whole leg."},"leg_cost":{"type":"number"},"leg_subsidy":{"type":"number"},"split_basis":{"type":"string","enum":["RENTAL_VALUE","EQUAL","NO_ASSETS"]},"asset_count":{"type":"integer"},"split":{"type":"array","description":"The full split, heaviest share first, even when asset_id is set.","items":{"$ref":"#/components/schemas/delivery_subsidy_split_piece"}}}},"delivery_subsidy_result":{"type":"object","properties":{"group_by":{"type":"string","enum":["customer","salesperson","asset","leg"]},"rows":{"type":"array","description":"The row shape follows group_by.","items":{"oneOf":[{"$ref":"#/components/schemas/delivery_subsidy_customer_row"},{"$ref":"#/components/schemas/delivery_subsidy_salesperson_row"},{"$ref":"#/components/schemas/delivery_subsidy_asset_row"},{"$ref":"#/components/schemas/delivery_subsidy_leg_row"}]}},"totals":{"description":"Every leg in the filtered window (not just the page).","allOf":[{"$ref":"#/components/schemas/delivery_subsidy_figures"}]},"window":{"type":"object","properties":{"date_from":{"type":"string","format":"date","example":"2026-09-01"},"date_to":{"type":"string","format":"date","example":"2026-09-30"},"from":{"type":"string","format":"date-time"},"to":{"type":"string","format":"date-time"},"timezone":{"type":"string","example":"America/Chicago"},"defaulted":{"type":"boolean","description":"No date was sent - the 12 complete months ending last month."}}},"pagination":{"$ref":"#/components/schemas/pagination"},"supported_formats":{"type":"array","items":{"type":"string"},"example":["json","csv","excel"]}}},"complete_asset_body":{"type":"object","example":{"search":{"format":"json","validation_failure":["All"]},"paginate":{"currentPage":1,"perPage":50}},"properties":{"search":{"$ref":"#/components/schemas/complete_asset_search"},"paginate":{"$ref":"#/components/schemas/paginate"}}},"complete_asset_search":{"type":"object","description":"Every field is optional, and an absent one means all; unknown keys are refused.","properties":{"format":{"type":"string","enum":["json","csv","excel","pdf"],"default":"json","example":"json"},"category":{"type":"array","description":"Category ids; an asset matches when its sub category is one of them or sits under one of them.\n","items":{"type":"string","format":"uuid"}},"validation_failure":{"type":"array","description":"[\"None\"] = assets that pass validation; [\"All\"] = assets that fail any check; otherwise the field keys an asset must fail at least one of.\n","items":{"type":"string"},"example":["All"]},"missing_fields":{"type":"array","description":"Assets missing at least one of these fields.","items":{"type":"string","enum":["manufacturer","model_number","model_year","serial_number","capitalization","latest_maintenance_date","latest_maintenance_hour_meter","latest_inspection_date","in_service_date","purchase_date","purchase_price","rental_rates","sell_price","depreciation_schedules","accumulated_depreciation","ending_book_value"]}}}},"complete_asset_row":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","nullable":true},"asset_number":{"type":"string"},"capitalization":{"type":"boolean","description":"Only when the org has capitalization on."},"latest_maintenance_date":{"type":"string","nullable":true,"description":"MM-DD-YYYY in the org's timezone.","example":"07-07-2026"},"latest_maintenance_hour_meter":{"type":"number","nullable":true},"latest_inspection_date":{"type":"string","nullable":true,"description":"MM-DD-YYYY in the org's timezone."},"in_service_date":{"type":"string","nullable":true,"description":"MM-DD-YYYY in the org's timezone."},"manufacturer":{"type":"string","nullable":true},"model_number":{"type":"string","nullable":true},"purchase_date":{"type":"string","nullable":true,"description":"MM-DD-YYYY in the org's timezone."},"purchase_price":{"type":"number","nullable":true},"rental_rates":{"type":"object","nullable":true,"description":"The asset's own rates by period.","example":{"day":150,"week":400,"4week":1200}},"inherited_rental_rates":{"type":"object","nullable":true,"description":"The rates the asset inherits from its category."},"sell_price":{"type":"number","nullable":true},"serial_number":{"type":"string","nullable":true},"model_year":{"type":"string","nullable":true},"hour_meter":{"type":"number","nullable":true},"validated":{"type":"boolean"},"validation_failures":{"type":"object","nullable":true,"description":"Per field, whether it passes the org's field settings."},"category":{"type":"string","description":"The category path, \"Super -> Category -> Sub\".","example":"Construction -> Compactors -> Vibrator"},"media_count":{"type":"string","description":"The asset's live media files (a count, as Postgres returns it).","example":"3"},"depreciation_schedules":{"type":"array","description":"Only when the org has depreciation on; the schedule names.","items":{"type":"string"}},"accumulated_depreciation":{"type":"number","nullable":true,"description":"Only when the org has depreciation on; this month's entry."},"ending_book_value":{"type":"number","nullable":true,"description":"Only when the org has depreciation on; this month's entry."}}},"complete_asset_result":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/complete_asset_row"}},"pagination":{"type":"object","description":"knex-paginate's envelope: page fields when paginate is sent, else the total and nulls.\n"},"supported_formats":{"type":"array","items":{"type":"string"},"example":["json","csv","excel","pdf"]}}},"credit_notes_body":{"type":"object","example":{"search":{"format":"json","order_type":["RENTAL"],"date_from":"2026-10-01T05:00:00.000Z","date_to":"2026-11-01T04:59:59.999Z"},"paginate":{"currentPage":1,"perPage":50}},"properties":{"search":{"$ref":"#/components/schemas/credit_notes_search"},"paginate":{"$ref":"#/components/schemas/paginate"}}},"credit_notes_search":{"type":"object","description":"Every field is optional, and an absent one means all; unknown keys are refused.","properties":{"format":{"type":"string","enum":["json","csv","excel","pdf"],"default":"json","example":"json"},"customer_id":{"type":"string","format":"uuid","description":"Only this customer's credit notes."},"order_type":{"type":"array","description":"Order types to include (case-insensitive).","items":{"type":"string","enum":["RENTAL","SALE","WORK_ORDER"]},"example":["RENTAL"]},"date_from":{"type":"string","format":"date-time","description":"First invoice date included.","example":"2026-10-01T05:00:00.000Z"},"date_to":{"type":"string","format":"date-time","description":"Last invoice date included. Must not be before date_from when both are sent; either bound may be sent alone.\n","example":"2026-11-01T04:59:59.999Z"}}},"credit_notes_row":{"type":"object","properties":{"invoice_id":{"type":"string","format":"uuid"},"invoice_number":{"type":"string","example":"INV-R-20261007-003-02"},"order_id":{"type":"string","format":"uuid","nullable":true},"order_number":{"type":"string","nullable":true,"example":"R-20261007-003"},"order_type":{"type":"string","enum":["RENTAL","SALE","WORK_ORDER"]},"order_status":{"type":"string","nullable":true},"customer_id":{"type":"string","format":"uuid","nullable":true},"company_name":{"type":"string","nullable":true,"example":"ALAMON INC"},"customer_number":{"nullable":true,"description":"The customer's number as stored.","example":5048},"status":{"type":"string","description":"The credit note's invoice status.","example":"PAID"},"total_amount":{"type":"number","description":"The credit, negative.","example":-1151.73},"amount_paid":{"type":"number"},"balance_due":{"type":"number"},"currency":{"type":"string","nullable":true},"invoice_date":{"type":"string","format":"date-time"},"created_at":{"type":"string","format":"date-time"},"absorption_payment_id":{"type":"string","format":"uuid","nullable":true,"description":"The REFUND_ABSORB deposit that carried the credit into the balance."},"absorption_amount":{"type":"number","description":"That deposit's amount; 0 when the credit has none.","example":1151.73},"absorbed_at":{"type":"string","format":"date-time","nullable":true}}},"credit_notes_totals":{"type":"object","description":"Totals over every row of the filtered set, not just the page.","properties":{"by_order_type":{"type":"object","description":"The credit total per order type present (RENTAL, SALE, WORK_ORDER).","additionalProperties":{"type":"number"},"example":{"RENTAL":-1151.73}},"grand_total":{"type":"number","example":-1151.73},"total_absorbed":{"type":"number","example":1151.73}}},"credit_notes_result":{"type":"object","properties":{"rows":{"type":"array","items":{"$ref":"#/components/schemas/credit_notes_row"}},"totals":{"$ref":"#/components/schemas/credit_notes_totals"},"pagination":{"$ref":"#/components/schemas/pagination"},"supported_formats":{"type":"array","items":{"type":"string"},"example":["json","csv","excel","pdf"]}}},"average_days_to_pay_body":{"type":"object","example":{"search":{"format":"json","date_from":"2026-10-01T04:00:00.000Z","date_to":"2026-10-23T03:59:59.999Z"},"paginate":{"currentPage":1,"perPage":25}},"properties":{"search":{"$ref":"#/components/schemas/average_days_to_pay_search"},"paginate":{"$ref":"#/components/schemas/paginate"}}},"average_days_to_pay_search":{"type":"object","description":"Every field is optional; unknown keys are refused.","properties":{"format":{"type":"string","enum":["json","csv","excel","pdf"],"default":"json","example":"json"},"date_from":{"type":"string","format":"date-time","description":"Start of the window, by the date each invoice was settled. Defaults to 365 days before date_to.\n","example":"2026-10-01T04:00:00.000Z"},"date_to":{"type":"string","format":"date-time","description":"End of the window (exclusive). Defaults to now. Must not be before date_from when both are sent; either bound may be sent alone.\n","example":"2026-10-23T03:59:59.999Z"},"customer_id":{"type":"string","format":"uuid","description":"Only this customer's invoices."},"order_by":{"type":"array","description":"Sort keys, applied in order (an entry without order sorts ascending, nulls last). Defaults to avg_days_to_pay descending, nulls last. The column must be one of customer_number, company_name, invoices_paid, avg_days_to_pay, trend_days, on_time_pct or last_payment_date.\n","items":{"type":"object","properties":{"column":{"type":"string","enum":["customer_number","company_name","invoices_paid","avg_days_to_pay","trend_days","on_time_pct","last_payment_date"],"example":"avg_days_to_pay"},"order":{"type":"string","enum":["asc","desc","ASC","DESC"],"example":"desc"},"nulls":{"type":"string","enum":["first","last","FIRST","LAST"],"example":"last"}}}}}},"average_days_to_pay_row":{"type":"object","required":["customer_id","customer_number","company_name","terms","invoices_paid","avg_days_to_pay","prev_avg_days_to_pay","trend_days","on_time_pct","last_payment_date"],"properties":{"customer_id":{"type":"string","format":"uuid"},"customer_number":{"type":"integer","example":5048},"company_name":{"type":"string","example":"ALAMON INC"},"terms":{"type":"string","description":"The customer's terms, e.g. NET 30 or 1% 10/NET 30; an em dash when unset.","example":"NET 30"},"invoices_paid":{"type":"integer","description":"Fully-paid invoices settled in the window.","example":5},"avg_days_to_pay":{"type":"number","description":"Average days from invoice date to the last payment applied, one decimal.","example":5.6},"prev_avg_days_to_pay":{"type":"number","nullable":true,"description":"The same average over the preceding window; null when it has no invoices.","example":4.2},"trend_days":{"type":"number","nullable":true,"description":"avg_days_to_pay minus prev_avg_days_to_pay. Negative = paying faster; null without a prior average.\n","example":1.4},"on_time_pct":{"type":"number","nullable":true,"description":"Percent (0-100) of the invoices with a due date paid by it.","example":80},"last_payment_date":{"type":"string","format":"date-time","description":"The latest settled date in the window."}}},"average_days_to_pay_totals":{"type":"object","required":["invoices_paid","customers_count","avg_days_to_pay","prev_avg_days_to_pay","trend_days","on_time_pct","date_from","date_to"],"description":"Pooled over every invoice in the result, not an average of the rows, plus the window as resolved.\n","properties":{"invoices_paid":{"type":"integer","example":6},"customers_count":{"type":"integer","example":2},"avg_days_to_pay":{"type":"number","nullable":true,"example":4.7},"prev_avg_days_to_pay":{"type":"number","nullable":true},"trend_days":{"type":"number","nullable":true},"on_time_pct":{"type":"number","nullable":true,"example":83.3},"date_from":{"type":"string","format":"date-time"},"date_to":{"type":"string","format":"date-time"}}},"average_days_to_pay_result":{"type":"object","required":["rows","totals","pagination","supported_formats"],"properties":{"rows":{"type":"array","items":{"$ref":"#/components/schemas/average_days_to_pay_row"}},"totals":{"$ref":"#/components/schemas/average_days_to_pay_totals"},"pagination":{"$ref":"#/components/schemas/report_pagination"},"supported_formats":{"type":"array","items":{"type":"string"},"example":["json","csv","excel","pdf"]}}},"dispatched_assets_body":{"type":"object","example":{"search":{"format":"json","date_from":"2026-10-01T05:00:00.000Z","date_to":"2026-11-01T04:59:59.999Z"},"paginate":{"currentPage":1,"perPage":50}},"properties":{"search":{"$ref":"#/components/schemas/dispatched_assets_search"},"paginate":{"$ref":"#/components/schemas/paginate"}}},"dispatched_assets_search":{"type":"object","description":"Every field is optional, and an absent one means all; unknown keys are refused.","properties":{"format":{"type":"string","enum":["json","csv","excel","pdf"],"default":"json","example":"json"},"date_from":{"type":"string","format":"date-time","description":"First actual dispatched_date included.","example":"2026-10-01T05:00:00.000Z"},"date_to":{"type":"string","format":"date-time","description":"Last actual dispatched_date included. Must not be before date_from when both are sent; sent alone, the window has no start and covers every dispatch up to it.\n","example":"2026-11-01T04:59:59.999Z"}}},"dispatched_assets_row":{"type":"object","properties":{"asset_number":{"type":"string","nullable":true,"example":"9751"},"name":{"type":"string","nullable":true,"description":"The asset's name."},"manufacturer":{"type":"string","nullable":true},"serial_number":{"type":"string","nullable":true},"company_name":{"type":"string","nullable":true,"description":"The order's customer."},"customer_number":{"type":"integer","nullable":true,"example":17},"order_number":{"type":"string","example":"R-20261016-003"},"dispatch_at":{"type":"string","format":"date-time","nullable":true,"description":"The line item's expected dispatch date."},"return_at":{"type":"string","format":"date-time","nullable":true,"description":"The line item's expected return date."},"dispatched_date":{"type":"string","format":"date-time","nullable":true,"description":"When the asset actually went out; null until it is dispatched."},"off_rent_date":{"type":"string","format":"date-time","nullable":true},"status":{"type":"string","description":"The dispatch status as stored (never RELEASED).","example":"DISPATCHED"}}},"dispatched_assets_result":{"type":"object","properties":{"rows":{"type":"array","items":{"$ref":"#/components/schemas/dispatched_assets_row"}},"pagination":{"$ref":"#/components/schemas/pagination"}}},"vendor_invoice_export_body":{"type":"object","example":{"search":{"format":"json","date_from":"2026-10-01","date_to":"2026-10-31","status":"APPROVED"},"paginate":{"currentPage":1,"perPage":50}},"properties":{"search":{"$ref":"#/components/schemas/vendor_invoice_export_search"},"paginate":{"$ref":"#/components/schemas/paginate"}}},"vendor_invoice_export_search":{"type":"object","description":"Every field is optional; unknown keys are refused.","properties":{"format":{"type":"string","enum":["json","excel","csv","pdf"],"default":"json","example":"json"},"date_from":{"type":"string","format":"date","description":"First invoice date included, a calendar day in the org's timezone.","example":"2026-10-01"},"date_to":{"type":"string","format":"date","description":"Last invoice date included. Must not be before date_from when both are sent; either bound may be sent alone.\n","example":"2026-10-31"},"status":{"type":"string","enum":["MATCHED","AWAITING_RECEIPT","PENDING_APPROVAL","APPROVED"],"description":"The invoice's own status (case-insensitive). Omitted or empty, all of them.","example":"APPROVED"}}},"vendor_invoice_export_row":{"type":"object","properties":{"invoice_id":{"type":"string","format":"uuid"},"purchase_order_id":{"type":"string","format":"uuid"},"invoice_number":{"type":"string","example":"INV-1042"},"invoice_date":{"type":"string","format":"date-time","description":"The stored invoice date (the picked day at org-local midnight), ISO 8601 UTC. Render it in the org's timezone, as the PO's Invoices tab does. The excel, csv and pdf files print its org-local day as YYYY-MM-DD.\n","example":"2026-10-01T04:00:00.000Z"},"po_number":{"type":"string","description":"The PO's current number.","example":"002954"},"vendor_name":{"type":"string","description":"The vendor the invoice was recorded against; empty when unknown.","example":"FASTSIGN"},"item_number":{"type":"string","description":"A part's item number or an asset's asset number; empty for a custom line and a charge row.\n","example":"10263"},"description":{"type":"string","description":"The part, asset or custom line's name, or Freight / Sales Tax / Miscellaneous on a charge row.\n","example":"cactus"},"quantity":{"type":"number","description":"Quantity invoiced; 1 on a charge row.","example":7},"price":{"type":"number","description":"Unit price as invoiced (up to 4 decimals); the charge on a charge row.","example":10},"amount":{"type":"number","description":"The line total (quantity x price, rounded to cents); the charge on a charge row.","example":70}}},"vendor_invoice_export_result":{"type":"object","properties":{"rows":{"type":"array","items":{"$ref":"#/components/schemas/vendor_invoice_export_row"}},"pagination":{"$ref":"#/components/schemas/pagination"},"supported_formats":{"type":"array","items":{"type":"string"},"example":["json","excel","csv","pdf"]}}},"gl_transactions_body":{"type":"object","example":{"search":{"format":"json","date_from":"2026-10-01T04:00:00.000Z","date_to":"2026-10-24T03:59:59.999Z","include_legacy":true},"paginate":{"currentPage":1,"perPage":50}},"properties":{"search":{"$ref":"#/components/schemas/gl_transactions_search"},"paginate":{"$ref":"#/components/schemas/paginate"}}},"gl_transactions_search":{"type":"object","description":"Every field is optional, and an absent one means all; unknown keys are refused.","properties":{"format":{"type":"string","enum":["json","csv","excel","pdf"],"default":"json","example":"json"},"date_from":{"type":"string","format":"date-time","description":"Start of the window. Defaults to the first of date_to's month in the org's timezone.\n","example":"2026-10-01T04:00:00.000Z"},"date_to":{"type":"string","format":"date-time","description":"End of the window (inclusive). Defaults to the end of today in the org's timezone. Must not be before date_from when both are sent; either bound may be sent alone.\n","example":"2026-10-24T03:59:59.999Z"},"customer_id":{"type":"string","format":"uuid","description":"Only this customer's events. Drawer closes have no customer and drop out."},"entry_types":{"type":"array","description":"Entry types to include. Send them uppercase - a lowercase value passes validation but matches nothing.\n","items":{"type":"string","enum":["PAYMENT_RECEIPT","SYSTEM_CREDIT","DEPOSIT_APPLICATION","DISCOUNT_GRANT","DISCOUNT_CLAWBACK","REFUND_ABSORPTION","REVENUE_POSTING","DRAWER_CLOSE"]}},"accounts":{"type":"array","description":"GL catalog accounts to include, as (category, label) pairs, uppercase; a pair the catalog doesn't have is a 400 (\"Unknown GL account: ...\"), and so is a lowercase pair. With gl_code too, the intersection.\n","items":{"type":"object","required":["category","label"],"properties":{"category":{"type":"string","enum":["ASSET","EXPENSE","PURCHASE_ORDER","INCOME","LIABILITY"],"example":"LIABILITY"},"label":{"type":"string","example":"CUSTOMER_DEPOSITS"}}}},"gl_code":{"type":"string","maxLength":64,"description":"The accounts the org assigned this QuickBooks code to."},"include_legacy":{"type":"boolean","default":true,"description":"false drops the legacy (pre deposit-first) payment receipts. Send a JSON boolean - the string \"false\" passes validation but keeps them.\n","example":true},"with_opening_balance":{"type":"boolean","default":false,"description":"Single-account requests only - derive the opening balance from all history before date_from. A heavier query. Send a JSON boolean - the string \"true\" passes validation but gives no opening balance.\n"},"order_by":{"type":"array","description":"Sort keys, applied in order. Omitted, the journal order (date, journal #, debit first). The column must be one of txn_date, entry_type, account_code, account_label, company_name, debit or credit.\n","items":{"type":"object","properties":{"column":{"type":"string","enum":["txn_date","entry_type","account_code","account_label","company_name","debit","credit"],"example":"account_code"},"order":{"type":"string","enum":["asc","desc","ASC","DESC"],"example":"asc"},"nulls":{"type":"string","enum":["first","last","FIRST","LAST"],"example":"last"}}}}}},"gl_transactions_line":{"type":"object","required":["event_id","txn_date","source_id","customer_id","company_name","customer_number","order_id","order_number","order_type","invoice_id","invoice_number","reference","description","payment_type","card_brand","cc_last_four","bank_date","drawer_name","actor_name","tax_state_code","tax_county_name","tax_city_name","taxable_base","legacy","flags","flow","entry_type","account_category","account_label","account_code","account_display_name","debit","credit","segment"],"description":"One side of a journal entry. Every line carries every key, null when it doesn't apply, except running_balance, which is present only on single-account (ledger) requests.\n","properties":{"event_id":{"type":"string","description":"The journal","example":"JE-IN-A3AC2879"},"txn_date":{"type":"string","format":"date-time"},"source_id":{"type":"string","format":"uuid"},"flow":{"type":"string","description":"The account flow that produced the line."},"entry_type":{"type":"string","example":"REVENUE_POSTING"},"account_category":{"type":"string","example":"ASSET"},"account_label":{"type":"string","example":"ACCOUNTS_RECEIVABLE"},"account_code":{"type":"string","nullable":true,"description":"The org's QuickBooks code for the account; null when unassigned."},"account_display_name":{"type":"string","example":"Accounts Receivable"},"debit":{"type":"number","description":"0 on a credit line.","example":15.9},"credit":{"type":"number","description":"0 on a debit line.","example":0},"running_balance":{"type":"number","description":"Single-account (ledger) requests only, in the account's normal direction."},"segment":{"type":"string","nullable":true,"description":"The deposit segment, on Customer Deposits lines only."},"customer_id":{"type":"string","format":"uuid","nullable":true},"company_name":{"type":"string","nullable":true},"customer_number":{"type":"integer","nullable":true,"example":5048},"order_id":{"type":"string","format":"uuid","nullable":true},"order_number":{"type":"string","nullable":true},"order_type":{"type":"string","nullable":true},"invoice_id":{"type":"string","format":"uuid","nullable":true},"invoice_number":{"type":"string","nullable":true},"reference":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"payment_type":{"type":"string","nullable":true},"card_brand":{"type":"string","nullable":true},"cc_last_four":{"type":"string","nullable":true},"bank_date":{"type":"string","format":"date-time","nullable":true},"drawer_name":{"type":"string","nullable":true},"actor_name":{"type":"string","nullable":true},"tax_state_code":{"type":"string","nullable":true},"tax_county_name":{"type":"string","nullable":true},"tax_city_name":{"type":"string","nullable":true},"taxable_base":{"type":"number","nullable":true},"legacy":{"type":"boolean"},"flags":{"type":"array","items":{"type":"string","example":"UNMAPPED_ACCOUNT_CODE"}}}},"gl_transactions_account_total":{"type":"object","required":["category","label","code","display_name","debits","credits","net"],"properties":{"category":{"type":"string"},"label":{"type":"string"},"code":{"type":"string","nullable":true},"display_name":{"type":"string"},"debits":{"type":"number"},"credits":{"type":"number"},"net":{"type":"number","description":"debits minus credits."}}},"gl_transactions_totals":{"type":"object","required":["total_debits","total_credits","balanced","by_account","by_entry_type","by_segment","unmapped_accounts","resolved_accounts","ledger","line_count","date_from","date_to","include_legacy"],"description":"Over every line of the filtered set, not just the page.","properties":{"total_debits":{"type":"number","example":12441.86},"total_credits":{"type":"number","example":12441.86},"balanced":{"type":"boolean","description":"Debits equal credits. Expected true on the unfiltered journal only."},"by_account":{"type":"array","description":"Per account, in catalog order.","items":{"$ref":"#/components/schemas/gl_transactions_account_total"}},"by_entry_type":{"type":"object","description":"Keyed by entry type.","additionalProperties":{"type":"object","properties":{"lines":{"type":"integer"},"debits":{"type":"number"},"credits":{"type":"number"}}}},"by_segment":{"type":"object","nullable":true,"description":"Customer Deposits split by segment (credit-normal net); null without such lines.","additionalProperties":{"type":"object","properties":{"debits":{"type":"number"},"credits":{"type":"number"},"net":{"type":"number"}}}},"unmapped_accounts":{"type":"array","description":"Accounts in the result with no QuickBooks code assigned.","items":{"type":"object","properties":{"category":{"type":"string"},"label":{"type":"string"},"display_name":{"type":"string"}}}},"resolved_accounts":{"type":"array","nullable":true,"description":"The account keys the request resolved to; null means every reportable account, [] that gl_code matched no account (no lines).\n","items":{"type":"string","example":"LIABILITY.CUSTOMER_DEPOSITS"}},"ledger":{"type":"object","nullable":true,"description":"Single-account requests only.","properties":{"account":{"type":"string","example":"LIABILITY.CUSTOMER_DEPOSITS"},"normal_side":{"type":"string","enum":["DR","CR"]},"opening_balance":{"type":"number","nullable":true,"description":"Null unless with_opening_balance was sent."},"period_net":{"type":"number"},"closing_balance":{"type":"number","nullable":true}}},"line_count":{"type":"integer"},"date_from":{"type":"string","description":"As sent, else the default as an ISO date-time."},"date_to":{"type":"string","description":"As sent, else the default as an ISO date-time."},"include_legacy":{"type":"boolean"}}},"gl_transactions_result":{"type":"object","required":["rows","totals","pagination","supported_formats"],"properties":{"rows":{"type":"array","items":{"$ref":"#/components/schemas/gl_transactions_line"}},"totals":{"$ref":"#/components/schemas/gl_transactions_totals"},"pagination":{"$ref":"#/components/schemas/report_pagination"},"supported_formats":{"type":"array","items":{"type":"string"},"example":["json","csv","excel","pdf"]}}},"parts_inventory_body":{"type":"object","example":{"search":{"format":"json","query":"megacrimp","bin_location":"A-12","stock":true,"order_by":[{"column":"bin_location","order":"asc"}]},"paginate":{"currentPage":1,"perPage":50}},"properties":{"search":{"$ref":"#/components/schemas/parts_inventory_search"},"paginate":{"$ref":"#/components/schemas/paginate"}}},"parts_inventory_search":{"type":"object","description":"Every field is optional; unknown keys are refused.","properties":{"format":{"type":"string","enum":["json","csv","excel","pdf"],"default":"json","example":"json"},"query":{"type":"string","maxLength":200,"description":"The parts picker's search (DOZ-7907) - every word must appear somewhere in the item number, part number, name, description, make, model or serial number.\n","example":"megacrimp"},"bin_location":{"type":"string","maxLength":200,"description":"Case-insensitive substring of the bin location; % and _ match literally.","example":"A-12"},"stock":{"type":"boolean","nullable":true,"description":"true - quantity above 0; false - quantity 0 or below (negatives included). Omitted or null, every part.\n","example":true},"order_by":{"allOf":[{"$ref":"#/components/schemas/order_by"}],"description":"Columns - item_number, name, part_number, manufacturer, model_number, model_year, serial_number, bin_location, quantity. Omitted or empty, name then part number.\n"}}},"parts_inventory_row":{"type":"object","properties":{"id":{"type":"string","format":"uuid","readOnly":true,"description":"The part's id - what the web opens the part editor with (GET /parts/{id})."},"item_number":{"type":"integer","readOnly":true,"example":10263},"name":{"type":"string","nullable":true,"readOnly":true,"example":"10G-12MJ Megacrimp"},"part_number":{"type":"string","nullable":true,"readOnly":true,"example":"G25165-1012"},"manufacturer":{"type":"string","nullable":true,"readOnly":true,"description":"The part's make (stored as parts.manufacture).","example":"Gates"},"model_number":{"type":"string","nullable":true,"readOnly":true,"example":"MJ-12"},"model_year":{"type":"string","nullable":true,"readOnly":true,"example":"2024"},"serial_number":{"type":"string","nullable":true,"readOnly":true,"example":"SN-29590"},"bin_location":{"type":"string","nullable":true,"readOnly":true,"example":"A-12"},"quantity":{"type":"integer","readOnly":true,"description":"On-hand quantity; 0 when never set, negative when consumed past the count.","example":4}}},"parts_inventory_result":{"type":"object","properties":{"rows":{"type":"array","items":{"$ref":"#/components/schemas/parts_inventory_row"}},"pagination":{"$ref":"#/components/schemas/pagination"},"supported_formats":{"type":"array","items":{"type":"string"},"example":["json","csv","excel","pdf"]}}},"financial_audit_action":{"type":"string","enum":["DEPOSIT_RECEIVED","CC_DEPOSIT_INITIATED","CC_PAYMENT_INITIATED","CC_DEPOSIT_CONFIRMED","CC_PAYMENT_CONFIRMED","CC_FEE_CHARGES_RECORDED","CC_PAYMENT_FAILED","STRIPE_REFUND_FLAGGED","PAYMENT_RECEIVED","PAYMENT_APPLIED","DISCOUNT_GRANTED","DISCOUNT_CLAWED_BACK","REFUND_CREDIT_ABSORBED","OVERPAYMENT_CREDITED","INVOICE_CREATED","INVOICE_UPDATED","INVOICE_APPROVED","INVOICE_VOIDED","ORDER_VOIDED","INVOICE_EMAILED","STATEMENT_EMAILED","DRAWER_OPENED","DRAWER_CLOSED","CUSTOMER_CREDIT_UPDATED","GL_CONFIG_UPDATED","ORG_CONFIG_UPDATED","PAYMENT_METHOD_UPDATED","CATEGORY_CONFIG_UPDATED","DEPRECIATION_SCHEDULE_UPDATED"]},"financial_audit_entity_type":{"type":"string","enum":["PAYMENT","PAYMENT_APPLICATION","INVOICE","ORDER","CASH_DRAWER","CASH_DRAWER_CLOSING","CUSTOMER","STATEMENT","ORG","GL_ACCOUNT","CATEGORY","DEPRECIATION_SCHEDULE"]},"financial_audit_source":{"type":"string","enum":["USER","SYSTEM","WEBHOOK","CRON"]},"financial_audit_body":{"type":"object","example":{"search":{"format":"json","date_from":"2026-10-01T04:00:00.000Z","date_to":"2026-10-24T03:59:59.999Z","actions":["PAYMENT_RECEIVED"]},"paginate":{"currentPage":1,"perPage":50}},"properties":{"search":{"$ref":"#/components/schemas/financial_audit_search"},"paginate":{"$ref":"#/components/schemas/paginate"}}},"financial_audit_search":{"type":"object","description":"Every field is optional, and an absent one means all; unknown keys are refused. actions, entity_type and source must be sent uppercase - a lowercase value passes validation but matches nothing. An order_by entry without order sorts descending.\n","properties":{"format":{"type":"string","enum":["json","csv","excel","pdf"],"default":"json","example":"json"},"date_from":{"type":"string","format":"date-time","description":"Start of the window, on event_at. Defaults to the first of date_to's month in the org's timezone.\n","example":"2026-10-01T04:00:00.000Z"},"date_to":{"type":"string","format":"date-time","description":"End of the window (inclusive). Defaults to the end of today in the org's timezone. Must not be before date_from when both are sent; either bound may be sent alone.\n","example":"2026-10-24T03:59:59.999Z"},"actor_user_id":{"type":"string","format":"uuid","description":"Only events this user performed."},"actions":{"type":"array","description":"Actions to include, uppercase.","items":{"$ref":"#/components/schemas/financial_audit_action"},"example":["PAYMENT_RECEIVED"]},"entity_type":{"$ref":"#/components/schemas/financial_audit_entity_type"},"source":{"$ref":"#/components/schemas/financial_audit_source"},"customer_id":{"type":"string","format":"uuid"},"order_id":{"type":"string","format":"uuid"},"invoice_id":{"type":"string","format":"uuid"},"entity_id":{"type":"string","format":"uuid","description":"One entity's trail - every event against that payment, invoice, drawer, etc."},"entity_number":{"type":"string","maxLength":64,"description":"Contains-match on the display reference (invoice number, drawer name, ref)."},"order_by":{"type":"array","description":"Sort keys, applied in order. Defaults to event_at descending. The column must be one of event_at, action, entity_type, entity_number, actor_username, amount or source.\n","items":{"type":"object","properties":{"column":{"type":"string","enum":["event_at","action","entity_type","entity_number","actor_username","amount","source"],"example":"event_at"},"order":{"type":"string","enum":["asc","desc","ASC","DESC"],"example":"desc"},"nulls":{"type":"string","enum":["first","last","FIRST","LAST"],"description":"Accepted and ignored; the journal sorts without a nulls clause."}}}}}},"financial_audit_row":{"type":"object","required":["id","org","action","entity_type","entity_id","entity_number","customer_id","order_id","invoice_id","amount","actor_user_id","actor_username","source","detail","event_at","created_at","actor_name","customer_name","customer_number","summary"],"properties":{"id":{"type":"string","format":"uuid","readOnly":true},"org":{"type":"string","format":"uuid","readOnly":true},"action":{"$ref":"#/components/schemas/financial_audit_action"},"entity_type":{"$ref":"#/components/schemas/financial_audit_entity_type"},"entity_id":{"type":"string","format":"uuid","nullable":true},"entity_number":{"type":"string","nullable":true,"description":"The display reference (invoice number, drawer name, deposit ref).","example":"INV-R-20260916-001-01"},"customer_id":{"type":"string","format":"uuid","nullable":true},"order_id":{"type":"string","format":"uuid","nullable":true},"invoice_id":{"type":"string","format":"uuid","nullable":true},"amount":{"type":"number","nullable":true,"description":"The action's money amount, where it has one.","example":15.9},"actor_user_id":{"type":"string","format":"uuid","nullable":true},"actor_username":{"type":"string","nullable":true},"source":{"$ref":"#/components/schemas/financial_audit_source"},"request_id":{"type":"string","format":"uuid","nullable":true,"readOnly":true,"description":"The X-Request-Id of the request that wrote the row, the same id its journal and PO audit rows and log lines carry. Null for scheduled jobs and for rows written before DOZ-7840.\n"},"detail":{"type":"object","description":"The action's original values (N-001), e.g. changes or payment context."},"event_at":{"type":"string","format":"date-time","description":"When the business event happened."},"created_at":{"type":"string","format":"date-time"},"actor_name":{"type":"string","nullable":true},"customer_name":{"type":"string","nullable":true},"customer_number":{"type":"integer","nullable":true,"example":5048},"summary":{"type":"string","description":"The composed business sentence, rendered at read time.","example":"Asim Sansi approved invoice INV-R-20260916-001-01 — $15.90"}}},"financial_audit_totals":{"type":"object","required":["count","by_action","date_from","date_to"],"description":"Over every row of the filtered set, not just the page.","properties":{"count":{"type":"integer","example":30},"by_action":{"type":"object","description":"Keyed by action.","additionalProperties":{"type":"object","properties":{"count":{"type":"integer"},"amount":{"type":"number","nullable":true,"description":"Null when the action carries no amount."}}}},"date_from":{"type":"string","description":"As sent, else the default as an ISO date-time."},"date_to":{"type":"string","description":"As sent, else the default as an ISO date-time."}}},"financial_audit_result":{"type":"object","required":["rows","totals","pagination","supported_formats"],"properties":{"rows":{"type":"array","items":{"$ref":"#/components/schemas/financial_audit_row"}},"totals":{"$ref":"#/components/schemas/financial_audit_totals"},"pagination":{"$ref":"#/components/schemas/report_pagination"},"supported_formats":{"type":"array","items":{"type":"string"},"example":["json","csv","excel","pdf"]}}},"po_status_body":{"type":"object","example":{"search":{"format":"json","states":["released","closed"],"reconciliation":["WITH_VARIANCE"],"date_basis":"CREATED","date_from":"2026-09-01","date_to":"2026-09-30"},"paginate":{"currentPage":1,"perPage":50}},"properties":{"search":{"$ref":"#/components/schemas/po_status_search"},"paginate":{"$ref":"#/components/schemas/paginate"}}},"po_status_search":{"type":"object","description":"Every field is optional, and an absent one means all; unknown keys are refused.","properties":{"format":{"type":"string","enum":["json","csv","excel","pdf"],"default":"json","example":"json"},"states":{"type":"array","description":"PO states to include (case-insensitive). Omitted or empty, every state.","items":{"type":"string","enum":["new","released","partially_received","received","closed"]},"example":["released","closed"]},"reconciliation":{"type":"array","description":"Buckets to keep (case-insensitive). Narrows the closed rows only - rows in any other selected state pass through.\n","items":{"type":"string","enum":["NO_VARIANCE","WITH_VARIANCE","NOT_RECONCILED"]},"example":["WITH_VARIANCE"]},"date_basis":{"type":"string","enum":["CREATED","CLOSED"],"default":"CREATED","description":"The date the window applies to (case-insensitive). CLOSED windows the close date, so it also drops every PO that is not closed.\n","example":"CREATED"},"date_from":{"type":"string","format":"date","description":"First day included, a calendar day in the org's timezone.","example":"2026-09-01"},"date_to":{"type":"string","format":"date","description":"Last day included. Must not be before date_from when both are sent; either bound may be sent alone.\n","example":"2026-09-30"},"vendor_id":{"type":"string","format":"uuid","description":"Only this vendor's POs."},"po_number":{"type":"string","maxLength":64,"description":"Matches PO numbers containing this text (case-insensitive).","example":"0029"},"order_by":{"type":"array","description":"Sort order; defaults to created_at, newest first.","items":{"type":"object","properties":{"column":{"type":"string","enum":["po_number","vendor_name","state","created_at","closed_at","invoice_state","ordered","received","invoiced","uninvoiced_receipts","difference","reconciliation","variance_total"]},"order":{"type":"string","enum":["asc","desc","ASC","DESC"]},"nulls":{"type":"string","enum":["first","last","FIRST","LAST"]}}},"example":[{"column":"created_at","order":"desc"}]}}},"po_status_row":{"type":"object","properties":{"po_id":{"type":"string","format":"uuid"},"po_number":{"type":"string","example":"002954"},"vendor_id":{"type":"string","format":"uuid","nullable":true},"vendor_name":{"type":"string","nullable":true,"example":"FASTSIGN"},"state":{"type":"string","enum":["new","released","partially_received","received","closed"]},"created_at":{"type":"string","format":"date-time"},"closed_at":{"type":"string","format":"date-time","nullable":true,"description":"Null unless the PO is closed."},"closed_by":{"type":"string","nullable":true,"description":"The username that closed the PO; null unless closed."},"closing_reason":{"type":"string","nullable":true,"description":"The reason given at the close; null unless closed."},"invoice_state":{"type":"string","nullable":true,"enum":["not_invoiced","partially_invoiced","invoiced",null]},"invoice_count":{"type":"integer","description":"Live vendor invoices recorded against the PO."},"ordered":{"type":"number","description":"The PO's landed value as ordered.","example":160},"received":{"type":"number","example":30},"invoiced":{"type":"number","example":200},"uninvoiced_receipts":{"type":"number","description":"Value received but not yet invoiced.","example":0},"difference":{"type":"number","description":"Invoiced minus ordered (landed). Non-zero for a correctly reconciled short close too - a measure against the order, not a variance flag.\n","example":40},"reconciliation_status":{"type":"string","nullable":true,"enum":["NOT_INVOICED","OUTSTANDING","MATCHED_WITH_VARIANCE","FULLY_MATCHED",null],"description":"The reconciliation summary's status (the close's, for a closed PO)."},"reconciliation":{"type":"string","nullable":true,"enum":["NO_VARIANCE","WITH_VARIANCE","NOT_RECONCILED",null],"description":"The bucket a closed PO closed into; null for any other state."},"reconciliation_source":{"type":"string","enum":["CLOSE_SNAPSHOT","LIVE"],"description":"CLOSE_SNAPSHOT when the figures come from the close's frozen snapshot, LIVE when summarized now.\n"},"variance_count":{"type":"integer"},"variance_total":{"type":"number","example":40},"charge_variance":{"type":"boolean","description":"A header charge was billed over the org's tolerance."},"charge_variances":{"type":"array","items":{"type":"string","enum":["freight","sales_tax","miscellaneous"]}},"short_receipt_amount":{"type":"number"},"awaiting_receipt_amount":{"type":"number"},"pending_approval_count":{"type":"integer","description":"Invoices whose variances await an approver; always 0 on a closed PO."}}},"po_status_totals":{"type":"object","description":"Totals over every row of the filtered set, not just the page.","properties":{"po_count":{"type":"integer"},"by_state":{"type":"object","description":"Row count per state.","additionalProperties":{"type":"integer"},"example":{"new":0,"released":8,"partially_received":1,"received":2,"closed":0}},"by_reconciliation":{"type":"object","description":"Closed-row count per bucket.","additionalProperties":{"type":"integer"},"example":{"NO_VARIANCE":0,"WITH_VARIANCE":0,"NOT_RECONCILED":0}},"ordered":{"type":"number"},"received":{"type":"number"},"invoiced":{"type":"number"},"uninvoiced_receipts":{"type":"number"},"variance_total":{"type":"number"}}},"po_status_result":{"type":"object","properties":{"rows":{"type":"array","items":{"$ref":"#/components/schemas/po_status_row"}},"totals":{"$ref":"#/components/schemas/po_status_totals"},"pagination":{"$ref":"#/components/schemas/pagination"},"supported_formats":{"type":"array","items":{"type":"string"},"example":["json","csv","excel","pdf"]}}},"transaction_history_body":{"type":"object","example":{"search":{"format":"json","date_from":"2026-09-01T04:00:00.000Z","date_to":"2026-10-01T03:59:59.999Z"}},"description":"paginate is validated when sent (a bad one is a 400), then ignored.","properties":{"search":{"$ref":"#/components/schemas/transaction_history_search"},"paginate":{"$ref":"#/components/schemas/paginate"}}},"transaction_history_search":{"type":"object","description":"Every field is optional ('' and null mean unset); unknown keys are refused. With neither date the window is today in the org's timezone. The dates are checked as dates, not as ISO strings, and reach the query as sent - so send ISO date-times; a number (epoch milliseconds) passes validation and then fails with a 500.\n","properties":{"format":{"type":"string","enum":["json","csv","excel","pdf","iif"],"default":"json","example":"json"},"date_from":{"type":"string","format":"date-time","description":"First instant included (order created_at, asset sold_date).","example":"2026-09-01T04:00:00.000Z"},"date_to":{"type":"string","format":"date-time","description":"Last instant included. Must not be before date_from when both are sent; sent alone it has no lower bound.\n","example":"2026-10-01T03:59:59.999Z"}}},"transaction_history_day":{"type":"object","required":["created_at","rental_income","parts_billed_to_customer","trucking_fees","env_fee","state_sales_tax","county_sales_tax","city_sales_tax","special_sales_tax"],"description":"One day's GL buckets, each summed over the live charges of the orders created that day.","properties":{"created_at":{"type":"string","description":"The calendar day in the org's timezone, MM-DD-YYYY.","example":"09-16-2026"},"rental_income":{"type":"number","description":"GL 4100 - rental and sale orders' base lines, net of discounts.","example":14.29},"parts_billed_to_customer":{"type":"number","description":"GL 4771 - work orders' base lines, net of discounts."},"trucking_fees":{"type":"number","description":"GL 4761 - delivery and return freight fees."},"env_fee":{"type":"number","description":"GL 4770."},"state_sales_tax":{"type":"number","description":"GL 2300.","example":0.61},"county_sales_tax":{"type":"number","description":"GL 2301."},"city_sales_tax":{"type":"number","description":"GL 2302."},"special_sales_tax":{"type":"number","description":"GL 2303."}}},"transaction_history_sold_day":{"type":"object","required":["sold_date","data"],"properties":{"sold_date":{"type":"string","description":"The calendar day in the org's timezone, MM-DD-YYYY.","example":"10-20-2026"},"data":{"type":"array","description":"The assets sold that day.","items":{"type":"object","required":["name","sell_price"],"properties":{"name":{"type":"string","nullable":true},"sell_price":{"type":"number","example":2490}}}}}},"transaction_history_result":{"type":"object","required":["data","pagination","supported_formats"],"properties":{"data":{"type":"object","required":["orderDetails","soldAssets"],"properties":{"orderDetails":{"type":"array","items":{"$ref":"#/components/schemas/transaction_history_day"}},"soldAssets":{"type":"array","items":{"$ref":"#/components/schemas/transaction_history_sold_day"}}}},"pagination":{"$ref":"#/components/schemas/report_pagination"},"supported_formats":{"type":"array","items":{"type":"string"},"example":["json","csv","excel","pdf","iif"]}}},"rental_revenue_body":{"type":"object","example":{"search":{"format":"json","date_from":"2025-01-01","date_to":"2025-12-31"},"paginate":{"currentPage":1,"perPage":50}},"properties":{"search":{"type":"object","description":"Every field is optional; unknown keys are refused.","properties":{"format":{"type":"string","enum":["json","excel"],"default":"json"},"date_from":{"type":"string","format":"date","description":"First day of the window, a whole day in the org's timezone, included. Send both dates or neither; with neither, everything is all-time. The window applies to rental income, repair and maintenance and times out on rent (and to the overhead, gain/loss and margin worked out from them). Depreciation is accumulated to date and current monthly income is always the last 30 days, whatever the window.\n"},"date_to":{"type":"string","format":"date","description":"Last day of the window, a whole day in the org's timezone, included. Send both dates or neither; not before date_from.\n"}},"example":{"format":"json","date_from":"2025-01-01","date_to":"2025-12-31"}},"paginate":{"$ref":"#/components/schemas/paginate"}}},"rental_revenue_figures":{"type":"object","description":"The figures every machine row and every subtotal carry, in dollars unless named _percent (a 0-100 number rounded to 2 places). A subtotal's money figures are the sums of its machines' rows; its two percents are worked out again from those sums. The letters are the excel sheet's columns.\n","properties":{"rental_income":{"type":"number","description":"K - rent in the window (all-time with none): the imported history dated before the cutover, plus CloudERM billing from it - issued invoices by invoice date, credit notes negative, a swapped line split by the days each machine held it. An imported invoice dated before the cutover is left out; the imported history already holds it.\n","example":459},"depreciation":{"type":"number","nullable":true,"description":"M - accumulated depreciation at the latest internal-value schedule entry dated today or earlier; 0 for a machine with no internal-value schedule. null when the org has depreciation turned off (see depreciation_enabled).\n","example":1800},"maintenance":{"type":"number","description":"O - repair and maintenance in the window: the imported repairs plus each CloudERM work order's internal cost, dated when the order was completed.\n","example":130},"overhead":{"type":"number","description":"Q - rental_income x overhead_percent.","example":78.03},"total_costs":{"type":"number","description":"S - depreciation + maintenance + overhead (without depreciation when it is null).","example":2008.03},"monthly_costs":{"type":"number","nullable":true,"description":"T - total_costs / months_in_service on a machine row (null when it has no start date or 0 months); the sum of those on a subtotal.\n","example":42.65},"current_monthly_rate":{"type":"number","nullable":true,"description":"U - the machine's 4-week rate, its own else its category's (null when it has neither); the sum of those on a subtotal.\n","example":612},"current_monthly_income":{"type":"number","description":"V - rent in the last 30 days (today and the 30 days before it, in the org's timezone), from the same two sources as rental_income. Never windowed.\n","example":0},"current_utilization_percent":{"type":"number","nullable":true,"description":"W - current_monthly_income / current_monthly_rate x 100; null when the rate is missing or 0.","example":0},"rental_gain_loss":{"type":"number","description":"Y - rental_income - total_costs.","example":-1549.03},"rental_margin_percent":{"type":"number","nullable":true,"description":"AA - rental_gain_loss / rental_income x 100; null when rental_income is 0.","example":-337.48},"times_out_on_rent":{"type":"integer","description":"AC - CloudERM rental contracts whose dispatch of the machine began in the window (all-time with none), counted per order - a swap is one contract, a renewal is its own. The imported history has no contracts, so this counts CloudERM rentals only.\n","example":0}}},"rental_revenue_tree":{"type":"object","description":"The machine's place in the category tree. category_* is the sub-category the machine is filed under, parent_category_* its category, super_category_* the top. Group and key on the ids - two sibling sub-categories can share a name.\n","properties":{"super_category_id":{"type":"string","format":"uuid"},"super_category_name":{"type":"string","example":"Attachments"},"parent_category_id":{"type":"string","format":"uuid"},"parent_category_name":{"type":"string","example":"Excavator Attachments"},"category_id":{"type":"string","format":"uuid"},"category_name":{"type":"string","example":"Excavator Bucket - 120CL"}}},"rental_revenue_row":{"description":"One machine. Rows come in category-tree order, by asset number within a sub-category.","allOf":[{"type":"object","properties":{"asset_id":{"type":"string","format":"uuid"},"asset_number":{"type":"string","example":"6973"},"description":{"type":"string","description":"The machine's name.","example":"36\" DE 120CL HD BUCKET"},"model_year":{"type":"string","nullable":true,"example":"2022"},"hours":{"type":"number","nullable":true,"description":"The hour meter.","example":1872.3},"start_date":{"type":"string","format":"date","nullable":true,"description":"The purchase date.","example":"2022-12-01"},"sold_date":{"type":"string","format":"date","nullable":true},"months_in_service":{"type":"number","nullable":true,"description":"I - months (of 30 days) from start_date to now, to 2 places; null with no start_date.","example":47.08}}},{"$ref":"#/components/schemas/rental_revenue_tree"},{"$ref":"#/components/schemas/rental_revenue_figures"}]},"rental_revenue_total":{"description":"A subtotal over the whole result. level says which node it is; the ids and names below that level are null (all of them on the grand total).\n","allOf":[{"type":"object","properties":{"level":{"type":"string","enum":["SUB_CATEGORY","CATEGORY","SUPER_CATEGORY","GRAND_TOTAL"]},"asset_count":{"type":"integer","example":73}}},{"$ref":"#/components/schemas/rental_revenue_tree"},{"$ref":"#/components/schemas/rental_revenue_figures"}]},"rental_revenue_result":{"type":"object","properties":{"rows":{"type":"array","items":{"$ref":"#/components/schemas/rental_revenue_row"}},"totals":{"type":"array","description":"Every sub-category, category and super category in the result, then the grand total last - over every row, not just the page. Each level comes before the levels under it.\n","items":{"$ref":"#/components/schemas/rental_revenue_total"}},"window":{"type":"object","description":"The dates sent, or both null for all-time rental income.","properties":{"date_from":{"type":"string","format":"date","nullable":true,"example":"2025-01-01"},"date_to":{"type":"string","format":"date","nullable":true,"example":"2025-12-31"}}},"overhead_percent":{"type":"number","description":"The rate behind overhead, as a 0-100 number.","example":17},"depreciation_enabled":{"type":"boolean","description":"False when the org has depreciation turned off - every depreciation is then null and left out of total_costs, so hide the column.\n"},"cutover_date":{"type":"string","format":"date","nullable":true,"description":"The org's first day of CloudERM rental billing (its first issued rental invoice on an order that was not imported from the previous system). Rental income before it is the history imported from the previous system; null when the org has no such invoice yet, so all of its income is history.\n","example":"2026-09-24"},"pagination":{"$ref":"#/components/schemas/pagination"},"supported_formats":{"type":"array","items":{"type":"string"},"example":["json","excel"]}}},"daily_summary_body":{"type":"object","example":{"search":{"format":"json","date_from":"2026-10-01","date_to":"2026-10-21"}},"properties":{"search":{"$ref":"#/components/schemas/daily_summary_search"}}},"daily_summary_search":{"type":"object","description":"Every field is optional; unknown keys are refused. A pair is sent whole or not at all; with neither pair the window is the trailing 24 hours.\n","properties":{"format":{"type":"string","enum":["json","csv","excel","pdf"],"default":"json","example":"json"},"date_from":{"type":"string","format":"date","description":"The window's first day (or instant). A date-only value is the whole day in the org's timezone. Sent together with date_to.\n","example":"2026-10-01"},"date_to":{"type":"string","format":"date","description":"The window's last day (or instant). Sent together with date_from.","example":"2026-10-21"},"start_date":{"type":"string","format":"date","description":"The older name for date_from, still read. Sent together with end_date."},"end_date":{"type":"string","format":"date","description":"The older name for date_to, still read. Sent together with start_date."}}},"daily_summary_order":{"type":"object","description":"A quote, reservation, contract or sales order in the window.","additionalProperties":true,"properties":{"id":{"type":"string","format":"uuid"},"order_number":{"type":"string","example":"R-20261007-001"},"type":{"type":"string","example":"RENTAL"},"status":{"type":"string","example":"OPEN_CONTRACT"},"total":{"type":"number","example":1911.52},"customer_details":{"type":"object","nullable":true,"additionalProperties":true},"assignee_details":{"type":"object","nullable":true,"additionalProperties":true},"asset_details":{"nullable":true},"items":{"type":"array","items":{"type":"object","additionalProperties":true,"properties":{"item_type":{"type":"string","example":"ASSET"},"status":{"type":"string","nullable":true},"item_details":{"type":"object","additionalProperties":true}}}}}},"daily_summary_task":{"type":"object","description":"A work order or an inspection in the window.","additionalProperties":true,"properties":{"id":{"type":"string","format":"uuid"},"order_number":{"type":"string","nullable":true},"record_type":{"type":"string","nullable":true},"type":{"type":"string"},"status":{"type":"string"},"priority":{"nullable":true},"passed":{"type":"boolean","nullable":true},"quick_pass":{"type":"boolean","nullable":true},"created_at":{"type":"string","format":"date-time"},"due_date":{"type":"string","format":"date-time","nullable":true},"due_date_formatted":{"type":"string","nullable":true},"customer_details":{"type":"object","nullable":true,"additionalProperties":true},"assignee_details":{"nullable":true},"asset_details":{"nullable":true},"task_level_details":{"nullable":true}}},"daily_summary_payload":{"type":"object","properties":{"org":{"type":"object","properties":{"name":{"type":"string"},"invoice_address":{"type":"string","nullable":true},"timezone":{"type":"string","example":"America/New_York"}}},"user":{"type":"object","description":"The caller, whose merged preferences shape the report.","additionalProperties":true},"summary":{"type":"object","description":"The high-level counts per desk - sales and rental (new / open / closed per record type) and service (new / pending / in progress / completed); a count that does not apply is null.\n","additionalProperties":true,"example":{"sales":{"quotes":{"new":1,"open":1,"closed":null}}}},"quotes":{"type":"array","items":{"$ref":"#/components/schemas/daily_summary_order"}},"reservations":{"type":"array","items":{"$ref":"#/components/schemas/daily_summary_order"}},"open_contracts":{"type":"array","items":{"$ref":"#/components/schemas/daily_summary_order"}},"closed_contracts":{"type":"array","items":{"$ref":"#/components/schemas/daily_summary_order"}},"sales_quotes":{"type":"array","items":{"$ref":"#/components/schemas/daily_summary_order"}},"open_sales_orders":{"type":"array","items":{"$ref":"#/components/schemas/daily_summary_order"}},"closed_sales_orders":{"type":"array","items":{"$ref":"#/components/schemas/daily_summary_order"}},"work_orders":{"type":"array","items":{"$ref":"#/components/schemas/daily_summary_task"}},"dispatch_inspections":{"type":"array","items":{"$ref":"#/components/schemas/daily_summary_task"}},"inspections_off_rented":{"type":"array","items":{"$ref":"#/components/schemas/daily_summary_task"}},"done_inspections":{"type":"array","items":{"$ref":"#/components/schemas/daily_summary_task"}},"done_dispatch_inspections":{"type":"array","items":{"$ref":"#/components/schemas/daily_summary_task"}},"report_preferences":{"type":"object","description":"The merged org and user preferences - hideEmptySections, then highLevelSummary, salesDesk, rentalDesk and serviceDesk, each with hidden and its sections' hidden flags.\n","additionalProperties":true},"client_url":{"type":"string","description":"The web app's base URL, used for the order and work-order links."},"hoursBefore":{"type":"integer","description":"The window's length in hours.","example":24},"nowTimestamp":{"type":"string","description":"The window's end, formatted in the org's timezone."},"hoursBeforeTimestamp":{"type":"string","description":"The window's start, formatted in the org's timezone."},"reporting_window":{"type":"object","properties":{"from":{"type":"string","format":"date-time"},"to":{"type":"string","format":"date-time"},"hours":{"type":"integer"},"custom":{"type":"boolean","description":"True when the request sent a window."}}}}},"daily_summary_result":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/daily_summary_payload"},"supported_formats":{"type":"array","items":{"type":"string"},"example":["json","csv","excel","pdf"]}}},"customer_sold_assets_body":{"type":"object","required":["search"],"example":{"search":{"format":"json","customer_id":"ed767323-1c2b-4a68-b84a-827fa3bb1d18","group_by":"sold_month"},"paginate":{"currentPage":1,"perPage":25}},"properties":{"search":{"$ref":"#/components/schemas/customer_sold_assets_search"},"paginate":{"$ref":"#/components/schemas/paginate"}}},"customer_sold_assets_search":{"type":"object","required":["customer_id"],"description":"Only customer_id is required; unknown keys are refused.","properties":{"format":{"type":"string","enum":["json","csv","excel","pdf"],"default":"json","example":"json"},"customer_id":{"type":"string","format":"uuid","description":"The customer the report is for. A soft-deleted customer is still reported; one outside the caller's organization is a 404.\n","example":"ed767323-1c2b-4a68-b84a-827fa3bb1d18"},"date_from":{"type":"string","format":"date","nullable":true,"description":"First org-local day of the sold-date window, read at day granularity off the value's UTC face. Omitted, the window has no start.\n","example":"2026-01-01"},"date_to":{"type":"string","format":"date","nullable":true,"description":"Last org-local day of the window (inclusive); not before date_from. Omitted, the window has no end - with neither date the report covers every sale.\n","example":"2026-12-31"},"group_by":{"type":"string","enum":["sold_month","sold_year","order","none"],"default":"sold_month","description":"sold_month and sold_year use the org's timezone; order groups by sales order, ordered by its sale date; none returns no groups.\n","example":"sold_month"},"subtotals":{"type":"boolean","default":true,"description":"csv / excel / pdf only - print a subtotal row under each group.","example":true},"show_serial_numbers":{"type":"boolean","default":true,"description":"csv / excel / pdf only - include the Serial # column. The json always carries serial_number.\n","example":true},"hide_zero_value":{"type":"boolean","default":false,"description":"Leave out rows sold for $0, from the rows, the groups and the totals.","example":false},"order_by":{"allOf":[{"$ref":"#/components/schemas/order_by"}],"description":"Orders rows within each group; the groups stay in date order. Columns - sold_at, order_number, asset_number, name, serial_number, category_lineage, sale_price. Omitted or empty, oldest first.\n"}}},"customer_sold_assets_row":{"type":"object","properties":{"line_item_id":{"type":"string","format":"uuid","readOnly":true},"order_id":{"type":"string","format":"uuid","readOnly":true},"order_number":{"type":"string","readOnly":true,"example":"1052"},"customer_po":{"type":"string","nullable":true,"readOnly":true},"sold_at":{"type":"string","format":"date-time","readOnly":true,"description":"When the sale closed (the order's status change for sales closed before snapshots existed). A raw timestamp - format it in the org's timezone.\n","example":"2026-02-15T18:00:00.000Z"},"asset_id":{"type":"string","format":"uuid","readOnly":true},"asset_number":{"type":"string","nullable":true,"readOnly":true,"example":"375"},"name":{"type":"string","nullable":true,"readOnly":true,"example":"2003 Freightliner FL50"},"manufacturer":{"type":"string","nullable":true,"readOnly":true},"model_number":{"type":"string","nullable":true,"readOnly":true},"model_year":{"type":"string","nullable":true,"readOnly":true},"serial_number":{"type":"string","nullable":true,"readOnly":true,"example":"1FVABKBW83HK69210"},"asset_type":{"type":"string","nullable":true,"readOnly":true,"description":"NORMAL, or ADD_ON for an attachment or accessory.","example":"NORMAL"},"category_id":{"type":"string","format":"uuid","nullable":true,"readOnly":true},"category":{"type":"string","nullable":true,"readOnly":true,"description":"The leaf category as of the sale.","example":"Service Truck"},"category_parent":{"type":"string","nullable":true,"readOnly":true,"example":"Trucks"},"category_super":{"type":"string","nullable":true,"readOnly":true,"example":"Transportation"},"category_lineage":{"type":"string","nullable":true,"readOnly":true,"example":"Transportation -> Trucks -> Service Truck"},"sale_price":{"type":"number","readOnly":true,"description":"The revenue for this asset; 0 for an add-on that went free.","example":30000},"group_key":{"type":"string","nullable":true,"readOnly":true,"description":"The row's group - YYYY-MM, YYYY or the order id; null when group_by is none.\n","example":"2026-02"},"group_label":{"type":"string","nullable":true,"readOnly":true,"example":"Feb 2026"}}},"customer_sold_assets_group":{"type":"object","properties":{"key":{"type":"string","readOnly":true,"example":"2026-02"},"label":{"type":"string","readOnly":true,"example":"Feb 2026"},"units":{"type":"integer","readOnly":true,"example":3},"total_sale_price":{"type":"number","readOnly":true,"example":35276}}},"customer_sold_assets_result":{"type":"object","properties":{"customer":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"company_name":{"type":"string","example":"JK Tobin Construction Company Inc"},"customer_number":{"type":"integer","nullable":true,"example":5426}}},"rows":{"type":"array","items":{"$ref":"#/components/schemas/customer_sold_assets_row"}},"groups":{"type":"array","description":"Every group in display order, over the whole filtered set.","items":{"$ref":"#/components/schemas/customer_sold_assets_group"}},"totals":{"type":"object","properties":{"units_sold":{"type":"integer","example":94},"total_sale_price":{"type":"number","example":1475771.18},"first_sold_at":{"type":"string","format":"date-time","nullable":true},"last_sold_at":{"type":"string","format":"date-time","nullable":true}}},"filters":{"type":"object","description":"The window and grouping the report used.","properties":{"date_from":{"type":"string","format":"date","nullable":true},"date_to":{"type":"string","format":"date","nullable":true},"group_by":{"type":"string","example":"sold_month"}}},"pagination":{"$ref":"#/components/schemas/pagination"},"supported_formats":{"type":"array","items":{"type":"string"},"example":["json","csv","excel","pdf"]}}},"over_credit_limit_body":{"type":"object","example":{"search":{"format":"json","over_limit_only":true,"exposure_basis":"BALANCE_PLUS_OPEN_ORDERS","order_type":["RENTAL"]},"paginate":{"currentPage":1,"perPage":25}},"properties":{"search":{"$ref":"#/components/schemas/over_credit_limit_search"},"paginate":{"$ref":"#/components/schemas/paginate"}}},"over_credit_limit_search":{"type":"object","description":"Every field is optional; unknown keys are refused.","properties":{"format":{"type":"string","enum":["json","csv","excel","pdf"],"default":"json","example":"json"},"over_limit_only":{"type":"boolean","description":"Only customers whose over_by is above 0 under the chosen exposure basis.","example":true},"exposure_basis":{"type":"string","enum":["BALANCE","BALANCE_PLUS_OPEN_ORDERS"],"default":"BALANCE_PLUS_OPEN_ORDERS","description":"Whether open (uninvoiced) order value counts toward total_exposure. open_orders is returned either way.\n","example":"BALANCE_PLUS_OPEN_ORDERS"},"order_type":{"type":"array","description":"Scope current_balance and open_orders to these order types. Send the values in uppercase (RENTAL, SALE, WORK_ORDER): a lowercase value passes validation but matches nothing, so every row's current_balance and open_orders come back 0. Omit for all.\n","items":{"type":"string","enum":["RENTAL","SALE","WORK_ORDER"]},"example":["RENTAL"]},"order_by":{"type":"array","description":"Sort keys, applied in order. Defaults to over_by descending, nulls last. The column must be one of customer_number, company_name, credit_limit, current_balance, open_orders, total_exposure, over_by or status.\n","items":{"type":"object","properties":{"column":{"type":"string","enum":["customer_number","company_name","credit_limit","current_balance","open_orders","total_exposure","over_by","status"],"example":"over_by"},"order":{"type":"string","enum":["asc","desc","ASC","DESC"],"example":"desc"},"nulls":{"type":"string","enum":["first","last","FIRST","LAST"],"example":"last"}}}}}},"over_credit_limit_row":{"type":"object","properties":{"customer_id":{"type":"string","format":"uuid"},"customer_number":{"type":"string"},"company_name":{"type":"string"},"credit_limit":{"type":"number","example":5000},"current_balance":{"type":"number","description":"Sum of balance_due on the customer's UNPAID and PARTIALLY_PAID invoices for the scoped order types. Not customers.balance (the unconsumed deposit float).","example":4200.5},"open_orders":{"type":"number","description":"Value of open (uninvoiced) orders. Present under both exposure bases.","example":1500},"total_exposure":{"type":"number","description":"current_balance, plus open_orders under BALANCE_PLUS_OPEN_ORDERS.","example":5700.5},"over_by":{"type":"number","description":"total_exposure minus credit_limit. Above 0 means over the limit.","example":700.5},"status":{"type":"string","enum":["WARN","BLOCK"],"description":"The customer's credit limit action."},"banned":{"type":"boolean"}}},"over_credit_limit_totals":{"type":"object","properties":{"grand_total_over_limit":{"type":"number","description":"Sum of over_by across the customers over the limit.","example":700.5},"count_over_limit":{"type":"integer","example":1},"exposure_basis":{"type":"string","enum":["BALANCE","BALANCE_PLUS_OPEN_ORDERS"]},"order_type":{"type":"array","nullable":true,"items":{"type":"string"},"description":"The order_type scope as sent; null when all."}}},"over_credit_limit_result":{"type":"object","properties":{"rows":{"type":"array","items":{"$ref":"#/components/schemas/over_credit_limit_row"}},"totals":{"$ref":"#/components/schemas/over_credit_limit_totals"},"pagination":{"$ref":"#/components/schemas/pagination"},"supported_formats":{"type":"array","items":{"type":"string"},"example":["json","csv"]}}},"saved_filter_fields":{"type":"object","description":"The writable fields of a saved filter (validSavedFilterCreate / _Update).","properties":{"name":{"type":"string","minLength":1,"description":"The tab's label; unique per user and `type`.","example":"Due back this week"},"type":{"type":"string","minLength":1,"description":"The table the tab belongs to. Free text, but the web reads `rental`, `sale` and `service`; `task` is stored as `service`.\n","example":"rental"},"filters":{"type":"object","description":"The table's search object, stored as sent (any keys).","additionalProperties":true,"example":{"status":["on_rent"],"period":"This Week"}},"weight":{"type":"integer","minimum":0,"description":"Sort position among the user's tabs, lowest first. Omitted on create = 0.","example":3},"replaces_default_view":{"type":"boolean","description":"The table opens on this tab instead of its built-in default tab. Omitted on create = false. The API does not keep it to one tab per table; the web unsets the others.\n","example":false}}},"saved_filter_create":{"description":"Body of a create. Any other key is refused.","allOf":[{"$ref":"#/components/schemas/saved_filter_fields"},{"type":"object","required":["name","type","filters"]}],"example":{"name":"Due back this week","type":"rental","filters":{"status":["on_rent"],"period":"This Week"},"weight":3}},"saved_filter_update":{"description":"Body of an update: at least one of these keys. `type` and any other key are refused.\n","type":"object","minProperties":1,"additionalProperties":false,"properties":{"name":{"type":"string","minLength":1,"example":"Due back next week"},"filters":{"type":"object","additionalProperties":true,"example":{"status":["on_rent"],"period":"Next Week"}},"weight":{"type":"integer","minimum":0,"example":3},"replaces_default_view":{"type":"boolean","example":false}},"example":{"name":"Due back next week"}},"saved_filter":{"description":"A stored saved_filters row.","allOf":[{"$ref":"#/components/schemas/saved_filter_fields"},{"type":"object","required":["id","org","user_id","type","name","filters","weight","replaces_default_view","created_at","updated_at"],"properties":{"id":{"type":"string","format":"uuid","readOnly":true,"example":"5a7c3e19-8b2d-4f61-a0e4-9c1d7b3f2e86"},"org":{"type":"string","format":"uuid","readOnly":true,"example":"6e1b3f8d-2a7c-4c59-8e40-1d9a5b7c3f62"},"user_id":{"type":"string","format":"uuid","readOnly":true,"description":"The owner; always the caller.","example":"2f8a6c19-5e4b-4d7a-9c03-8b1e7f6a2d54"},"created_at":{"type":"string","format":"date-time","readOnly":true,"example":"2026-03-14T09:30:00.000Z"},"updated_at":{"type":"string","format":"date-time","readOnly":true,"description":"Set by every update call, including one that changed nothing.","example":"2026-03-14T09:30:00.000Z"}}}]},"saved_filter_search_body":{"type":"object","description":"Optional. `search` and `paginate` are read; any other top-level key is ignored.\n","properties":{"search":{"type":"object","description":"The filters. Omitted, `{}` or null lists every tab. Any other key is refused.","additionalProperties":false,"properties":{"name":{"type":"string","nullable":true,"description":"Case-insensitive substring of the name. `''` or null applies no filter. `%` and `_` are not escaped, so they act as wildcards.\n","example":"due"},"type":{"type":"string","nullable":true,"description":"Exact table key; `task` is read as `service`. `''` or null applies no filter.\n","example":"rental"}}},"paginate":{"$ref":"#/components/schemas/saved_filter_paginate"}},"example":{"search":{"type":"rental"},"paginate":{"currentPage":1,"perPage":25}}},"saved_filter_paginate":{"type":"object","nullable":true,"description":"Optional page (validPaginateBody); omitted or null returns every tab. Numeric strings (\"1\") are converted.\n","required":["currentPage","perPage"],"additionalProperties":false,"properties":{"currentPage":{"type":"integer","minimum":1,"example":1},"perPage":{"type":"integer","minimum":1,"maximum":100,"example":25}}},"saved_filter_write_result":{"type":"object","required":["message","data"],"properties":{"message":{"type":"string","example":"Saved filter created successfully"},"data":{"$ref":"#/components/schemas/saved_filter"}}},"schedule_type_value":{"type":"string","enum":["FEDERAL_TAX","INTERNAL_VALUE","BANK_VALUE","TAX_VALUE","PROPERTY"]},"schedule_method":{"type":"string","enum":["STRAIGHT_LINE","DOUBLE_DECLINING_BALANCE","MACRS"]},"schedule_type":{"type":"object","required":["desc","value"],"properties":{"desc":{"type":"string","example":"Federal Tax"},"value":{"$ref":"#/components/schemas/schedule_type_value"}}},"schedule_create":{"type":"object","description":"The create body (validSchedule). Unknown keys are refused.","additionalProperties":false,"required":["name","method","type","term"],"properties":{"id":{"type":"string","format":"uuid","description":"Optional; stored as the new schedule's id (an existing one is a 409)."},"name":{"type":"string","minLength":1,"example":"Straight line 5y (internal)"},"description":{"type":"string","nullable":true},"method":{"$ref":"#/components/schemas/schedule_method"},"type":{"$ref":"#/components/schemas/schedule_type_value"},"term":{"type":"integer","minimum":1,"description":"The term in years.","example":5},"calculation_window_days":{"type":"integer","minimum":1,"default":30},"convention":{"type":"string","enum":["FULL_MONTH"],"default":"FULL_MONTH"}},"example":{"name":"Straight line 5y (internal)","method":"STRAIGHT_LINE","type":"INTERNAL_VALUE","term":5}},"schedule_update":{"type":"object","description":"The update body (validScheduleUpdate). `id` and `type` are checked by the handler rather than the schema, so leaving them out is a 400 with the handler's message. Unknown keys, including `method` and `term`, are refused.\n","additionalProperties":false,"required":["id","type"],"properties":{"id":{"type":"string","format":"uuid","example":"3f9a8b7c-6d5e-4f4a-9b3c-2d1e0f9a8b7c"},"name":{"type":"string","minLength":1},"description":{"type":"string","nullable":true},"type":{"$ref":"#/components/schemas/schedule_type_value"}},"example":{"id":"3f9a8b7c-6d5e-4f4a-9b3c-2d1e0f9a8b7c","name":"Straight line 5y (internal)","type":"INTERNAL_VALUE"}},"schedule":{"type":"object","description":"A stored depreciation_schedules row.","required":["id","org","name","description","method","type","term","calculation_window_days","convention","deleted","created_at","updated_at"],"properties":{"id":{"type":"string","format":"uuid"},"org":{"type":"string","format":"uuid","readOnly":true},"name":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"method":{"type":"string","description":"One of the schedule_method values on anything written through this API."},"type":{"type":"string","description":"One of the schedule_type_value values on anything written through this API."},"term":{"type":"number","description":"The term in years (a real column). Rows not written through this API can hold 0.\n"},"calculation_window_days":{"type":"integer"},"convention":{"type":"string","example":"FULL_MONTH"},"deleted":{"type":"boolean","readOnly":true},"created_at":{"type":"string","format":"date-time","readOnly":true},"updated_at":{"type":"string","format":"date-time","readOnly":true,"description":"Not advanced by an update made through this API."}}},"mobile_log_metric":{"type":"object","description":"One performance sample. Keys other than these are accepted and stored with the metric.\n","required":["type","size_bytes","duration_ms","throughput_kbps","timestamp"],"properties":{"type":{"type":"string","description":"What was measured.","enum":["media_upload","media_download","inspection_save","task_save","api_fetch"],"example":"media_upload"},"entity_id":{"type":"string","format":"uuid","description":"The related record (an inspection, task or media id). Optional, but `null` is refused.\n","example":"4c2e9a71-8d3b-4f60-9e15-2a7b6c0d8f13"},"size_bytes":{"type":"number","exclusiveMinimum":true,"minimum":0,"description":"Payload size in bytes; any number above 0.","example":5242880},"duration_ms":{"type":"number","exclusiveMinimum":true,"minimum":0,"description":"How long the operation took, in milliseconds; any number above 0.","example":3200},"throughput_kbps":{"type":"number","exclusiveMinimum":true,"minimum":0,"description":"Size over time, in kilobits per second, as the client computed it.","example":13107.2},"timestamp":{"type":"string","format":"date-time","description":"When the sample was taken. Any value Joi parses as a date.","example":"2026-03-14T09:30:05.000Z"}}},"mobile_log_fields":{"type":"object","description":"The writable fields of a device log (validMobileLog).","properties":{"timestamp":{"type":"string","format":"date-time","description":"When the error happened on the device. Any string Joi parses as a date; the raw string is written, so one without a zone is read in the database's time zone.\n","example":"2026-03-14T09:30:00.000Z"},"api_env":{"type":"string","enum":["staging","production","non-production"],"description":"The API environment the client was pointed at.","example":"production"},"client_type":{"type":"string","enum":["mobile","web"],"description":"Who sent the log. Omitted = `mobile` (the column default).","example":"mobile"},"device_information":{"type":"object","description":"The device. Every key is required; other keys are stored too.","required":["model","os","os_version","app_version"],"properties":{"model":{"type":"string","example":"Pixel 8"},"os":{"type":"string","example":"Android"},"os_version":{"type":"string","example":"15"},"app_version":{"type":"string","example":"4.2.0"}}},"error_information":{"type":"object","description":"What went wrong. Required on every log, with its `message`, even one that only carries `metrics`. Other keys are stored too.\n","required":["message"],"properties":{"code":{"type":"number","description":"An HTTP or app error code. A numeric string passes and is stored as sent.","example":500},"message":{"type":"string","example":"Failed to upload inspection media"},"context":{"type":"string","description":"Free text; `''` is allowed.","example":"Upload timed out after 30 s"}}},"api_endpoint_information":{"type":"object","description":"The request that failed. Optional, but when sent all three keys are required; other keys are stored too.\n","required":["endpoint","request_body","query_params"],"properties":{"endpoint":{"type":"string","example":"/v1/media/inspection/upload"},"request_body":{"type":"object","additionalProperties":true,"example":{"file_name":"inspection_photo.jpg"}},"query_params":{"type":"object","additionalProperties":true,"example":{"retry":"true"}}}},"metrics":{"type":"array","description":"Performance samples. Optional, but when sent at least one.","minItems":1,"items":{"$ref":"#/components/schemas/mobile_log_metric"}}}},"mobile_log_create":{"description":"Body of a log create. Top-level keys other than these are accepted and dropped, not refused.\n","allOf":[{"$ref":"#/components/schemas/mobile_log_fields"},{"type":"object","required":["timestamp","api_env","device_information","error_information"]}],"example":{"timestamp":"2026-03-14T09:30:00.000Z","api_env":"production","client_type":"mobile","device_information":{"model":"Pixel 8","os":"Android","os_version":"15","app_version":"4.2.0"},"error_information":{"code":500,"message":"Failed to upload inspection media","context":"Upload timed out after 30 s"},"api_endpoint_information":{"endpoint":"/v1/media/inspection/upload","request_body":{"file_name":"inspection_photo.jpg"},"query_params":{"retry":"true"}},"metrics":[{"type":"media_upload","entity_id":"4c2e9a71-8d3b-4f60-9e15-2a7b6c0d8f13","size_bytes":5242880,"duration_ms":3200,"throughput_kbps":13107.2,"timestamp":"2026-03-14T09:30:05.000Z"}]}},"mobile_log":{"type":"object","description":"A stored mobile_device_logs row, as the search returns it.","required":["id","user","timestamp","api_env","client_type","device_information","error_information","api_endpoint_information","org","metrics"],"properties":{"id":{"type":"string","format":"uuid","readOnly":true,"example":"9b4d2e71-3c5a-4f8e-b061-7d2a9c4e1f35"},"user":{"type":"string","format":"uuid","readOnly":true,"description":"The user who sent the log (from the session).","example":"2f8a6c19-5e4b-4d7a-9c03-8b1e7f6a2d54"},"org":{"type":"string","format":"uuid","readOnly":true,"description":"The sender's org (from the session).","example":"6e1b3f8d-2a7c-4c59-8e40-1d9a5b7c3f62"},"timestamp":{"type":"string","format":"date-time","example":"2026-03-14T09:30:00.000Z"},"api_env":{"type":"string","enum":["staging","production","non-production"],"example":"production"},"client_type":{"type":"string","enum":["mobile","web"],"example":"mobile"},"device_information":{"type":"object","description":"As sent, extra keys included.","additionalProperties":true,"example":{"model":"Pixel 8","os":"Android","os_version":"15","app_version":"4.2.0"}},"error_information":{"type":"object","description":"As sent, extra keys included.","additionalProperties":true,"example":{"code":500,"message":"Failed to upload inspection media","context":"Upload timed out after 30 s"}},"api_endpoint_information":{"type":"object","description":"As sent; when it was omitted, the column default with all three keys null.\n","properties":{"endpoint":{"type":"string","nullable":true},"request_body":{"type":"object","nullable":true,"additionalProperties":true},"query_params":{"type":"object","nullable":true,"additionalProperties":true}},"additionalProperties":true,"example":{"endpoint":"/v1/media/inspection/upload","request_body":{"file_name":"inspection_photo.jpg"},"query_params":{"retry":"true"}}},"metrics":{"description":"The samples as sent. Known issue: when `metrics` was omitted, the column default is the JSON string \"[]\", not an empty array.\n","oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/mobile_log_metric"}},{"type":"string","enum":["[]"]}],"example":[{"type":"media_upload","entity_id":"4c2e9a71-8d3b-4f60-9e15-2a7b6c0d8f13","size_bytes":5242880,"duration_ms":3200,"throughput_kbps":13107.2,"timestamp":"2026-03-14T09:30:05.000Z"}]}}},"mobile_log_search_body":{"type":"object","required":["search","paginate"],"description":"`search` and `paginate` are read; any other top-level key is ignored. Keys inside `search` other than these are refused.\n","properties":{"search":{"type":"object","description":"The filters (validErrorLogsSearch). `{}` matches every log of the org. A string filter can't be `''`. The five device and endpoint filters are exact, case-sensitive matches on the stored JSON text.\n","additionalProperties":false,"properties":{"from":{"type":"string","format":"date-time","description":"Logs with `timestamp` at or after this. Any value Joi parses as a date.","example":"2026-03-01T00:00:00Z"},"to":{"type":"string","format":"date-time","description":"Logs with `timestamp` at or before this.","example":"2026-03-31T23:59:59Z"},"api_env":{"type":"array","description":"Any of these environments. `[]` applies no filter.","items":{"type":"string","enum":["staging","production","non-production"]},"example":["production"]},"user":{"type":"string","format":"uuid","description":"The user who sent the log.","example":"2f8a6c19-5e4b-4d7a-9c03-8b1e7f6a2d54"},"os_version":{"type":"string","description":"Exact match on `device_information.os_version`.","example":"15"},"app_version":{"type":"string","description":"Exact match on `device_information.app_version`.","example":"4.2.0"},"os":{"type":"string","description":"Exact match on `device_information.os`.","example":"Android"},"model":{"type":"string","description":"Exact match on `device_information.model`.","example":"Pixel 8"},"endpoint":{"type":"string","description":"Exact match on `api_endpoint_information.endpoint`.","example":"/v1/media/inspection/upload"}}},"paginate":{"$ref":"#/components/schemas/mobile_log_paginate"}},"example":{"search":{"from":"2026-03-01T00:00:00Z","to":"2026-03-31T23:59:59Z","api_env":["production"]},"paginate":{"currentPage":1,"perPage":20}}},"mobile_log_paginate":{"type":"object","description":"Which page to return (validPaginateBody). Numeric strings (\"1\") are converted.\n","required":["currentPage","perPage"],"additionalProperties":false,"properties":{"currentPage":{"type":"integer","minimum":1,"example":1},"perPage":{"type":"integer","minimum":1,"maximum":100,"example":20}}},"mobile_log_search_result":{"type":"object","required":["data","pagination"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/mobile_log"}},"pagination":{"$ref":"#/components/schemas/pagination"}}},"signnow_user":{"type":"object","description":"SignNow's user object. Nested objects are SignNow's and are not described here.\n","required":["id","first_name","last_name","primary_email","emails"],"additionalProperties":true,"properties":{"id":{"type":"string","description":"SignNow's id of the account.","example":"8f3a1c6e2b9d4f7a0e5c1b8d3a6f9e2c4b7d0a13"},"first_name":{"type":"string","example":"Jordan"},"last_name":{"type":"string","example":"Avery"},"active":{"type":"string","description":"\"1\" when the account is active.","example":"1"},"verified":{"type":"boolean","example":true},"created":{"type":"string","description":"Unix seconds, as a string.","example":"1714056000"},"primary_email":{"type":"string","example":"esign@example.com"},"emails":{"type":"array","items":{"type":"string"},"example":["esign@example.com"]},"is_logged_in":{"type":"boolean","example":true},"credits":{"type":"number","example":0},"document_count":{"type":"number","example":42},"lifetime_document_count":{"type":"number","example":1310},"monthly_document_count":{"type":"string","example":"57"},"locale":{"type":"string","example":"en-US"},"subscriptions":{"type":"array","description":"The account's SignNow plans and seats.","items":{"type":"object","additionalProperties":true}},"premium_access":{"type":"object","description":"Plan and API-access details, as SignNow returns them.","additionalProperties":true},"billing_period":{"type":"object","additionalProperties":true},"teams":{"type":"array","items":{"type":"object","additionalProperties":true}},"settings":{"type":"object","description":"SignNow account settings, keyed by setting name.","additionalProperties":true},"organization":{"type":"object","additionalProperties":true}}},"item_type":{"type":"object","description":"A `types` row. Read-only through the API; there is no write route.","required":["id","name","type"],"properties":{"id":{"type":"string","format":"uuid","readOnly":true,"example":"3d9f1a7c-6b2e-4c84-9a15-e07b2c5d8f41"},"name":{"type":"string","nullable":true,"readOnly":true,"description":"Display name, stored as entered (some carry a leading space). Not unique: the same name can appear twice in one group or in two groups, so key rows by `id`.\n","example":"Hour meter"},"type":{"type":"string","nullable":true,"readOnly":true,"description":"The group the type belongs to.","example":"rental"}}},"user_fields":{"type":"object","description":"The user columns a client may write. user_create and user_update add their own keys on top; the stored record (user) adds the server-set columns.\n","properties":{"username":{"type":"string","format":"email","nullable":true,"description":"The email the user signs in with and receives the password-setup link at. Unique across all orgs among live users (exact match). '' for a phone-only user.\n","example":"jane.doe@example.com"},"name":{"type":"string","nullable":true,"description":"Display name. '' or null allowed.","example":"Jane Doe"},"phone":{"type":"string","nullable":true,"pattern":"^\\+[1-9]\\d{1,14}$","description":"E.164 mobile number, the SMS-code (OTP) login key. Unique across all orgs among live users. '' or null allowed.\n","example":"+15555550123"},"preferences":{"$ref":"#/components/schemas/user_preference_settings"},"roles":{"type":"array","description":"Role ids. All of them must have the user's type - `cust` roles for a customer user (customer_id set), `org` roles otherwise. A newly added role must also be a live role of this org or a global default (a customer-specific role only on that customer's users), and must not belong to a module the org has not enabled, else 400 (\"Invalid role\" / \"The following roles belong to modules that are not enabled for this organization: ...\").\n","items":{"type":"string","format":"uuid"},"example":["6a1f0c3e-2b7d-4f8a-9e15-0c3d5b7a9e21"]},"customer_id":{"type":"string","format":"uuid","nullable":true,"description":"Set for a customer-portal user (the customer they belong to), null for a staff user. Must be a customer of the caller's org (404 \"Customer not found\"); a customer-portal session's own customer is always used instead.\n","example":null},"override_pin":{"type":"string","nullable":true,"pattern":"^[0-9]{6}$","writeOnly":true,"description":"Saved 6 digit pin used instead of the SMS code on Inspect-app login. Write-only: never returned, only has_override_pin. '' or null clears it; leaving the key out keeps the stored pin. Any other value than 6 digits is refused (\"Override pin must be exactly 6 digits\").\n","example":"482913"}}},"user_create":{"description":"Body of POST /platform/users. Unknown keys (e.g. is_service, deleted) are refused with 400 (\"x\" is not allowed). No single key is required, but at least one of a valid email `username` or a valid E.164 `phone` must be sent.\n","allOf":[{"$ref":"#/components/schemas/user_fields"},{"type":"object","properties":{"password":{"type":"string","nullable":true,"writeOnly":true,"maxLength":128,"description":"Optional initial password: at least 8 characters with one uppercase letter, one digit and one special character. '' or null (or leaving it out) stores a random password; the user then sets theirs through the emailed link.\n"},"org":{"type":"string","format":"uuid","nullable":true,"description":"Accepted, but always replaced by the session org."}}}]},"user_update":{"description":"Body of POST /platform/users/{id}. Every key is optional and only sent keys are written; at least one key is needed (\"Nothing to update\"). Unknown keys - password and org included - are refused with 400 (\"x\" is not allowed).\n","allOf":[{"$ref":"#/components/schemas/user_fields"},{"type":"object","properties":{"username":{"type":"string","format":"email","nullable":true,"description":"Only accepted while the user has no username (400 \"User already has a username set\" otherwise, even for the same value). Setting one resets the password to a random value and emails a 15-minute password-setup link to it. '' or null is stored as sent and removes the username.\n","example":"jane.doe@example.com"}}}]},"user_self_update":{"type":"object","description":"Body of POST /platform/users/self. Only these three keys are accepted; any other is refused with 400 (\"x\" is not allowed). At least one key is needed (\"Nothing to update\").\n","properties":{"name":{"type":"string","nullable":true,"example":"Jane Doe"},"phone":{"type":"string","nullable":true,"pattern":"^\\+[1-9]\\d{1,14}$","description":"E.164 number, unique across all orgs among live users (409).","example":"+15555550123"},"preferences":{"$ref":"#/components/schemas/user_preference_settings"}}},"user":{"description":"A users row as GET/POST /platform/users/self and POST /platform/users/{id} return it: every column except password and override_pin. readOnly columns are set by the server. `preferences` is merged with the shipped defaults only by GET /platform/users/self; the POST answers return it as stored, which is null for a user who never saved any and may lack keys added to the defaults since it was saved.\n","allOf":[{"$ref":"#/components/schemas/user_fields"},{"type":"object","properties":{"id":{"type":"string","format":"uuid","readOnly":true},"org":{"type":"string","format":"uuid","readOnly":true,"description":"The organization the user belongs to."},"deleted":{"type":"boolean","readOnly":true,"description":"Soft-delete flag."},"mobile_log_2xx":{"type":"boolean","nullable":true,"readOnly":true,"description":"Whether the mobile app logs its successful (2xx) API calls. Default true."},"is_service":{"type":"boolean","readOnly":true,"description":"True for a service-API credential's actor (never a person)."},"saved_filters_seeded_at":{"type":"string","format":"date-time","nullable":true,"readOnly":true,"description":"When the default saved filters were copied to the user; null until then."},"has_override_pin":{"type":"boolean","readOnly":true,"description":"True when the user has a saved 6 digit login pin. The pin itself is never returned."},"preferences":{"allOf":[{"$ref":"#/components/schemas/user_preference_settings"}],"nullable":true,"description":"As stored - null for a user who never saved any."}}}]},"user_role_ref":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","example":"Admin"}}},"user_customer_details":{"type":"object","nullable":true,"description":"The user's customer, looked up in the caller's org; null for a staff user (and when no customer of the org has that id). company_name is left out when it is null.\n","properties":{"id":{"type":"string","format":"uuid"},"company_name":{"type":"string","example":"Main Yard Rentals LLC"},"deleted":{"type":"boolean"}}},"user_search_row":{"type":"object","description":"One row of POST /platform/users/search - an explicit column list, so no preferences, org, password or override_pin.\n","properties":{"id":{"type":"string","format":"uuid"},"username":{"type":"string","nullable":true,"description":"'' for a phone-only user.","example":"jane.doe@example.com"},"name":{"type":"string","nullable":true,"example":"Jane Doe"},"phone":{"type":"string","nullable":true,"example":"+15555550123"},"roles":{"type":"array","description":"The user's roles; ids that match no role are left out.","items":{"$ref":"#/components/schemas/user_role_ref"}},"customer_id":{"type":"string","format":"uuid","nullable":true},"deleted":{"type":"boolean"},"has_override_pin":{"type":"boolean"},"customer_details":{"$ref":"#/components/schemas/user_customer_details"}}},"user_detail":{"description":"GET /platform/users/{id} - a search row plus the user's preferences, merged with the shipped defaults.","allOf":[{"$ref":"#/components/schemas/user_search_row"},{"type":"object","properties":{"preferences":{"$ref":"#/components/schemas/user_preference_settings"}}}]},"user_search_body":{"type":"object","required":["search","paginate"],"properties":{"search":{"type":"object","description":"Every filter is optional; null or '' means no filter. Unknown keys are refused (400).\n","properties":{"name":{"type":"string","nullable":true,"description":"Case-insensitive substring of the name OR the username.","example":"jane"},"permissions":{"type":"array","description":"Only users holding ANY of these permission names through their roles.","items":{"type":"string"},"example":["ALLOW_ASSIGNEE_OWNERSHIP"]},"type":{"type":"string","nullable":true,"description":"customer = customer-portal users only (customer_id set), org = staff users only (customer_id null). Any other string is accepted and filters nothing.\n","example":"org"},"customer":{"type":"string","format":"uuid","nullable":true,"description":"Only the users of this customer."},"deleted":{"type":"boolean","nullable":true,"description":"true = deleted users only, false = live users only, left out = both.","example":false},"tasks_assigned":{"type":"boolean","nullable":true,"description":"true = users assigned to at least one non-deleted work order or inspection (any status, done included), false = users assigned to none.\n"},"order_by":{"description":"Sortable columns - username, name, phone, deleted (the user id breaks ties). Without it the list is sorted by the upper-cased username only.\n","allOf":[{"$ref":"#/components/schemas/order_by"}]}}},"paginate":{"type":"object","required":["currentPage","perPage"],"properties":{"currentPage":{"type":"integer","minimum":1,"example":1},"perPage":{"type":"integer","minimum":1,"maximum":100,"example":15}}}},"example":{"search":{"name":"","type":null,"customer":null,"deleted":false,"order_by":[{"column":"name","order":"asc"}]},"paginate":{"currentPage":1,"perPage":15}}},"user_search_result":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/user_search_row"}},"pagination":{"description":"perPage, currentPage, from and to are always present; total, lastPage, prevPage and nextPage only on page 1.\n","allOf":[{"$ref":"#/components/schemas/lotus_order_pagination"}]},"orderByColumnNamesList":{"allOf":[{"$ref":"#/components/schemas/orderByColumnNamesList"}],"example":["username","name","phone","deleted"]}}},"user_notification_setting":{"type":"object","properties":{"notification_key":{"type":"string","example":"quote"},"notification_name":{"type":"string","example":"New Quotes"},"notification_group":{"type":"string","description":"Rentals, Service, Service.Independent, Service.Purchase, Service.Rental, Service.Sale, Invoicing or System.","example":"Rentals"},"email_active":{"type":"boolean","description":"Whether the user receives this notification's emails; false when never set."},"is_overridden":{"type":"boolean","description":"True when the user has a saved setting for this key (on or off)."},"can_edit":{"type":"boolean","description":"Whether the user may switch it ON - true when the user holds the row's required permission, or the row needs none. Switching off is always allowed.\n"}}},"user_notification_setting_update":{"type":"object","description":"Unknown keys are refused (400).","required":["notification_key"],"properties":{"notification_key":{"type":"string","description":"A key of the catalogue (GET /platform/users/notifications/search), else 400.","example":"quote"},"email_active":{"type":"boolean","description":"Left out, the setting is written as false (switched off).","example":true}}},"user_preference_settings":{"type":"object","description":"The user's UI preferences (users.preferences, jsonb). On write it is validated as a whole and stored as a whole: `columnSettings` is required whenever preferences is sent, and no object at any level accepts keys beyond the ones listed here (\"x\" is not allowed) - only `filters[].value` is free. Reads through GET /platform/users/self, GET /platform/users/{id} and the login session (userPreferences of POST /login, /otp/login and /login/refresh) fill missing keys from the shipped defaults (utils/data/defaults/default_user_preferences.json).\n","required":["columnSettings"],"properties":{"rentalOrderCreation":{"type":"object","description":"Defaults of the rental order creation screen. Every key required when the object is sent.","required":["revenueLookBackPeriod","hourLookBackPeriod","quoteSort","hideNonCompliantCustomer","hideBannedCustomers","showInStockAssets","hideUnavailableAssets","hideNonValidatedAssets"],"properties":{"revenueLookBackPeriod":{"type":"string","enum":["all","m12","m6","m3"],"example":"m6"},"hourLookBackPeriod":{"type":"string","enum":["all","m12","m6","m3"],"example":"m6"},"quoteSort":{"type":"string","enum":["revenue","hour"],"example":"hour"},"hideNonCompliantCustomer":{"type":"boolean","example":true},"hideBannedCustomers":{"type":"boolean","example":true},"showInStockAssets":{"type":"boolean","example":true},"hideUnavailableAssets":{"type":"boolean","example":true},"hideNonValidatedAssets":{"type":"boolean","example":false}}},"filters":{"type":"array","description":"The filter chips shown on the list screens, one per filter key and group.","items":{"$ref":"#/components/schemas/user_preference_filter"}},"assetFilters":{"type":"object","description":"Saved asset-list filter values (all optional).","properties":{"on_rent":{"type":"boolean","nullable":true},"out_of_service":{"type":"boolean","nullable":true},"asset_number":{"type":"string","nullable":true},"show_sibling_assets":{"type":"boolean","nullable":true},"category":{"type":"array","nullable":true,"items":{"type":"string","format":"uuid"}},"add_on":{"type":"boolean","nullable":true},"facility":{"type":"string","format":"uuid","nullable":true,"description":"A facility id, or ''."},"validation_failure":{"type":"array","nullable":true,"items":{"type":"string"}},"show_sold":{"type":"boolean","nullable":true},"purchase_order":{"description":"A purchase order id, true, false or ''.","oneOf":[{"type":"string","nullable":true},{"type":"boolean"}]},"transaction_group":{"type":"array","nullable":true,"items":{"type":"string","enum":["sale","rental"]}},"type":{"type":"array","nullable":true,"items":{"type":"string","enum":["ADD_ON","NORMAL"]}},"transaction_group_exact_match":{"type":"boolean","nullable":true},"checklist_status":{"type":"array","nullable":true,"items":{"type":"string","enum":["onboarding","onboarded","selling","sale_approved"]}},"name":{"type":"string","nullable":true},"stock":{"type":"boolean","nullable":true},"zero_cost_rentals":{"type":"boolean","nullable":true},"manufacturer":{"type":"string","nullable":true},"sales_customer":{"type":"string","format":"uuid","nullable":true},"deleted":{"type":"boolean","nullable":true},"serial_number":{"description":"A serial number, '' or false.","oneOf":[{"type":"string","nullable":true},{"type":"boolean"}]}}},"transactionsPage":{"type":"object","description":"Which tabs and statuses the transactions page shows. Every key required when the object is sent.","required":["showQuotesTab","showReservationsTab","showOpenContractsTab","showClosedContractsTab","showCompletedContractsTab","showInvoicesTab","showCreateQuoteButton"],"properties":{"showQuotesTab":{"type":"boolean","example":true},"showReservationsTab":{"type":"boolean","example":true},"showOpenContractsTab":{"type":"boolean","example":true},"showClosedContractsTab":{"type":"boolean","example":true},"showCompletedContractsTab":{"type":"boolean","example":true},"showInvoicesTab":{"type":"boolean","example":true},"showCreateQuoteButton":{"type":"boolean","example":true},"showStatus":{"type":"object","required":["offRent","inspection","released"],"properties":{"offRent":{"type":"boolean","example":true},"inspection":{"type":"boolean","example":true},"released":{"type":"boolean","example":true}}}}},"taskFilters":{"type":"object","description":"Saved task-list filter values. Only showTasksWithinDays is required when the object is sent.","required":["showTasksWithinDays"],"properties":{"showTasksWithinDays":{"type":"number","example":3},"priority":{"type":"integer","nullable":true,"enum":[0,1,null]},"type":{"type":"array","nullable":true,"items":{"type":"string","enum":["work_order","dispatch_inspection","off_rent_inspection","sale_inspection","independent_inspection"]}},"status":{"type":"array","nullable":true,"items":{"type":"string","enum":["pending","ready_for_inspection","partially_inspected","ready","in_progress","paused","work_completed","done"]}},"category":{"type":"array","nullable":true,"items":{"type":"string","format":"uuid"}},"due_date":{"type":"string","nullable":true,"description":"A date, or the strings 'null' / 'not_null'."},"assignee":{"type":"string","nullable":true,"description":"A user id, 'unassigned', 'anyone' or ''."},"asset_id":{"type":"string","format":"uuid","nullable":true},"task_level":{"type":"array","nullable":true,"description":"Ids, or the strings 'null' / 'not_null'.","items":{"type":"string"}},"asset_number":{"type":"string","nullable":true},"manufacturer":{"type":"string","nullable":true},"model_number":{"type":"string","nullable":true},"customer_name":{"type":"string","nullable":true},"customer_number":{"type":"string","nullable":true},"order_number":{"type":"string","nullable":true},"skip_days_limit":{"type":"boolean","nullable":true},"off_site":{"type":"boolean","nullable":true},"out_of_service":{"type":"boolean","nullable":true},"deleted":{"type":"boolean","nullable":true},"tasks_with_inspections":{"type":"boolean","nullable":true},"logical_operation":{"type":"string","nullable":true,"enum":["or","and",null]},"completed_from":{"type":"string","format":"date-time","nullable":true},"completed_till":{"type":"string","format":"date-time","nullable":true},"is_recurring":{"type":"boolean","nullable":true},"maintenance_trigger":{"type":"string","nullable":true,"enum":["HOUR_METER","CALENDAR_CADENCE",null]},"date_from":{"type":"string","format":"date-time","nullable":true},"date_to":{"type":"string","format":"date-time","nullable":true}}},"poFilters":{"type":"object","properties":{"state":{"type":"array","items":{"type":"string","enum":["new","released","partially_received","received","closed"]}}}},"partFilters":{"type":"object","properties":{"type":{"type":"array","items":{"type":"string","enum":["supplies","inventoried","non_inventoried"]}}}},"dashboardWidgets":{"type":"object","description":"Layout of the dashboard widgets (all keys optional).","properties":{"activeAssets":{"allOf":[{"$ref":"#/components/schemas/user_preference_dashboard_widget"},{"type":"object","properties":{"filterDefaults":{"type":"object","properties":{"status":{"type":"array","items":{"type":"string","enum":["dispatched","off_rent","off_rent_inspection","released"]}},"due":{"type":"boolean","nullable":true}}}}}]},"onOrderAssets":{"$ref":"#/components/schemas/user_preference_dashboard_widget"},"onOrderParts":{"$ref":"#/components/schemas/user_preference_dashboard_widget"},"invoices":{"allOf":[{"$ref":"#/components/schemas/user_preference_dashboard_widget"},{"type":"object","properties":{"filterDefaults":{"type":"object","properties":{"payment_status":{"type":"string","nullable":true,"enum":["unpaid","paid","",null]}}}}}]},"unassignedPastDueTasks":{"$ref":"#/components/schemas/user_preference_dashboard_widget"},"myPendingAssets":{"$ref":"#/components/schemas/user_preference_dashboard_widget"}}},"reports":{"type":"object","properties":{"dailySummaryReport":{"type":"object","description":"What the user's Daily Summary Report email shows (all keys optional).","properties":{"hideEmptySections":{"type":"boolean","example":false},"highLevelSummary":{"$ref":"#/components/schemas/user_preference_report_section"},"salesDesk":{"type":"object","properties":{"hidden":{"type":"boolean"},"sections":{"type":"object","properties":{"newQuotes":{"$ref":"#/components/schemas/user_preference_report_section"},"newOpenSalesOrders":{"$ref":"#/components/schemas/user_preference_report_section"},"newClosedSalesOrders":{"$ref":"#/components/schemas/user_preference_report_section"}}}}},"rentalDesk":{"type":"object","properties":{"hidden":{"type":"boolean"},"sections":{"type":"object","properties":{"newQuotes":{"$ref":"#/components/schemas/user_preference_report_section"},"newReservations":{"$ref":"#/components/schemas/user_preference_report_section"},"newOpenContracts":{"$ref":"#/components/schemas/user_preference_report_section"},"newClosedContracts":{"$ref":"#/components/schemas/user_preference_report_section"}}}}},"serviceDesk":{"type":"object","properties":{"hidden":{"type":"boolean"},"showDueNull":{"type":"boolean","description":"Retired - still accepted, no longer used by the report."},"sections":{"type":"object","properties":{"tasks":{"$ref":"#/components/schemas/user_preference_report_section"},"dispatchInspections":{"$ref":"#/components/schemas/user_preference_report_section"},"completedDispatchInspections":{"$ref":"#/components/schemas/user_preference_report_section"},"offRentInspections":{"$ref":"#/components/schemas/user_preference_report_section"},"completedOffRentInspections":{"$ref":"#/components/schemas/user_preference_report_section"}}}}}}}}},"inspectionSavePreferences":{"type":"object","properties":{"markReadyForInspection":{"type":"boolean","example":false},"setForAllSiblings":{"type":"boolean","example":false}}},"columnSettings":{"type":"object","description":"Column visibility of four list pages. All four pages are required, each page must list exactly its columns below, and each column's columnName must be the fixed label shown.\n","required":["pendingAssetPage","assetPage","customerPage","servicePage"],"properties":{"pendingAssetPage":{"type":"object","required":["salesInfo","id","name","category","checklistProcess","mediaCount","modelYear","sellPrice","hourMeter"],"properties":{"salesInfo":{"$ref":"#/components/schemas/user_preference_column"},"id":{"$ref":"#/components/schemas/user_preference_column"},"name":{"$ref":"#/components/schemas/user_preference_column"},"category":{"$ref":"#/components/schemas/user_preference_column"},"checklistProcess":{"$ref":"#/components/schemas/user_preference_column"},"mediaCount":{"$ref":"#/components/schemas/user_preference_column"},"modelYear":{"$ref":"#/components/schemas/user_preference_column"},"sellPrice":{"$ref":"#/components/schemas/user_preference_column"},"hourMeter":{"$ref":"#/components/schemas/user_preference_column"}},"description":"Labels - salesInfo \"Sales Info\", id \"ID\", name \"Name\", category \"Category\", checklistProcess \"Checklist Process\", mediaCount \"Media Count\", modelYear \"Year\", sellPrice \"Sell Price\", hourMeter \"Hours\".\n"},"assetPage":{"type":"object","required":["id","name","category","checklistProcess","mediaCount","modelYear","sellPrice","hourMeter"],"properties":{"id":{"$ref":"#/components/schemas/user_preference_column"},"name":{"$ref":"#/components/schemas/user_preference_column"},"category":{"$ref":"#/components/schemas/user_preference_column"},"checklistProcess":{"$ref":"#/components/schemas/user_preference_column"},"mediaCount":{"$ref":"#/components/schemas/user_preference_column"},"modelYear":{"$ref":"#/components/schemas/user_preference_column"},"sellPrice":{"$ref":"#/components/schemas/user_preference_column"},"hourMeter":{"$ref":"#/components/schemas/user_preference_column"}},"description":"Same labels as pendingAssetPage, without salesInfo."},"customerPage":{"type":"object","required":["id","salesPerson","name"],"properties":{"id":{"$ref":"#/components/schemas/user_preference_column"},"salesPerson":{"$ref":"#/components/schemas/user_preference_column"},"name":{"$ref":"#/components/schemas/user_preference_column"}},"description":"Labels - id \"ID\", salesPerson \"Sales Person\", name \"Name\"."},"servicePage":{"type":"object","required":["priority","type","status","startDate","workTime","dueBy","assignee","asset","customer","orderNumber","dueAmount","notes"],"properties":{"priority":{"$ref":"#/components/schemas/user_preference_column"},"type":{"$ref":"#/components/schemas/user_preference_column"},"status":{"$ref":"#/components/schemas/user_preference_column"},"startDate":{"$ref":"#/components/schemas/user_preference_column"},"workTime":{"$ref":"#/components/schemas/user_preference_column"},"dueBy":{"$ref":"#/components/schemas/user_preference_column"},"assignee":{"$ref":"#/components/schemas/user_preference_column"},"asset":{"$ref":"#/components/schemas/user_preference_column"},"customer":{"$ref":"#/components/schemas/user_preference_column"},"orderNumber":{"$ref":"#/components/schemas/user_preference_column"},"dueAmount":{"$ref":"#/components/schemas/user_preference_column"},"notes":{"$ref":"#/components/schemas/user_preference_column"}},"description":"Labels - priority \"Priority\", type \"Type\", status \"Status\", startDate \"Start Date\", workTime \"Work Time\", dueBy \"Due By\", assignee \"Assignee\", asset \"Asset\", customer \"Customer\", orderNumber \"Order Number\", dueAmount \"Due Amount\", notes \"Notes\".\n"}},"example":{"pendingAssetPage":{"salesInfo":{"columnName":"Sales Info","visible":true},"id":{"columnName":"ID","visible":true},"name":{"columnName":"Name","visible":true},"category":{"columnName":"Category","visible":true},"checklistProcess":{"columnName":"Checklist Process","visible":true},"mediaCount":{"columnName":"Media Count","visible":true},"modelYear":{"columnName":"Year","visible":true},"sellPrice":{"columnName":"Sell Price","visible":true},"hourMeter":{"columnName":"Hours","visible":true}},"assetPage":{"id":{"columnName":"ID","visible":true},"name":{"columnName":"Name","visible":true},"category":{"columnName":"Category","visible":true},"checklistProcess":{"columnName":"Checklist Process","visible":true},"mediaCount":{"columnName":"Media Count","visible":true},"modelYear":{"columnName":"Year","visible":true},"sellPrice":{"columnName":"Sell Price","visible":false},"hourMeter":{"columnName":"Hours","visible":true}},"customerPage":{"id":{"columnName":"ID","visible":true},"salesPerson":{"columnName":"Sales Person","visible":true},"name":{"columnName":"Name","visible":true}},"servicePage":{"priority":{"columnName":"Priority","visible":true},"type":{"columnName":"Type","visible":true},"status":{"columnName":"Status","visible":true},"startDate":{"columnName":"Start Date","visible":true},"workTime":{"columnName":"Work Time","visible":true},"dueBy":{"columnName":"Due By","visible":true},"assignee":{"columnName":"Assignee","visible":true},"asset":{"columnName":"Asset","visible":true},"customer":{"columnName":"Customer","visible":true},"orderNumber":{"columnName":"Order Number","visible":true},"dueAmount":{"columnName":"Due Amount","visible":true},"notes":{"columnName":"Notes","visible":true}}}}}},"user_preference_filter":{"type":"object","description":"One filter chip. All five keys required.","required":["key","name","hidden","value","group"],"properties":{"key":{"type":"string","example":"on_rent"},"name":{"type":"string","example":"Only Show On Rent Assets"},"hidden":{"type":"boolean","example":false},"value":{"description":"The filter's value - any JSON value (null, boolean, string, array ...).","nullable":true,"example":null},"group":{"type":"string","description":"The preferences key the filter belongs to - assetFilters, rentalOrderCreation, taskFilters, poFilters or partFilters.","example":"assetFilters"}}},"user_preference_column":{"type":"object","description":"One list column. columnName must equal the column's fixed label (see the page's description).","required":["columnName","visible"],"properties":{"columnName":{"type":"string","example":"Sell Price"},"visible":{"type":"boolean","example":true}}},"user_preference_dashboard_widget":{"type":"object","description":"One dashboard widget (all keys optional). filterDefaults takes no keys except on activeAssets (status, due) and invoices (payment_status).\n","properties":{"hidden":{"type":"boolean","example":false},"weight":{"type":"integer","description":"Sort order, ascending.","example":10},"rowLimit":{"type":"integer","example":10},"filterDefaults":{"type":"object"}}},"user_preference_report_section":{"type":"object","properties":{"hidden":{"type":"boolean","example":false}}},"vendor_contact_input":{"type":"object","description":"A contact as the create and update bodies take it. Any other key (a stored row's `deleted`, `vendor` or `org`) is refused, so a GET contact can't be sent back as is.\n","additionalProperties":false,"properties":{"id":{"type":"string","format":"uuid","description":"Update: one of this vendor's live contacts; omit for a new contact. Create: stored as the new contact's id.\n"},"title":{"type":"string","nullable":true,"example":"Accounts payable"},"phone":{"type":"string","nullable":true,"example":"555-0100"},"email":{"type":"string","format":"email","description":"Optional, but when sent it must be an email (null and '' are refused).","example":"ap@swiftsupply.example.com"}}},"vendor_contact":{"type":"object","description":"A stored vendor_contacts row.","required":["id","title","phone","email","deleted","vendor","org"],"properties":{"id":{"type":"string","format":"uuid","readOnly":true},"title":{"type":"string","nullable":true},"phone":{"type":"string","nullable":true},"email":{"type":"string","nullable":true},"deleted":{"type":"boolean","nullable":true,"readOnly":true},"vendor":{"type":"string","format":"uuid","nullable":true,"readOnly":true},"org":{"type":"string","format":"uuid","nullable":true,"readOnly":true}}},"vendor_fields":{"type":"object","description":"The writable vendor columns, plus the contacts written with the vendor.","properties":{"name":{"type":"string","minLength":1,"example":"Swift Supply Co."},"description":{"type":"string","nullable":true},"primary_address":{"type":"string","nullable":true,"example":"12 Harbor Rd."},"secondary_address":{"type":"string","nullable":true},"city":{"type":"string","nullable":true,"example":"Springfield"},"state":{"type":"string","nullable":true,"example":"IL"},"zip":{"type":"string","nullable":true,"example":"62701"},"website_url":{"type":"string","nullable":true},"notes":{"type":"string","nullable":true},"contacts":{"type":"array","items":{"$ref":"#/components/schemas/vendor_contact_input"}},"default_contact":{"type":"string","format":"uuid","nullable":true,"description":"The vendor's main contact (a vendor_contacts id). See the create and update descriptions for how the server sets it.\n"},"id":{"type":"string","format":"uuid","description":"Create: stored as the new vendor's id (an existing one is a 409). Update: ignored, the path id wins.\n"},"org":{"type":"string","format":"uuid","description":"Accepted and ignored; the session's org is used."}}},"vendor_create":{"description":"The create body. Unknown keys are refused (`\"<key>\" is not allowed`), so the server-set columns (vendor_number, the purchase totals, validated, validation_failures, deleted) can't be sent. Which other fields are required depends on the org's field settings (see the operation).\n","allOf":[{"$ref":"#/components/schemas/vendor_fields"},{"type":"object","required":["name"]}],"example":{"name":"Swift Supply Co.","primary_address":"12 Harbor Rd.","city":"Springfield","state":"IL","zip":"62701","contacts":[{"title":"Accounts payable","phone":"555-0100","email":"ap@swiftsupply.example.com"}]}},"vendor_update":{"description":"The update body: the create schema again, so `name` is required and unknown keys are refused. Omitted fields keep their values.\n","allOf":[{"$ref":"#/components/schemas/vendor_fields"},{"type":"object","required":["name"]}],"example":{"name":"Swift Supply Co.","primary_address":"14 Harbor Rd.","city":"Springfield","state":"IL","zip":"62701","default_contact":"37c93757-1020-4ab0-a82b-77315abe45d7","contacts":[{"id":"37c93757-1020-4ab0-a82b-77315abe45d7","title":"Accounts payable","phone":"555-0100","email":"ap@swiftsupply.example.com"},{"title":"Sales","phone":"555-0101","email":"sales@swiftsupply.example.com"}]}},"vendor_validation_entry":{"type":"object","description":"One field's check. A passing entry is `{name, valid: true, reason: null}`; a failing one is `{valid: false, reason}` with no `name`.\n","required":["valid","reason"],"properties":{"name":{"type":"string","description":"The field's label (e.g. \"Primary Address\"); absent on a failing entry."},"valid":{"type":"boolean"},"reason":{"type":"string","nullable":true,"example":"Please provide the 'city' field on vendor"}}},"vendor_validation_failures":{"type":"object","description":"Every field the vendor field settings cover, keyed by field. Only fields at \"validation\" or \"requirement\" are checked; the others always read valid.\n","required":["city","name","primary_address","secondary_address","primary_contact","state","website_url","zip"],"properties":{"city":{"$ref":"#/components/schemas/vendor_validation_entry"},"name":{"$ref":"#/components/schemas/vendor_validation_entry"},"primary_address":{"$ref":"#/components/schemas/vendor_validation_entry"},"secondary_address":{"$ref":"#/components/schemas/vendor_validation_entry"},"primary_contact":{"allOf":[{"$ref":"#/components/schemas/vendor_validation_entry"}],"description":"Fails when the vendor has no contacts, no default_contact, or a contact without a valid email (a contact sent without one is stored with a null email, which fails it). On a create or update that leaves `contacts` out only default_contact is checked. The failing reason always reads \"Please provide the 'default_contact' field on vendor\".\n"},"state":{"$ref":"#/components/schemas/vendor_validation_entry"},"website_url":{"$ref":"#/components/schemas/vendor_validation_entry"},"zip":{"$ref":"#/components/schemas/vendor_validation_entry"}}},"vendor":{"type":"object","description":"A stored vendors row.","required":["id","name","vendor_number","description","primary_address","secondary_address","city","state","zip","website_url","notes","year_to_date_purchase","last_year_purchase","total_purchase","deleted","default_contact","org","validated","validation_failures"],"properties":{"id":{"type":"string","format":"uuid","readOnly":true},"name":{"type":"string","nullable":true},"vendor_number":{"type":"integer","nullable":true,"readOnly":true,"description":"Assigned on create as the org's highest vendor_number + 1, deleted vendors included. Not unique in the database (known issue: two creates at once can get the same number).\n"},"description":{"type":"string","nullable":true},"primary_address":{"type":"string","nullable":true},"secondary_address":{"type":"string","nullable":true},"city":{"type":"string","nullable":true},"state":{"type":"string","nullable":true},"zip":{"type":"string","nullable":true},"website_url":{"type":"string","nullable":true},"notes":{"type":"string","nullable":true},"year_to_date_purchase":{"type":"number","nullable":true,"readOnly":true},"last_year_purchase":{"type":"number","nullable":true,"readOnly":true},"total_purchase":{"type":"number","nullable":true,"readOnly":true},"deleted":{"type":"boolean","nullable":true,"readOnly":true},"default_contact":{"type":"string","format":"uuid","nullable":true},"org":{"type":"string","format":"uuid","nullable":true,"readOnly":true},"validated":{"type":"boolean","nullable":true,"readOnly":true,"description":"False when any checked field fails (see validation_failures)."},"validation_failures":{"allOf":[{"$ref":"#/components/schemas/vendor_validation_failures"}],"nullable":true,"readOnly":true}}},"vendor_detail":{"description":"GET /vendors/{id}, the vendor with its live contacts.","allOf":[{"$ref":"#/components/schemas/vendor"},{"type":"object","required":["contacts"],"properties":{"contacts":{"type":"array","description":"The live contacts, in no set order; `[]` when there are none.","items":{"$ref":"#/components/schemas/vendor_contact"}}}}]},"vendor_write_result":{"description":"The create / update answer, the stored vendor plus the contacts that were sent.","allOf":[{"$ref":"#/components/schemas/vendor"},{"type":"object","properties":{"contacts":{"type":"array","description":"The sent contacts as stored; `[]` after `contacts: []`, absent when `contacts` was left out.\n","items":{"$ref":"#/components/schemas/vendor_contact"}}}}]},"vendor_search_body":{"type":"object","required":["paginate"],"description":"`search` and `paginate` are read; any other top-level key is ignored. Keys inside `search` other than these are refused.\n","properties":{"search":{"type":"object","additionalProperties":false,"properties":{"name":{"type":"string","nullable":true,"description":"Case-insensitive substring of the name (ILIKE `%name%`; a `%` or `_` in it acts as a wildcard). '' or null applies no filter.\n","example":"Supply"},"order_by":{"allOf":[{"$ref":"#/components/schemas/order_by"}],"description":"Columns vendor_number, name and total_purchase."}}},"paginate":{"$ref":"#/components/schemas/vendor_paginate"}},"example":{"search":{"name":"Supply","order_by":[{"column":"name","order":"asc"}]},"paginate":{"currentPage":1,"perPage":20}}},"vendor_paginate":{"type":"object","description":"Which page to return (validPaginateBody). Known issue: numeric strings (\"1\") pass, but are passed on unconverted; see the search 200.\n","required":["currentPage","perPage"],"properties":{"currentPage":{"type":"integer","minimum":1,"example":1},"perPage":{"type":"integer","minimum":1,"maximum":100,"example":20}}},"vendor_search_result":{"type":"object","required":["data","pagination","orderByColumnNamesList"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/vendor"}},"pagination":{"$ref":"#/components/schemas/pagination"},"orderByColumnNamesList":{"allOf":[{"$ref":"#/components/schemas/orderByColumnNamesList"}],"example":["vendor_number","name","total_purchase"]}}}},"responses":{"BadRequest":{"description":"Validation failed (Joi or a malformed uuid), or the request was refused","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Unauthorized":{"description":"Missing or invalid token, or the caller lacks the route's required permission","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Forbidden":{"description":"Authenticated, but refused by a rule inside the handler","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"NotFound":{"description":"The record does not exist in the caller's org","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Conflict":{"description":"The record's current state forbids the change","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"ServerError":{"description":"Unexpected server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"po_allocation_ai_connector_forbidden":{"description":"An AI-connector (MCP) session - no purchase-order allocation route is on the connector allowlist (\"Not available to AI connectors\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"po_allocation_not_found":{"description":"DELETE's 404. No live allocation with this id on this purchase order in the caller's org (\"Allocation not found.\"), the purchase order is voided (\"Purchase order not found.\"), or the work order is gone (\"Work order not found.\"). The PO line and the work-order line are not looked up.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"po_invoice_ai_connector_forbidden":{"description":"An AI-connector (MCP) session - no purchase-order invoice route is on the connector allowlist (\"Not available to AI connectors\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"po_invoice_bad_params":{"description":"A path id is not a uuid (e.g. \"Validation error: \"id\" must be a valid GUID\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"po_invoice_po_not_found":{"description":"No live purchase order with this id in the caller's org (\"Purchase order doesn't exist\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"po_invoice_not_found":{"description":"No live purchase order with this id in the caller's org, or no live invoice with this id on it (\"Vendor invoice doesn't exist on this purchase order\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"po_invoice_write_refused":{"description":"A path id is not a uuid or the body failed validation (\"Validation error: ...\", e.g. \"invoice_number\" is required, or variance_resolution sent with a per-kind resolution); the purchase order cannot take an invoice (\"Release the purchase order before recording a vendor invoice against it\" when new, \"A vendor invoice can't be recorded against a 'closed' purchase order\"); a line is not on the purchase order (\"Line item <id> is not on this purchase order\") or appears twice; the derived totals overflow the columns; or the invoice has neither a non-zero line nor a charge (\"A vendor invoice needs at least one invoiced line item or a charge\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"po_invoice_acceptance_forbidden":{"description":"A variance kind accepted by a caller without that kind's authority (\"You do not have permission to approve <kinds> on this purchase order. Save without accepting to leave it|them pending approval. <who can approve>\"). details.unapproved_variances names each refused line, with po_item_id, item_id, name, quantity_invoiced, invoiceable_quantity, awaitable_quantity, po_unit_price, invoice_unit_price (rounded to 2 decimals here), quantity_status, quantity_variance, price_variance, variance_amount, variance_kinds and unapproved_kinds (PRICE and / or QUANTITY). Save without accepting to leave them pending. An AI-connector (MCP) session gets this status before any of this (\"Not available to AI connectors\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"AuthSessionUnauthorized":{"description":"From the `authorization` middleware: no Authorization header or not a Bearer one (\"No Authorization Provided\"); a token that is expired, badly signed or malformed (\"JWT validation fail\"); a token already logged out or refreshed (\"JWT does not exist\"); a failed signed request or OAuth token, or a service API credential (\"Unauthorized\"). Code UNAUTHORIZED.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"category_ai_connector_forbidden":{"description":"An AI-connector (MCP) session - only GET /categories is on the connector allowlist (\"Not available to AI connectors\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"CategoryUpdated":{"description":"The level of the updated category","content":{"application/json":{"schema":{"type":"string","enum":["SUPER_CAT updated","CAT updated","SUB_CAT updated"],"example":"SUB_CAT updated"}}}},"CategoryWriteBadRequest":{"description":"Validation failed (Joi: a missing body is \"name\" is required; `add_on` is \"not allowed\"; so is any key the body schema lacks, such as the `use_count`, `inherited`, `price_per_asset` or `assignment_count` a GET returns), or one of: \"Category not found\"; \"Invalid form templates attached\"; \"Form template (<description>) is the New Asset Detail Checklist template and cannot be attached to a category\"; \"Send a weight on every form template or on none\"; \"You're Not Allowed To Attach Depreciation Schedules To Categories\"; the attachment-link messages listed on `category_write_fields`; \"Invalid data type provided\" (a fraction in an integer column).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"CategoryDepreciationDisabled":{"description":"\"Depreciation feature is disabled for this organization\": a non-empty `depreciation_schedules` was sent while the org has depreciation switched off. An empty one is dropped instead, so `[]` never trips this 403. Also the answer to an AI-connector (MCP) session (\"Not available to AI connectors\"): no category route is on the connector allowlist.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"customer_ai_connector_forbidden":{"description":"An AI-connector (MCP) session - only POST /customers/search and GET /customers/{id} are on the connector allowlist (\"Not available to AI connectors\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"customer_stripe_error":{"description":"Unexpected server error, or an error from the Stripe API. Where error details are hidden (ERROR_DEBUG_CONTEXT not true and NODE_ENV production, as in production) every one of them is masked as 500 \"Oops something went wrong on our end!\". Where they are shown (ERROR_DEBUG_CONTEXT=true, or NODE_ENV other than production) a Stripe error keeps Stripe's own HTTP status and message instead, e.g. a 404 for an unknown payment method or Stripe customer id.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"form_template_ai_connector_forbidden":{"description":"An AI-connector (MCP) session - only POST /form_template/search and GET /form_template/{id} are on the connector allowlist (\"Not available to AI connectors\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"freight_company_ai_connector_forbidden":{"description":"An AI-connector (MCP) session - only POST /freight_companies/search is on the connector allowlist (\"Not available to AI connectors\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"media_ai_connector_forbidden":{"description":"An AI-connector (MCP) session - no /media route is on the connector allowlist (\"Not available to AI connectors\")","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"part_ai_connector_forbidden":{"description":"An AI-connector (MCP) session - no parts route is on the connector allowlist (\"Not available to AI connectors\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"platform_ai_connector_forbidden":{"description":"An AI-connector (MCP) session - only GET /platform/facilities is on the connector allowlist (\"Not available to AI connectors\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"purchase_order_ai_connector_forbidden":{"description":"An AI-connector (MCP) session - no purchase-order route is on the connector allowlist (\"Not available to AI connectors\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"saved_filter_ai_connector_forbidden":{"description":"An AI-connector (MCP) session - no saved-filter route is on the connector allowlist (\"Not available to AI connectors\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"schedule_ai_connector_forbidden":{"description":"An AI-connector (MCP) session - no schedules route is on the connector allowlist (\"Not available to AI connectors\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"mobile_log_ai_connector_forbidden":{"description":"An AI-connector (MCP) session - no server-log route is on the connector allowlist (\"Not available to AI connectors\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"signnow_ai_connector_forbidden":{"description":"An AI-connector (MCP) session - the route is not is on the connector allowlist (\"Not available to AI connectors\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"types_ai_connector_forbidden":{"description":"An AI-connector (MCP) session - the route is not is on the connector allowlist (\"Not available to AI connectors\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"vendor_ai_connector_forbidden":{"description":"An AI-connector (MCP) session - no vendor route is on the connector allowlist (\"Not available to AI connectors\")\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"},"inspectionLinkToken":{"type":"apiKey","in":"header","name":"access-token","description":"The access token of an inspection link (lotus_inspection_links), sent instead of a login. Only GET /media/platform/resource/{path} accepts it, and only for that inspection's images.\n"}},"parameters":{"po_allocation_po_id":{"in":"path","name":"id","required":true,"description":"The purchase order id","schema":{"type":"string","format":"uuid"}},"po_allocation_id":{"in":"path","name":"allocationId","required":true,"description":"A lotus_parts_fulfillment row id - the id of a consumption_history row on GET /purchase_orders/{id}","schema":{"type":"string","format":"uuid"}},"po_invoice_po_id":{"in":"path","name":"id","required":true,"description":"The purchase order id","schema":{"type":"string","format":"uuid"}},"po_invoice_id":{"in":"path","name":"invoiceId","required":true,"description":"The vendor invoice id","schema":{"type":"string","format":"uuid"}},"category_id_path":{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}},"category_hidden_filter":{"in":"query","name":"hidden","required":false,"description":"`true` returns only hidden categories, `false` only visible ones; any other value (or none) returns both.\n","schema":{"type":"string","example":"false"}},"freight_company_id":{"in":"path","name":"id","required":true,"description":"The freight company's id","schema":{"type":"string","format":"uuid"},"example":"e4ddffd5-f67c-462b-9654-fe57df9043c0"},"oauth_grant_id":{"in":"path","name":"id","required":true,"description":"The connection (oauth_grants) id - an opaque URL-safe string of 16 to 128 letters, digits, \"_\" or \"-\", not a uuid.\n","schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{16,128}$","minLength":16,"maxLength":128,"example":"pX3k9vQ2LmN8rT5wYz1aB"}},"part_id":{"in":"path","name":"id","required":true,"description":"The part's id","schema":{"type":"string","format":"uuid"},"example":"7d1c2f4e-5a6b-4c3d-8e9f-0a1b2c3d4e5f"},"vendor_id":{"in":"path","name":"id","required":true,"description":"The vendor's id","schema":{"type":"string","format":"uuid"},"example":"e4644974-4f34-48f5-9ab0-85ab8a2818b9"}}},"tags":[{"name":"Invoices","description":"Customer invoices of rental, sale and work orders (Lotus)"},{"name":"Platform Settings","description":"The org settings store: the settings page payload and the diff save. The store is the only place a client reads org settings from.\n"},{"name":"Rentals","description":"Rental orders - quotes, reservations and contracts, their dispatches and documents (Lotus)"},{"name":"Sales","description":"Sales orders - quotes, open and closed sales orders, their documents and SPP warranty forms (Lotus)"},{"name":"Search","description":"The global search bar (lotus/services/searchService.js): one query across every entity the caller may see, at most 10 rows per entity.\n"},{"name":"Inspections","description":"Inspections - the dispatch, off-rent, sale and independent inspection records on an asset, their form items, hour meter and photos (Lotus). Inspections are listed by POST /lotus/work-orders/search (its rows mix work orders, inspections and NADC checklists).\n"},{"name":"Work Orders","description":"Work orders - service, onboarding, maintenance and sale-prep jobs on an asset, their parts, notes and time logs (Lotus)"},{"name":"Assets","description":"Assets Endpoints"},{"name":"Auth","description":"Sign-in, sign-out, token refresh and password endpoints"},{"name":"Category","description":"Category Endpoints"},{"name":"Checklists","description":"The org's asset checklist definitions - the Buy (purchase / onboarding) and Sale sign-off sections and the questions (items) in each. No route creates or deletes a checklist itself (they are provisioned outside the API); these routes read the definitions and add, edit and delete items. An asset's own copy of a checklist and its answers live under /assets/checklists.\n"},{"name":"Customer","description":"Customer Endpoints"},{"name":"Form Templates","description":"Form Templates Endpoints"},{"name":"Freight Companies","description":"Endpoints for managing freight companies within an organization"},{"name":"Journal","description":"Change history (the journal table)"},{"name":"Media","description":"Media Endpoints"},{"name":"OAuth","description":"AI-assistant (MCP connector) connections - the OAuth grants a user approved. A user can list and revoke their own; an org admin can list and revoke every connection in the org. None of these routes is open to an AI-connector session itself.\n"},{"name":"Parts","description":"Search, create, read, update and delete an organization's parts"},{"name":"Platform","description":"The caller's organization - its profile (name, address, logo, notification recipients, Slack and Stripe keys), field (validation) settings, notification settings, roles and permissions, facilities, part bundles, the org's own Stripe payment method and service API credentials. Org settings are not here: they live in the platform settings store (GET/PUT /lotus/platform-settings).\n"},{"name":"Purchase Orders","description":"Purchase Orders Endpoints"},{"name":"Reports","description":"Reports Endpoints"},{"name":"Saved Filters","description":"Each user's own saved tabs on the Rentals, Sales and Service tables"},{"name":"Schedules","description":"List, create and update an organization's depreciation schedules"},{"name":"Server Logs","description":"Store and search the device logs the mobile app and the web send"},{"name":"SignNow","description":"The platform's SignNow account"},{"name":"Types","description":"The platform-wide item type lookup list"},{"name":"Vendors","description":"The org's suppliers and their contacts; purchase orders are raised against a vendor.\n"}]}