Download OpenAPI specification:Download
Pharus exposes every action in the platform as a REST endpoint. Use this API to build integrations, sync data with other systems, or back your own interface.
/api. Paths below are shown after that prefix.Authorization: Bearer <firebase-id-token> together with
X-Org-Id: <org-id> on every request. A few token-gated routes used during invitation
acceptance are public.{ "error": { "code", "message", "details?" } }, where
code is a stable identifier such as not_found, in_use, invalid_transition,
validation_failed, forbidden, unauthorized, conflict, or internal_error.{ "deletedId": "uuid" }. There are no 204 responses.?cursor=<opaque>&limit=<int> and return
{ "data": [...], "nextCursor": string | null }.{ "rows": [...] } and return a per-row { index, status, id? | error? }.Create a warehouse/site location. Only name is required and must be unique in the org (409 conflict otherwise); the address is optional. New locations are active by default. customFields carries opaque key/value pairs the platform never interprets (max 50 fields; key <= 64 chars, value <= 512 chars; keys unique case-insensitively).
| name required | string [ 1 .. 100 ] characters |
object or null | |
| notes | string or null <= 2000 characters |
Array of objects |
{- "name": "string",
- "address": {
- "line1": "string",
- "city": "string",
- "state": "string",
- "postalCode": "string",
- "country": "st"
}, - "notes": "string",
- "customFields": [
- {
- "key": "string",
- "value": "string"
}
]
}{- "id": "string",
- "name": "string",
- "sortOrder": 0,
- "address": {
- "line1": "string",
- "city": "string",
- "state": "string",
- "postalCode": "string",
- "country": "string"
}, - "notes": "string",
- "isActive": true,
- "customFields": [
- {
- "key": "string",
- "value": "string"
}
], - "createdAt": "2019-08-24T14:15:22Z",
- "updatedAt": "2019-08-24T14:15:22Z"
}List one page of the org's locations. Narrow with active (active vs archived), a free-text search over the name and notes, territory filters (city/state/country, case-insensitive exact), or a customFieldKey/customFieldValue lookup. Sort with sort (name, createdAt, updatedAt; default name; empty values always last) and order (asc/desc); page with limit (default 50, max 200) and offset. meta.total counts the filtered set, so pagers can render page counts under active filters.
| sort | string Enum: "name" "createdAt" "updatedAt" |
| order | string Enum: "asc" "desc" |
| limit | integer [ 1 .. 200 ] |
| offset | integer >= 0 |
| active | string Enum: "true" "false" |
| search | string |
| city | string |
| state | string |
| country | string |
| customFieldKey | string |
| customFieldValue | string |
{- "data": [
- {
- "id": "string",
- "name": "string",
- "sortOrder": 0,
- "address": {
- "line1": "string",
- "city": "string",
- "state": "string",
- "postalCode": "string",
- "country": "string"
}, - "notes": "string",
- "isActive": true,
- "customFields": [
- {
- "key": "string",
- "value": "string"
}
], - "createdAt": "2019-08-24T14:15:22Z",
- "updatedAt": "2019-08-24T14:15:22Z"
}
], - "meta": {
- "total": 0,
- "limit": 0,
- "offset": 0
}
}Update a location's fields (partial; only send what changes). Set isActive false to archive or true to reactivate. Renaming to a name another location already uses is rejected with 409 conflict. customFields, when sent, replaces the whole set (max 50 fields; key <= 64 chars, value <= 512 chars; keys unique case-insensitively).
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| name | string [ 1 .. 100 ] characters |
object or null | |
| notes | string or null <= 2000 characters |
Array of objects | |
| sortOrder | integer >= 0 |
| isActive | boolean |
{- "name": "string",
- "address": {
- "line1": "string",
- "city": "string",
- "state": "string",
- "postalCode": "string",
- "country": "st"
}, - "notes": "string",
- "customFields": [
- {
- "key": "string",
- "value": "string"
}
], - "sortOrder": 0,
- "isActive": true
}{- "id": "string",
- "name": "string",
- "sortOrder": 0,
- "address": {
- "line1": "string",
- "city": "string",
- "state": "string",
- "postalCode": "string",
- "country": "string"
}, - "notes": "string",
- "isActive": true,
- "customFields": [
- {
- "key": "string",
- "value": "string"
}
], - "createdAt": "2019-08-24T14:15:22Z",
- "updatedAt": "2019-08-24T14:15:22Z"
}Permanently delete a location. Blocked with 409 in_use when a purchase/work/sales order, a transfer, or either inventory ledger references it; the error details carry the per-source blocking counts (purchaseOrderCount, workOrderCount, salesOrderCount, inventoryLedgerCount, transferCount). Archive it instead (PATCH with isActive=false) for routine cleanup.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "deletedId": "string"
}Fetch a single location with the contacts linked to it embedded under contacts (each a hydrated link + contact + methods row), so the facility page renders in one call.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "id": "string",
- "name": "string",
- "sortOrder": 0,
- "address": {
- "line1": "string",
- "city": "string",
- "state": "string",
- "postalCode": "string",
- "country": "string"
}, - "notes": "string",
- "isActive": true,
- "customFields": [
- {
- "key": "string",
- "value": "string"
}
], - "createdAt": "2019-08-24T14:15:22Z",
- "updatedAt": "2019-08-24T14:15:22Z",
- "contacts": [
- {
- "link": {
- "id": "string",
- "role": "string",
- "isPrimary": true,
- "notes": "string",
- "isActive": true,
- "createdAt": "string",
- "updatedAt": "string"
}, - "contact": {
- "id": "string",
- "displayName": "string",
- "firstName": "string",
- "lastName": "string",
- "title": "string",
- "notes": "string",
- "isActive": true,
- "createdAt": "string",
- "updatedAt": "string"
}, - "methods": [
- {
- "id": "string",
- "methodType": "email",
- "value": "string",
- "label": "string",
- "isPrimary": true,
- "isActive": true,
- "createdAt": "string",
- "updatedAt": "string"
}
]
}
]
}Create a vendor for the caller's org. Name is unique per org (case-insensitive), as is QBO id when present; a collision returns 409 conflict.
| name required | string [ 1 .. 255 ] characters |
object or null | |
| term | string or null <= 255 characters |
| qboId | string or null <= 255 characters |
| notes | string or null <= 2000 characters |
Array of objects |
{- "name": "string",
- "address": {
- "line1": "string",
- "city": "string",
- "state": "string",
- "postalCode": "string",
- "country": "st"
}, - "term": "string",
- "qboId": "string",
- "notes": "string",
- "customFields": [
- {
- "key": "string",
- "value": "string"
}
]
}{- "id": "string",
- "name": "string",
- "address": {
- "line1": "string",
- "city": "string",
- "state": "string",
- "postalCode": "string",
- "country": "string"
}, - "term": "string",
- "qboId": "string",
- "notes": "string",
- "isActive": true,
- "customFields": [
- {
- "key": "string",
- "value": "string"
}
], - "createdAt": "string",
- "updatedAt": "string"
}List one page of the org's vendors. Narrow with active (active vs archived), a free-text search over the name, notes, and payment term, territory filters (city/state/country, case-insensitive exact), a term filter (case-insensitive exact, e.g. Net 30), or a custom-field key/value lookup. Sort with sort (name, term, createdAt, updatedAt; default name; empty values always last) and order (asc/desc); page with limit (default 50, max 200) and offset. meta.total counts the filtered set, so pagers can render page counts under active filters.
| sort | string Enum: "name" "term" "createdAt" "updatedAt" |
| order | string Enum: "asc" "desc" |
| limit | integer [ 1 .. 200 ] |
| offset | integer >= 0 |
| active | string Enum: "true" "false" |
| search | string non-empty |
| city | string |
| state | string |
| country | string |
| term | string |
| customFieldKey | string |
| customFieldValue | string |
{- "data": [
- {
- "id": "string",
- "name": "string",
- "address": {
- "line1": "string",
- "city": "string",
- "state": "string",
- "postalCode": "string",
- "country": "string"
}, - "term": "string",
- "qboId": "string",
- "notes": "string",
- "isActive": true,
- "customFields": [
- {
- "key": "string",
- "value": "string"
}
], - "createdAt": "string",
- "updatedAt": "string"
}
], - "meta": {
- "total": 0,
- "limit": 0,
- "offset": 0
}
}Patch a vendor's header fields. Setting isActive archives (false) or reactivates (true) the vendor; there is no separate reactivate route. A name or QBO id that collides with another vendor returns 409 conflict.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| name | string [ 1 .. 255 ] characters |
object or null | |
| term | string or null <= 255 characters |
| qboId | string or null <= 255 characters |
| notes | string or null <= 2000 characters |
Array of objects | |
| isActive | boolean |
{- "name": "string",
- "address": {
- "line1": "string",
- "city": "string",
- "state": "string",
- "postalCode": "string",
- "country": "st"
}, - "term": "string",
- "qboId": "string",
- "notes": "string",
- "customFields": [
- {
- "key": "string",
- "value": "string"
}
], - "isActive": true
}{- "id": "string",
- "name": "string",
- "address": {
- "line1": "string",
- "city": "string",
- "state": "string",
- "postalCode": "string",
- "country": "string"
}, - "term": "string",
- "qboId": "string",
- "notes": "string",
- "isActive": true,
- "customFields": [
- {
- "key": "string",
- "value": "string"
}
], - "createdAt": "string",
- "updatedAt": "string"
}Hard-delete a vendor. Blocked with 409 in_use when any purchase order references it; archive it instead (PATCH isActive=false) to retire a supplier while keeping its history.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "deletedId": "string"
}Fetch a single vendor by id, scoped to the caller's org, including its linked contacts (each with the person, their email and phone methods, and the link role). Returns 404 when no such vendor exists.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "id": "string",
- "name": "string",
- "address": {
- "line1": "string",
- "city": "string",
- "state": "string",
- "postalCode": "string",
- "country": "string"
}, - "term": "string",
- "qboId": "string",
- "notes": "string",
- "isActive": true,
- "customFields": [
- {
- "key": "string",
- "value": "string"
}
], - "createdAt": "string",
- "updatedAt": "string",
- "contacts": [
- {
- "link": {
- "id": "string",
- "role": "string",
- "isPrimary": true,
- "notes": "string",
- "isActive": true,
- "createdAt": "string",
- "updatedAt": "string"
}, - "contact": {
- "id": "string",
- "displayName": "string",
- "firstName": "string",
- "lastName": "string",
- "title": "string",
- "notes": "string",
- "isActive": true,
- "createdAt": "string",
- "updatedAt": "string"
}, - "methods": [
- {
- "id": "string",
- "methodType": "email",
- "value": "string",
- "label": "string",
- "isPrimary": true,
- "isActive": true,
- "createdAt": "string",
- "updatedAt": "string"
}
]
}
]
}Create many vendors in one atomic batch from parsed rows (e.g. a CSV/Excel upload). All rows commit together: the first row whose name or QBO id collides aborts the whole import (409 conflict) and nothing is inserted. Returns the created ids in row order.
required | Array of objects | ||||||||||||
Array
| |||||||||||||
{- "rows": [
- {
- "name": "string",
- "address": {
- "line1": "string",
- "city": "string",
- "state": "string",
- "postalCode": "string",
- "country": "st"
}, - "term": "string",
- "qboId": "string",
- "notes": "string",
- "customFields": [
- {
- "key": "string",
- "value": "string"
}
]
}
]
}{- "ids": [
- "string"
]
}Create a raw material, the entity BOMs consume and the materials ledger tracks. Requires a material type. The code must be unique in the org (409 conflict otherwise). customFields carries opaque key/value pairs the platform never interprets (max 50 fields; key <= 64 chars, value <= 512 chars; keys unique case-insensitively).
| code required | string [ 1 .. 100 ] characters |
| name required | string [ 1 .. 255 ] characters |
| typeId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| notes | string or null <= 2000 characters |
Array of objects |
{- "code": "string",
- "name": "string",
- "typeId": "string",
- "notes": "string",
- "customFields": [
- {
- "key": "string",
- "value": "string"
}
]
}{- "id": "string",
- "code": "string",
- "name": "string",
- "type": {
- "id": "string",
- "name": "string"
}, - "notes": "string",
- "isActive": true,
- "customFields": [
- {
- "key": "string",
- "value": "string"
}
], - "createdAt": "string",
- "updatedAt": "string"
}List one page of the org's materials. Every filter is optional and AND-ed: active, a CSV type id multi-select, a free-text search over the material code, name, joined type name, and notes, and a customFieldKey/customFieldValue lookup for a custom-field code. Sort with sort (code, name, type, createdAt, updatedAt; default name; empty values always last) and order (asc/desc); page with limit (default 50, max 200) and offset. meta.total counts the filtered set, so pagers can render page counts under active filters.
| sort | string Enum: "code" "name" "type" "createdAt" "updatedAt" |
| order | string Enum: "asc" "desc" |
| limit | integer [ 1 .. 200 ] |
| offset | integer >= 0 |
| active | string Enum: "true" "false" |
| type | string |
| search | string |
| customFieldKey | string |
| customFieldValue | string |
{- "data": [
- {
- "id": "string",
- "code": "string",
- "name": "string",
- "type": {
- "id": "string",
- "name": "string"
}, - "notes": "string",
- "isActive": true,
- "customFields": [
- {
- "key": "string",
- "value": "string"
}
], - "createdAt": "string",
- "updatedAt": "string"
}
], - "meta": {
- "total": 0,
- "limit": 0,
- "offset": 0
}
}Fetch a single material by id: its header fields, custom fields, and its material type embedded as type with id and name.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "id": "string",
- "code": "string",
- "name": "string",
- "type": {
- "id": "string",
- "name": "string"
}, - "notes": "string",
- "isActive": true,
- "customFields": [
- {
- "key": "string",
- "value": "string"
}
], - "createdAt": "string",
- "updatedAt": "string"
}Update a material's fields (partial; only send what changes). Set isActive false to archive or true to reactivate. Changing the code to one another material already uses is rejected with 409 conflict. customFields, when sent, replaces the whole set (max 50 fields; key <= 64 chars, value <= 512 chars; keys unique case-insensitively).
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| code | string [ 1 .. 100 ] characters |
| name | string [ 1 .. 255 ] characters |
| typeId | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| notes | string or null <= 2000 characters |
Array of objects | |
| isActive | boolean |
{- "code": "string",
- "name": "string",
- "typeId": "string",
- "notes": "string",
- "customFields": [
- {
- "key": "string",
- "value": "string"
}
], - "isActive": true
}{- "id": "string",
- "code": "string",
- "name": "string",
- "type": {
- "id": "string",
- "name": "string"
}, - "notes": "string",
- "isActive": true,
- "customFields": [
- {
- "key": "string",
- "value": "string"
}
], - "createdAt": "string",
- "updatedAt": "string"
}Permanently delete a material. Blocked with 409 in_use when a purchase-order item, BOM input, materials-ledger entry, or transfer references it (a blocking transfer surfaces its transferCount in the error details); archive it instead (PATCH with isActive=false) to keep the history.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "deletedId": "string"
}Create a material type, the classification every material carries. The name must be unique among the org's material types (409 conflict otherwise).
| name required | string [ 1 .. 100 ] characters |
{- "name": "string"
}{- "id": "string",
- "name": "string",
- "sortOrder": 0,
- "isActive": true,
- "createdAt": "string"
}List the org's material types in sort order, active and archived alike. Pass a free-text search to match names (case-insensitive contains).
| search | string |
{- "data": [
- {
- "id": "string",
- "name": "string",
- "sortOrder": 0,
- "isActive": true,
- "createdAt": "string"
}
]
}Update a material type's name, sort order, or active flag (partial update, only send what changes). Set isActive false to archive the type or true to reactivate it; there is no separate archive endpoint. Renaming to a name another type already uses is rejected with 409 conflict.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| name | string [ 1 .. 100 ] characters |
| sortOrder | integer |
| isActive | boolean |
{- "name": "string",
- "sortOrder": 0,
- "isActive": true
}{- "id": "string",
- "name": "string",
- "sortOrder": 0,
- "isActive": true,
- "createdAt": "string"
}Permanently delete a material type. Blocked with 409 in_use while any material references it; the error details carry the blocking materialCount.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "deletedId": "string"
}Create many materials in one atomic call from an array of parsed rows (Data page upload), at most 1000 rows per call. The whole batch is one transaction: the first invalid row (unknown type, or a 409 duplicate code) rolls back all of them. Returns the created ids in row order.
required | Array of objects [ 1 .. 1000 ] items | ||||||||||
Array ([ 1 .. 1000 ] items)
| |||||||||||
{- "rows": [
- {
- "code": "string",
- "name": "string",
- "typeId": "string",
- "notes": "string",
- "customFields": [
- {
- "key": "string",
- "value": "string"
}
]
}
]
}{- "ids": [
- "string"
]
}Create a SKU (finished-goods product): the entity work orders produce, sales orders sell, and the finished-goods ledger tracks. Requires a SKU type. The code must be unique in the org (409 conflict otherwise). customFields carries opaque key/value pairs the platform never interprets (max 50 fields; key <= 64 chars, value <= 512 chars; keys unique case-insensitively). itemsPerUnit defaults to 1 when omitted.
| code required | string [ 1 .. 100 ] characters |
| name required | string [ 1 .. 255 ] characters |
| typeId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| itemsPerUnit | integer >= 1 Default: 1 |
| ozPerItem | number or null |
| qboId | string or null <= 255 characters |
| notes | string or null <= 2000 characters |
Array of objects |
{- "code": "string",
- "name": "string",
- "typeId": "string",
- "itemsPerUnit": 1,
- "ozPerItem": 0,
- "qboId": "string",
- "notes": "string",
- "customFields": [
- {
- "key": "string",
- "value": "string"
}
]
}{- "id": "string",
- "code": "string",
- "name": "string",
- "type": {
- "id": "string",
- "name": "string"
}, - "itemsPerUnit": 0,
- "ozPerItem": "string",
- "qboId": "string",
- "notes": "string",
- "isActive": true,
- "customFields": [
- {
- "key": "string",
- "value": "string"
}
], - "createdAt": "string",
- "updatedAt": "string"
}List one page of the org's SKUs. Every filter is optional and AND-ed: active, a CSV type id multi-select, inclusive itemsPerUnitMin/Max and totalOzMin/Max ranges (total oz = oz_per_item * items_per_unit; NULL-oz rows drop out once an oz bound is set), a free-text search over the SKU code, name, joined type name, and notes, and a customFieldKey/customFieldValue lookup. Sort with sort (code, name, type, itemsPerUnit, totalOz, createdAt, updatedAt; default code; empty values always last) and order (asc/desc); page with limit (default 50, max 200) and offset. meta.total counts the filtered set, so pagers can render page counts under active filters.
| sort | string Enum: "code" "name" "type" "itemsPerUnit" "totalOz" "createdAt" "updatedAt" |
| order | string Enum: "asc" "desc" |
| limit | integer [ 1 .. 200 ] |
| offset | integer >= 0 |
| active | string Enum: "true" "false" |
| type | string |
| itemsPerUnitMin | number |
| itemsPerUnitMax | number |
| totalOzMin | number |
| totalOzMax | number |
| search | string |
| customFieldKey | string |
| customFieldValue | string |
{- "data": [
- {
- "id": "string",
- "code": "string",
- "name": "string",
- "type": {
- "id": "string",
- "name": "string"
}, - "itemsPerUnit": 0,
- "ozPerItem": "string",
- "qboId": "string",
- "notes": "string",
- "isActive": true,
- "customFields": [
- {
- "key": "string",
- "value": "string"
}
], - "createdAt": "string",
- "updatedAt": "string"
}
], - "meta": {
- "total": 0,
- "limit": 0,
- "offset": 0
}
}Fetch a single SKU by id: its header fields, pack size (items per unit, oz per item), custom fields, and its SKU type embedded as type with id and name.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "id": "string",
- "code": "string",
- "name": "string",
- "type": {
- "id": "string",
- "name": "string"
}, - "itemsPerUnit": 0,
- "ozPerItem": "string",
- "qboId": "string",
- "notes": "string",
- "isActive": true,
- "customFields": [
- {
- "key": "string",
- "value": "string"
}
], - "createdAt": "string",
- "updatedAt": "string"
}Update a SKU's fields (partial; only send what changes). Set isActive false to archive or true to reactivate. Changing the code to one another SKU already uses is rejected with 409 conflict. customFields, when sent, replaces the whole set (max 50 fields; key <= 64 chars, value <= 512 chars; keys unique case-insensitively).
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| code | string [ 1 .. 100 ] characters |
| name | string [ 1 .. 255 ] characters |
| typeId | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| itemsPerUnit | integer >= 1 |
| ozPerItem | number or null |
| qboId | string or null <= 255 characters |
| notes | string or null <= 2000 characters |
Array of objects | |
| isActive | boolean |
{- "code": "string",
- "name": "string",
- "typeId": "string",
- "itemsPerUnit": 1,
- "ozPerItem": 0,
- "qboId": "string",
- "notes": "string",
- "customFields": [
- {
- "key": "string",
- "value": "string"
}
], - "isActive": true
}{- "id": "string",
- "code": "string",
- "name": "string",
- "type": {
- "id": "string",
- "name": "string"
}, - "itemsPerUnit": 0,
- "ozPerItem": "string",
- "qboId": "string",
- "notes": "string",
- "isActive": true,
- "customFields": [
- {
- "key": "string",
- "value": "string"
}
], - "createdAt": "string",
- "updatedAt": "string"
}Permanently delete a SKU. Blocked with 409 in_use when a purchase/work/sales-order item, BOM, finished-goods-ledger entry, or transfer references it (a blocking transfer surfaces its transferCount in the error details); archive it instead (PATCH with isActive=false) to keep the history.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "deletedId": "string"
}Create a SKU type, the classification every SKU carries. The name must be unique among the org's SKU types (409 conflict otherwise).
| name required | string [ 1 .. 100 ] characters |
{- "name": "string"
}{- "id": "string",
- "name": "string",
- "sortOrder": 0,
- "isActive": true,
- "createdAt": "string"
}List the org's SKU types in sort order, active and archived alike. Pass a free-text search to match names (case-insensitive contains).
| search | string |
{- "data": [
- {
- "id": "string",
- "name": "string",
- "sortOrder": 0,
- "isActive": true,
- "createdAt": "string"
}
]
}Update a SKU type's name, sort order, or active flag (partial update, only send what changes). Set isActive false to archive the type or true to reactivate it; there is no separate archive endpoint. Renaming to a name another type already uses is rejected with 409 conflict.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| name | string [ 1 .. 100 ] characters |
| sortOrder | integer |
| isActive | boolean |
{- "name": "string",
- "sortOrder": 0,
- "isActive": true
}{- "id": "string",
- "name": "string",
- "sortOrder": 0,
- "isActive": true,
- "createdAt": "string"
}Permanently delete a SKU type. Blocked with 409 in_use while any SKU references it; the error details carry the blocking skuCount.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "deletedId": "string"
}Create many SKUs in one atomic call from an array of parsed rows (Data page upload), at most 1000 rows per call. The whole batch is one transaction: the first invalid row (unknown type, or a 409 duplicate code) rolls back all of them. Returns the created ids in row order.
required | Array of objects [ 1 .. 1000 ] items | ||||||||||||||||
Array ([ 1 .. 1000 ] items)
| |||||||||||||||||
{- "rows": [
- {
- "code": "string",
- "name": "string",
- "typeId": "string",
- "itemsPerUnit": 1,
- "ozPerItem": 0,
- "qboId": "string",
- "notes": "string",
- "customFields": [
- {
- "key": "string",
- "value": "string"
}
]
}
]
}{- "ids": [
- "string"
]
}List one page of the output SKUs that have a BOM defined, each with the count of its input items. Every filter is optional and AND-ed: containsMaterialId and containsSkuId are CSV id multi-selects (each OR-ed within itself) that keep only the BOMs whose input lines include one of those materials / input SKUs; search is a case-insensitive substring matched against the output SKU's code and name and against the input lines' content (the codes and names of input materials and input SKUs). Sort with sort (code, name, itemCount; default code ascending; the summary carries no timestamps) and order (asc/desc); page with limit (default 50, max 200) and offset. meta.total counts the filtered BOMs (distinct output SKUs), so pagers can render page counts under active filters.
| sort | string Enum: "code" "name" "itemCount" |
| order | string Enum: "asc" "desc" |
| limit | integer [ 1 .. 200 ] |
| offset | integer >= 0 |
| containsMaterialId | string |
| containsSkuId | string |
| search | string |
{- "data": [
- {
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "itemCount": 0
}
], - "meta": {
- "total": 0,
- "limit": 0,
- "offset": 0
}
}Fetch the bill of materials for one output SKU: its input lines, each an input material or input SKU with a quantity. :id is the SKU id. Returns 404 when the SKU does not exist or has no BOM defined.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "items": [
- {
- "id": "string",
- "input": {
- "kind": "material",
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string"
}
]
}Create or replace the bill of materials for an output SKU in one idempotent call: the items array sent becomes the SKU's entire composition (a full replace, not a partial merge). :id is the SKU id. Each item sets exactly one of materialId or inputSkuId with a positive quantity. Listing the same material or input SKU twice is a 409 conflict. Returns 404 if the output SKU or any referenced material or input SKU does not exist.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
required | Array of objects [ 1 .. 500 ] items | ||||||
Array ([ 1 .. 500 ] items)
| |||||||
{- "items": [
- {
- "materialId": "string",
- "inputSkuId": "string",
- "quantity": 0
}
]
}{- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "items": [
- {
- "id": "string",
- "input": {
- "kind": "material",
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string"
}
]
}Delete a SKU's BOM, removing all its composition lines. :id is the SKU id. Existing work orders are unaffected (their inputs are snapshotted at creation). Returns 404 when the SKU has no BOM.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "deletedId": "string"
}Create a contact in the org's shared address book. It starts with no methods and no entity links; attach an email or phone with add-method and link it to a customer, vendor, or location separately.
| firstName required | string [ 1 .. 255 ] characters |
| lastName | string or null <= 255 characters |
| title | string or null <= 255 characters |
| notes | string or null <= 2000 characters |
| displayName | string <= 255 characters |
{- "firstName": "string",
- "lastName": "string",
- "title": "string",
- "notes": "string",
- "displayName": "string"
}{- "id": "string",
- "displayName": "string",
- "firstName": "string",
- "lastName": "string",
- "title": "string",
- "notes": "string",
- "isActive": true,
- "createdAt": "string",
- "updatedAt": "string"
}List one page of the org's contacts, each with its entity links hydrated and named so the address book can render where a contact is attached as navigable chips. Pass search to match names, title, notes, or a method value (find a contact by email or phone fragment); active filters to active or archived; linkedEntityType keeps contacts linked to at least one customer/vendor/location; hasLinks=false finds orphan contacts; methodType keeps contacts with at least one email or phone. Sort with sort (name, title, createdAt, updatedAt; default name; empty values always last) and order (asc/desc); page with limit (default 50, max 200) and offset. meta.total counts the filtered set, so pagers can render page counts under active filters.
| sort | string Enum: "name" "title" "createdAt" "updatedAt" |
| order | string Enum: "asc" "desc" |
| limit | integer [ 1 .. 200 ] |
| offset | integer >= 0 |
| search | string |
| active | string Enum: "true" "false" |
| linkedEntityType | string Enum: "customer" "vendor" "location" |
| hasLinks | string Enum: "true" "false" |
| methodType | string Enum: "email" "phone" |
{- "data": [
- {
- "id": "string",
- "displayName": "string",
- "firstName": "string",
- "lastName": "string",
- "title": "string",
- "notes": "string",
- "isActive": true,
- "createdAt": "string",
- "updatedAt": "string",
- "links": [
- {
- "id": "string",
- "entity": {
- "type": "customer",
- "id": "string",
- "name": "string"
}, - "role": "string",
- "isPrimary": true,
- "notes": "string",
- "isActive": true,
- "createdAt": "string",
- "updatedAt": "string"
}
]
}
], - "meta": {
- "total": 0,
- "limit": 0,
- "offset": 0
}
}Patch a contact's header fields. Setting isActive archives (false) or reactivates (true) the contact; there is no separate reactivate route.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| firstName | string <= 255 characters |
| lastName | string or null <= 255 characters |
| title | string or null <= 255 characters |
| notes | string or null <= 2000 characters |
| displayName | string <= 255 characters |
| isActive | boolean |
{- "firstName": "string",
- "lastName": "string",
- "title": "string",
- "notes": "string",
- "displayName": "string",
- "isActive": true
}{- "id": "string",
- "displayName": "string",
- "firstName": "string",
- "lastName": "string",
- "title": "string",
- "notes": "string",
- "isActive": true,
- "createdAt": "string",
- "updatedAt": "string"
}Hard-delete a contact. Its methods and entity links cascade away with it. Archive instead (PATCH isActive=false) to retire a contact while keeping its links and history.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "deletedId": "string"
}Fetch a single contact with its methods and entity links (each named) nested. Returns 404 when no such contact exists.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "id": "string",
- "displayName": "string",
- "firstName": "string",
- "lastName": "string",
- "title": "string",
- "notes": "string",
- "isActive": true,
- "createdAt": "string",
- "updatedAt": "string",
- "methods": [
- {
- "id": "string",
- "methodType": "email",
- "value": "string",
- "label": "string",
- "isPrimary": true,
- "isActive": true,
- "createdAt": "string",
- "updatedAt": "string"
}
], - "links": [
- {
- "id": "string",
- "entity": {
- "type": "customer",
- "id": "string",
- "name": "string"
}, - "role": "string",
- "isPrimary": true,
- "notes": "string",
- "isActive": true,
- "createdAt": "string",
- "updatedAt": "string"
}
]
}Attach an email or phone to a contact (given by contactId). Marking it primary demotes the contact's current primary of that type (one primary per type). A duplicate (same type and value on the contact) returns 409 conflict.
| contactId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| type required | string Enum: "email" "phone" |
| value required | string [ 1 .. 255 ] characters |
| label | string or null <= 255 characters |
| isPrimary | boolean |
{- "contactId": "string",
- "type": "email",
- "value": "string",
- "label": "string",
- "isPrimary": true
}{- "id": "string",
- "methodType": "email",
- "value": "string",
- "label": "string",
- "isPrimary": true,
- "isActive": true,
- "createdAt": "string",
- "updatedAt": "string"
}List the email and phone methods for the contact given by contactId, primary first. Returns 404 when the contact does not exist.
| contactId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "data": [
- {
- "id": "string",
- "methodType": "email",
- "value": "string",
- "label": "string",
- "isPrimary": true,
- "isActive": true,
- "createdAt": "string",
- "updatedAt": "string"
}
]
}Patch a method's value, label, or primary flag. Promoting it to primary demotes the current primary of its type. A change that collides with an existing method on the contact returns 409 conflict.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| type | string Enum: "email" "phone" |
| value | string <= 255 characters |
| label | string or null <= 255 characters |
| isPrimary | boolean |
| isActive | boolean |
{- "type": "email",
- "value": "string",
- "label": "string",
- "isPrimary": true,
- "isActive": true
}{- "id": "string",
- "methodType": "email",
- "value": "string",
- "label": "string",
- "isPrimary": true,
- "isActive": true,
- "createdAt": "string",
- "updatedAt": "string"
}Link a contact (contactId) to a customer, vendor, or location (entityType + entityId). Both ends must exist (404 otherwise). A duplicate link (same contact, entity, and role) returns 409 conflict.
| contactId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| entityType required | string Enum: "customer" "vendor" "location" |
| entityId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| role | string or null <= 255 characters |
| notes | string or null <= 2000 characters |
| isPrimary | boolean |
{- "contactId": "string",
- "entityType": "customer",
- "entityId": "string",
- "role": "string",
- "notes": "string",
- "isPrimary": true
}{- "id": "string",
- "entity": {
- "type": "customer",
- "id": "string",
- "name": "string"
}, - "role": "string",
- "isPrimary": true,
- "notes": "string",
- "isActive": true,
- "createdAt": "string",
- "updatedAt": "string"
}List the contacts linked to one entity, filtered by entityType and entityId, each hydrated with the link and the contact's methods. This is how a customer, vendor, or facility detail renders its contacts.
| entityType required | string Enum: "customer" "vendor" "location" |
| entityId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "data": [
- {
- "link": {
- "id": "string",
- "role": "string",
- "isPrimary": true,
- "notes": "string",
- "isActive": true,
- "createdAt": "string",
- "updatedAt": "string"
}, - "contact": {
- "id": "string",
- "displayName": "string",
- "firstName": "string",
- "lastName": "string",
- "title": "string",
- "notes": "string",
- "isActive": true,
- "createdAt": "string",
- "updatedAt": "string"
}, - "methods": [
- {
- "id": "string",
- "methodType": "email",
- "value": "string",
- "label": "string",
- "isPrimary": true,
- "isActive": true,
- "createdAt": "string",
- "updatedAt": "string"
}
]
}
]
}Patch a link's role, notes, or primary flag. The contact row and the target entity are unaffected. Changing the role to one that collides with another link on the same contact and entity returns 409 conflict.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| role | string or null <= 255 characters |
| notes | string or null <= 2000 characters |
| isPrimary | boolean |
| isActive | boolean |
{- "role": "string",
- "notes": "string",
- "isPrimary": true,
- "isActive": true
}{- "id": "string",
- "entity": {
- "type": "customer",
- "id": "string",
- "name": "string"
}, - "role": "string",
- "isPrimary": true,
- "notes": "string",
- "isActive": true,
- "createdAt": "string",
- "updatedAt": "string"
}Remove a contact's link to an entity. The contact row itself is unaffected; only the link is deleted.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "deletedId": "string"
}Record a manual stock-count correction for a material at a location as an ADJUST ledger row. The quantity is signed: positive raises on-hand (found stock), negative lowers it (a count entered too high); zero is rejected (422). Use disposals for write-offs that carry waste semantics. Returns 404 if the material or location does not exist.
| materialId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| locationId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| quantity required | number |
| notes | string or null <= 2000 characters |
| eventDate | string <date> |
{- "materialId": "string",
- "locationId": "string",
- "quantity": 0,
- "notes": "string",
- "eventDate": "2019-08-24"
}{- "id": "string",
- "code": "string",
- "material": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "location": {
- "id": "string",
- "name": "string"
}, - "eventType": "ORDER",
- "quantity": "string",
- "unitCost": "string",
- "ref": "string",
- "notes": "string",
- "eventDate": "string",
- "sourceType": "po_receipt",
- "lotNumber": "string",
- "expirationDate": "string",
- "createdAt": "string",
- "lotLineIndex": 0,
- "lotLineCount": 0
}Write off material stock (damage, loss, expiry) as a DISPOSE ledger row, permanently removing the quantity from stock. The quantity is a positive magnitude (zero or negative is rejected, 422). Returns 404 if the material or location does not exist.
| materialId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| locationId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| quantity required | number > 0 |
| notes | string or null <= 2000 characters |
| eventDate | string <date> |
{- "materialId": "string",
- "locationId": "string",
- "quantity": 0,
- "notes": "string",
- "eventDate": "2019-08-24"
}{- "id": "string",
- "code": "string",
- "material": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "location": {
- "id": "string",
- "name": "string"
}, - "eventType": "ORDER",
- "quantity": "string",
- "unitCost": "string",
- "ref": "string",
- "notes": "string",
- "eventDate": "string",
- "sourceType": "po_receipt",
- "lotNumber": "string",
- "expirationDate": "string",
- "createdAt": "string",
- "lotLineIndex": 0,
- "lotLineCount": 0
}Compute the full inventory position per (material, location) from the ledger: on-hand, on-order, allocated, available, in-stock, demand, wasted, in-transit, net position, and weeklyDemand, the real outflow of the trailing window divided by its weeks (transfers between your own locations and disposals are not demand, so they are excluded). The window defaults to 4 whole weeks; pass demandWindowWeeks (1..52) for a steadier, longer-memory rate, e.g. 13 for a trailing quarter. Each row also splits its open commitment by who wants it: openSalesDemand (sales orders and receipts not yet fulfilled) and openJobDemand (work/production-order inputs not yet consumed or settled), so customer demand is never conflated with production demand. weeklyDemandDtc and weeklyDemandWholesale split the same trailing rate by channel (receipt consumption vs order consumption). inTransitProduction is the quantity on production orders currently in transit toward the location (still inside on-order, since their ORDER is active until receipt). Every filter is optional and AND-ed: materialId and locationId are CSV id multi-selects (each OR-ed within itself), and search is a case-insensitive substring over the material's code and name. Pass includeOnHandValue=true to also get onHandValue, the costed value of the stock on hand under the org's costing method; it replays the ledger per row, so leave it off unless you need the money. This is the go-to read for how much of X is on hand and where.
| materialId | string |
| locationId | string |
| search | string |
| includeOnHandValue | string Enum: "true" "false" |
| demandWindowWeeks | integer [ 1 .. 52 ] |
{- "data": [
- {
- "material": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "location": {
- "id": "string",
- "name": "string"
}, - "onHand": "string",
- "onOrder": "string",
- "allocated": "string",
- "available": "string",
- "inStock": "string",
- "demand": "string",
- "wasted": "string",
- "inTransit": "string",
- "inTransitProduction": "string",
- "netPosition": "string",
- "weeklyDemand": "string",
- "weeklyDemandDtc": "string",
- "weeklyDemandWholesale": "string",
- "openSalesDemand": "string",
- "openJobDemand": "string",
- "onHandValue": 0
}
]
}The open job commitments behind one material: every work and production order still holding open input demand for it, one row per order with its reference ({ id, number, type }), status, the required quantity, and what is still open after consumption and settles, netted server-side by the same rules the summary's openJobDemand uses. Rows are ordered by open quantity, largest first; orders fully consumed or settled are omitted. This is the drill-down behind the summary's openJobDemand figure.
| materialId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "data": [
- {
- "order": {
- "id": "string",
- "number": "string",
- "type": "work_order"
}, - "status": "string",
- "required": "string",
- "open": "string"
}
]
}List one page of materials ledger entries, each expanded to one line per lot with its derived unit cost and its embedded material and location. Every filter is optional and AND-ed: the CSV multi-selects materialId, locationId, eventType and sourceType (each OR-ed within itself; sourceType is the source-document kind, e.g. transfer), ref (a single exact match on the row's correlator, e.g. a TFR-... transfer code), lotNumber (case-insensitive exact), and one inclusive YYYY-MM-DD From/To pair per date field: eventDateFrom/eventDateTo on the business event date, expirationFrom/expirationTo on the lot expiration date. search is a case-insensitive substring matched against the row code (MIL-...), ref, the material's code and name, the location name, the lot number, and notes. Sort with sort (code, material, location, eventDate, quantity, createdAt; default eventDate descending, newest first; the append-only ledger has no updatedAt) and order (asc/desc); page with limit (default 50, max 200) and offset. The page is cut over the stored rows, so a row that splits into per-lot lines can make data slightly longer than limit; meta.total counts the filtered stored rows, so pagers can render page counts under active filters.
| sort | string Enum: "code" "material" "location" "eventDate" "quantity" "createdAt" |
| order | string Enum: "asc" "desc" |
| limit | integer [ 1 .. 200 ] |
| offset | integer >= 0 |
| materialId | string |
| locationId | string |
| eventType | string |
| sourceType | string |
| ref | string |
| lotNumber | string |
| search | string |
| eventDateFrom | string^\d{4}-\d{2}-\d{2}$ |
| eventDateTo | string^\d{4}-\d{2}-\d{2}$ |
| expirationFrom | string^\d{4}-\d{2}-\d{2}$ |
| expirationTo | string^\d{4}-\d{2}-\d{2}$ |
{- "data": [
- {
- "id": "string",
- "code": "string",
- "material": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "location": {
- "id": "string",
- "name": "string"
}, - "eventType": "ORDER",
- "quantity": "string",
- "unitCost": "string",
- "ref": "string",
- "notes": "string",
- "eventDate": "string",
- "sourceType": "po_receipt",
- "lotNumber": "string",
- "expirationDate": "string",
- "createdAt": "string",
- "lotLineIndex": 0,
- "lotLineCount": 0
}
], - "meta": {
- "total": 0,
- "limit": 0,
- "offset": 0
}
}Fetch a single materials ledger entry by id, with its derived unit cost. Unlike the list, the single read is not expanded per lot.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "id": "string",
- "code": "string",
- "material": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "location": {
- "id": "string",
- "name": "string"
}, - "eventType": "ORDER",
- "quantity": "string",
- "unitCost": "string",
- "ref": "string",
- "notes": "string",
- "eventDate": "string",
- "sourceType": "po_receipt",
- "lotNumber": "string",
- "expirationDate": "string",
- "createdAt": "string",
- "lotLineIndex": 0,
- "lotLineCount": 0
}Record a manual stock-count correction for a SKU at a location as an ADJUST ledger row. The quantity is signed: positive raises on-hand (found stock), negative lowers it (a count entered too high); zero is rejected (422). Use disposals for write-offs that carry waste semantics. Returns 404 if the SKU or location does not exist.
| skuId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| locationId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| quantity required | number |
| notes | string or null <= 2000 characters |
| eventDate | string <date> |
{- "skuId": "string",
- "locationId": "string",
- "quantity": 0,
- "notes": "string",
- "eventDate": "2019-08-24"
}{- "id": "string",
- "code": "string",
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "location": {
- "id": "string",
- "name": "string"
}, - "eventType": "ORDER",
- "quantity": "string",
- "unitCost": "string",
- "ref": "string",
- "notes": "string",
- "eventDate": "string",
- "sourceType": "po_receipt",
- "lotNumber": "string",
- "expirationDate": "string",
- "createdAt": "string",
- "lotLineIndex": 0,
- "lotLineCount": 0
}Write off finished-goods stock (damage, loss, expiry) as a DISPOSE ledger row, permanently removing the quantity from stock. The quantity is a positive magnitude (zero or negative is rejected, 422). Returns 404 if the SKU or location does not exist.
| skuId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| locationId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| quantity required | number > 0 |
| notes | string or null <= 2000 characters |
| eventDate | string <date> |
{- "skuId": "string",
- "locationId": "string",
- "quantity": 0,
- "notes": "string",
- "eventDate": "2019-08-24"
}{- "id": "string",
- "code": "string",
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "location": {
- "id": "string",
- "name": "string"
}, - "eventType": "ORDER",
- "quantity": "string",
- "unitCost": "string",
- "ref": "string",
- "notes": "string",
- "eventDate": "string",
- "sourceType": "po_receipt",
- "lotNumber": "string",
- "expirationDate": "string",
- "createdAt": "string",
- "lotLineIndex": 0,
- "lotLineCount": 0
}Compute the full inventory position per (SKU, location) from the ledger: on-hand, on-order, allocated, available, in-stock, demand, wasted, in-transit, net position, and weeklyDemand, the real outflow of the trailing window divided by its weeks (transfers between your own locations and disposals are not demand, so they are excluded). The window defaults to 4 whole weeks; pass demandWindowWeeks (1..52) for a steadier, longer-memory rate, e.g. 13 for a trailing quarter. Each row also splits its open commitment by who wants it: openSalesDemand (sales orders and receipts not yet fulfilled) and openJobDemand (work/production-order inputs not yet consumed or settled), so customer demand is never conflated with production demand. weeklyDemandDtc and weeklyDemandWholesale split the same trailing rate by channel (receipt consumption vs order consumption). inTransitProduction is the quantity on production orders currently in transit toward the location (still inside on-order, since their ORDER is active until receipt). Every filter is optional and AND-ed: skuId and locationId are CSV id multi-selects (each OR-ed within itself), and search is a case-insensitive substring over the SKU's code and name. Pass includeOnHandValue=true to also get onHandValue, the costed value of the stock on hand under the org's costing method; it replays the ledger per row, so leave it off unless you need the money. This is the go-to read for how much of a SKU is on hand and where.
| skuId | string |
| locationId | string |
| search | string |
| includeOnHandValue | string Enum: "true" "false" |
| demandWindowWeeks | integer [ 1 .. 52 ] |
{- "data": [
- {
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "location": {
- "id": "string",
- "name": "string"
}, - "onHand": "string",
- "onOrder": "string",
- "allocated": "string",
- "available": "string",
- "inStock": "string",
- "demand": "string",
- "wasted": "string",
- "inTransit": "string",
- "inTransitProduction": "string",
- "netPosition": "string",
- "weeklyDemand": "string",
- "weeklyDemandDtc": "string",
- "weeklyDemandWholesale": "string",
- "openSalesDemand": "string",
- "openJobDemand": "string",
- "onHandValue": 0
}
]
}List one page of finished-goods ledger entries, each expanded to one line per lot with its derived unit cost and its embedded SKU and location. Every filter is optional and AND-ed: the CSV multi-selects skuId, locationId, eventType and sourceType (each OR-ed within itself; sourceType is the source-document kind, e.g. transfer), ref (a single exact match on the row's correlator, e.g. a TFR-... transfer code), lotNumber (case-insensitive exact), and one inclusive YYYY-MM-DD From/To pair per date field: eventDateFrom/eventDateTo on the business event date, expirationFrom/expirationTo on the lot expiration date. search is a case-insensitive substring matched against the row code (FIL-...), ref, the SKU's code and name, the location name, the lot number, and notes. Sort with sort (code, sku, location, eventDate, quantity, createdAt; default eventDate descending, newest first; the append-only ledger has no updatedAt) and order (asc/desc); page with limit (default 50, max 200) and offset. The page is cut over the stored rows, so a row that splits into per-lot lines can make data slightly longer than limit; meta.total counts the filtered stored rows, so pagers can render page counts under active filters.
| sort | string Enum: "code" "sku" "location" "eventDate" "quantity" "createdAt" |
| order | string Enum: "asc" "desc" |
| limit | integer [ 1 .. 200 ] |
| offset | integer >= 0 |
| skuId | string |
| locationId | string |
| eventType | string |
| sourceType | string |
| ref | string |
| lotNumber | string |
| search | string |
| eventDateFrom | string^\d{4}-\d{2}-\d{2}$ |
| eventDateTo | string^\d{4}-\d{2}-\d{2}$ |
| expirationFrom | string^\d{4}-\d{2}-\d{2}$ |
| expirationTo | string^\d{4}-\d{2}-\d{2}$ |
{- "data": [
- {
- "id": "string",
- "code": "string",
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "location": {
- "id": "string",
- "name": "string"
}, - "eventType": "ORDER",
- "quantity": "string",
- "unitCost": "string",
- "ref": "string",
- "notes": "string",
- "eventDate": "string",
- "sourceType": "po_receipt",
- "lotNumber": "string",
- "expirationDate": "string",
- "createdAt": "string",
- "lotLineIndex": 0,
- "lotLineCount": 0
}
], - "meta": {
- "total": 0,
- "limit": 0,
- "offset": 0
}
}Fetch a single finished-goods ledger entry by id, with its derived unit cost. Unlike the list, the single read is not expanded per lot.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "id": "string",
- "code": "string",
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "location": {
- "id": "string",
- "name": "string"
}, - "eventType": "ORDER",
- "quantity": "string",
- "unitCost": "string",
- "ref": "string",
- "notes": "string",
- "eventDate": "string",
- "sourceType": "po_receipt",
- "lotNumber": "string",
- "expirationDate": "string",
- "createdAt": "string",
- "lotLineIndex": 0,
- "lotLineCount": 0
}Create a stock transfer between two different locations, under a server-assigned TFR code. The movement is the items array: one line per moved material or SKU, each with its own itemKind, itemId and quantity, so a single transfer can carry raw materials and finished goods together and post to both inventory ledgers. At least one line is required and the same item may not appear twice. It opens in Planning (no inventory effect) unless a later status is supplied, in which case the opening immediately projects every line's ledger legs (In Transit the outbound CONSUME at the source, Completed also the inbound RECEIVE). The per-status dates auto-stamp from the opening status; explicit values in the optional statusDates object (keyed planning / inTransit / completed) override. The response embeds the resolved location references, nests the items with their resolved item references, and groups the timestamps in statusDates. Returns 404 if an item or either location does not exist.
| sourceLocationId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| destinationLocationId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
required | Array of objects non-empty |
| status | string Enum: "Planning" "In Transit" "Completed" |
object | |
| notes | string or null <= 2000 characters |
{- "sourceLocationId": "string",
- "destinationLocationId": "string",
- "items": [
- {
- "itemKind": "material",
- "itemId": "string",
- "quantity": 0
}
], - "status": "Planning",
- "statusDates": {
- "planning": "2019-08-24",
- "inTransit": "2019-08-24",
- "completed": "2019-08-24"
}, - "notes": "string"
}{- "id": "string",
- "code": "string",
- "sourceLocation": {
- "id": "string",
- "name": "string"
}, - "destinationLocation": {
- "id": "string",
- "name": "string"
}, - "items": [
- {
- "id": "string",
- "item": {
- "kind": "material",
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string"
}
], - "status": "Planning",
- "statusDates": {
- "planning": "string",
- "inTransit": "string",
- "completed": "string"
}, - "notes": "string",
- "createdAt": "string",
- "updatedAt": "string"
}List one page of transfers, newest first by default. Each row embeds its source and destination locations ({ id, name }) and nests its moved items (each { id, item: { kind, id, code, name }, quantity }), the same shape as the detail. Every filter is optional and AND-ed: itemKind (material or sku) and itemId both match on the LINES, so they return every transfer carrying such a line and a transfer that mixes kinds matches either; the CSV multi-selects status (Planning, In Transit, Completed), sourceLocationId and destinationLocationId (each OR-ed within itself); and the per-status date filters, one inclusive YYYY-MM-DD From/To pair per status key (planningFrom/planningTo, inTransitFrom/inTransitTo, completedFrom/completedTo), each bounding that status's own date. search is a case-insensitive substring matched against the transfer's TFR code, any line's item code and name, both location names, and notes. Sort with sort (number, item, sourceLocation, destinationLocation, status, createdAt, updatedAt; item sorts by the alphabetically first line's code; default createdAt descending; empty values always last) and order (asc/desc); page with limit (default 50, max 200) and offset. Paging counts transfers, not lines, so meta.total counts the filtered documents.
| sort | string Enum: "number" "item" "sourceLocation" "destinationLocation" "status" "createdAt" "updatedAt" |
| order | string Enum: "asc" "desc" |
| limit | integer [ 1 .. 200 ] |
| offset | integer >= 0 |
| itemKind | string Enum: "material" "sku" |
| itemId | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| status | string |
| sourceLocationId | string |
| destinationLocationId | string |
| search | string |
| planningFrom | string^\d{4}-\d{2}-\d{2}$ |
| planningTo | string^\d{4}-\d{2}-\d{2}$ |
| inTransitFrom | string^\d{4}-\d{2}-\d{2}$ |
| inTransitTo | string^\d{4}-\d{2}-\d{2}$ |
| completedFrom | string^\d{4}-\d{2}-\d{2}$ |
| completedTo | string^\d{4}-\d{2}-\d{2}$ |
{- "data": [
- {
- "id": "string",
- "code": "string",
- "sourceLocation": {
- "id": "string",
- "name": "string"
}, - "destinationLocation": {
- "id": "string",
- "name": "string"
}, - "items": [
- {
- "id": "string",
- "item": {
- "kind": "material",
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string"
}
], - "status": "Planning",
- "statusDates": {
- "planning": "string",
- "inTransit": "string",
- "completed": "string"
}, - "notes": "string",
- "createdAt": "string",
- "updatedAt": "string"
}
], - "meta": {
- "total": 0,
- "limit": 0,
- "offset": 0
}
}Update a transfer. A status change moves it through the Planning -> In Transit -> Completed machine, projecting every line's outbound leg on In Transit and its inbound RECEIVE on Completed (a step back unwinds the legs it added). The statusDates object merges PER KEY (only the sent keys change); a date edit re-dates the matching ledger leg of every line, and a reached status's date cannot be cleared (422). The items and the locations are editable only while the transfer is still in Planning: sent together with a status change, the edits apply first, then the transition runs. Unlike statusDates, items does not merge: it is the movement, so a sent array REPLACES the whole line set, needs at least one line, and may not repeat an item. Past Planning items and locations are edit-locked and rejected with 409 invalid_transition, as is an illegal transition.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| sourceLocationId | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| destinationLocationId | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
Array of objects non-empty | |
| status | string Enum: "Planning" "In Transit" "Completed" |
object | |
| notes | string or null <= 2000 characters |
{- "sourceLocationId": "string",
- "destinationLocationId": "string",
- "items": [
- {
- "itemKind": "material",
- "itemId": "string",
- "quantity": 0
}
], - "status": "Planning",
- "statusDates": {
- "planning": "2019-08-24",
- "inTransit": "2019-08-24",
- "completed": "2019-08-24"
}, - "notes": "string"
}{- "id": "string",
- "code": "string",
- "sourceLocation": {
- "id": "string",
- "name": "string"
}, - "destinationLocation": {
- "id": "string",
- "name": "string"
}, - "items": [
- {
- "id": "string",
- "item": {
- "kind": "material",
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string"
}
], - "status": "Planning",
- "statusDates": {
- "planning": "string",
- "inTransit": "string",
- "completed": "string"
}, - "notes": "string",
- "createdAt": "string",
- "updatedAt": "string"
}Permanently delete a transfer and its ledger movement (both legs of every moved item, on whichever ledgers they landed). There is no in-use gate: on-hand and in-transit recompute live from the remaining ledger rows.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "deletedId": "string"
}Fetch a single transfer by id, with its source and destination location references ({ id, name }), its moved items (each { id, item: { kind, id, code, name }, quantity }, in item-code order), status, and the per-status timestamps grouped in statusDates.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "id": "string",
- "code": "string",
- "sourceLocation": {
- "id": "string",
- "name": "string"
}, - "destinationLocation": {
- "id": "string",
- "name": "string"
}, - "items": [
- {
- "id": "string",
- "item": {
- "kind": "material",
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string"
}
], - "status": "Planning",
- "statusDates": {
- "planning": "string",
- "inTransit": "string",
- "completed": "string"
}, - "notes": "string",
- "createdAt": "string",
- "updatedAt": "string"
}List the transfer status vocabulary (Planning, In Transit, Completed) as a plain string array, in machine order. These are the legal status values for create, for update, and for the list's status filter.
{- "data": [
- "string"
]
}Create a sales order header and its line items in one call. It opens in the given order status, stamping the matching per-status date and projecting the DEMAND/ALLOCATE rows into the finished-goods ledger. soNumber is optional: omit it to auto-generate the next canonical number, or supply your own. Returns 404 if the customer, order channel, fulfillment location, broker, or any referenced SKU does not exist, and 409 if the supplied soNumber is already taken.
| customerId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| channelId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| status required | string Enum: "Planning" "Placed" "In Transit" "Invoiced" "Paid" "Rejected" "In-Dispute" "Completed" |
| fulfillmentStatus required | string Enum: "Pending" "Partial" "Fulfilled" |
| fulfillmentLocationId | string or null^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| brokerId | string or null^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| brokerFeePercent | number or null |
| brokerFees | number >= 0 |
| fulfillmentCosts | number >= 0 |
| freightCosts | number >= 0 |
| promoCosts | number >= 0 |
| otherCosts | number >= 0 |
| mustArriveBy required | string <date> |
| orderDate | string <date> |
object | |
| notes | string or null <= 2000 characters |
| qboId | string or null <= 255 characters |
| soNumber | string [ 1 .. 255 ] characters |
required | Array of objects |
{- "customerId": "string",
- "channelId": "string",
- "status": "Planning",
- "fulfillmentStatus": "Pending",
- "fulfillmentLocationId": "string",
- "brokerId": "string",
- "brokerFeePercent": 0,
- "brokerFees": 0,
- "fulfillmentCosts": 0,
- "freightCosts": 0,
- "promoCosts": 0,
- "otherCosts": 0,
- "mustArriveBy": "2019-08-24",
- "orderDate": "2019-08-24",
- "statusDates": {
- "planning": "2019-08-24",
- "placed": "2019-08-24",
- "inTransit": "2019-08-24",
- "invoiced": "2019-08-24",
- "paid": "2019-08-24",
- "rejected": "2019-08-24",
- "inDispute": "2019-08-24",
- "completed": "2019-08-24"
}, - "notes": "string",
- "qboId": "string",
- "soNumber": "string",
- "items": [
- {
- "skuId": "string",
- "quantity": 1,
- "unitPrice": 0
}
]
}{- "id": "string",
- "soNumber": "string",
- "customer": {
- "id": "string",
- "name": "string"
}, - "channel": {
- "id": "string",
- "name": "string"
}, - "orderStatus": "Planning",
- "fulfillmentStatus": "Pending",
- "fulfillmentLocation": {
- "id": "string",
- "name": "string"
}, - "broker": {
- "id": "string",
- "name": "string",
- "feePercent": "string"
}, - "orderDate": "string",
- "mustArriveBy": "string",
- "statusDates": {
- "planning": "string",
- "placed": "string",
- "inTransit": "string",
- "invoiced": "string",
- "paid": "string",
- "rejected": "string",
- "inDispute": "string",
- "completed": "string"
}, - "financials": {
- "fulfillmentCosts": 0,
- "freightCosts": 0,
- "promoCosts": 0,
- "otherCosts": 0,
- "brokerFees": 0,
- "revenue": 0,
- "inventoryCost": 0,
- "grossProfit": 0,
- "profit": 0,
- "margin": 0
}, - "notes": "string",
- "qboId": "string",
- "createdAt": "string",
- "updatedAt": "string",
- "items": [
- {
- "id": "string",
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": 0,
- "unitPrice": "string"
}
]
}List one page of sales order headers, each with the customer, channel, fulfillment location and broker embedded as references, the per-status dates in statusDates, and the derived financials rollup. Every filter is optional and AND-ed: the CSV multi-selects (status, customerId, channelId, fulfillmentLocationId, brokerId; values within one OR together), single-valued fulfillmentStatus, containsSkuId (orders with a line item for that SKU), and the inclusive From/To calendar-date pairs: orderDateFrom/orderDateTo, mustArriveByFrom/mustArriveByTo, plus one pair per statusDates key (e.g. placedFrom/placedTo). search is a case-insensitive substring over the SO number, customer name, channel name, fulfillment location name, notes, and line-item SKU codes and names. Filter by order value with inclusive revenueMin/revenueMax (the line-sum revenue). Sort with sort (number, customer, channel, fulfillmentLocation, status, orderDate, mustArriveBy, revenue, createdAt, updatedAt; default orderDate descending, newest first; empty values always last) and order (asc/desc); page with limit (default 50, max 200) and offset. meta.total counts the filtered set. The engine-derived financials (inventory cost, gross profit, profit and margin) appear on single-order reads only, so pagers can render page counts under active filters. Line items are nested per order on the detail endpoint.
| sort | string Enum: "number" "customer" "channel" "fulfillmentLocation" "status" "orderDate" "mustArriveBy" "createdAt" "updatedAt" "revenue" |
| order | string Enum: "asc" "desc" |
| limit | integer [ 1 .. 200 ] |
| offset | integer >= 0 |
| status | string |
| fulfillmentStatus | string Enum: "Pending" "Partial" "Fulfilled" |
| customerId | string |
| channelId | string |
| fulfillmentLocationId | string |
| brokerId | string |
| containsSkuId | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| revenueMin | number >= 0 |
| revenueMax | number >= 0 |
| search | string |
| orderDateFrom | string^\d{4}-\d{2}-\d{2}$ |
| orderDateTo | string^\d{4}-\d{2}-\d{2}$ |
| mustArriveByFrom | string^\d{4}-\d{2}-\d{2}$ |
| mustArriveByTo | string^\d{4}-\d{2}-\d{2}$ |
| planningFrom | string^\d{4}-\d{2}-\d{2}$ |
| planningTo | string^\d{4}-\d{2}-\d{2}$ |
| placedFrom | string^\d{4}-\d{2}-\d{2}$ |
| placedTo | string^\d{4}-\d{2}-\d{2}$ |
| inTransitFrom | string^\d{4}-\d{2}-\d{2}$ |
| inTransitTo | string^\d{4}-\d{2}-\d{2}$ |
| invoicedFrom | string^\d{4}-\d{2}-\d{2}$ |
| invoicedTo | string^\d{4}-\d{2}-\d{2}$ |
| paidFrom | string^\d{4}-\d{2}-\d{2}$ |
| paidTo | string^\d{4}-\d{2}-\d{2}$ |
| rejectedFrom | string^\d{4}-\d{2}-\d{2}$ |
| rejectedTo | string^\d{4}-\d{2}-\d{2}$ |
| inDisputeFrom | string^\d{4}-\d{2}-\d{2}$ |
| inDisputeTo | string^\d{4}-\d{2}-\d{2}$ |
| completedFrom | string^\d{4}-\d{2}-\d{2}$ |
| completedTo | string^\d{4}-\d{2}-\d{2}$ |
{- "data": [
- {
- "id": "string",
- "soNumber": "string",
- "customer": {
- "id": "string",
- "name": "string"
}, - "channel": {
- "id": "string",
- "name": "string"
}, - "orderStatus": "Planning",
- "fulfillmentStatus": "Pending",
- "fulfillmentLocation": {
- "id": "string",
- "name": "string"
}, - "broker": {
- "id": "string",
- "name": "string",
- "feePercent": "string"
}, - "orderDate": "string",
- "mustArriveBy": "string",
- "statusDates": {
- "planning": "string",
- "placed": "string",
- "inTransit": "string",
- "invoiced": "string",
- "paid": "string",
- "rejected": "string",
- "inDispute": "string",
- "completed": "string"
}, - "financials": {
- "fulfillmentCosts": 0,
- "freightCosts": 0,
- "promoCosts": 0,
- "otherCosts": 0,
- "brokerFees": 0,
- "revenue": 0
}, - "notes": "string",
- "qboId": "string",
- "createdAt": "string",
- "updatedAt": "string"
}
], - "meta": {
- "total": 0,
- "limit": 0,
- "offset": 0
}
}Update a sales order's header fields, status, or per-status dates (sent in the statusDates object, merged per key). A status change reconciles the finished-goods ledger to the new status (forward transitions add DEMAND/ALLOCATE/CONSUME rows, backward or terminal ones reverse them), and a pure date edit re-dates the active ledger rows. customerId, channelId and fulfillmentLocationId are editable only while the order is in Planning. Returns 409 if a supplied soNumber collides with another order, or if a structural field is edited after the order has left Planning.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| soNumber | string or null non-empty |
| customerId | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| channelId | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| fulfillmentLocationId | string or null^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| status | string Enum: "Planning" "Placed" "In Transit" "Invoiced" "Paid" "Rejected" "In-Dispute" "Completed" |
| fulfillmentStatus | string Enum: "Pending" "Partial" "Fulfilled" |
| orderDate | string or null <date> |
| mustArriveBy | string <date> |
object | |
| brokerId | string or null^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| brokerFeePercent | number or null |
| brokerFees | number >= 0 |
| fulfillmentCosts | number >= 0 |
| freightCosts | number >= 0 |
| promoCosts | number >= 0 |
| otherCosts | number >= 0 |
| notes | string or null <= 2000 characters |
| qboId | string or null <= 255 characters |
{- "soNumber": "string",
- "customerId": "string",
- "channelId": "string",
- "fulfillmentLocationId": "string",
- "status": "Planning",
- "fulfillmentStatus": "Pending",
- "orderDate": "2019-08-24",
- "mustArriveBy": "2019-08-24",
- "statusDates": {
- "planning": "2019-08-24",
- "placed": "2019-08-24",
- "inTransit": "2019-08-24",
- "invoiced": "2019-08-24",
- "paid": "2019-08-24",
- "rejected": "2019-08-24",
- "inDispute": "2019-08-24",
- "completed": "2019-08-24"
}, - "brokerId": "string",
- "brokerFeePercent": 0,
- "brokerFees": 0,
- "fulfillmentCosts": 0,
- "freightCosts": 0,
- "promoCosts": 0,
- "otherCosts": 0,
- "notes": "string",
- "qboId": "string"
}{- "id": "string",
- "soNumber": "string",
- "customer": {
- "id": "string",
- "name": "string"
}, - "channel": {
- "id": "string",
- "name": "string"
}, - "orderStatus": "Planning",
- "fulfillmentStatus": "Pending",
- "fulfillmentLocation": {
- "id": "string",
- "name": "string"
}, - "broker": {
- "id": "string",
- "name": "string",
- "feePercent": "string"
}, - "orderDate": "string",
- "mustArriveBy": "string",
- "statusDates": {
- "planning": "string",
- "placed": "string",
- "inTransit": "string",
- "invoiced": "string",
- "paid": "string",
- "rejected": "string",
- "inDispute": "string",
- "completed": "string"
}, - "financials": {
- "fulfillmentCosts": 0,
- "freightCosts": 0,
- "promoCosts": 0,
- "otherCosts": 0,
- "brokerFees": 0,
- "revenue": 0
}, - "notes": "string",
- "qboId": "string",
- "createdAt": "string",
- "updatedAt": "string"
}Permanently delete a sales order. Its items, timeline notes, and projected ledger events cascade away, and on-hand recomputes live from the remaining ledger rows. There is no in-use gate.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "deletedId": "string"
}Get one sales order as a composite: the header at the top level (customer, channel, fulfillment location and broker embedded as references, the per-status dates in statusDates), the derived financials (revenue, inventory cost, gross profit, and margin), and its line items nested under items, each with its SKU embedded.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "id": "string",
- "soNumber": "string",
- "customer": {
- "id": "string",
- "name": "string"
}, - "channel": {
- "id": "string",
- "name": "string"
}, - "orderStatus": "Planning",
- "fulfillmentStatus": "Pending",
- "fulfillmentLocation": {
- "id": "string",
- "name": "string"
}, - "broker": {
- "id": "string",
- "name": "string",
- "feePercent": "string"
}, - "orderDate": "string",
- "mustArriveBy": "string",
- "statusDates": {
- "planning": "string",
- "placed": "string",
- "inTransit": "string",
- "invoiced": "string",
- "paid": "string",
- "rejected": "string",
- "inDispute": "string",
- "completed": "string"
}, - "financials": {
- "fulfillmentCosts": 0,
- "freightCosts": 0,
- "promoCosts": 0,
- "otherCosts": 0,
- "brokerFees": 0,
- "revenue": 0,
- "inventoryCost": 0,
- "grossProfit": 0,
- "profit": 0,
- "margin": 0
}, - "notes": "string",
- "qboId": "string",
- "createdAt": "string",
- "updatedAt": "string",
- "items": [
- {
- "id": "string",
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": 0,
- "unitPrice": "string"
}
]
}Import many sales orders in one atomic call, used by the Data page. Each row creates an order with an auto-generated number, its items, and the projected ledger entries for its status. The whole batch rolls back if any row fails. Admins and owners only. Returns 404 if a row references a customer, channel, location, broker, or SKU that does not exist.
required | Array of objects | ||||||||||||||||||||||||||||||||||||
Array
| |||||||||||||||||||||||||||||||||||||
{- "rows": [
- {
- "customerId": "string",
- "channelId": "string",
- "status": "Planning",
- "fulfillmentStatus": "Pending",
- "fulfillmentLocationId": "string",
- "brokerId": "string",
- "brokerFeePercent": 0,
- "brokerFees": 0,
- "fulfillmentCosts": 0,
- "freightCosts": 0,
- "promoCosts": 0,
- "otherCosts": 0,
- "mustArriveBy": "2019-08-24",
- "orderDate": "2019-08-24",
- "statusDates": {
- "planning": "2019-08-24",
- "placed": "2019-08-24",
- "inTransit": "2019-08-24",
- "invoiced": "2019-08-24",
- "paid": "2019-08-24",
- "rejected": "2019-08-24",
- "inDispute": "2019-08-24",
- "completed": "2019-08-24"
}, - "notes": "string",
- "qboId": "string",
- "items": [
- {
- "skuId": "string",
- "quantity": 1,
- "unitPrice": 0
}
]
}
]
}{- "ids": [
- "string"
]
}Add a SKU line item to an existing sales order, named by salesOrderId in the body. Adding to an ordered SO projects the matching DEMAND/ALLOCATE ledger rows for the new line. Returns 404 if the sales order or the SKU does not exist, and 409 if the SKU is already a line on this order.
| skuId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| quantity required | integer >= 1 |
| unitPrice | number >= 0 |
| salesOrderId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "skuId": "string",
- "quantity": 1,
- "unitPrice": 0,
- "salesOrderId": "string"
}{- "id": "string",
- "salesOrderId": "string",
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": 0,
- "unitPrice": "string"
}List sales order line items. Pass salesOrderId to scope the list to one order, or omit it to read lines across every order (a flat items view); skuId narrows by the sold SKU. An unknown salesOrderId returns an empty list rather than a 404.
| salesOrderId | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| skuId | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "data": [
- {
- "id": "string",
- "salesOrderId": "string",
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": 0,
- "unitPrice": "string"
}
]
}Update a sales order line item (SKU, quantity, or unit price). A SKU or quantity change re-projects the line's DEMAND/ALLOCATE/CONSUME ledger rows to the order's current status; a price-only change leaves the ledger untouched.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| skuId | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| quantity | integer >= 1 |
| unitPrice | number >= 0 |
{- "skuId": "string",
- "quantity": 1,
- "unitPrice": 0
}{- "id": "string",
- "salesOrderId": "string",
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": 0,
- "unitPrice": "string"
}Remove a line item from its sales order, unwinding the DEMAND/ALLOCATE ledger rows the line projected.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "deletedId": "string"
}Create a direct-to-consumer sales receipt with its line items in one call. The customer is typed text (a name plus an optional source-system reference), not an entity link, and the acquisition source is required. It opens in the given status (defaults to Placed), stamping the matching per-status date and projecting the DEMAND/ALLOCATE/CONSUME rows into the finished-goods ledger; item quantities are decimal cases. srNumber is optional: omit it to auto-generate the next canonical number, or supply your own. Returns 404 if the order channel, acquisition source, ship-from location, or any referenced SKU does not exist, and 409 if the supplied srNumber is already taken.
| customerName | string or null <= 255 characters |
| customerRef | string or null <= 255 characters |
| isSubscription | boolean |
| isRecurring | boolean |
| channelId | string or null^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| acquisitionSourceId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| status | string Enum: "Placed" "Paid" "Shipped" "Completed" "Cancelled" |
| locationId | string or null^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| fulfillmentCosts | number >= 0 |
| shippingCosts | number >= 0 |
| merchantCosts | number >= 0 |
| otherCosts | number >= 0 |
| orderDate | string <date> |
object | |
| notes | string or null <= 2000 characters |
| srNumber | string [ 1 .. 255 ] characters |
required | Array of objects non-empty |
{- "customerName": "string",
- "customerRef": "string",
- "isSubscription": true,
- "isRecurring": true,
- "channelId": "string",
- "acquisitionSourceId": "string",
- "status": "Placed",
- "locationId": "string",
- "fulfillmentCosts": 0,
- "shippingCosts": 0,
- "merchantCosts": 0,
- "otherCosts": 0,
- "orderDate": "2019-08-24",
- "statusDates": {
- "placed": "2019-08-24",
- "paid": "2019-08-24",
- "shipped": "2019-08-24",
- "completed": "2019-08-24",
- "cancelled": "2019-08-24"
}, - "notes": "string",
- "srNumber": "string",
- "items": [
- {
- "skuId": "string",
- "quantity": 0,
- "unitPrice": 0
}
]
}{- "id": "string",
- "customerName": "string",
- "customerRef": "string",
- "isSubscription": true,
- "isRecurring": true,
- "srNumber": "string",
- "channel": {
- "id": "string",
- "name": "string"
}, - "acquisitionSource": {
- "id": "string",
- "name": "string"
}, - "status": "Placed",
- "location": {
- "id": "string",
- "name": "string"
}, - "financials": {
- "fulfillmentCosts": 0,
- "shippingCosts": 0,
- "merchantCosts": 0,
- "otherCosts": 0,
- "revenue": 0,
- "inventoryCost": 0,
- "grossProfit": 0,
- "profit": 0,
- "margin": 0
}, - "orderDate": "string",
- "statusDates": {
- "placed": "string",
- "paid": "string",
- "shipped": "string",
- "completed": "string",
- "cancelled": "string"
}, - "notes": "string",
- "createdAt": "string",
- "updatedAt": "string",
- "items": [
- {
- "id": "string",
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string",
- "unitPrice": "string"
}
]
}List one page of sales receipt headers, each with the channel, acquisition source and location embedded as references, the per-status dates in statusDates, and the derived financials rollup. Every filter is optional and AND-ed: the CSV multi-selects (status, channelId, acquisitionSourceId, locationId; values within one OR together), containsSkuId (receipts with a line item for that SKU), and the inclusive From/To calendar-date pairs: orderDateFrom/orderDateTo plus one pair per statusDates key (e.g. placedFrom/placedTo). search is a case-insensitive substring over the SR number, the typed customer name and external ref, notes, channel name, acquisition-source name, location name, and line-item SKU codes and names. Sort with sort (number, customerName, channel, acquisitionSource, location, status, orderDate, createdAt, updatedAt; default orderDate descending, newest first; empty values always last) and order (asc/desc); page with limit (default 50, max 200) and offset. meta.total counts the filtered set. The engine-derived financials (inventory cost, gross profit, profit and margin) appear on single-order reads only, so pagers can render page counts under active filters. Line items are nested per receipt on the detail endpoint.
| sort | string Enum: "number" "customerName" "channel" "acquisitionSource" "location" "status" "orderDate" "createdAt" "updatedAt" "netRevenue" |
| order | string Enum: "asc" "desc" |
| limit | integer [ 1 .. 200 ] |
| offset | integer >= 0 |
| status | string |
| channelId | string |
| acquisitionSourceId | string |
| locationId | string |
| containsSkuId | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| netRevenueMin | number |
| netRevenueMax | number |
| search | string |
| orderDateFrom | string^\d{4}-\d{2}-\d{2}$ |
| orderDateTo | string^\d{4}-\d{2}-\d{2}$ |
| placedFrom | string^\d{4}-\d{2}-\d{2}$ |
| placedTo | string^\d{4}-\d{2}-\d{2}$ |
| paidFrom | string^\d{4}-\d{2}-\d{2}$ |
| paidTo | string^\d{4}-\d{2}-\d{2}$ |
| shippedFrom | string^\d{4}-\d{2}-\d{2}$ |
| shippedTo | string^\d{4}-\d{2}-\d{2}$ |
| completedFrom | string^\d{4}-\d{2}-\d{2}$ |
| completedTo | string^\d{4}-\d{2}-\d{2}$ |
| cancelledFrom | string^\d{4}-\d{2}-\d{2}$ |
| cancelledTo | string^\d{4}-\d{2}-\d{2}$ |
{- "data": [
- {
- "id": "string",
- "customerName": "string",
- "customerRef": "string",
- "isSubscription": true,
- "isRecurring": true,
- "srNumber": "string",
- "channel": {
- "id": "string",
- "name": "string"
}, - "acquisitionSource": {
- "id": "string",
- "name": "string"
}, - "status": "Placed",
- "location": {
- "id": "string",
- "name": "string"
}, - "financials": {
- "fulfillmentCosts": 0,
- "shippingCosts": 0,
- "merchantCosts": 0,
- "otherCosts": 0,
- "revenue": 0
}, - "orderDate": "string",
- "statusDates": {
- "placed": "string",
- "paid": "string",
- "shipped": "string",
- "completed": "string",
- "cancelled": "string"
}, - "notes": "string",
- "createdAt": "string",
- "updatedAt": "string"
}
], - "meta": {
- "total": 0,
- "limit": 0,
- "offset": 0
}
}Update a sales receipt's header fields, status, or per-status dates (sent in the statusDates object, merged per key). A status change reconciles the finished-goods ledger to the new status, and a pure date edit re-dates the active ledger rows. The channel, acquisition source and location stay editable throughout (a receipt has no draft stage). Returns 404 if a supplied channel, acquisition source, or location does not exist, and 409 if a supplied srNumber collides with another receipt.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| srNumber | string or null non-empty |
| customerName | string or null <= 255 characters |
| customerRef | string or null <= 255 characters |
| isSubscription | boolean |
| isRecurring | boolean |
| channelId | string or null^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| acquisitionSourceId | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| status | string Enum: "Placed" "Paid" "Shipped" "Completed" "Cancelled" |
| locationId | string or null^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| orderDate | string or null <date> |
object | |
| fulfillmentCosts | number >= 0 |
| shippingCosts | number >= 0 |
| merchantCosts | number >= 0 |
| otherCosts | number >= 0 |
| notes | string or null <= 2000 characters |
{- "srNumber": "string",
- "customerName": "string",
- "customerRef": "string",
- "isSubscription": true,
- "isRecurring": true,
- "channelId": "string",
- "acquisitionSourceId": "string",
- "status": "Placed",
- "locationId": "string",
- "orderDate": "2019-08-24",
- "statusDates": {
- "placed": "2019-08-24",
- "paid": "2019-08-24",
- "shipped": "2019-08-24",
- "completed": "2019-08-24",
- "cancelled": "2019-08-24"
}, - "fulfillmentCosts": 0,
- "shippingCosts": 0,
- "merchantCosts": 0,
- "otherCosts": 0,
- "notes": "string"
}{- "id": "string",
- "customerName": "string",
- "customerRef": "string",
- "isSubscription": true,
- "isRecurring": true,
- "srNumber": "string",
- "channel": {
- "id": "string",
- "name": "string"
}, - "acquisitionSource": {
- "id": "string",
- "name": "string"
}, - "status": "Placed",
- "location": {
- "id": "string",
- "name": "string"
}, - "financials": {
- "fulfillmentCosts": 0,
- "shippingCosts": 0,
- "merchantCosts": 0,
- "otherCosts": 0,
- "revenue": 0
}, - "orderDate": "string",
- "statusDates": {
- "placed": "string",
- "paid": "string",
- "shipped": "string",
- "completed": "string",
- "cancelled": "string"
}, - "notes": "string",
- "createdAt": "string",
- "updatedAt": "string"
}Permanently delete a sales receipt. Its items and projected ledger rows are removed, and on-hand recomputes live from the remaining ledger. There is no in-use gate.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "deletedId": "string"
}Get one sales receipt as a composite: the header at the top level (channel, acquisition source and location embedded as references, the per-status dates in statusDates), the derived financials (revenue, inventory cost, gross profit, and margin), and its line items nested under items, each with its SKU embedded.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "id": "string",
- "customerName": "string",
- "customerRef": "string",
- "isSubscription": true,
- "isRecurring": true,
- "srNumber": "string",
- "channel": {
- "id": "string",
- "name": "string"
}, - "acquisitionSource": {
- "id": "string",
- "name": "string"
}, - "status": "Placed",
- "location": {
- "id": "string",
- "name": "string"
}, - "financials": {
- "fulfillmentCosts": 0,
- "shippingCosts": 0,
- "merchantCosts": 0,
- "otherCosts": 0,
- "revenue": 0,
- "inventoryCost": 0,
- "grossProfit": 0,
- "profit": 0,
- "margin": 0
}, - "orderDate": "string",
- "statusDates": {
- "placed": "string",
- "paid": "string",
- "shipped": "string",
- "completed": "string",
- "cancelled": "string"
}, - "notes": "string",
- "createdAt": "string",
- "updatedAt": "string",
- "items": [
- {
- "id": "string",
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string",
- "unitPrice": "string"
}
]
}Add a SKU line item (quantity in decimal cases, optional per-case price) to an existing sales receipt, named by salesReceiptId in the body, projecting the matching finished-goods ledger rows for the new line. Returns 404 if the sales receipt or the SKU does not exist, and 409 if the SKU is already a line on this receipt.
| skuId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| quantity required | number > 0 |
| unitPrice | number >= 0 |
| salesReceiptId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "skuId": "string",
- "quantity": 0,
- "unitPrice": 0,
- "salesReceiptId": "string"
}{- "id": "string",
- "salesReceiptId": "string",
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string",
- "unitPrice": "string"
}List MANUAL sales receipt line items. Pass salesReceiptId to scope the list to one receipt, or omit it to read lines across every manual receipt (a flat items view); skuId narrows by the sold SKU. Synced marketplace lines are excluded here, exactly like the manual header list: they are served by the connected receipt endpoints. An unknown salesReceiptId returns an empty list rather than a 404.
| salesReceiptId | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| skuId | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "data": [
- {
- "id": "string",
- "salesReceiptId": "string",
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string",
- "unitPrice": "string"
}
]
}Update a sales receipt line item (SKU, decimal-case quantity, or per-case price). A SKU or quantity change re-projects the line's finished-goods ledger rows to the receipt's current status; a price-only change leaves the ledger untouched.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| skuId | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| quantity | number > 0 |
| unitPrice | number >= 0 |
{- "skuId": "string",
- "quantity": 0,
- "unitPrice": 0
}{- "id": "string",
- "salesReceiptId": "string",
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string",
- "unitPrice": "string"
}Remove a line item from its sales receipt, unwinding the finished-goods ledger rows the line projected.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "deletedId": "string"
}List one page of the org's connected sales receipts: marketplace orders (Shopify and Amazon Seller Central, whichever feeds the org has connected) served from the rows the sync engine materializes, federated into one list, newest first. Receipts before the org's connected go-live date are stored but not served here (#1252): the date is system-wide, so display and inventory always agree; a null date serves everything. Filter by derived status (CSV of Placed/Paid/Shipped/Completed/Cancelled), by order date (orderDateFrom/orderDateTo, inclusive calendar dates, floored at the go-live date), by source (CSV of shopify/amazon), by fulfillmentLocation (CSV of the display names from list_connected_receipt_locations), and by inclusive netRevenueMin/netRevenueMax bounds; all apply in SQL before pagination. Sort with sort (orderDate, srNumber, status, source, netRevenue, fulfillmentLocation) and order; status sorts by lifecycle rank. search matches the order number, the buyer name (Shopify), and the line SKUs and listing titles. The surface is read-only (the sync owns the writes), and rows are addressed by the marketplace's own order id rather than a Pharus UUID (each row's source says which feed it came from). Page with limit (default 50, max 200) and offset; meta.total counts the filtered set. configured is false when the org has no connected feed, meaning the surface does not apply rather than being empty. netRevenue is merchandise net of discounts, excluding shipping and tax; dataAsOf is the marketplace-extraction watermark for the row.
| search | string |
| status | string |
| source | string |
| fulfillmentLocation | string |
| netRevenueMin | number |
| netRevenueMax | number |
| sort | string Enum: "orderDate" "srNumber" "status" "source" "netRevenue" "fulfillmentLocation" |
| order | string Enum: "asc" "desc" |
| limit | integer [ 1 .. 200 ] |
| offset | integer >= 0 |
| orderDateFrom | string^\d{4}-\d{2}-\d{2}$ |
| orderDateTo | string^\d{4}-\d{2}-\d{2}$ |
{- "data": [
- {
- "id": "string",
- "srNumber": "string",
- "source": "shopify",
- "status": "Placed",
- "orderDate": "string",
- "customerName": "string",
- "fulfillmentLocation": "string",
- "fulfillmentLocationKey": "string",
- "location": {
- "id": "string",
- "name": "string"
}, - "netRevenue": 0,
- "dataAsOf": "string"
}
], - "meta": {
- "total": 0
}, - "configured": true
}Get one connected sales receipt by its external (marketplace) order id, served from the rows the sync engine materializes; the id's shape routes the read (Amazon order ids are xxx-xxxxxxx-xxxxxxx, Shopify's plain integers). A receipt before the org's connected go-live date is not served (404, like the list excluding it): the date is system-wide (#1252). The detail carries identity with the external ids and a best-effort admin deep link, the derived lifecycle with per-status dates and the marketplace's raw source status alongside, customer identity with the retention join key (Shopify; Amazon anonymizes buyers), destination and carrier, the full buyer-side money picture (list subtotal, discounts with codes, shipping charged, tax, total paid, a to-the-cent reconciliation flag, refunds, and processor fees when they exist), the source's line items exactly as sold (bundle-exploded duplicates included), and refund events with their restock linkage. When every line is mapped to a Pharus SKU the response also carries financials (inventory cost, gross profit, margin), computed on read from the costing engine and never stored; financials is null while any line is unmapped. 404 when the id matches nothing or the org has no connected feed.
| externalId required | string [ 1 .. 64 ] characters |
{- "id": "string",
- "srNumber": "string",
- "source": "shopify",
- "externalOrderNumber": "string",
- "externalOrderId": "string",
- "adminUrl": "string",
- "store": {
- "name": "string",
- "domain": "string"
}, - "dataAsOf": "string",
- "status": "Placed",
- "sourceStatus": "string",
- "completedByFallback": true,
- "statusDates": {
- "placed": "string",
- "paid": "string",
- "shipped": "string",
- "completed": "string",
- "cancelled": "string"
}, - "customer": {
- "name": "string",
- "ref": "string",
- "orderCount": 0
}, - "isSubscription": true,
- "isRecurring": true,
- "destination": {
- "city": "string",
- "state": "string",
- "country": "string"
}, - "shippingMethod": "string",
- "carrier": "string",
- "trackingNumber": "string",
- "fulfillmentLocation": "string",
- "fulfillmentLocationKey": "string",
- "location": {
- "id": "string",
- "name": "string"
}, - "money": {
- "currency": "string",
- "subtotalList": 0,
- "discounts": 0,
- "discountCodes": [
- "string"
], - "subtotalNet": 0,
- "shippingCharged": 0,
- "tax": 0,
- "totalPaid": 0,
- "reconciles": true,
- "refunded": 0,
- "fees": 0
}, - "items": [
- {
- "externalLineId": "string",
- "variantId": "string",
- "listingTitle": "string",
- "variantTitle": "string",
- "listingSku": "string",
- "quantity": 0,
- "unitPrice": 0,
- "lineDiscount": 0,
- "lineTotal": 0,
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}
}
], - "refunds": [
- {
- "externalId": "string",
- "date": "string",
- "note": "string",
- "restocked": true
}
], - "financials": {
- "inventoryCost": 0,
- "grossProfit": 0,
- "margin": 0
}
}The distinct fulfillment-location names appearing on the org's connected sales receipts, exactly as the list displays them (Shopify assigned fulfillment locations, Amazon fulfillment-channel labels), sorted. These are the valid values for the connected list's fulfillmentLocation filter; manual receipts' locations come from the locations domain instead. configured is false when the org has no data-plane connection.
{- "data": [
- "string"
], - "configured": true
}The location-mapping worksheet: every fulfillment location the feed's connected receipts reference on or after the connected go-live date (a null date scans everything), keyed by the store's stable identity (the Shopify location id, the Amazon fulfillment-channel code) with its display name, merged with the stored mappings to Pharus locations (null while unmapped). source picks the feed, shopify by default. Mapped locations overlay onto connected rows in the receipts list and detail, and the fulfillment-location filter matches mapped rows through them. configured is false when the org has no data-plane connection for that feed.
| source | string Enum: "shopify" "amazon" |
{- "data": [
- {
- "externalKey": "string",
- "externalName": "string",
- "location": {
- "id": "string",
- "name": "string"
}, - "receiptCount": 0
}
], - "configured": true
}Map one connected fulfillment location (by its stable externalKey from the worksheet) to a Pharus location, or clear the mapping by passing locationId null. Upserts: re-mapping replaces the previous mapping. The referenced location must exist in the org (404 otherwise). source says which feed, shopify by default.
| source | string Enum: "shopify" "amazon" |
| externalKey required | string [ 1 .. 64 ] characters |
| externalName | string or null <= 200 characters |
| locationId required | string or null^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "source": "shopify",
- "externalKey": "string",
- "externalName": "string",
- "locationId": "string"
}{- "externalKey": "string",
- "location": {
- "id": "string",
- "name": "string"
}
}The mapping-blocked summary behind the sales receipts page's alert badges: per feed and kind, how many marketplace listings and fulfillment locations are still unmapped, and how many stored receipt lines (with their unit total) or receipts they hold out of inventory. Scoped to the org's connected-ledger go-live window, so history that would never post is not counted. Map the pending entries in the SKU and location worksheets (list_connected_sku_mappings, list_connected_location_mappings); mapping backfills the ledger immediately.
{- "skus": [
- {
- "source": "shopify",
- "pendingListings": 0,
- "affectedLines": 0,
- "affectedUnits": 0
}
], - "locations": [
- {
- "source": "shopify",
- "pendingLocations": 0,
- "affectedReceipts": 0
}
]
}The sync health of one connected feed (source, shopify by default or amazon): when it last synced successfully, when it last tried, the stored failure message when the latest run errored (cleared on success), and where the initial history copy stands (done plus the oldest stored order day, or the day the walk has reached mid-copy). Reads the org's own sync bookkeeping; nothing here queries the data plane. configured is false when the org has no such feed.
| source | string Enum: "shopify" "amazon" |
{- "configured": true,
- "lastSyncedAt": "string",
- "lastAttemptAt": "string",
- "lastError": "string",
- "backfill": {
- "done": true,
- "from": "string",
- "reached": "string",
- "error": "string"
}
}The listing-to-SKU worksheet: one page of the distinct marketplace listings found on the org's stored connected order lines on or after the connected go-live date (retired pre-go-live products are not pending work; a null date scans everything) for one feed (source, shopify by default or amazon), merged with the stored mappings. Listings are identified by their store's stable key, the variant id on Shopify or the seller SKU on Amazon (both travel in variantId). Each row carries the listing's own sku code and titles, how much it sold, and the Pharus SKU it maps to (null when unmapped). Filter with search (case-insensitive substring over the listing sku, titles, and key) and mapped (false = still to map, true = already mapped); sort with sort (unitsSold, lineCount, listingSku, listingTitle; unitsSold descending by default) and order; page with limit (default 50, max 200) and offset, with meta.total counting the filtered set. configured is false when the org has no connected feed of that kind.
| source | string Enum: "shopify" "amazon" |
| search | string |
| mapped | string Enum: "true" "false" |
| sort | string Enum: "unitsSold" "lineCount" "listingSku" "listingTitle" |
| order | string Enum: "asc" "desc" |
| limit | integer [ 1 .. 200 ] |
| offset | integer >= 0 |
{- "data": [
- {
- "variantId": "string",
- "listingSku": "string",
- "listingTitle": "string",
- "variantTitle": "string",
- "lineCount": 0,
- "unitsSold": 0,
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}
}
], - "meta": {
- "total": 0,
- "limit": 0,
- "offset": 0
}, - "configured": true
}Map one marketplace listing to a Pharus SKU, or clear the mapping by passing skuId null. The listing key travels in variantId: the variant id for Shopify listings, the seller SKU for Amazon ones (source says which feed, shopify by default). Upserts: re-mapping a listing replaces its previous mapping. The referenced SKU must exist in the org (404 otherwise). Mapped SKUs appear on connected receipt detail lines.
| source | string Enum: "shopify" "amazon" |
| variantId required | string [ 1 .. 64 ] characters |
| listingSku | string or null <= 200 characters |
| listingTitle | string or null <= 500 characters |
| skuId required | string or null^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "source": "shopify",
- "variantId": "string",
- "listingSku": "string",
- "listingTitle": "string",
- "skuId": "string"
}{- "variantId": "string",
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}
}Create a sales return against a completed sales receipt: the header plus its lines, each targeting one receipt line with a partial quantity (capped at what remains returnable). It opens in the given status (defaults to Planning), and reaching Completed posts one restock RECEIVE per line into the finished-goods ledger at the restock location. The return stores no money; the restock value is derived from the originating receipt line on read. returnNumber is optional: omit it to auto-generate the next canonical number, or supply your own. Returns 404 if the sales receipt or restock location does not exist, 409 if the supplied returnNumber is already taken, and 422 if a line exceeds the returnable quantity or the receipt is not Completed.
| status | string Enum: "Planning" "Placed" "In Transit" "Completed" "Cancelled" |
| locationId | string or null^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| returnDate | string <date> |
object | |
| notes | string or null <= 2000 characters |
| salesReceiptId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| returnNumber | string [ 1 .. 255 ] characters |
required | Array of objects non-empty |
{- "status": "Planning",
- "locationId": "string",
- "returnDate": "2019-08-24",
- "statusDates": {
- "planning": "2019-08-24",
- "placed": "2019-08-24",
- "inTransit": "2019-08-24",
- "completed": "2019-08-24",
- "cancelled": "2019-08-24"
}, - "notes": "string",
- "salesReceiptId": "string",
- "returnNumber": "string",
- "items": [
- {
- "salesReceiptItemId": "string",
- "quantity": 0
}
]
}{- "id": "string",
- "salesReceipt": {
- "id": "string",
- "number": "string"
}, - "returnNumber": "string",
- "status": "Planning",
- "location": {
- "id": "string",
- "name": "string"
}, - "returnDate": "string",
- "statusDates": {
- "planning": "string",
- "placed": "string",
- "inTransit": "string",
- "completed": "string",
- "cancelled": "string"
}, - "notes": "string",
- "createdAt": "string",
- "updatedAt": "string",
- "items": [
- {
- "id": "string",
- "salesReceiptItemId": "string",
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string"
}
]
}List one page of sales return headers, each with the originating receipt embedded as { id, number }, the restock location as a reference, and the per-status dates in statusDates. Every filter is optional and AND-ed: the CSV multi-selects (status, salesReceiptId, locationId; values within one OR together), containsSkuId (returns with a line whose receipt line references that SKU), and the inclusive From/To calendar-date pairs: returnDateFrom/returnDateTo plus one pair per statusDates key (e.g. placedFrom/placedTo). search is a case-insensitive substring over the return number, the originating receipt's number, customer name and external ref, notes, and line-item SKU codes and names. Sort with sort (number, receiptNumber, customerName, restockLocation, status, returnDate, createdAt, updatedAt; default returnDate descending, newest first; empty values always last) and order (asc/desc); page with limit (default 50, max 200) and offset. meta.total counts the filtered set, so pagers can render page counts under active filters. Line items are nested per return on the detail endpoint.
| sort | string Enum: "number" "receiptNumber" "customerName" "restockLocation" "status" "returnDate" "createdAt" "updatedAt" |
| order | string Enum: "asc" "desc" |
| limit | integer [ 1 .. 200 ] |
| offset | integer >= 0 |
| status | string |
| salesReceiptId | string |
| locationId | string |
| containsSkuId | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| search | string |
| returnDateFrom | string^\d{4}-\d{2}-\d{2}$ |
| returnDateTo | string^\d{4}-\d{2}-\d{2}$ |
| planningFrom | string^\d{4}-\d{2}-\d{2}$ |
| planningTo | string^\d{4}-\d{2}-\d{2}$ |
| placedFrom | string^\d{4}-\d{2}-\d{2}$ |
| placedTo | string^\d{4}-\d{2}-\d{2}$ |
| inTransitFrom | string^\d{4}-\d{2}-\d{2}$ |
| inTransitTo | string^\d{4}-\d{2}-\d{2}$ |
| completedFrom | string^\d{4}-\d{2}-\d{2}$ |
| completedTo | string^\d{4}-\d{2}-\d{2}$ |
| cancelledFrom | string^\d{4}-\d{2}-\d{2}$ |
| cancelledTo | string^\d{4}-\d{2}-\d{2}$ |
{- "data": [
- {
- "id": "string",
- "salesReceipt": {
- "id": "string",
- "number": "string"
}, - "returnNumber": "string",
- "status": "Planning",
- "location": {
- "id": "string",
- "name": "string"
}, - "returnDate": "string",
- "statusDates": {
- "planning": "string",
- "placed": "string",
- "inTransit": "string",
- "completed": "string",
- "cancelled": "string"
}, - "notes": "string",
- "createdAt": "string",
- "updatedAt": "string"
}
], - "meta": {
- "total": 0,
- "limit": 0,
- "offset": 0
}
}Update a sales return's header fields, status, or per-status dates (sent in the statusDates object, merged per key). The originating receipt is fixed for life and cannot be changed. A status change reconciles the restock RECEIVE to the new status (posting it on Completed, unwinding it on a step back), and a pure date edit re-dates the active ledger rows. Returns 404 if a supplied restock location does not exist, and 409 if a supplied returnNumber collides with another return.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| returnNumber | string non-empty |
| status | string Enum: "Planning" "Placed" "In Transit" "Completed" "Cancelled" |
| locationId | string or null^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| returnDate | string or null <date> |
object | |
| notes | string or null <= 2000 characters |
{- "returnNumber": "string",
- "status": "Planning",
- "locationId": "string",
- "returnDate": "2019-08-24",
- "statusDates": {
- "planning": "2019-08-24",
- "placed": "2019-08-24",
- "inTransit": "2019-08-24",
- "completed": "2019-08-24",
- "cancelled": "2019-08-24"
}, - "notes": "string"
}{- "id": "string",
- "salesReceipt": {
- "id": "string",
- "number": "string"
}, - "returnNumber": "string",
- "status": "Planning",
- "location": {
- "id": "string",
- "name": "string"
}, - "returnDate": "string",
- "statusDates": {
- "planning": "string",
- "placed": "string",
- "inTransit": "string",
- "completed": "string",
- "cancelled": "string"
}, - "notes": "string",
- "createdAt": "string",
- "updatedAt": "string"
}Permanently delete a sales return. Its items and any restock ledger rows are removed, and on-hand recomputes live from the remaining ledger. There is no in-use gate.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "deletedId": "string"
}Get one sales return as a composite: the header at the top level (the originating receipt embedded as { id, number }, the restock location as a reference, the per-status dates in statusDates) and its line items nested under items, each carrying the SKU joined through the originating receipt line.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "id": "string",
- "salesReceipt": {
- "id": "string",
- "number": "string"
}, - "returnNumber": "string",
- "status": "Planning",
- "location": {
- "id": "string",
- "name": "string"
}, - "returnDate": "string",
- "statusDates": {
- "planning": "string",
- "placed": "string",
- "inTransit": "string",
- "completed": "string",
- "cancelled": "string"
}, - "notes": "string",
- "createdAt": "string",
- "updatedAt": "string",
- "items": [
- {
- "id": "string",
- "salesReceiptItemId": "string",
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string"
}
]
}Add a line to an existing sales return, named by salesReturnId in the body: a receipt line (salesReceiptItemId) plus the decimal-case quantity coming back, capped at that line's remaining returnable quantity. Returns 404 if the sales return or the receipt line does not exist, 409 if the receipt line is already on this return, and 422 if the quantity exceeds the cap or the receipt line does not belong to the return's receipt.
| salesReceiptItemId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| quantity required | number > 0 |
| salesReturnId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "salesReceiptItemId": "string",
- "quantity": 0,
- "salesReturnId": "string"
}{- "id": "string",
- "salesReturnId": "string",
- "salesReceiptItemId": "string",
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string"
}List sales return line items, each carrying the SKU joined through the originating receipt line. Pass salesReturnId to scope the list to one return, or omit it to read lines across every return; an unknown salesReturnId returns an empty list rather than a 404.
| salesReturnId | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "data": [
- {
- "id": "string",
- "salesReturnId": "string",
- "salesReceiptItemId": "string",
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string"
}
]
}Update a return line's quantity (re-pointing it to a different receipt line is a remove plus add). The new quantity is re-checked against the receipt line's remaining returnable cap, and the restock RECEIVE re-projects. Returns 422 if the quantity exceeds the cap.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| quantity | number > 0 |
{- "quantity": 0
}{- "id": "string",
- "salesReturnId": "string",
- "salesReceiptItemId": "string",
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string"
}Remove a line from its sales return, unwinding the restock RECEIVE ledger row it projected. Its quantity frees back up against the receipt line's returnable cap.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "deletedId": "string"
}Create an order channel (a lookup for where an order came from, e.g. Shopify, wholesale, direct). Admins and owners only. Returns 409 if a channel with the same name already exists in the org.
| name required | string [ 1 .. 100 ] characters |
{- "name": "string"
}{- "id": "string",
- "name": "string",
- "sortOrder": 0,
- "isActive": true,
- "createdAt": "string"
}List all order channels in sort order, both active and deactivated (each row carries an isActive flag). A channel id from here feeds the channelId on sales orders and sales receipts. Pass a free-text search to match names (case-insensitive contains).
| search | string |
{- "data": [
- {
- "id": "string",
- "name": "string",
- "sortOrder": 0,
- "isActive": true,
- "createdAt": "string"
}
]
}Rename, reorder, or toggle the active flag of an order channel. Deactivating is the soft-delete path (it stays linkable to historical orders). Admins and owners only. Returns 409 if the new name collides with another channel.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| name | string [ 1 .. 100 ] characters |
| sortOrder | integer |
| isActive | boolean |
{- "name": "string",
- "sortOrder": 0,
- "isActive": true
}{- "id": "string",
- "name": "string",
- "sortOrder": 0,
- "isActive": true,
- "createdAt": "string"
}Permanently delete an order channel. Admins and owners only. Returns 409 in_use if any sales order or sales receipt still references it, with the blocking salesOrderCount and salesReceiptCount in the error details; deactivate it instead to retire it while keeping history.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "deletedId": "string"
}Create an acquisition source (a lookup for how a DTC customer was acquired, e.g. a TikTok ad). Admins and owners only. Returns 409 if a source with the same name already exists in the org.
| name required | string [ 1 .. 100 ] characters |
{- "name": "string"
}{- "id": "string",
- "name": "string",
- "sortOrder": 0,
- "isActive": true,
- "createdAt": "string"
}List all acquisition sources in sort order, both active and deactivated (each row carries an isActive flag). A source id from here feeds the acquisitionSourceId required on sales receipts. Pass a free-text search to match names (case-insensitive contains).
| search | string |
{- "data": [
- {
- "id": "string",
- "name": "string",
- "sortOrder": 0,
- "isActive": true,
- "createdAt": "string"
}
]
}Rename, reorder, or toggle the active flag of an acquisition source. Deactivating is the soft-delete path (it stays linkable to historical receipts). Admins and owners only. Returns 409 if the new name collides with another source.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| name | string [ 1 .. 100 ] characters |
| sortOrder | integer |
| isActive | boolean |
{- "name": "string",
- "sortOrder": 0,
- "isActive": true
}{- "id": "string",
- "name": "string",
- "sortOrder": 0,
- "isActive": true,
- "createdAt": "string"
}Permanently delete an acquisition source. Admins and owners only. Returns 409 in_use if any sales receipt still references it, with the blocking salesReceiptCount in the error details; deactivate it instead to retire it while keeping history.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "deletedId": "string"
}List the org's connections to external tools, one entry per supported provider: providers without a connection come back with status notConnected and a null id. Credentials are never returned; config carries only non-secret details. Admins and owners only.
{- "data": [
- {
- "id": "string",
- "provider": "shopify",
- "status": "notConnected",
- "config": {
- "property1": "string",
- "property2": "string"
}, - "errorMessage": "string",
- "connectedAt": "string"
}
]
}For the Powered-by-Fivetran providers (Shopify, Meta Ads, Google Ads, TikTok Ads, Amazon Seller Central, ShipHero, QuickBooks Online): creates the Fivetran connection on first call (the row appears as pending) and returns a hosted Connect Card URL to open for the user, where they authorize the source with Fivetran directly; credentials never touch Pharus. Amazon Seller Central additionally requires merchantId (the account's Merchant Token) on the first call, since Fivetran cannot create the connection without it (422 when missing). Calling again returns a fresh card for the same connection, to resume or re-authorize. Admins and owners only.
| provider required | string Enum: "shopify" "amazonSellerCentral" "metaAds" "googleAds" "tiktokAds" "shiphero" "quickbooksOnline" |
| merchantId | string [ 4 .. 64 ] characters |
{- "merchantId": "string"
}{- "uri": "string",
- "connection": {
- "id": "string",
- "provider": "shopify",
- "status": "notConnected",
- "config": {
- "property1": "string",
- "property2": "string"
}, - "errorMessage": "string",
- "connectedAt": "string"
}
}Admins and owners only. Where a connected provider's data is, in one vocabulary sourced from the data plane: waiting (connected but the data has not arrived in the warehouse yet; the external connector usually takes hours and can take days), backfill (marketplace feeds only: the platform is copying the store's history into its own tables), synced (marketplace: history copied and periodic syncs healthy; other connectors: the data is available where reporting reads it), or unsynced (marketplace only: a periodic sync failed or went overdue; detail carries the reason). 404 for providers with no connection row.
| provider required | string Enum: "shopify" "shiphero" "amazonSellerCentral" "metaAds" "googleAds" "tiktokAds" "quickbooksOnline" |
{- "provider": "shopify",
- "phase": "waiting",
- "detail": "string"
}Disconnect the org from a provider, deleting the stored connection and its encrypted credentials. Returns 404 when the provider was not connected. Admins and owners only.
| provider required | string Enum: "shopify" "shiphero" "amazonSellerCentral" "metaAds" "googleAds" "tiktokAds" "quickbooksOnline" |
{- "deletedId": "string"
}Create a purchase order header and its line items in one call. The order targets either materials or finished-goods SKUs (targetType), and each line names exactly one materialId or skuId to match. It opens in the given status, stamping the matching per-status date (override via the optional statusDates object, keyed by camelCase status name) and projecting one ORDER ledger event per line for any ordered status (Planning has no inventory effect). poNumber is optional: omit it to auto-generate the next canonical number, or supply your own. The 201 is the same composite the detail serves: the header's fields at top level (vendor and ship-to location embedded as { id, name }, the per-status timestamps grouped in statusDates), the nested items (each line's material or SKU as { id, code, name }), and receipts (empty on create). Returns 404 if the vendor, ship-to location, or any referenced material or SKU does not exist, and 409 if the supplied poNumber is already taken.
| vendorId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| targetType required | string Enum: "materials" "finished_goods" |
| status required | string Enum: "Planning" "Placed" "In Transit" "Partial" "Received" "Completed" "Cancelled" |
| shipToLocationId | string or null^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| orderDate | string or null <date> |
| shipDate | string or null <date> |
| expectedDeliveryDate | string or null <date> |
object | |
| shippingCosts | number >= 0 |
| setupCosts | number >= 0 |
| otherCosts | number >= 0 |
| notes | string or null <= 2000 characters |
| poNumber | string [ 1 .. 255 ] characters |
required | Array of objects |
{- "vendorId": "string",
- "targetType": "materials",
- "status": "Planning",
- "shipToLocationId": "string",
- "orderDate": "2019-08-24",
- "shipDate": "2019-08-24",
- "expectedDeliveryDate": "2019-08-24",
- "statusDates": {
- "planning": "2019-08-24",
- "placed": "2019-08-24",
- "inTransit": "2019-08-24",
- "partial": "2019-08-24",
- "received": "2019-08-24",
- "completed": "2019-08-24",
- "cancelled": "2019-08-24"
}, - "shippingCosts": 0,
- "setupCosts": 0,
- "otherCosts": 0,
- "notes": "string",
- "poNumber": "string",
- "items": [
- {
- "materialId": "string",
- "skuId": "string",
- "quantity": 0,
- "unitCost": 0
}
]
}{- "id": "string",
- "poNumber": "string",
- "vendor": {
- "id": "string",
- "name": "string"
}, - "targetType": "materials",
- "status": "Planning",
- "shipToLocation": {
- "id": "string",
- "name": "string"
}, - "orderDate": "string",
- "shipDate": "string",
- "expectedDeliveryDate": "string",
- "statusDates": {
- "planning": "string",
- "placed": "string",
- "inTransit": "string",
- "partial": "string",
- "received": "string",
- "completed": "string",
- "cancelled": "string"
}, - "financials": {
- "shippingCosts": 0,
- "setupCosts": 0,
- "otherCosts": 0,
- "goodsCost": 0,
- "totalCost": 0
}, - "notes": "string",
- "createdAt": "string",
- "updatedAt": "string",
- "items": [
- {
- "id": "string",
- "material": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string",
- "unitCost": "string"
}
], - "receipts": [
- {
- "id": "string",
- "receiptDate": "string",
- "createdAt": "string",
- "updatedAt": "string",
- "items": [
- {
- "id": "string",
- "material": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string",
- "unitCost": "string",
- "lotNumber": "string",
- "expirationDate": "string",
- "createdAt": "string"
}
]
}
]
}List one page of purchase order headers, newest first by default, each with its vendor and ship-to location embedded as { id, name } references, the per-status timestamps grouped in statusDates, and the full financials block: the stored operational costs plus the EXPECTED goodsCost (the order's own lines, qty x unit cost) and totalCost, all computed in plain SQL from the order itself. Every filter is optional and AND-ed: the CSV multi-selects status, vendorId, and shipToLocationId (values OR within each), targetType, containsMaterialId / containsSkuId (only orders with a line for that material / SKU), and inclusive From/To calendar-day pairs on the document dates (orderDateFrom/orderDateTo, shipDateFrom/shipDateTo, expectedDeliveryFrom/expectedDeliveryTo) plus one pair per status key (planning, placed, inTransit, partial, received, completed, cancelled). search is a case-insensitive substring over the PO number, the vendor and ship-to location names, notes, and the line items' material/SKU codes and names. Filter by total cost with inclusive costMin/costMax. Sort with sort (number, vendor, shipTo, status, orderDate, shipDate, expectedDeliveryDate, totalCost, createdAt, updatedAt; default orderDate descending; empty values always last) and order (asc/desc); page with limit (default 50, max 200) and offset. meta.total counts the filtered set, so pagers can render page counts under active filters. Line items and receipts are fetched per order through the detail endpoint.
| sort | string Enum: "number" "vendor" "shipTo" "status" "orderDate" "shipDate" "expectedDeliveryDate" "createdAt" "updatedAt" "totalCost" |
| order | string Enum: "asc" "desc" |
| limit | integer [ 1 .. 200 ] |
| offset | integer >= 0 |
| status | string |
| vendorId | string |
| shipToLocationId | string |
| targetType | string Enum: "materials" "finished_goods" |
| containsMaterialId | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| containsSkuId | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| costMin | number >= 0 |
| costMax | number >= 0 |
| search | string |
| orderDateFrom | string^\d{4}-\d{2}-\d{2}$ |
| orderDateTo | string^\d{4}-\d{2}-\d{2}$ |
| shipDateFrom | string^\d{4}-\d{2}-\d{2}$ |
| shipDateTo | string^\d{4}-\d{2}-\d{2}$ |
| expectedDeliveryFrom | string^\d{4}-\d{2}-\d{2}$ |
| expectedDeliveryTo | string^\d{4}-\d{2}-\d{2}$ |
| planningFrom | string^\d{4}-\d{2}-\d{2}$ |
| planningTo | string^\d{4}-\d{2}-\d{2}$ |
| placedFrom | string^\d{4}-\d{2}-\d{2}$ |
| placedTo | string^\d{4}-\d{2}-\d{2}$ |
| inTransitFrom | string^\d{4}-\d{2}-\d{2}$ |
| inTransitTo | string^\d{4}-\d{2}-\d{2}$ |
| partialFrom | string^\d{4}-\d{2}-\d{2}$ |
| partialTo | string^\d{4}-\d{2}-\d{2}$ |
| receivedFrom | string^\d{4}-\d{2}-\d{2}$ |
| receivedTo | string^\d{4}-\d{2}-\d{2}$ |
| completedFrom | string^\d{4}-\d{2}-\d{2}$ |
| completedTo | string^\d{4}-\d{2}-\d{2}$ |
| cancelledFrom | string^\d{4}-\d{2}-\d{2}$ |
| cancelledTo | string^\d{4}-\d{2}-\d{2}$ |
{- "data": [
- {
- "id": "string",
- "poNumber": "string",
- "vendor": {
- "id": "string",
- "name": "string"
}, - "targetType": "materials",
- "status": "Planning",
- "shipToLocation": {
- "id": "string",
- "name": "string"
}, - "orderDate": "string",
- "shipDate": "string",
- "expectedDeliveryDate": "string",
- "statusDates": {
- "planning": "string",
- "placed": "string",
- "inTransit": "string",
- "partial": "string",
- "received": "string",
- "completed": "string",
- "cancelled": "string"
}, - "financials": {
- "shippingCosts": 0,
- "setupCosts": 0,
- "otherCosts": 0,
- "goodsCost": 0,
- "totalCost": 0
}, - "notes": "string",
- "createdAt": "string",
- "updatedAt": "string"
}
], - "meta": {
- "total": 0,
- "limit": 0,
- "offset": 0
}
}Update a purchase order's header fields, status, or per-status dates (line items are managed through the item endpoints). The statusDates object merges PER KEY: only the sent keys change, unsent keys are untouched, and null clears a date where the clearing rules allow. Moving the status projects or unwinds the matching ORDER ledger events, and editing a per-status date re-dates them. vendorId and shipToLocationId are editable only while the PO is in Planning (409 invalid_transition afterwards); targetType is immutable after create. Returns 409 if a supplied poNumber collides with another order.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| poNumber | string [ 1 .. 255 ] characters |
| vendorId | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| shipToLocationId | string or null^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| status | string Enum: "Planning" "Placed" "In Transit" "Partial" "Received" "Completed" "Cancelled" |
| orderDate | string or null <date> |
| shipDate | string or null <date> |
| expectedDeliveryDate | string or null <date> |
object | |
| shippingCosts | number >= 0 |
| setupCosts | number >= 0 |
| otherCosts | number >= 0 |
| notes | string or null <= 2000 characters |
{- "poNumber": "string",
- "vendorId": "string",
- "shipToLocationId": "string",
- "status": "Planning",
- "orderDate": "2019-08-24",
- "shipDate": "2019-08-24",
- "expectedDeliveryDate": "2019-08-24",
- "statusDates": {
- "planning": "2019-08-24",
- "placed": "2019-08-24",
- "inTransit": "2019-08-24",
- "partial": "2019-08-24",
- "received": "2019-08-24",
- "completed": "2019-08-24",
- "cancelled": "2019-08-24"
}, - "shippingCosts": 0,
- "setupCosts": 0,
- "otherCosts": 0,
- "notes": "string"
}{- "id": "string",
- "poNumber": "string",
- "vendor": {
- "id": "string",
- "name": "string"
}, - "targetType": "materials",
- "status": "Planning",
- "shipToLocation": {
- "id": "string",
- "name": "string"
}, - "orderDate": "string",
- "shipDate": "string",
- "expectedDeliveryDate": "string",
- "statusDates": {
- "planning": "string",
- "placed": "string",
- "inTransit": "string",
- "partial": "string",
- "received": "string",
- "completed": "string",
- "cancelled": "string"
}, - "financials": {
- "shippingCosts": 0,
- "setupCosts": 0,
- "otherCosts": 0,
- "goodsCost": 0,
- "totalCost": 0
}, - "notes": "string",
- "createdAt": "string",
- "updatedAt": "string"
}Permanently delete a purchase order. Its items, receipts, and projected ledger events cascade away, and on-hand recomputes live from the remaining ledger rows. There is no in-use gate.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "deletedId": "string"
}Get one purchase order as a composite: the header's fields at top level (no wrapper), including the derived financials (operational costs plus goods and total cost) and the per-status timestamps grouped in statusDates, its line items nested under items, and its receipts under receipts, each receipt nesting its own received lines under items. The vendor and ship-to location are embedded { id, name } references and each line's material or SKU an { id, code, name } reference; nested rows omit the redundant purchaseOrderId / receiptId back-references.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "id": "string",
- "poNumber": "string",
- "vendor": {
- "id": "string",
- "name": "string"
}, - "targetType": "materials",
- "status": "Planning",
- "shipToLocation": {
- "id": "string",
- "name": "string"
}, - "orderDate": "string",
- "shipDate": "string",
- "expectedDeliveryDate": "string",
- "statusDates": {
- "planning": "string",
- "placed": "string",
- "inTransit": "string",
- "partial": "string",
- "received": "string",
- "completed": "string",
- "cancelled": "string"
}, - "financials": {
- "shippingCosts": 0,
- "setupCosts": 0,
- "otherCosts": 0,
- "goodsCost": 0,
- "totalCost": 0
}, - "notes": "string",
- "createdAt": "string",
- "updatedAt": "string",
- "items": [
- {
- "id": "string",
- "material": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string",
- "unitCost": "string"
}
], - "receipts": [
- {
- "id": "string",
- "receiptDate": "string",
- "createdAt": "string",
- "updatedAt": "string",
- "items": [
- {
- "id": "string",
- "material": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string",
- "unitCost": "string",
- "lotNumber": "string",
- "expirationDate": "string",
- "createdAt": "string"
}
]
}
]
}Import many purchase orders in one atomic call, used by the Data page. Each row creates a PO with an auto-generated number, its line items, and the projected ORDER events for its status. The whole batch rolls back if any row fails. Admins and owners only. Returns 404 if a row references a vendor, material, or SKU that does not exist.
required | Array of objects | ||||||||||||||||||||||||||
Array
| |||||||||||||||||||||||||||
{- "rows": [
- {
- "vendorId": "string",
- "targetType": "materials",
- "status": "Planning",
- "shipToLocationId": "string",
- "orderDate": "2019-08-24",
- "shipDate": "2019-08-24",
- "expectedDeliveryDate": "2019-08-24",
- "statusDates": {
- "planning": "2019-08-24",
- "placed": "2019-08-24",
- "inTransit": "2019-08-24",
- "partial": "2019-08-24",
- "received": "2019-08-24",
- "completed": "2019-08-24",
- "cancelled": "2019-08-24"
}, - "shippingCosts": 0,
- "setupCosts": 0,
- "otherCosts": 0,
- "notes": "string",
- "items": [
- {
- "materialId": "string",
- "skuId": "string",
- "quantity": 0,
- "unitCost": 0
}
]
}
]
}{- "ids": [
- "string"
]
}Add a line item to an existing purchase order, named by purchaseOrderId in the body. The line names exactly one materialId or skuId, matching the order's target. Adding to an ordered PO projects a matching ORDER ledger event for the new line. Returns 404 if the purchase order or the referenced material or SKU does not exist, and 409 once any receipt exists against the PO: received orders lock their line items.
| materialId | string or null^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| skuId | string or null^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| quantity required | number > 0 |
| unitCost | number >= 0 |
| purchaseOrderId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "materialId": "string",
- "skuId": "string",
- "quantity": 0,
- "unitCost": 0,
- "purchaseOrderId": "string"
}{- "id": "string",
- "purchaseOrderId": "string",
- "material": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string",
- "unitCost": "string"
}List purchase order line items, each with its material or SKU embedded as an { id, code, name } reference. Pass purchaseOrderId to scope the list to one order, or omit it to read lines across every order (a flat items view); materialId / skuId narrow by the referenced catalog row. An unknown filter id returns an empty list rather than a 404.
| purchaseOrderId | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| materialId | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| skuId | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "data": [
- {
- "id": "string",
- "purchaseOrderId": "string",
- "material": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string",
- "unitCost": "string"
}
]
}Update a purchase order line item: its quantity, unit cost, or the referenced material or SKU. A quantity or target change re-projects the line's ORDER ledger event. Unit cost accepts up to 6 decimals. Returns 404 if a newly referenced material or SKU does not exist, and 409 once any receipt exists against the PO: received orders lock their line items.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| materialId | string or null^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| skuId | string or null^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| quantity | number > 0 |
| unitCost | number >= 0 |
{- "materialId": "string",
- "skuId": "string",
- "quantity": 0,
- "unitCost": 0
}{- "id": "string",
- "purchaseOrderId": "string",
- "material": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string",
- "unitCost": "string"
}Remove a line item from its purchase order, unwinding the ORDER ledger event the line had projected. Returns 409 once any receipt exists against the PO: received orders lock their line items.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "deletedId": "string"
}Record goods received against a purchase order, named by purchaseOrderId in the body: writes the receipt and its lines, one RECEIVE row per line into the matching inventory ledger (ref = the PO number), and moves the PO to Partial. Receiving against a manually closed (Received) PO reopens it to Partial. Unit costs accept up to 6 decimals and feed the costing engine, which reads them live off the receipt lines. The 201 is two-keyed: the created receipt with its lines nested under items (each line's material or SKU embedded), and the updated purchase order header in flat wire shape. Returns 404 if the purchase order or a referenced material or SKU does not exist, and 409 while it is not open for receiving (Planning, Completed, or Cancelled).
| purchaseOrderId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| receiptDate | string <date> |
required | Array of objects |
{- "purchaseOrderId": "string",
- "receiptDate": "2019-08-24",
- "items": [
- {
- "materialId": "string",
- "skuId": "string",
- "quantity": 0,
- "unitCost": 0,
- "lotNumber": "string",
- "expirationDate": "2019-08-24"
}
]
}{- "receipt": {
- "id": "string",
- "purchaseOrderId": "string",
- "receiptDate": "string",
- "createdAt": "string",
- "updatedAt": "string",
- "items": [
- {
- "id": "string",
- "material": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string",
- "unitCost": "string",
- "lotNumber": "string",
- "expirationDate": "string",
- "createdAt": "string"
}
]
}, - "purchaseOrder": {
- "id": "string",
- "poNumber": "string",
- "vendor": {
- "id": "string",
- "name": "string"
}, - "targetType": "materials",
- "status": "Planning",
- "shipToLocation": {
- "id": "string",
- "name": "string"
}, - "orderDate": "string",
- "shipDate": "string",
- "expectedDeliveryDate": "string",
- "statusDates": {
- "planning": "string",
- "placed": "string",
- "inTransit": "string",
- "partial": "string",
- "received": "string",
- "completed": "string",
- "cancelled": "string"
}, - "financials": {
- "shippingCosts": 0,
- "setupCosts": 0,
- "otherCosts": 0,
- "goodsCost": 0,
- "totalCost": 0
}, - "notes": "string",
- "createdAt": "string",
- "updatedAt": "string"
}
}List goods receipts in receipt-date order. Pass purchaseOrderId to scope the list to one order, or omit it to read receipts across every order. An unknown purchaseOrderId returns an empty list rather than a 404.
| purchaseOrderId | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "data": [
- {
- "id": "string",
- "purchaseOrderId": "string",
- "receiptDate": "string",
- "createdAt": "string",
- "updatedAt": "string"
}
]
}Update a goods receipt's date. Re-dating the receipt re-dates its RECEIVE ledger rows, but never changes the purchase order's status.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| receiptDate | string <date> |
{- "receiptDate": "2019-08-24"
}{- "id": "string",
- "purchaseOrderId": "string",
- "receiptDate": "string",
- "createdAt": "string",
- "updatedAt": "string"
}Delete a goods receipt and its lines. Its RECEIVE ledger rows are removed too, so on-hand recomputes live from the remaining ledger rows, and the PO status walks back to what the surviving receipts support (Partial, or Placed when none remain; deleting a receipt from a Received PO reopens it). Returns 409 while the PO is Completed: reopen it first.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "deletedId": "string"
}Get one goods receipt: the receipt header's fields at top level (no wrapper, purchaseOrderId included as the parent correlation) with its received line items nested under items, each line's material or SKU embedded as an { id, code, name } reference (the nested lines omit the redundant receiptId back-reference).
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "id": "string",
- "purchaseOrderId": "string",
- "receiptDate": "string",
- "createdAt": "string",
- "updatedAt": "string",
- "items": [
- {
- "id": "string",
- "material": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string",
- "unitCost": "string",
- "lotNumber": "string",
- "expirationDate": "string",
- "createdAt": "string"
}
]
}Edit a received line in place: quantity, lot number, or expiration date propagate to the line's RECEIVE ledger row, and a quantity change reruns the receipt reconcile. Unit cost is not stored, it is read live by the costing engine. Returns 409 if the purchase order is already Completed.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| quantity | number > 0 |
| unitCost | number >= 0 |
| lotNumber | string or null <= 255 characters |
| expirationDate | string or null <date> |
{- "quantity": 0,
- "unitCost": 0,
- "lotNumber": "string",
- "expirationDate": "2019-08-24"
}{- "id": "string",
- "receiptId": "string",
- "material": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string",
- "unitCost": "string",
- "lotNumber": "string",
- "expirationDate": "string",
- "createdAt": "string"
}Generate a print-ready PDF of the purchase order, streamed as an application/pdf attachment (no JSON envelope). All rates and amounts are included. Use the POST variant to render with per-document overrides or a price-less layout.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "error": {
- "code": "not_found",
- "message": "string",
- "details": null
}
}Render the purchase order PDF with transient per-document overrides from the body, and optionally a price-less layout (withPrices: false renders every rate and amount as zero). The overrides apply to this render only and are never written back to the order, vendor, or location. Streamed as an application/pdf attachment.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
object | |
| withPrices | boolean |
{- "overrides": {
- "terms": "string",
- "shipVia": "string",
- "orderDate": "2019-08-24",
- "expectedDeliveryDate": "2019-08-24",
- "vendor": {
- "displayName": "string",
- "name": "string",
- "address": "string",
- "city": "string",
- "state": "string",
- "zipCode": "string"
}, - "shipTo": {
- "attention": "string",
- "name": "string",
- "address": "string",
- "city": "string",
- "state": "string",
- "zipCode": "string"
}, - "items": [
- {
- "id": "string",
- "description": "string",
- "vendorPart": "string"
}
], - "notes": "string"
}, - "withPrices": true
}{- "error": {
- "code": "not_found",
- "message": "string",
- "details": null
}
}Return the prefilled document values for the PO PDF form: exactly what the default PDF would print. Use it to populate an editable form before rendering with overrides through the POST endpoint.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "data": {
- "poNumber": "string",
- "terms": "string",
- "shipVia": "string",
- "orderDate": "string",
- "expectedDeliveryDate": "string",
- "vendor": {
- "displayName": "string",
- "name": "string",
- "address": "string",
- "city": "string",
- "state": "string",
- "zipCode": "string"
}, - "shipTo": {
- "attention": "string",
- "name": "string",
- "address": "string",
- "city": "string",
- "state": "string",
- "zipCode": "string"
}, - "items": [
- {
- "id": "string",
- "itemName": "string",
- "description": "string",
- "vendorPart": "string",
- "quantity": 0,
- "unitCost": 0
}
], - "shippingCosts": 0,
- "setupCosts": 0,
- "notes": "string"
}
}Create a work order to make finished goods in-house: the header, its output-SKU line items, and a recipe snapshot of each SKU's BOM inputs copied at creation time (later BOM edits do not change an existing WO). It opens in the given status, stamping the matching per-status date (override via the optional statusDates object, keyed by camelCase status name) and projecting the commitment ledger events. woNumber is optional: omit it to auto-generate the next canonical number, or supply your own. The 201 is the same composite the detail serves: the header's fields at top level (the work-site location embedded as { id, name }, the per-status timestamps grouped in statusDates), the nested items (each output SKU embedded, its snapshotted recipe inputs nested one level deeper with their material or component SKU embedded), and receipts (empty on create). Returns 404 if the work-site location or any output SKU does not exist, and 409 if the supplied woNumber is already taken.
| status required | string Enum: "Planning" "Placed" "Production" "Partial" "Received" "Completed" "Cancelled" |
| workSiteLocationId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| orderDate | string or null <date> |
| expectedDeliveryDate | string or null <date> |
object | |
| inShippingCosts | number >= 0 |
| outShippingCosts | number >= 0 |
| otherCosts | number >= 0 |
| notes | string or null <= 2000 characters |
| woNumber | string [ 1 .. 255 ] characters |
required | Array of objects |
{- "status": "Planning",
- "workSiteLocationId": "string",
- "orderDate": "2019-08-24",
- "expectedDeliveryDate": "2019-08-24",
- "statusDates": {
- "planning": "2019-08-24",
- "placed": "2019-08-24",
- "production": "2019-08-24",
- "partial": "2019-08-24",
- "received": "2019-08-24",
- "completed": "2019-08-24",
- "cancelled": "2019-08-24"
}, - "inShippingCosts": 0,
- "outShippingCosts": 0,
- "otherCosts": 0,
- "notes": "string",
- "woNumber": "string",
- "items": [
- {
- "skuId": "string",
- "quantity": 1,
- "conversionCost": 0
}
]
}{- "id": "string",
- "woNumber": "string",
- "status": "Planning",
- "workSite": {
- "id": "string",
- "name": "string"
}, - "orderDate": "string",
- "expectedDeliveryDate": "string",
- "statusDates": {
- "planning": "string",
- "placed": "string",
- "production": "string",
- "partial": "string",
- "received": "string",
- "completed": "string",
- "cancelled": "string"
}, - "financials": {
- "inShippingCosts": 0,
- "outShippingCosts": 0,
- "otherCosts": 0,
- "goodsCost": 0,
- "totalCost": 0
}, - "notes": "string",
- "createdAt": "string",
- "updatedAt": "string",
- "items": [
- {
- "id": "string",
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string",
- "conversionCost": "string",
- "inputs": [
- {
- "id": "string",
- "material": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "inputSku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string"
}
]
}
], - "receipts": [
- {
- "id": "string",
- "receiptDate": "string",
- "createdAt": "string",
- "items": [
- {
- "id": "string",
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string",
- "conversionCost": "string",
- "lotNumber": "string",
- "expirationDate": "string",
- "createdAt": "string"
}
]
}
]
}List one page of work order headers, newest first by default, each with its work-site location embedded as an { id, name } reference, the per-status timestamps grouped in statusDates, and the stored operational costs in financials (the engine-derived goodsCost/totalCost appear on single-order reads only). Every filter is optional and AND-ed: the CSV multi-selects status and workSiteId (values OR within each), containsSkuId (only orders with an output line for that SKU), and inclusive From/To calendar-day pairs on the document dates (orderDateFrom/orderDateTo, expectedDeliveryFrom/expectedDeliveryTo) plus one pair per status key (planning, placed, production, partial, received, completed, cancelled). search is a case-insensitive substring over the WO number, the work-site name, notes, and the output SKUs' codes and names. Sort with sort (number, workSite, status, orderDate, expectedDeliveryDate, createdAt, updatedAt; default orderDate descending; empty values always last) and order (asc/desc); page with limit (default 50, max 200) and offset. meta.total counts the filtered set, so pagers can render page counts under active filters. Line items, recipe inputs, and receipts are fetched per order through the detail endpoint.
| sort | string Enum: "number" "workSite" "status" "orderDate" "expectedDeliveryDate" "createdAt" "updatedAt" |
| order | string Enum: "asc" "desc" |
| limit | integer [ 1 .. 200 ] |
| offset | integer >= 0 |
| status | string |
| workSiteId | string |
| containsSkuId | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| search | string |
| orderDateFrom | string^\d{4}-\d{2}-\d{2}$ |
| orderDateTo | string^\d{4}-\d{2}-\d{2}$ |
| expectedDeliveryFrom | string^\d{4}-\d{2}-\d{2}$ |
| expectedDeliveryTo | string^\d{4}-\d{2}-\d{2}$ |
| planningFrom | string^\d{4}-\d{2}-\d{2}$ |
| planningTo | string^\d{4}-\d{2}-\d{2}$ |
| placedFrom | string^\d{4}-\d{2}-\d{2}$ |
| placedTo | string^\d{4}-\d{2}-\d{2}$ |
| productionFrom | string^\d{4}-\d{2}-\d{2}$ |
| productionTo | string^\d{4}-\d{2}-\d{2}$ |
| partialFrom | string^\d{4}-\d{2}-\d{2}$ |
| partialTo | string^\d{4}-\d{2}-\d{2}$ |
| receivedFrom | string^\d{4}-\d{2}-\d{2}$ |
| receivedTo | string^\d{4}-\d{2}-\d{2}$ |
| completedFrom | string^\d{4}-\d{2}-\d{2}$ |
| completedTo | string^\d{4}-\d{2}-\d{2}$ |
| cancelledFrom | string^\d{4}-\d{2}-\d{2}$ |
| cancelledTo | string^\d{4}-\d{2}-\d{2}$ |
{- "data": [
- {
- "id": "string",
- "woNumber": "string",
- "status": "Planning",
- "workSite": {
- "id": "string",
- "name": "string"
}, - "orderDate": "string",
- "expectedDeliveryDate": "string",
- "statusDates": {
- "planning": "string",
- "placed": "string",
- "production": "string",
- "partial": "string",
- "received": "string",
- "completed": "string",
- "cancelled": "string"
}, - "financials": {
- "inShippingCosts": 0,
- "outShippingCosts": 0,
- "otherCosts": 0
}, - "notes": "string",
- "createdAt": "string",
- "updatedAt": "string"
}
], - "meta": {
- "total": 0,
- "limit": 0,
- "offset": 0
}
}Update a work order's header fields, status, or per-status dates (line items are managed through the item endpoints). The statusDates object merges PER KEY: only the sent keys change, unsent keys are untouched, and null clears a date where the clearing rules allow. Moving the status projects the commitment ledger events; closing at Received settles the open commitments and reopening removes them. workSiteLocationId is editable only while the WO is in Planning (409 invalid_transition afterwards). Returns 409 if a supplied woNumber collides with another order.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| woNumber | string [ 1 .. 255 ] characters |
| workSiteLocationId | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| status | string Enum: "Planning" "Placed" "Production" "Partial" "Received" "Completed" "Cancelled" |
| orderDate | string or null <date> |
| expectedDeliveryDate | string or null <date> |
object | |
| inShippingCosts | number >= 0 |
| outShippingCosts | number >= 0 |
| otherCosts | number >= 0 |
| notes | string or null <= 2000 characters |
{- "woNumber": "string",
- "workSiteLocationId": "string",
- "status": "Planning",
- "orderDate": "2019-08-24",
- "expectedDeliveryDate": "2019-08-24",
- "statusDates": {
- "planning": "2019-08-24",
- "placed": "2019-08-24",
- "production": "2019-08-24",
- "partial": "2019-08-24",
- "received": "2019-08-24",
- "completed": "2019-08-24",
- "cancelled": "2019-08-24"
}, - "inShippingCosts": 0,
- "outShippingCosts": 0,
- "otherCosts": 0,
- "notes": "string"
}{- "id": "string",
- "woNumber": "string",
- "status": "Planning",
- "workSite": {
- "id": "string",
- "name": "string"
}, - "orderDate": "string",
- "expectedDeliveryDate": "string",
- "statusDates": {
- "planning": "string",
- "placed": "string",
- "production": "string",
- "partial": "string",
- "received": "string",
- "completed": "string",
- "cancelled": "string"
}, - "financials": {
- "inShippingCosts": 0,
- "outShippingCosts": 0,
- "otherCosts": 0
}, - "notes": "string",
- "createdAt": "string",
- "updatedAt": "string"
}Permanently delete a work order. Its items, recipe inputs, receipts, and projected ledger events cascade away, and on-hand recomputes live from the remaining ledger rows. There is no in-use gate.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "deletedId": "string"
}Get one work order as a composite: the header's fields at top level (no wrapper), including the derived financials (operational costs plus goods and total cost) and the per-status timestamps grouped in statusDates, its output-SKU line items nested under items, and its receipts under receipts, each receipt nesting its own produced lines under items. The work-site location is an embedded { id, name } reference; each output line's SKU and each recipe input's material or component SKU is an { id, code, name } reference, with the recipe inputs snapshotted from the BOM nested under each item's inputs. Nested rows omit the redundant workOrderId / receiptId back-references.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "id": "string",
- "woNumber": "string",
- "status": "Planning",
- "workSite": {
- "id": "string",
- "name": "string"
}, - "orderDate": "string",
- "expectedDeliveryDate": "string",
- "statusDates": {
- "planning": "string",
- "placed": "string",
- "production": "string",
- "partial": "string",
- "received": "string",
- "completed": "string",
- "cancelled": "string"
}, - "financials": {
- "inShippingCosts": 0,
- "outShippingCosts": 0,
- "otherCosts": 0,
- "goodsCost": 0,
- "totalCost": 0
}, - "notes": "string",
- "createdAt": "string",
- "updatedAt": "string",
- "items": [
- {
- "id": "string",
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string",
- "conversionCost": "string",
- "inputs": [
- {
- "id": "string",
- "material": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "inputSku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string"
}
]
}
], - "receipts": [
- {
- "id": "string",
- "receiptDate": "string",
- "createdAt": "string",
- "items": [
- {
- "id": "string",
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string",
- "conversionCost": "string",
- "lotNumber": "string",
- "expirationDate": "string",
- "createdAt": "string"
}
]
}
]
}Import many work orders in one atomic call, used by the Data page. Each row creates a WO with an auto-generated number, its items, the recipe snapshots, and the projected commitment events for its status. The whole batch rolls back if any row fails. Admins and owners only. Returns 404 if a row references a work-site location or output SKU that does not exist.
required | Array of objects | ||||||||||||||||||||
Array
| |||||||||||||||||||||
{- "rows": [
- {
- "status": "Planning",
- "workSiteLocationId": "string",
- "orderDate": "2019-08-24",
- "expectedDeliveryDate": "2019-08-24",
- "statusDates": {
- "planning": "2019-08-24",
- "placed": "2019-08-24",
- "production": "2019-08-24",
- "partial": "2019-08-24",
- "received": "2019-08-24",
- "completed": "2019-08-24",
- "cancelled": "2019-08-24"
}, - "inShippingCosts": 0,
- "outShippingCosts": 0,
- "otherCosts": 0,
- "notes": "string",
- "items": [
- {
- "skuId": "string",
- "quantity": 1,
- "conversionCost": 0
}
]
}
]
}{- "ids": [
- "string"
]
}Add an output SKU to an existing work order, named by workOrderId in the body, and snapshot its BOM inputs at add time. Adding to an ordered WO projects the new line's commitment ledger events. conversionCost accepts up to 6 decimals. The response is the line in wire shape: its output SKU embedded and its snapshotted recipe inputs nested (each input's material or component SKU embedded). Returns 404 if the work order does not exist, and 409 if a receipt already exists against the WO (invalid_transition; its items are locked) or the SKU is already an output line on this WO (conflict).
| skuId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| quantity required | integer >= 1 |
| conversionCost | number >= 0 |
| workOrderId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "skuId": "string",
- "quantity": 1,
- "conversionCost": 0,
- "workOrderId": "string"
}{- "id": "string",
- "workOrderId": "string",
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string",
- "conversionCost": "string",
- "inputs": [
- {
- "id": "string",
- "material": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "inputSku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string"
}
]
}List work order output line items, each with its produced SKU embedded as an { id, code, name } reference and its snapshotted recipe inputs nested under inputs (each input's material or component SKU embedded). Pass workOrderId to scope the list to one order, or omit it to read lines across every order (a flat items view); skuId narrows by the produced SKU. An unknown workOrderId returns an empty list rather than a 404.
| workOrderId | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| skuId | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "data": [
- {
- "id": "string",
- "workOrderId": "string",
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string",
- "conversionCost": "string",
- "inputs": [
- {
- "id": "string",
- "material": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "inputSku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string"
}
]
}
]
}Update an output line's quantity or conversion cost (up to 6 decimals). A quantity change re-scales the item's snapshotted BOM inputs proportionally and re-projects the line's commitment ledger events. The response is the line in wire shape: its output SKU embedded and its recipe inputs nested. Returns 409 invalid_transition once any receipt exists against the WO: its line items are locked.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| quantity | integer >= 1 |
| conversionCost | number >= 0 |
{- "quantity": 1,
- "conversionCost": 0
}{- "id": "string",
- "workOrderId": "string",
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string",
- "conversionCost": "string",
- "inputs": [
- {
- "id": "string",
- "material": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "inputSku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string"
}
]
}Remove an output SKU line from a work order, unwinding the commitment ledger events it had projected; its snapshotted recipe inputs cascade away. Returns 409 invalid_transition once any receipt exists against the WO: its line items are locked.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "deletedId": "string"
}Record production received against a work order, named by workOrderId in the body: writes the receipt and its produced lines, then BOTH ledger legs, a RECEIVE row per produced SKU on the finished-goods ledger and a CONSUME row per snapshotted input (materials and any component SKUs) on their ledgers, all tagged ref = the WO number, and advances the WO to Partial (receiving against a Received WO reopens it). conversionCost feeds the costing engine, which reads it live off the lines, never rounded. The 201 is two-keyed: the created receipt with its produced lines nested under items (each line's SKU embedded), and the updated work order header in flat wire shape. Returns 404 if the work order does not exist, and 409 invalid_transition if it is already in a terminal state (Completed or Cancelled) or still in Planning/Placed (receiving opens in Production).
| workOrderId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| receiptDate | string <date> |
required | Array of objects |
{- "workOrderId": "string",
- "receiptDate": "2019-08-24",
- "items": [
- {
- "skuId": "string",
- "quantity": 1,
- "lotNumber": "string",
- "expirationDate": "2019-08-24"
}
]
}{- "receipt": {
- "id": "string",
- "workOrderId": "string",
- "receiptDate": "string",
- "createdAt": "string",
- "items": [
- {
- "id": "string",
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string",
- "conversionCost": "string",
- "lotNumber": "string",
- "expirationDate": "string",
- "createdAt": "string"
}
]
}, - "workOrder": {
- "id": "string",
- "woNumber": "string",
- "status": "Planning",
- "workSite": {
- "id": "string",
- "name": "string"
}, - "orderDate": "string",
- "expectedDeliveryDate": "string",
- "statusDates": {
- "planning": "string",
- "placed": "string",
- "production": "string",
- "partial": "string",
- "received": "string",
- "completed": "string",
- "cancelled": "string"
}, - "financials": {
- "inShippingCosts": 0,
- "outShippingCosts": 0,
- "otherCosts": 0
}, - "notes": "string",
- "createdAt": "string",
- "updatedAt": "string"
}
}List production receipts in receipt-date order. Pass workOrderId to scope the list to one order, or omit it to read receipts across every order. An unknown workOrderId returns an empty list rather than a 404.
| workOrderId | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "data": [
- {
- "id": "string",
- "workOrderId": "string",
- "receiptDate": "string",
- "createdAt": "string"
}
]
}Update a production receipt's date. Re-dating the receipt re-dates its ledger rows but never changes the work order's status.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| receiptDate | string <date> |
{- "receiptDate": "2019-08-24"
}{- "id": "string",
- "workOrderId": "string",
- "receiptDate": "string",
- "createdAt": "string"
}Delete a production receipt and its lines. Its CONSUME and RECEIVE ledger rows on both ledgers are removed too, so on-hand recomputes live from the remaining ledger rows, and the WO status walks back to what the surviving receipts support (Partial, or Production when none remain; deleting a receipt from a Received WO reopens it). Returns 409 invalid_transition while the WO is Completed: reopen it first.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "deletedId": "string"
}Get one production receipt: the receipt header's fields at top level (no wrapper, workOrderId included as the parent correlation) with its produced line items nested under items, each line's SKU embedded as an { id, code, name } reference (the nested lines omit the redundant receiptId back-reference).
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "id": "string",
- "workOrderId": "string",
- "receiptDate": "string",
- "createdAt": "string",
- "items": [
- {
- "id": "string",
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string",
- "conversionCost": "string",
- "lotNumber": "string",
- "expirationDate": "string",
- "createdAt": "string"
}
]
}Edit a produced line in place: a quantity change propagates to the finished-goods RECEIVE row and rescales the input CONSUME rows by the same recipe formula; lot number and expiration date update the RECEIVE row. Conversion cost (up to 6 decimals) is read live by the costing engine, never stored. The response is the line in wire shape, its produced SKU embedded. Returns 409 invalid_transition if the work order is already Completed.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| quantity | integer >= 1 |
| conversionCost | number >= 0 |
| lotNumber | string or null <= 255 characters |
| expirationDate | string or null <date> |
{- "quantity": 1,
- "conversionCost": 0,
- "lotNumber": "string",
- "expirationDate": "2019-08-24"
}{- "id": "string",
- "receiptId": "string",
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string",
- "conversionCost": "string",
- "lotNumber": "string",
- "expirationDate": "string",
- "createdAt": "string"
}Create a production order for subcontracted (toll) manufacturing: the header, its output-SKU line items (each with an optional per-unit production cost, up to 6 decimals), and a recipe snapshot of each SKU's BOM exploded to raw materials at creation time (later BOM edits do not change an existing order). It opens in the given status, stamping the matching per-status date (override via the optional statusDates object, keyed by camelCase status name) and projecting the commitment ledger events. prodNumber is optional: omit it to auto-generate the next canonical number, or supply your own. The 201 is the same composite the detail serves: the header's fields at top level (the location embedded as { id, name }, the per-status timestamps grouped in statusDates), the nested items (each output SKU embedded, its snapshotted recipe inputs nested one level deeper with their material embedded), and receipts (empty on create). Returns 404 if the location or any output SKU does not exist, and 409 if the supplied prodNumber is already taken.
| status required | string Enum: "Planning" "Placed" "Production" "In Transit" "Partial" "Received" "Completed" "Cancelled" |
| locationId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| orderDate | string or null <date> |
| expectedDeliveryDate | string or null <date> |
object | |
| inShippingCosts | number >= 0 |
| outShippingCosts | number >= 0 |
| otherCosts | number >= 0 |
| notes | string or null <= 2000 characters |
| prodNumber | string [ 1 .. 255 ] characters |
required | Array of objects |
{- "status": "Planning",
- "locationId": "string",
- "orderDate": "2019-08-24",
- "expectedDeliveryDate": "2019-08-24",
- "statusDates": {
- "planning": "2019-08-24",
- "placed": "2019-08-24",
- "production": "2019-08-24",
- "inTransit": "2019-08-24",
- "partial": "2019-08-24",
- "received": "2019-08-24",
- "completed": "2019-08-24",
- "cancelled": "2019-08-24"
}, - "inShippingCosts": 0,
- "outShippingCosts": 0,
- "otherCosts": 0,
- "notes": "string",
- "prodNumber": "string",
- "items": [
- {
- "skuId": "string",
- "quantity": 1,
- "productionCost": 0
}
]
}{- "id": "string",
- "prodNumber": "string",
- "status": "Planning",
- "location": {
- "id": "string",
- "name": "string"
}, - "orderDate": "string",
- "expectedDeliveryDate": "string",
- "statusDates": {
- "planning": "string",
- "placed": "string",
- "production": "string",
- "inTransit": "string",
- "partial": "string",
- "received": "string",
- "completed": "string",
- "cancelled": "string"
}, - "financials": {
- "inShippingCosts": 0,
- "outShippingCosts": 0,
- "otherCosts": 0,
- "goodsCost": 0,
- "totalCost": 0
}, - "notes": "string",
- "createdAt": "string",
- "updatedAt": "string",
- "items": [
- {
- "id": "string",
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string",
- "productionCost": "string",
- "inputs": [
- {
- "id": "string",
- "material": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string"
}
]
}
], - "receipts": [
- {
- "id": "string",
- "receiptDate": "string",
- "createdAt": "string",
- "items": [
- {
- "id": "string",
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string",
- "productionCost": "string",
- "lotNumber": "string",
- "expirationDate": "string",
- "createdAt": "string"
}
]
}
]
}List one page of production order headers, newest first by default, each with its location embedded as an { id, name } reference, the per-status timestamps grouped in statusDates, and the stored operational costs in financials (the engine-derived goodsCost/totalCost appear on single-order reads only). Every filter is optional and AND-ed: the CSV multi-selects status and locationId (values OR within each), containsSkuId (only orders with an output line for that SKU), and inclusive From/To calendar-day pairs on the document dates (orderDateFrom/orderDateTo, expectedDeliveryFrom/expectedDeliveryTo) plus one pair per status key (planning, placed, production, inTransit, partial, received, completed, cancelled). search is a case-insensitive substring over the order number, the location name, notes, and the output SKUs' codes and names. Sort with sort (number, location, status, orderDate, expectedDeliveryDate, createdAt, updatedAt; default orderDate descending; empty values always last) and order (asc/desc); page with limit (default 50, max 200) and offset. meta.total counts the filtered set, so pagers can render page counts under active filters. Line items, recipe inputs, and receipts are fetched per order through the detail endpoint.
| sort | string Enum: "number" "location" "status" "orderDate" "expectedDeliveryDate" "createdAt" "updatedAt" |
| order | string Enum: "asc" "desc" |
| limit | integer [ 1 .. 200 ] |
| offset | integer >= 0 |
| status | string |
| locationId | string |
| containsSkuId | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| search | string |
| orderDateFrom | string^\d{4}-\d{2}-\d{2}$ |
| orderDateTo | string^\d{4}-\d{2}-\d{2}$ |
| expectedDeliveryFrom | string^\d{4}-\d{2}-\d{2}$ |
| expectedDeliveryTo | string^\d{4}-\d{2}-\d{2}$ |
| planningFrom | string^\d{4}-\d{2}-\d{2}$ |
| planningTo | string^\d{4}-\d{2}-\d{2}$ |
| placedFrom | string^\d{4}-\d{2}-\d{2}$ |
| placedTo | string^\d{4}-\d{2}-\d{2}$ |
| productionFrom | string^\d{4}-\d{2}-\d{2}$ |
| productionTo | string^\d{4}-\d{2}-\d{2}$ |
| inTransitFrom | string^\d{4}-\d{2}-\d{2}$ |
| inTransitTo | string^\d{4}-\d{2}-\d{2}$ |
| partialFrom | string^\d{4}-\d{2}-\d{2}$ |
| partialTo | string^\d{4}-\d{2}-\d{2}$ |
| receivedFrom | string^\d{4}-\d{2}-\d{2}$ |
| receivedTo | string^\d{4}-\d{2}-\d{2}$ |
| completedFrom | string^\d{4}-\d{2}-\d{2}$ |
| completedTo | string^\d{4}-\d{2}-\d{2}$ |
| cancelledFrom | string^\d{4}-\d{2}-\d{2}$ |
| cancelledTo | string^\d{4}-\d{2}-\d{2}$ |
{- "data": [
- {
- "id": "string",
- "prodNumber": "string",
- "status": "Planning",
- "location": {
- "id": "string",
- "name": "string"
}, - "orderDate": "string",
- "expectedDeliveryDate": "string",
- "statusDates": {
- "planning": "string",
- "placed": "string",
- "production": "string",
- "inTransit": "string",
- "partial": "string",
- "received": "string",
- "completed": "string",
- "cancelled": "string"
}, - "financials": {
- "inShippingCosts": 0,
- "outShippingCosts": 0,
- "otherCosts": 0
}, - "notes": "string",
- "createdAt": "string",
- "updatedAt": "string"
}
], - "meta": {
- "total": 0,
- "limit": 0,
- "offset": 0
}
}Update a production order's header fields, status, or per-status dates (line items are managed through the item endpoints). The statusDates object merges PER KEY: only the sent keys change, unsent keys are untouched, and null clears a date where the clearing rules allow. Moving the status projects the commitment ledger events; closing at Received settles the open commitments and reopening removes them. locationId is editable only while the order is in Planning (409 invalid_transition afterwards). Returns 409 if a supplied prodNumber collides with another order.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| prodNumber | string [ 1 .. 255 ] characters |
| locationId | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| status | string Enum: "Planning" "Placed" "Production" "In Transit" "Partial" "Received" "Completed" "Cancelled" |
| orderDate | string or null <date> |
| expectedDeliveryDate | string or null <date> |
object | |
| inShippingCosts | number >= 0 |
| outShippingCosts | number >= 0 |
| otherCosts | number >= 0 |
| notes | string or null <= 2000 characters |
{- "prodNumber": "string",
- "locationId": "string",
- "status": "Planning",
- "orderDate": "2019-08-24",
- "expectedDeliveryDate": "2019-08-24",
- "statusDates": {
- "planning": "2019-08-24",
- "placed": "2019-08-24",
- "production": "2019-08-24",
- "inTransit": "2019-08-24",
- "partial": "2019-08-24",
- "received": "2019-08-24",
- "completed": "2019-08-24",
- "cancelled": "2019-08-24"
}, - "inShippingCosts": 0,
- "outShippingCosts": 0,
- "otherCosts": 0,
- "notes": "string"
}{- "id": "string",
- "prodNumber": "string",
- "status": "Planning",
- "location": {
- "id": "string",
- "name": "string"
}, - "orderDate": "string",
- "expectedDeliveryDate": "string",
- "statusDates": {
- "planning": "string",
- "placed": "string",
- "production": "string",
- "inTransit": "string",
- "partial": "string",
- "received": "string",
- "completed": "string",
- "cancelled": "string"
}, - "financials": {
- "inShippingCosts": 0,
- "outShippingCosts": 0,
- "otherCosts": 0
}, - "notes": "string",
- "createdAt": "string",
- "updatedAt": "string"
}Permanently delete a production order. Its items, recipe inputs, receipts, and projected ledger events cascade away, and on-hand recomputes live from the remaining ledger rows. There is no in-use gate.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "deletedId": "string"
}Get one production order as a composite: the header's fields at top level (no wrapper), including the derived financials (operational costs plus goods and total cost) and the per-status timestamps grouped in statusDates, its output-SKU line items nested under items, and its receipts under receipts, each receipt nesting its own produced lines under items. The location is an embedded { id, name } reference; each output line's SKU and each recipe input's material is an { id, code, name } reference, with the recipe inputs snapshotted from the exploded BOM nested under each item's inputs. Nested rows omit the redundant productionOrderId / receiptId back-references.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "id": "string",
- "prodNumber": "string",
- "status": "Planning",
- "location": {
- "id": "string",
- "name": "string"
}, - "orderDate": "string",
- "expectedDeliveryDate": "string",
- "statusDates": {
- "planning": "string",
- "placed": "string",
- "production": "string",
- "inTransit": "string",
- "partial": "string",
- "received": "string",
- "completed": "string",
- "cancelled": "string"
}, - "financials": {
- "inShippingCosts": 0,
- "outShippingCosts": 0,
- "otherCosts": 0,
- "goodsCost": 0,
- "totalCost": 0
}, - "notes": "string",
- "createdAt": "string",
- "updatedAt": "string",
- "items": [
- {
- "id": "string",
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string",
- "productionCost": "string",
- "inputs": [
- {
- "id": "string",
- "material": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string"
}
]
}
], - "receipts": [
- {
- "id": "string",
- "receiptDate": "string",
- "createdAt": "string",
- "items": [
- {
- "id": "string",
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string",
- "productionCost": "string",
- "lotNumber": "string",
- "expirationDate": "string",
- "createdAt": "string"
}
]
}
]
}Import many production orders in one atomic call, used by the Data page. Each row creates an order with an auto-generated number, its items, the recipe snapshots, and the projected commitment events for its status. The whole batch rolls back if any row fails. Admins and owners only. Returns 404 if a row references a location or output SKU that does not exist.
required | Array of objects | ||||||||||||||||||||
Array
| |||||||||||||||||||||
{- "rows": [
- {
- "status": "Planning",
- "locationId": "string",
- "orderDate": "2019-08-24",
- "expectedDeliveryDate": "2019-08-24",
- "statusDates": {
- "planning": "2019-08-24",
- "placed": "2019-08-24",
- "production": "2019-08-24",
- "inTransit": "2019-08-24",
- "partial": "2019-08-24",
- "received": "2019-08-24",
- "completed": "2019-08-24",
- "cancelled": "2019-08-24"
}, - "inShippingCosts": 0,
- "outShippingCosts": 0,
- "otherCosts": 0,
- "notes": "string",
- "items": [
- {
- "skuId": "string",
- "quantity": 1,
- "productionCost": 0
}
]
}
]
}{- "ids": [
- "string"
]
}Add an output SKU to an existing production order, named by productionOrderId in the body, and snapshot its exploded BOM (raw materials) at add time. Adding to an ordered order projects the new line's commitment ledger events. productionCost is optional here and accepts up to 6 decimals (it can be set later, but must be present to receive). The response is the line in wire shape: its output SKU embedded and its snapshotted recipe inputs nested (each input's material embedded). Returns 404 if the production order does not exist, and 409 if a receipt already exists against the order (invalid_transition; its items are locked) or the SKU is already an output line on it (conflict).
| skuId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| quantity required | integer >= 1 |
| productionCost | number or null >= 0 |
| productionOrderId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "skuId": "string",
- "quantity": 1,
- "productionCost": 0,
- "productionOrderId": "string"
}{- "id": "string",
- "productionOrderId": "string",
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string",
- "productionCost": "string",
- "inputs": [
- {
- "id": "string",
- "material": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string"
}
]
}List production order output line items, each with its produced SKU embedded as an { id, code, name } reference and its snapshotted recipe inputs nested under inputs (each input's material embedded). Pass productionOrderId to scope the list to one order, or omit it to read lines across every order (a flat items view); skuId narrows by the produced SKU. An unknown productionOrderId returns an empty list rather than a 404.
| productionOrderId | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| skuId | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "data": [
- {
- "id": "string",
- "productionOrderId": "string",
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string",
- "productionCost": "string",
- "inputs": [
- {
- "id": "string",
- "material": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string"
}
]
}
]
}Update an output line's quantity or per-unit production cost (up to 6 decimals). A quantity change re-scales the item's snapshotted exploded-BOM inputs proportionally and re-projects the line's commitment ledger events. The per-unit production cost, by contrast, stays editable for the WHOLE lifecycle: even after a receipt exists and even once the order is Completed, so a cost-only edit never trips the receipt lock. The response is the line in wire shape: its output SKU embedded and its recipe inputs nested. Returns 409 invalid_transition on a quantity change once any receipt exists against the order.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| quantity | integer >= 1 |
| productionCost | number or null >= 0 |
{- "quantity": 1,
- "productionCost": 0
}{- "id": "string",
- "productionOrderId": "string",
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string",
- "productionCost": "string",
- "inputs": [
- {
- "id": "string",
- "material": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string"
}
]
}Remove an output SKU line from a production order, unwinding the commitment ledger events it had projected; its snapshotted recipe inputs cascade away. Returns 409 invalid_transition once any receipt exists against the order: its line items are locked.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "deletedId": "string"
}Record production received back from the subcontractor against an order, named by productionOrderId in the body: writes the receipt and its produced lines, then BOTH ledger legs, a RECEIVE row per produced SKU on the finished-goods ledger and a CONSUME row per snapshotted exploded-BOM material on the materials ledger, all tagged ref = the order number, and advances the order to Partial (receiving against a Received order reopens it). The produced unit cost (materials plus the per-unit production cost) is derived on read by the costing engine off the line, never rounded and never stored. The 201 is two-keyed: the created receipt with its produced lines nested under items (each line's SKU embedded), and the updated production order header in flat wire shape. Returns 404 if the production order does not exist, 422 if a produced SKU's per-unit production cost is still unset (it must be set to receive), and 409 invalid_transition if the order is already terminal (Completed or Cancelled) or still in Planning/Placed (receiving opens in Production).
| productionOrderId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| receiptDate | string <date> |
required | Array of objects |
{- "productionOrderId": "string",
- "receiptDate": "2019-08-24",
- "items": [
- {
- "skuId": "string",
- "quantity": 1,
- "lotNumber": "string",
- "expirationDate": "2019-08-24"
}
]
}{- "receipt": {
- "id": "string",
- "productionOrderId": "string",
- "receiptDate": "string",
- "createdAt": "string",
- "items": [
- {
- "id": "string",
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string",
- "productionCost": "string",
- "lotNumber": "string",
- "expirationDate": "string",
- "createdAt": "string"
}
]
}, - "productionOrder": {
- "id": "string",
- "prodNumber": "string",
- "status": "Planning",
- "location": {
- "id": "string",
- "name": "string"
}, - "orderDate": "string",
- "expectedDeliveryDate": "string",
- "statusDates": {
- "planning": "string",
- "placed": "string",
- "production": "string",
- "inTransit": "string",
- "partial": "string",
- "received": "string",
- "completed": "string",
- "cancelled": "string"
}, - "financials": {
- "inShippingCosts": 0,
- "outShippingCosts": 0,
- "otherCosts": 0
}, - "notes": "string",
- "createdAt": "string",
- "updatedAt": "string"
}
}List production receipts in receipt-date order. Pass productionOrderId to scope the list to one order, or omit it to read receipts across every order. An unknown productionOrderId returns an empty list rather than a 404.
| productionOrderId | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "data": [
- {
- "id": "string",
- "productionOrderId": "string",
- "receiptDate": "string",
- "createdAt": "string"
}
]
}Update a production receipt's date. Re-dating the receipt re-dates its ledger rows on both ledgers but never changes the order's status.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| receiptDate | string <date> |
{- "receiptDate": "2019-08-24"
}{- "id": "string",
- "productionOrderId": "string",
- "receiptDate": "string",
- "createdAt": "string"
}Delete a production receipt and its lines. Its CONSUME and RECEIVE ledger rows on both ledgers are removed too, so on-hand recomputes live from the remaining ledger rows, and the order status walks back to what the surviving receipts support (Partial, or Production when none remain; deleting a receipt from a Received order reopens it). Returns 409 invalid_transition while the order is Completed: reopen it first.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "deletedId": "string"
}Get one production receipt: the receipt header's fields at top level (no wrapper, productionOrderId included as the parent correlation) with its produced line items nested under items, each line's SKU embedded as an { id, code, name } reference (the nested lines omit the redundant receiptId back-reference).
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "id": "string",
- "productionOrderId": "string",
- "receiptDate": "string",
- "createdAt": "string",
- "items": [
- {
- "id": "string",
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string",
- "productionCost": "string",
- "lotNumber": "string",
- "expirationDate": "string",
- "createdAt": "string"
}
]
}Edit a produced line in place: a quantity change propagates to the finished-goods RECEIVE row and rescales the materials CONSUME rows by the same recipe formula; lot number and expiration date update the RECEIVE row. Production cost (up to 6 decimals) is read live by the costing engine, never stored. The response is the line in wire shape, its produced SKU embedded. Returns 409 invalid_transition if the order is already Completed.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| quantity | integer >= 1 |
| productionCost | number >= 0 |
| lotNumber | string or null <= 255 characters |
| expirationDate | string or null <date> |
{- "quantity": 1,
- "productionCost": 0,
- "lotNumber": "string",
- "expirationDate": "2019-08-24"
}{- "id": "string",
- "receiptId": "string",
- "sku": {
- "id": "string",
- "code": "string",
- "name": "string"
}, - "quantity": "string",
- "productionCost": "string",
- "lotNumber": "string",
- "expirationDate": "string",
- "createdAt": "string"
}List the production order status vocabulary (Planning, Placed, Production, In Transit, Partial, Received, Completed, Cancelled) as a plain string array, for populating status pickers.
{- "data": [
- "string"
]
}Attach a file to an existing entity. The multipart body carries the binary in the "file" part plus entityType (sales_order, purchase_order, or work_order) and entityId as text fields. Files cap at 10 MB, and the parent entity must exist in the caller's organization. The stored bytes live in object storage; the response returns only the document metadata row.
| file required | string <binary> |
| entityType required | string Enum: "sales_order" "purchase_order" "work_order" |
| entityId required | string <uuid> |
{- "id": "string",
- "entity": {
- "id": "string",
- "number": "string",
- "type": "sales_order"
}, - "name": "string",
- "mimeType": "string",
- "sizeBytes": "string",
- "createdAt": "string"
}List every document attached to one entity, newest first. Both entityType (sales_order, purchase_order, or work_order) and entityId are required query params; there is no unbounded cross-entity listing. An unknown or empty parent returns an empty list rather than a 404. Each row is metadata only; fetch a download URL with the get-document detail endpoint.
| entityType required | string Enum: "sales_order" "purchase_order" "work_order" |
| entityId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "data": [
- {
- "id": "string",
- "entity": {
- "id": "string",
- "number": "string",
- "type": "sales_order"
}, - "name": "string",
- "mimeType": "string",
- "sizeBytes": "string",
- "createdAt": "string"
}
]
}Permanently delete one document: its metadata row and the stored file. Irreversible, and there is no soft-delete or trash. Returns the deleted id.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "deletedId": "string"
}Get one document's metadata plus a freshly minted signed URL for downloading its file. The URL is short-lived (about 15 minutes) and generated per request, so it is never stored and must be used promptly.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "id": "string",
- "entity": {
- "id": "string",
- "number": "string",
- "type": "sales_order"
}, - "name": "string",
- "mimeType": "string",
- "sizeBytes": "string",
- "createdAt": "string",
- "signedUrl": "string"
}Append a free-text note to one entity's timeline. The body names the parent by entityType (sales_order, purchase_order, work_order, production_order, sales_receipt, sales_return, or transfer) and entityId, plus the note text in note; the parent must exist in the caller's organization. The response embeds the parent as an entity object carrying its document number as name, plus entityStatus, a server-stamped snapshot of the parent's status at write time (it never updates afterward and cannot be supplied by the caller), and createdBy, the author as { id, name }, stamped from the session user (also not caller-suppliable). Notes are permanent history: there is no update, only add and delete.
| entityType required | string Enum: "sales_order" "purchase_order" "work_order" "production_order" "sales_receipt" "sales_return" "transfer" |
| entityId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| note required | string [ 1 .. 2000 ] characters |
{- "entityType": "sales_order",
- "entityId": "string",
- "note": "string"
}{- "id": "string",
- "entity": {
- "id": "string",
- "number": "string",
- "type": "sales_order"
}, - "entityStatus": "string",
- "note": "string",
- "createdBy": {
- "id": "string",
- "name": "string"
}, - "createdAt": "string"
}List every timeline note attached to one entity, newest first. Both entityType (sales_order, purchase_order, work_order, production_order, sales_receipt, sales_return, or transfer) and entityId are required query params; there is no unbounded cross-entity listing. An unknown or empty parent returns an empty list rather than a 404. Each row embeds the parent as an entity object carrying its document number under number, plus entityStatus, the snapshot of the parent's status when the note was written (stamped at create time, never updated), and createdBy, the author as { id, name }, null on notes migrated from before authorship existed.
| entityType required | string Enum: "sales_order" "purchase_order" "work_order" "production_order" "sales_receipt" "sales_return" "transfer" |
| entityId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "data": [
- {
- "id": "string",
- "entity": {
- "id": "string",
- "number": "string",
- "type": "sales_order"
}, - "entityStatus": "string",
- "note": "string",
- "createdBy": {
- "id": "string",
- "name": "string"
}, - "createdAt": "string"
}
]
}Permanently delete one timeline note by its own id (not the parent entity's id). Irreversible. Returns the deleted id.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "deletedId": "string"
}Completed sales orders and sales receipts grouped by the (order channel, customer segment) pair over a trailing window (window=30d, 45d or 90d; default 45d), each row carrying orders, revenue, operational costs, engine-derived COGS, contribution margin and margin ratio, for the current window and the equal-length prior window side by side. A sales order's row pairs its order channel with its customer's segment at report time (segment null when the customer is unsegmented, a data-quality signal). Sales receipts have no customer entity to segment: their rows carry the fixed DTC segment dimension paired with the receipt's order channel (null when the receipt has none). Rows appear only when they have activity in the current or prior window; idle channels and segments are not listed. missingFreightCount flags completed orders in the current window with no freight or shipping cost recorded, meaning the margin reads high. Sales returns are not netted out yet.
| window | string Enum: "30d" "45d" "90d" |
{- "window": "30d",
- "current": {
- "from": "string",
- "to": "string"
}, - "prior": {
- "from": "string",
- "to": "string"
}, - "data": [
- {
- "group": {
- "kind": "segment",
- "channelId": "string",
- "channelName": "string",
- "segmentId": "string",
- "segmentName": "string"
}, - "current": {
- "orders": 0,
- "revenue": 0,
- "operationalCosts": 0,
- "inventoryCost": 0,
- "contributionMargin": 0,
- "margin": 0
}, - "prior": {
- "orders": 0,
- "revenue": 0,
- "operationalCosts": 0,
- "inventoryCost": 0,
- "contributionMargin": 0,
- "margin": 0
}
}
], - "total": {
- "current": {
- "orders": 0,
- "revenue": 0,
- "operationalCosts": 0,
- "inventoryCost": 0,
- "contributionMargin": 0,
- "margin": 0
}, - "prior": {
- "orders": 0,
- "revenue": 0,
- "operationalCosts": 0,
- "inventoryCost": 0,
- "contributionMargin": 0,
- "margin": 0
}
}, - "missingFreightCount": 0
}Cash committed to inbound goods across open purchase orders (Placed, In Transit, Partial), work orders (Placed, Production, Partial) and production orders (those plus In Transit), plus how much of it is expected to land within horizonDays (default 14, bounded 1..90). Committed means remainders clamped at zero: a purchase order's expected total minus what has been received, and a work or production order's unconsumed input quantities valued at the engine's current unit cost plus its stored fees. Orders already past their expected delivery date count as arriving soon.
| horizonDays | integer [ 1 .. 90 ] |
{- "horizonDays": 0,
- "purchaseOrders": {
- "count": 0,
- "committed": 0,
- "arrivingSoon": 0
}, - "workOrders": {
- "count": 0,
- "committed": 0,
- "arrivingSoon": 0
}, - "productionOrders": {
- "count": 0,
- "committed": 0,
- "arrivingSoon": 0
}, - "total": {
- "committed": 0,
- "arrivingSoon": 0
}
}Creates the platform user row for the Firebase identity the request is authenticated as; sign-up itself happens client-side in Firebase first. Idempotent: calling again returns the existing row. Accepting an invitation (POST /invitations/accept) creates the row on its own, so invited users never need this call.
| displayName | string or null <= 255 characters |
{- "displayName": "string"
}{- "id": "string",
- "email": "string",
- "displayName": "string",
- "isActive": true,
- "createdAt": "string",
- "updatedAt": "string"
}Returns the authenticated user together with every organization they belong to, each carrying the caller's role and the org's settings. The web client's session bootstrap; there is no separate list-organizations endpoint. Control plane: requires a Firebase session; PATs are rejected.
{- "user": {
- "id": "string",
- "email": "string",
- "displayName": "string",
- "isActive": true,
- "createdAt": "string",
- "updatedAt": "string"
}, - "organizations": [
- {
- "organization": {
- "id": "string",
- "name": "string",
- "slug": "string",
- "isActive": true,
- "createdAt": "string",
- "updatedAt": "string"
}, - "role": "owner",
- "settings": {
- "orgId": "string",
- "legalName": "string",
- "address": {
- "line1": "string",
- "city": "string",
- "state": "string",
- "postalCode": "string",
- "country": "string"
}, - "logoUrl": "string",
- "contact": {
- "email": "string",
- "phone": "string"
}, - "operatingModel": "Manufacturer",
- "costingMethod": "FIFO",
- "timezone": "string",
- "connectedLedgerFrom": "string",
- "createdAt": "string",
- "updatedAt": "string"
}
}
]
}Updates the authenticated user's own profile. Only displayName is editable; email is owned by the Firebase identity. An empty patch is a no-op that returns the current record. Control plane: requires a Firebase session, PATs are rejected.
| displayName | string or null <= 255 characters |
{- "displayName": "string"
}{- "id": "string",
- "email": "string",
- "displayName": "string",
- "isActive": true,
- "createdAt": "string",
- "updatedAt": "string"
}Deletes the caller's own account. Removes their memberships, purges every organization they solely own (destroying all of that org's data; co-owned orgs survive), and deletes their PATs and sent invitations, then removes the Firebase identity. Irreversible. Control plane: requires a Firebase session, PATs are rejected.
{- "deletedId": "string"
}Mints an org-scoped personal access token for programmatic data-plane access (scripts, agents, MCP). The token is bound to orgId (the data plane it can reach); the X-Org-Id header is ignored thereafter. The plaintext token is returned once in this response and is never retrievable again. Control plane: requires a Firebase session, PATs are rejected.
| name required | string [ 1 .. 255 ] characters |
| orgId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| expiresInDays | integer or null [ 1 .. 3650 ] |
{- "name": "string",
- "orgId": "string",
- "expiresInDays": 1
}{- "token": {
- "id": "string",
- "org": {
- "id": "string",
- "name": "string"
}, - "name": "string",
- "tokenPrefix": "string",
- "lastUsedAt": "string",
- "expiresAt": "string",
- "revokedAt": "string",
- "createdAt": "string"
}, - "plaintext": "string"
}Lists the caller's active (non-revoked) PATs with display-safe metadata (token prefix, expiry, last use). Secrets are never returned here. Control plane: requires a Firebase session, PATs are rejected.
{- "data": [
- {
- "id": "string",
- "org": {
- "id": "string",
- "name": "string"
}, - "name": "string",
- "tokenPrefix": "string",
- "lastUsedAt": "string",
- "expiresAt": "string",
- "revokedAt": "string",
- "createdAt": "string"
}
]
}Revokes one of the caller's PATs. Takes effect immediately: the token fails authentication from the next request on. Control plane: requires a Firebase session, PATs are rejected.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "deletedId": "string"
}Lists the pending invitations addressed to the caller's email, across all organizations, each enriched with the organization name and inviter name the banner renders. Backs the in-app accept/decline banner. Control plane: requires a Firebase session, PATs are rejected.
{- "data": [
- {
- "id": "string",
- "organization": {
- "id": "string",
- "name": "string"
}, - "role": "owner",
- "inviterName": "string",
- "token": "string",
- "expiresAt": "string",
- "createdAt": "string"
}
]
}Public, unauthenticated lookup backing the accept-invite landing page. Given the invitation's secret token, returns a redacted view (invitee email, org name, role, expiry, and derived status) with no ids and no inviter detail. Access is gated by possession of the unguessable token, not by a session.
| token required | string |
{- "invitation": {
- "email": "string",
- "role": "owner",
- "orgName": "string",
- "expiresAt": "string",
- "status": "pending"
}
}Accepts an invitation by its token and creates the membership, returning it together with the joined organization. A first-time invitee gets their user row created on the spot and may set displayName in the same call; the invitation email must match the authenticated identity. Control plane: requires a Firebase session, PATs are rejected.
| token required | string |
| displayName | string or null <= 255 characters |
{- "token": "string",
- "displayName": "string"
}{- "membership": {
- "id": "string",
- "userId": "string",
- "orgId": "string",
- "role": "owner",
- "isActive": true,
- "joinedAt": "string",
- "lastActiveAt": "string",
- "createdAt": "string",
- "updatedAt": "string"
}, - "organization": {
- "id": "string",
- "name": "string",
- "slug": "string",
- "isActive": true,
- "createdAt": "string",
- "updatedAt": "string"
}
}Declines one of the caller's pending invitations by id. Only pending invitations addressed to the caller's email can be declined. Control plane: requires a Firebase session, PATs are rejected.
| invitationId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "invitationId": "string"
}{- "declinedId": "string"
}Creates an organization with the caller as its owner, optionally seeding the operating model and costing method. The slug is unique across the platform. Control plane: requires a Firebase session, PATs are rejected.
| name required | string [ 1 .. 255 ] characters |
| slug required | string [ 2 .. 100 ] characters ^[a-z0-9][a-z0-9-]*[a-z0-9]$ |
| operatingModel | string Enum: "Manufacturer" "Reseller" |
| costingMethod | string Enum: "FIFO" "FEFO" "LIFO" "WEIGHTED_AVERAGE" |
{- "name": "string",
- "slug": "string",
- "operatingModel": "Manufacturer",
- "costingMethod": "FIFO"
}{- "organization": {
- "id": "string",
- "name": "string",
- "slug": "string",
- "isActive": true,
- "createdAt": "string",
- "updatedAt": "string"
}, - "membership": {
- "id": "string",
- "userId": "string",
- "orgId": "string",
- "role": "owner",
- "isActive": true,
- "joinedAt": "string",
- "lastActiveAt": "string",
- "createdAt": "string",
- "updatedAt": "string"
}, - "settings": {
- "orgId": "string",
- "legalName": "string",
- "address": {
- "line1": "string",
- "city": "string",
- "state": "string",
- "postalCode": "string",
- "country": "string"
}, - "logoUrl": "string",
- "contact": {
- "email": "string",
- "phone": "string"
}, - "operatingModel": "Manufacturer",
- "costingMethod": "FIFO",
- "timezone": "string",
- "connectedLedgerFrom": "string",
- "createdAt": "string",
- "updatedAt": "string"
}
}Returns the current organization (selected by the X-Org-Id header) together with its settings. Control plane: requires a Firebase session, PATs are rejected.
{- "organization": {
- "id": "string",
- "name": "string",
- "slug": "string",
- "isActive": true,
- "createdAt": "string",
- "updatedAt": "string"
}, - "settings": {
- "orgId": "string",
- "legalName": "string",
- "address": {
- "line1": "string",
- "city": "string",
- "state": "string",
- "postalCode": "string",
- "country": "string"
}, - "logoUrl": "string",
- "contact": {
- "email": "string",
- "phone": "string"
}, - "operatingModel": "Manufacturer",
- "costingMethod": "FIFO",
- "timezone": "string",
- "connectedLedgerFrom": "string",
- "createdAt": "string",
- "updatedAt": "string"
}
}Updates org identity (name) and/or settings (legal identity, contact, logoUrl, operating model, costing method) atomically. The slug is immutable after creation: warehouse datasets are named by it. Owner only. Control plane: requires a Firebase session, PATs are rejected.
| name | string [ 1 .. 255 ] characters |
| legalName | string or null |
object or null | |
| logoUrl | string or null |
object or null | |
| operatingModel | string or null Enum: "Manufacturer" "Reseller" null |
| costingMethod | string Enum: "FIFO" "FEFO" "LIFO" "WEIGHTED_AVERAGE" |
| timezone | string or null <= 64 characters |
| connectedLedgerFrom | string^\d{4}-\d{2}-\d{2}$ |
{- "name": "string",
- "legalName": "string",
- "address": {
- "line1": "string",
- "city": "string",
- "state": "string",
- "postalCode": "string",
- "country": "st"
}, - "logoUrl": "string",
- "contact": {
- "email": "string",
- "phone": "string"
}, - "operatingModel": "Manufacturer",
- "costingMethod": "FIFO",
- "timezone": "string",
- "connectedLedgerFrom": "string"
}{- "organization": {
- "id": "string",
- "name": "string",
- "slug": "string",
- "isActive": true,
- "createdAt": "string",
- "updatedAt": "string"
}, - "settings": {
- "orgId": "string",
- "legalName": "string",
- "address": {
- "line1": "string",
- "city": "string",
- "state": "string",
- "postalCode": "string",
- "country": "string"
}, - "logoUrl": "string",
- "contact": {
- "email": "string",
- "phone": "string"
}, - "operatingModel": "Manufacturer",
- "costingMethod": "FIFO",
- "timezone": "string",
- "connectedLedgerFrom": "string",
- "createdAt": "string",
- "updatedAt": "string"
}
}Irreversibly purges the current organization: all business data, memberships, and invitations are deleted. Owner only. Control plane: requires a Firebase session, PATs are rejected.
{- "deletedId": "string"
}Lists the org's members with their identity joined in: email, displayName, role, and activity timestamps. Control plane: requires a Firebase session, PATs are rejected.
{- "data": [
- {
- "user": {
- "id": "string",
- "name": "string",
- "email": "string"
}, - "role": "owner",
- "isActive": true,
- "joinedAt": "string",
- "lastActiveAt": "string"
}
]
}Sets a member's role. Promoting to owner is allowed (an org can have several owners); demoting the last remaining owner is blocked. Owner only. Control plane: requires a Firebase session, PATs are rejected.
| userId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| role required | string Enum: "owner" "admin" "member" "viewer" |
{- "role": "owner"
}{- "id": "string",
- "userId": "string",
- "orgId": "string",
- "role": "owner",
- "isActive": true,
- "joinedAt": "string",
- "lastActiveAt": "string",
- "createdAt": "string",
- "updatedAt": "string"
}Removes a member from the organization. The last owner cannot be removed, and self-removal goes through the leave endpoint instead. Owner only. Control plane: requires a Firebase session, PATs are rejected.
| userId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "deletedId": "string"
}The caller leaves the current organization. When the last owner leaves, the organization is purged with all its data; deletedOrg in the response reports which of the two happened. Control plane: requires a Firebase session, PATs are rejected.
{- "deletedOrg": true
}Invites an email to join the org as admin, member, or viewer (owner is granted by promotion, not invitation). Idempotent per email: an existing pending invitation is returned instead of duplicated. The invite email is delivered best-effort and emailSent reports whether it went out; the invitation exists either way. Control plane: requires a Firebase session, PATs are rejected.
| email required | string <email> |
| role required | string Enum: "admin" "member" "viewer" |
{- "email": "user@example.com",
- "role": "admin"
}{- "invitation": {
- "id": "string",
- "email": "string",
- "role": "owner",
- "inviterName": "string",
- "status": "pending",
- "expiresAt": "string",
- "createdAt": "string"
}, - "emailSent": true
}Lists the org's invitations, each with the inviter's name and a derived status (pending, accepted, revoked, or expired) joined in. Pass status to narrow to the stored pending, accepted, or revoked buckets; expired is a derived refinement of pending, not a filter. Control plane: requires a Firebase session, PATs are rejected.
| status | string Enum: "pending" "accepted" "revoked" |
{- "data": [
- {
- "id": "string",
- "email": "string",
- "role": "owner",
- "inviterName": "string",
- "status": "pending",
- "expiresAt": "string",
- "createdAt": "string"
}
]
}Re-sends the invite email for a pending invitation, rotating its token (old links stop working) and resetting the 7-day expiry. emailSent reports whether the email went out. Control plane: requires a Firebase session, PATs are rejected.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "invitation": {
- "id": "string",
- "email": "string",
- "role": "owner",
- "inviterName": "string",
- "status": "pending",
- "expiresAt": "string",
- "createdAt": "string"
}, - "emailSent": true
}Revokes a pending invitation so its token can no longer be accepted. Already-accepted invitations cannot be revoked; remove the member instead. Control plane: requires a Firebase session, PATs are rejected.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "deletedId": "string"
}Create a customer, the entity sales orders are placed against. Requires a customer type; segment and broker are optional. The name must be unique in the org (409 conflict otherwise). brokerFeePercent is a percentage, up to 6 decimal places. parentId, when set, must reference an existing customer in the org. customFields carries opaque key/value pairs the platform never interprets (max 50 fields; key <= 64 chars, value <= 512 chars; keys unique case-insensitively).
| name required | string [ 1 .. 255 ] characters |
| typeId required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| segmentId | string or null^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| brokerId | string or null^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| brokerFeePercent | number or null >= 0 |
object or null | |
| parentId | string or null^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| qboId | string or null <= 255 characters |
| notes | string or null <= 2000 characters |
Array of objects |
{- "name": "string",
- "typeId": "string",
- "segmentId": "string",
- "brokerId": "string",
- "brokerFeePercent": 0,
- "address": {
- "line1": "string",
- "city": "string",
- "state": "string",
- "postalCode": "string",
- "country": "st"
}, - "parentId": "string",
- "qboId": "string",
- "notes": "string",
- "customFields": [
- {
- "key": "string",
- "value": "string"
}
]
}{- "id": "string",
- "name": "string",
- "isActive": true,
- "type": {
- "id": "string",
- "name": "string"
}, - "segment": {
- "id": "string",
- "name": "string"
}, - "broker": {
- "id": "string",
- "name": "string",
- "feePercent": "string"
}, - "parent": {
- "id": "string",
- "name": "string"
}, - "address": {
- "line1": "string",
- "city": "string",
- "state": "string",
- "postalCode": "string",
- "country": "string"
}, - "qboId": "string",
- "notes": "string",
- "customFields": [
- {
- "key": "string",
- "value": "string"
}
], - "createdAt": "string",
- "updatedAt": "string"
}List one page of the org's customers. Every filter is optional and AND-ed: active, the CSV id multi-selects (type, segment, broker, parentCustomer), territory (city/state/country, case-insensitive exact), hasParent (true = subsidiaries only, false = top-level only), inclusive createdFrom/createdTo dates, a free-text search over the name, joined classifier names and notes, and a customFieldKey/customFieldValue lookup for a custom-field code. Sort with sort (name, type, segment, broker, parent, brokerFeePercent, createdAt, updatedAt; default name; empty values always last) and order (asc/desc); page with limit (default 50, max 200) and offset. meta.total counts the filtered set, so pagers can render page counts under active filters.
| sort | string Enum: "name" "type" "segment" "broker" "parent" "brokerFeePercent" "createdAt" "updatedAt" |
| order | string Enum: "asc" "desc" |
| limit | integer [ 1 .. 200 ] |
| offset | integer >= 0 |
| active | string Enum: "true" "false" |
| type | string |
| segment | string |
| broker | string |
| parentCustomer | string |
| city | string |
| state | string |
| country | string |
| hasParent | string Enum: "true" "false" |
| createdFrom | string^\d{4}-\d{2}-\d{2}$ |
| createdTo | string^\d{4}-\d{2}-\d{2}$ |
| search | string |
| customFieldKey | string |
| customFieldValue | string |
{- "data": [
- {
- "id": "string",
- "name": "string",
- "isActive": true,
- "type": {
- "id": "string",
- "name": "string"
}, - "segment": {
- "id": "string",
- "name": "string"
}, - "broker": {
- "id": "string",
- "name": "string",
- "feePercent": "string"
}, - "parent": {
- "id": "string",
- "name": "string"
}, - "address": {
- "line1": "string",
- "city": "string",
- "state": "string",
- "postalCode": "string",
- "country": "string"
}, - "qboId": "string",
- "notes": "string",
- "customFields": [
- {
- "key": "string",
- "value": "string"
}
], - "createdAt": "string",
- "updatedAt": "string"
}
], - "meta": {
- "total": 0,
- "limit": 0,
- "offset": 0
}
}Fetch a single customer with the contacts linked to it embedded under contacts (each a hydrated link + contact + methods row), so the detail page renders in one call.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "id": "string",
- "name": "string",
- "isActive": true,
- "type": {
- "id": "string",
- "name": "string"
}, - "segment": {
- "id": "string",
- "name": "string"
}, - "broker": {
- "id": "string",
- "name": "string",
- "feePercent": "string"
}, - "parent": {
- "id": "string",
- "name": "string"
}, - "address": {
- "line1": "string",
- "city": "string",
- "state": "string",
- "postalCode": "string",
- "country": "string"
}, - "qboId": "string",
- "notes": "string",
- "customFields": [
- {
- "key": "string",
- "value": "string"
}
], - "createdAt": "string",
- "updatedAt": "string",
- "contacts": [
- {
- "link": {
- "id": "string",
- "role": "string",
- "isPrimary": true,
- "notes": "string",
- "isActive": true,
- "createdAt": "string",
- "updatedAt": "string"
}, - "contact": {
- "id": "string",
- "displayName": "string",
- "firstName": "string",
- "lastName": "string",
- "title": "string",
- "notes": "string",
- "isActive": true,
- "createdAt": "string",
- "updatedAt": "string"
}, - "methods": [
- {
- "id": "string",
- "methodType": "email",
- "value": "string",
- "label": "string",
- "isPrimary": true,
- "isActive": true,
- "createdAt": "string",
- "updatedAt": "string"
}
]
}
]
}Update a customer's fields (partial; only send what changes). Set isActive false to archive or true to reactivate. Renaming to a name another customer already uses is rejected with 409 conflict. brokerFeePercent is a percentage, up to 6 decimal places. parentId, when set, must reference an existing customer and cannot be the customer's own id. customFields, when sent, replaces the whole set (max 50 fields; key <= 64 chars, value <= 512 chars; keys unique case-insensitively).
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| name | string [ 1 .. 255 ] characters |
| typeId | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| segmentId | string or null^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| brokerId | string or null^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| brokerFeePercent | number or null >= 0 |
object or null | |
| parentId | string or null^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| qboId | string or null <= 255 characters |
| notes | string or null <= 2000 characters |
Array of objects | |
| isActive | boolean |
{- "name": "string",
- "typeId": "string",
- "segmentId": "string",
- "brokerId": "string",
- "brokerFeePercent": 0,
- "address": {
- "line1": "string",
- "city": "string",
- "state": "string",
- "postalCode": "string",
- "country": "st"
}, - "parentId": "string",
- "qboId": "string",
- "notes": "string",
- "customFields": [
- {
- "key": "string",
- "value": "string"
}
], - "isActive": true
}{- "id": "string",
- "name": "string",
- "isActive": true,
- "type": {
- "id": "string",
- "name": "string"
}, - "segment": {
- "id": "string",
- "name": "string"
}, - "broker": {
- "id": "string",
- "name": "string",
- "feePercent": "string"
}, - "parent": {
- "id": "string",
- "name": "string"
}, - "address": {
- "line1": "string",
- "city": "string",
- "state": "string",
- "postalCode": "string",
- "country": "string"
}, - "qboId": "string",
- "notes": "string",
- "customFields": [
- {
- "key": "string",
- "value": "string"
}
], - "createdAt": "string",
- "updatedAt": "string"
}Permanently delete a customer. Blocked with 409 in_use when sales orders reference it (the error details carry salesOrderCount); being another customer's parent never blocks (the child's parent link is cleared). Archive instead (PATCH with isActive=false) to keep the history.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "deletedId": "string"
}Create a customer type, the required classification every customer carries. The name must be unique among the org's types (409 conflict otherwise).
| name required | string [ 1 .. 100 ] characters |
{- "name": "string"
}{- "id": "string",
- "name": "string",
- "sortOrder": 0,
- "isActive": true,
- "createdAt": "string"
}List the org's customer types in sort order, active and archived alike. Pass a free-text search to match names (case-insensitive contains).
| search | string |
{- "data": [
- {
- "id": "string",
- "name": "string",
- "sortOrder": 0,
- "isActive": true,
- "createdAt": "string"
}
]
}Update a customer type's name, sort order, or active flag (partial). Renaming to a name another type already uses is rejected with 409 conflict.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| name | string [ 1 .. 100 ] characters |
| sortOrder | integer |
| isActive | boolean |
{- "name": "string",
- "sortOrder": 0,
- "isActive": true
}{- "id": "string",
- "name": "string",
- "sortOrder": 0,
- "isActive": true,
- "createdAt": "string"
}Permanently delete a customer type. Blocked with 409 in_use while any customer references it; the error details carry the blocking customerCount.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "deletedId": "string"
}Create a customer segment, an optional grouping (e.g. retail/wholesale). The name must be unique among the org's segments (409 conflict otherwise).
| name required | string [ 1 .. 100 ] characters |
{- "name": "string"
}{- "id": "string",
- "name": "string",
- "sortOrder": 0,
- "isActive": true,
- "createdAt": "string"
}List the org's customer segments in sort order, active and archived alike. Pass a free-text search to match names (case-insensitive contains).
| search | string |
{- "data": [
- {
- "id": "string",
- "name": "string",
- "sortOrder": 0,
- "isActive": true,
- "createdAt": "string"
}
]
}Update a customer segment's name, sort order, or active flag (partial). Renaming to a name another segment already uses is rejected with 409 conflict.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| name | string [ 1 .. 100 ] characters |
| sortOrder | integer |
| isActive | boolean |
{- "name": "string",
- "sortOrder": 0,
- "isActive": true
}{- "id": "string",
- "name": "string",
- "sortOrder": 0,
- "isActive": true,
- "createdAt": "string"
}Permanently delete a customer segment. Blocked with 409 in_use while any customer references it; the error details carry the blocking customerCount.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "deletedId": "string"
}Create many customers in one atomic call from an array of parsed rows (Data page upload). The whole batch is one transaction: the first invalid row rolls back all of them. A row whose name already exists in the org is a 409 conflict (naming the row number). Returns the created ids in row order.
required | Array of objects <= 1000 items | ||||||||||||||||||||
Array (<= 1000 items)
| |||||||||||||||||||||
{- "rows": [
- {
- "name": "string",
- "typeId": "string",
- "segmentId": "string",
- "brokerId": "string",
- "brokerFeePercent": 0,
- "address": {
- "line1": "string",
- "city": "string",
- "state": "string",
- "postalCode": "string",
- "country": "st"
}, - "parentId": "string",
- "qboId": "string",
- "notes": "string",
- "customFields": [
- {
- "key": "string",
- "value": "string"
}
]
}
]
}{- "ids": [
- "string"
]
}Create a broker, a third-party intermediary that can be attached to customers and sales orders. The name must be unique among the org's brokers (409 conflict otherwise).
| name required | string [ 1 .. 100 ] characters |
{- "name": "string"
}{- "id": "string",
- "name": "string",
- "sortOrder": 0,
- "isActive": true,
- "createdAt": "string"
}List the org's brokers in sort order, active and archived alike. Pass a free-text search to match broker names (case-insensitive contains); omit it to return the whole list.
| search | string |
{- "data": [
- {
- "id": "string",
- "name": "string",
- "sortOrder": 0,
- "isActive": true,
- "createdAt": "string"
}
]
}Update a broker's name, sort order, or active flag (partial update, only send what changes). Set isActive false to archive the broker or true to reactivate it; there is no separate archive endpoint. Renaming to a name another broker already uses is rejected with 409 conflict.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
| name | string [ 1 .. 100 ] characters |
| sortOrder | integer |
| isActive | boolean |
{- "name": "string",
- "sortOrder": 0,
- "isActive": true
}{- "id": "string",
- "name": "string",
- "sortOrder": 0,
- "isActive": true,
- "createdAt": "string"
}Permanently delete a broker. Blocked with 409 in_use while any customer or sales order references it; the error details carry the blocking customerCount and salesOrderCount. Attempt the delete and read the 409 rather than pre-checking.
| id required | string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}... |
{- "deletedId": "string"
}