Skip to main content

Pharus API (1.17.0)

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.

Conventions

  • Base path. Every endpoint lives under /api. Paths below are shown after that prefix.
  • Authentication. Send 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.
  • Authorization. Each endpoint declares the minimum role required (member, admin, or owner), enforced before the request runs.
  • Errors. Non-2xx responses use { "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.
  • Deletes return 200. Soft deletes return the updated entity; hard deletes return { "deletedId": "uuid" }. There are no 204 responses.
  • Cursor pagination. Unbounded reads accept ?cursor=<opaque>&limit=<int> and return { "data": [...], "nextCursor": string | null }.
  • Bulk imports accept { "rows": [...] } and return a per-row { index, status, id? | error? }.

Locations

Create location

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

Request Body schema: application/json
name
required
string [ 1 .. 100 ] characters
object or null
notes
string or null <= 2000 characters
Array of objects

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "address": {
    },
  • "notes": "string",
  • "customFields": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "name": "string",
  • "sortOrder": 0,
  • "address": {
    },
  • "notes": "string",
  • "isActive": true,
  • "customFields": [
    ],
  • "createdAt": "2019-08-24T14:15:22Z",
  • "updatedAt": "2019-08-24T14:15:22Z"
}

List locations

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.

query Parameters
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

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {
    }
}

Update location

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

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
Request Body schema: application/json
name
string [ 1 .. 100 ] characters
object or null
notes
string or null <= 2000 characters
Array of objects
sortOrder
integer >= 0
isActive
boolean

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "address": {
    },
  • "notes": "string",
  • "customFields": [
    ],
  • "sortOrder": 0,
  • "isActive": true
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "name": "string",
  • "sortOrder": 0,
  • "address": {
    },
  • "notes": "string",
  • "isActive": true,
  • "customFields": [
    ],
  • "createdAt": "2019-08-24T14:15:22Z",
  • "updatedAt": "2019-08-24T14:15:22Z"
}

Delete location

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "deletedId": "string"
}

Get location detail

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "id": "string",
  • "name": "string",
  • "sortOrder": 0,
  • "address": {
    },
  • "notes": "string",
  • "isActive": true,
  • "customFields": [
    ],
  • "createdAt": "2019-08-24T14:15:22Z",
  • "updatedAt": "2019-08-24T14:15:22Z",
  • "contacts": [
    ]
}

Vendors

Create vendor

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.

Request Body schema: application/json
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

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "address": {
    },
  • "term": "string",
  • "qboId": "string",
  • "notes": "string",
  • "customFields": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "name": "string",
  • "address": {
    },
  • "term": "string",
  • "qboId": "string",
  • "notes": "string",
  • "isActive": true,
  • "customFields": [
    ],
  • "createdAt": "string",
  • "updatedAt": "string"
}

List vendors

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.

query Parameters
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

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {
    }
}

Update vendor

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
Request Body schema: application/json
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

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "address": {
    },
  • "term": "string",
  • "qboId": "string",
  • "notes": "string",
  • "customFields": [
    ],
  • "isActive": true
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "name": "string",
  • "address": {
    },
  • "term": "string",
  • "qboId": "string",
  • "notes": "string",
  • "isActive": true,
  • "customFields": [
    ],
  • "createdAt": "string",
  • "updatedAt": "string"
}

Delete vendor

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "deletedId": "string"
}

Get vendor detail

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "id": "string",
  • "name": "string",
  • "address": {
    },
  • "term": "string",
  • "qboId": "string",
  • "notes": "string",
  • "isActive": true,
  • "customFields": [
    ],
  • "createdAt": "string",
  • "updatedAt": "string",
  • "contacts": [
    ]
}

Bulk-import vendors

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.

Request Body schema: application/json
required
Array of objects
Array
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

Responses

Request samples

Content type
application/json
{
  • "rows": [
    ]
}

Response samples

Content type
application/json
{
  • "ids": [
    ]
}

Materials

Create material

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

Request Body schema: application/json
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

Responses

Request samples

Content type
application/json
{
  • "code": "string",
  • "name": "string",
  • "typeId": "string",
  • "notes": "string",
  • "customFields": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "code": "string",
  • "name": "string",
  • "type": {
    },
  • "notes": "string",
  • "isActive": true,
  • "customFields": [
    ],
  • "createdAt": "string",
  • "updatedAt": "string"
}

List materials

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.

query Parameters
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

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {
    }
}

Get material detail

Fetch a single material by id: its header fields, custom fields, and its material type embedded as type with id and name.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "id": "string",
  • "code": "string",
  • "name": "string",
  • "type": {
    },
  • "notes": "string",
  • "isActive": true,
  • "customFields": [
    ],
  • "createdAt": "string",
  • "updatedAt": "string"
}

Update material

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

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
Request Body schema: application/json
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

Responses

Request samples

Content type
application/json
{
  • "code": "string",
  • "name": "string",
  • "typeId": "string",
  • "notes": "string",
  • "customFields": [
    ],
  • "isActive": true
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "code": "string",
  • "name": "string",
  • "type": {
    },
  • "notes": "string",
  • "isActive": true,
  • "customFields": [
    ],
  • "createdAt": "string",
  • "updatedAt": "string"
}

Delete material

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "deletedId": "string"
}

Create material type

Create a material type, the classification every material carries. The name must be unique among the org's material types (409 conflict otherwise).

Request Body schema: application/json
name
required
string [ 1 .. 100 ] characters

Responses

Request samples

Content type
application/json
{
  • "name": "string"
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "name": "string",
  • "sortOrder": 0,
  • "isActive": true,
  • "createdAt": "string"
}

List material types

List the org's material types in sort order, active and archived alike. Pass a free-text search to match names (case-insensitive contains).

query Parameters
search
string

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

Update material type

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
Request Body schema: application/json
name
string [ 1 .. 100 ] characters
sortOrder
integer
isActive
boolean

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "sortOrder": 0,
  • "isActive": true
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "name": "string",
  • "sortOrder": 0,
  • "isActive": true,
  • "createdAt": "string"
}

Delete material type

Permanently delete a material type. Blocked with 409 in_use while any material references it; the error details carry the blocking materialCount.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "deletedId": "string"
}

Bulk-import materials

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.

Request Body schema: application/json
required
Array of objects [ 1 .. 1000 ] items
Array ([ 1 .. 1000 ] items)
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

Responses

Request samples

Content type
application/json
{
  • "rows": [
    ]
}

Response samples

Content type
application/json
{
  • "ids": [
    ]
}

Products

Create SKU

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.

Request Body schema: application/json
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

Responses

Request samples

Content type
application/json
{
  • "code": "string",
  • "name": "string",
  • "typeId": "string",
  • "itemsPerUnit": 1,
  • "ozPerItem": 0,
  • "qboId": "string",
  • "notes": "string",
  • "customFields": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "code": "string",
  • "name": "string",
  • "type": {
    },
  • "itemsPerUnit": 0,
  • "ozPerItem": "string",
  • "qboId": "string",
  • "notes": "string",
  • "isActive": true,
  • "customFields": [
    ],
  • "createdAt": "string",
  • "updatedAt": "string"
}

List SKUs

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.

query Parameters
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

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {
    }
}

Get SKU detail

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "id": "string",
  • "code": "string",
  • "name": "string",
  • "type": {
    },
  • "itemsPerUnit": 0,
  • "ozPerItem": "string",
  • "qboId": "string",
  • "notes": "string",
  • "isActive": true,
  • "customFields": [
    ],
  • "createdAt": "string",
  • "updatedAt": "string"
}

Update SKU

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

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
Request Body schema: application/json
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

Responses

Request samples

Content type
application/json
{
  • "code": "string",
  • "name": "string",
  • "typeId": "string",
  • "itemsPerUnit": 1,
  • "ozPerItem": 0,
  • "qboId": "string",
  • "notes": "string",
  • "customFields": [
    ],
  • "isActive": true
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "code": "string",
  • "name": "string",
  • "type": {
    },
  • "itemsPerUnit": 0,
  • "ozPerItem": "string",
  • "qboId": "string",
  • "notes": "string",
  • "isActive": true,
  • "customFields": [
    ],
  • "createdAt": "string",
  • "updatedAt": "string"
}

Delete SKU

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "deletedId": "string"
}

Create SKU type

Create a SKU type, the classification every SKU carries. The name must be unique among the org's SKU types (409 conflict otherwise).

Request Body schema: application/json
name
required
string [ 1 .. 100 ] characters

Responses

Request samples

Content type
application/json
{
  • "name": "string"
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "name": "string",
  • "sortOrder": 0,
  • "isActive": true,
  • "createdAt": "string"
}

List SKU types

List the org's SKU types in sort order, active and archived alike. Pass a free-text search to match names (case-insensitive contains).

query Parameters
search
string

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

Update SKU type

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
Request Body schema: application/json
name
string [ 1 .. 100 ] characters
sortOrder
integer
isActive
boolean

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "sortOrder": 0,
  • "isActive": true
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "name": "string",
  • "sortOrder": 0,
  • "isActive": true,
  • "createdAt": "string"
}

Delete SKU type

Permanently delete a SKU type. Blocked with 409 in_use while any SKU references it; the error details carry the blocking skuCount.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "deletedId": "string"
}

Bulk-import SKUs

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.

Request Body schema: application/json
required
Array of objects [ 1 .. 1000 ] items
Array ([ 1 .. 1000 ] items)
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

Responses

Request samples

Content type
application/json
{
  • "rows": [
    ]
}

Response samples

Content type
application/json
{
  • "ids": [
    ]
}

BOM

List SKUs with BOMs

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.

query Parameters
sort
string
Enum: "code" "name" "itemCount"
order
string
Enum: "asc" "desc"
limit
integer [ 1 .. 200 ]
offset
integer >= 0
containsMaterialId
string
containsSkuId
string
search
string

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {
    }
}

Get BOM for a SKU

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "sku": {
    },
  • "items": [
    ]
}

Set BOM for a SKU

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
Request Body schema: application/json
required
Array of objects [ 1 .. 500 ] items
Array ([ 1 .. 500 ] items)
materialId
string or null^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
inputSkuId
string or null^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
quantity
required
number > 0

Responses

Request samples

Content type
application/json
{
  • "items": [
    ]
}

Response samples

Content type
application/json
{
  • "sku": {
    },
  • "items": [
    ]
}

Delete BOM

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "deletedId": "string"
}

Contacts

Create contact

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.

Request Body schema: application/json
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

Responses

Request samples

Content type
application/json
{
  • "firstName": "string",
  • "lastName": "string",
  • "title": "string",
  • "notes": "string",
  • "displayName": "string"
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "displayName": "string",
  • "firstName": "string",
  • "lastName": "string",
  • "title": "string",
  • "notes": "string",
  • "isActive": true,
  • "createdAt": "string",
  • "updatedAt": "string"
}

List contacts with their entity links

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.

query Parameters
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"

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {
    }
}

Update contact

Patch a contact's header fields. Setting isActive archives (false) or reactivates (true) the contact; there is no separate reactivate route.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
Request Body schema: application/json
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

Responses

Request samples

Content type
application/json
{
  • "firstName": "string",
  • "lastName": "string",
  • "title": "string",
  • "notes": "string",
  • "displayName": "string",
  • "isActive": true
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "displayName": "string",
  • "firstName": "string",
  • "lastName": "string",
  • "title": "string",
  • "notes": "string",
  • "isActive": true,
  • "createdAt": "string",
  • "updatedAt": "string"
}

Delete contact

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "deletedId": "string"
}

Get contact detail

Fetch a single contact with its methods and entity links (each named) nested. Returns 404 when no such contact exists.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "id": "string",
  • "displayName": "string",
  • "firstName": "string",
  • "lastName": "string",
  • "title": "string",
  • "notes": "string",
  • "isActive": true,
  • "createdAt": "string",
  • "updatedAt": "string",
  • "methods": [
    ],
  • "links": [
    ]
}

Add contact method

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.

Request Body schema: application/json
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

Responses

Request samples

Content type
application/json
{
  • "contactId": "string",
  • "type": "email",
  • "value": "string",
  • "label": "string",
  • "isPrimary": true
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "methodType": "email",
  • "value": "string",
  • "label": "string",
  • "isPrimary": true,
  • "isActive": true,
  • "createdAt": "string",
  • "updatedAt": "string"
}

List methods for a contact

List the email and phone methods for the contact given by contactId, primary first. Returns 404 when the contact does not exist.

query Parameters
contactId
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

Update contact method

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
Request Body schema: application/json
type
string
Enum: "email" "phone"
value
string <= 255 characters
label
string or null <= 255 characters
isPrimary
boolean
isActive
boolean

Responses

Request samples

Content type
application/json
{
  • "type": "email",
  • "value": "string",
  • "label": "string",
  • "isPrimary": true,
  • "isActive": true
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "methodType": "email",
  • "value": "string",
  • "label": "string",
  • "isPrimary": true,
  • "isActive": true,
  • "createdAt": "string",
  • "updatedAt": "string"
}

Delete contact method

Remove an email or phone method from its contact.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "deletedId": "string"
}

Link contact to entity

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.

Request Body schema: application/json
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

Responses

Request samples

Content type
application/json
{
  • "contactId": "string",
  • "entityType": "customer",
  • "entityId": "string",
  • "role": "string",
  • "notes": "string",
  • "isPrimary": true
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "entity": {
    },
  • "role": "string",
  • "isPrimary": true,
  • "notes": "string",
  • "isActive": true,
  • "createdAt": "string",
  • "updatedAt": "string"
}

List contacts linked to an entity

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.

query Parameters
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}...

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

Update contact-entity link

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
Request Body schema: application/json
role
string or null <= 255 characters
notes
string or null <= 2000 characters
isPrimary
boolean
isActive
boolean

Responses

Request samples

Content type
application/json
{
  • "role": "string",
  • "notes": "string",
  • "isPrimary": true,
  • "isActive": true
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "entity": {
    },
  • "role": "string",
  • "isPrimary": true,
  • "notes": "string",
  • "isActive": true,
  • "createdAt": "string",
  • "updatedAt": "string"
}

Unlink contact from entity

Remove a contact's link to an entity. The contact row itself is unaffected; only the link is deleted.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "deletedId": "string"
}

MaterialsInventory

Adjust materials inventory

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.

Request Body schema: application/json
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>

Responses

Request samples

Content type
application/json
{
  • "materialId": "string",
  • "locationId": "string",
  • "quantity": 0,
  • "notes": "string",
  • "eventDate": "2019-08-24"
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "code": "string",
  • "material": {
    },
  • "location": {
    },
  • "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
}

Dispose materials inventory

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.

Request Body schema: application/json
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>

Responses

Request samples

Content type
application/json
{
  • "materialId": "string",
  • "locationId": "string",
  • "quantity": 0,
  • "notes": "string",
  • "eventDate": "2019-08-24"
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "code": "string",
  • "material": {
    },
  • "location": {
    },
  • "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
}

Get materials inventory summary

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.

query Parameters
materialId
string
locationId
string
search
string
includeOnHandValue
string
Enum: "true" "false"
demandWindowWeeks
integer [ 1 .. 52 ]

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

List open job requirements for a material

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.

query Parameters
materialId
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

List materials ledger entries

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.

query Parameters
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}$

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {
    }
}

Get materials ledger entry detail

Fetch a single materials ledger entry by id, with its derived unit cost. Unlike the list, the single read is not expanded per lot.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "id": "string",
  • "code": "string",
  • "material": {
    },
  • "location": {
    },
  • "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
}

FinishedGoodsInventory

Adjust FG inventory

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.

Request Body schema: application/json
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>

Responses

Request samples

Content type
application/json
{
  • "skuId": "string",
  • "locationId": "string",
  • "quantity": 0,
  • "notes": "string",
  • "eventDate": "2019-08-24"
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "code": "string",
  • "sku": {
    },
  • "location": {
    },
  • "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
}

Dispose FG inventory

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.

Request Body schema: application/json
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>

Responses

Request samples

Content type
application/json
{
  • "skuId": "string",
  • "locationId": "string",
  • "quantity": 0,
  • "notes": "string",
  • "eventDate": "2019-08-24"
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "code": "string",
  • "sku": {
    },
  • "location": {
    },
  • "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
}

Get FG inventory summary

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.

query Parameters
skuId
string
locationId
string
search
string
includeOnHandValue
string
Enum: "true" "false"
demandWindowWeeks
integer [ 1 .. 52 ]

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

List FG ledger entries

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.

query Parameters
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}$

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {
    }
}

Get FG ledger entry detail

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "id": "string",
  • "code": "string",
  • "sku": {
    },
  • "location": {
    },
  • "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
}

Transfers

Create transfer

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.

Request Body schema: application/json
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

Responses

Request samples

Content type
application/json
{
  • "sourceLocationId": "string",
  • "destinationLocationId": "string",
  • "items": [
    ],
  • "status": "Planning",
  • "statusDates": {
    },
  • "notes": "string"
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "code": "string",
  • "sourceLocation": {
    },
  • "destinationLocation": {
    },
  • "items": [
    ],
  • "status": "Planning",
  • "statusDates": {
    },
  • "notes": "string",
  • "createdAt": "string",
  • "updatedAt": "string"
}

List transfers

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.

query Parameters
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}$

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {
    }
}

Update transfer

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
Request Body schema: application/json
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

Responses

Request samples

Content type
application/json
{
  • "sourceLocationId": "string",
  • "destinationLocationId": "string",
  • "items": [
    ],
  • "status": "Planning",
  • "statusDates": {
    },
  • "notes": "string"
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "code": "string",
  • "sourceLocation": {
    },
  • "destinationLocation": {
    },
  • "items": [
    ],
  • "status": "Planning",
  • "statusDates": {
    },
  • "notes": "string",
  • "createdAt": "string",
  • "updatedAt": "string"
}

Delete transfer

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "deletedId": "string"
}

Get transfer detail

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "id": "string",
  • "code": "string",
  • "sourceLocation": {
    },
  • "destinationLocation": {
    },
  • "items": [
    ],
  • "status": "Planning",
  • "statusDates": {
    },
  • "notes": "string",
  • "createdAt": "string",
  • "updatedAt": "string"
}

List transfer status types

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.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

SalesOrders

Create sales order

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.

Request Body schema: application/json
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

Responses

Request samples

Content type
application/json
{
  • "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": {
    },
  • "notes": "string",
  • "qboId": "string",
  • "soNumber": "string",
  • "items": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "soNumber": "string",
  • "customer": {
    },
  • "channel": {
    },
  • "orderStatus": "Planning",
  • "fulfillmentStatus": "Pending",
  • "fulfillmentLocation": {
    },
  • "broker": {
    },
  • "orderDate": "string",
  • "mustArriveBy": "string",
  • "statusDates": {
    },
  • "financials": {
    },
  • "notes": "string",
  • "qboId": "string",
  • "createdAt": "string",
  • "updatedAt": "string",
  • "items": [
    ]
}

List sales orders

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.

query Parameters
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}$

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {
    }
}

Update sales order

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
Request Body schema: application/json
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

Responses

Request samples

Content type
application/json
{
  • "soNumber": "string",
  • "customerId": "string",
  • "channelId": "string",
  • "fulfillmentLocationId": "string",
  • "status": "Planning",
  • "fulfillmentStatus": "Pending",
  • "orderDate": "2019-08-24",
  • "mustArriveBy": "2019-08-24",
  • "statusDates": {
    },
  • "brokerId": "string",
  • "brokerFeePercent": 0,
  • "brokerFees": 0,
  • "fulfillmentCosts": 0,
  • "freightCosts": 0,
  • "promoCosts": 0,
  • "otherCosts": 0,
  • "notes": "string",
  • "qboId": "string"
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "soNumber": "string",
  • "customer": {
    },
  • "channel": {
    },
  • "orderStatus": "Planning",
  • "fulfillmentStatus": "Pending",
  • "fulfillmentLocation": {
    },
  • "broker": {
    },
  • "orderDate": "string",
  • "mustArriveBy": "string",
  • "statusDates": {
    },
  • "financials": {
    },
  • "notes": "string",
  • "qboId": "string",
  • "createdAt": "string",
  • "updatedAt": "string"
}

Delete sales order

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "deletedId": "string"
}

Get sales order detail

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "id": "string",
  • "soNumber": "string",
  • "customer": {
    },
  • "channel": {
    },
  • "orderStatus": "Planning",
  • "fulfillmentStatus": "Pending",
  • "fulfillmentLocation": {
    },
  • "broker": {
    },
  • "orderDate": "string",
  • "mustArriveBy": "string",
  • "statusDates": {
    },
  • "financials": {
    },
  • "notes": "string",
  • "qboId": "string",
  • "createdAt": "string",
  • "updatedAt": "string",
  • "items": [
    ]
}

Bulk-import sales orders

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.

Request Body schema: application/json
required
Array of objects
Array
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
required
Array of objects

Responses

Request samples

Content type
application/json
{
  • "rows": [
    ]
}

Response samples

Content type
application/json
{
  • "ids": [
    ]
}

Add item to sales order

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.

Request Body schema: application/json
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}...

Responses

Request samples

Content type
application/json
{
  • "skuId": "string",
  • "quantity": 1,
  • "unitPrice": 0,
  • "salesOrderId": "string"
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "salesOrderId": "string",
  • "sku": {
    },
  • "quantity": 0,
  • "unitPrice": "string"
}

List sales order items

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.

query Parameters
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}...

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

Update sales order item

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
Request Body schema: application/json
skuId
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
quantity
integer >= 1
unitPrice
number >= 0

Responses

Request samples

Content type
application/json
{
  • "skuId": "string",
  • "quantity": 1,
  • "unitPrice": 0
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "salesOrderId": "string",
  • "sku": {
    },
  • "quantity": 0,
  • "unitPrice": "string"
}

Remove item from sales order

Remove a line item from its sales order, unwinding the DEMAND/ALLOCATE ledger rows the line projected.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "deletedId": "string"
}

List sales order status types

List the sales order status vocabulary (Planning, Placed, In Transit, Invoiced, Paid, Rejected, In-Dispute, Completed) as a plain string array, for populating status pickers.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

List sales order fulfillment status types

List the fulfillment status vocabulary (Pending, Partial, Fulfilled) as a plain string array, for populating fulfillment-status pickers.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

SalesReceipts

Create sales receipt

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.

Request Body schema: application/json
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

Responses

Request samples

Content type
application/json
{
  • "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": {
    },
  • "notes": "string",
  • "srNumber": "string",
  • "items": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "customerName": "string",
  • "customerRef": "string",
  • "isSubscription": true,
  • "isRecurring": true,
  • "srNumber": "string",
  • "channel": {
    },
  • "acquisitionSource": {
    },
  • "status": "Placed",
  • "location": {
    },
  • "financials": {
    },
  • "orderDate": "string",
  • "statusDates": {
    },
  • "notes": "string",
  • "createdAt": "string",
  • "updatedAt": "string",
  • "items": [
    ]
}

List sales receipts

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.

query Parameters
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}$

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {
    }
}

Update sales receipt

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
Request Body schema: application/json
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

Responses

Request samples

Content type
application/json
{
  • "srNumber": "string",
  • "customerName": "string",
  • "customerRef": "string",
  • "isSubscription": true,
  • "isRecurring": true,
  • "channelId": "string",
  • "acquisitionSourceId": "string",
  • "status": "Placed",
  • "locationId": "string",
  • "orderDate": "2019-08-24",
  • "statusDates": {
    },
  • "fulfillmentCosts": 0,
  • "shippingCosts": 0,
  • "merchantCosts": 0,
  • "otherCosts": 0,
  • "notes": "string"
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "customerName": "string",
  • "customerRef": "string",
  • "isSubscription": true,
  • "isRecurring": true,
  • "srNumber": "string",
  • "channel": {
    },
  • "acquisitionSource": {
    },
  • "status": "Placed",
  • "location": {
    },
  • "financials": {
    },
  • "orderDate": "string",
  • "statusDates": {
    },
  • "notes": "string",
  • "createdAt": "string",
  • "updatedAt": "string"
}

Delete sales receipt

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "deletedId": "string"
}

Get sales receipt detail

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "id": "string",
  • "customerName": "string",
  • "customerRef": "string",
  • "isSubscription": true,
  • "isRecurring": true,
  • "srNumber": "string",
  • "channel": {
    },
  • "acquisitionSource": {
    },
  • "status": "Placed",
  • "location": {
    },
  • "financials": {
    },
  • "orderDate": "string",
  • "statusDates": {
    },
  • "notes": "string",
  • "createdAt": "string",
  • "updatedAt": "string",
  • "items": [
    ]
}

Add item to sales receipt

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.

Request Body schema: application/json
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}...

Responses

Request samples

Content type
application/json
{
  • "skuId": "string",
  • "quantity": 0,
  • "unitPrice": 0,
  • "salesReceiptId": "string"
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "salesReceiptId": "string",
  • "sku": {
    },
  • "quantity": "string",
  • "unitPrice": "string"
}

List sales receipt items

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.

query Parameters
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}...

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

Update sales receipt item

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
Request Body schema: application/json
skuId
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
quantity
number > 0
unitPrice
number >= 0

Responses

Request samples

Content type
application/json
{
  • "skuId": "string",
  • "quantity": 0,
  • "unitPrice": 0
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "salesReceiptId": "string",
  • "sku": {
    },
  • "quantity": "string",
  • "unitPrice": "string"
}

Remove item from sales receipt

Remove a line item from its sales receipt, unwinding the finished-goods ledger rows the line projected.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "deletedId": "string"
}

List connected sales receipts

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.

query Parameters
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}$

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {
    },
  • "configured": true
}

Get connected sales receipt

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.

path Parameters
externalId
required
string [ 1 .. 64 ] characters

Responses

Response samples

Content type
application/json
{
  • "id": "string",
  • "srNumber": "string",
  • "source": "shopify",
  • "externalOrderNumber": "string",
  • "externalOrderId": "string",
  • "adminUrl": "string",
  • "store": {
    },
  • "dataAsOf": "string",
  • "status": "Placed",
  • "sourceStatus": "string",
  • "completedByFallback": true,
  • "statusDates": {
    },
  • "customer": {
    },
  • "isSubscription": true,
  • "isRecurring": true,
  • "destination": {
    },
  • "shippingMethod": "string",
  • "carrier": "string",
  • "trackingNumber": "string",
  • "fulfillmentLocation": "string",
  • "fulfillmentLocationKey": "string",
  • "location": {
    },
  • "money": {
    },
  • "items": [
    ],
  • "refunds": [
    ],
  • "financials": {
    }
}

List connected receipt locations

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.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "configured": true
}

List connected location mappings

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.

query Parameters
source
string
Enum: "shopify" "amazon"

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "configured": true
}

Set connected location mapping

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.

Request Body schema: application/json
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}...

Responses

Request samples

Content type
application/json
{
  • "source": "shopify",
  • "externalKey": "string",
  • "externalName": "string",
  • "locationId": "string"
}

Response samples

Content type
application/json
{
  • "externalKey": "string",
  • "location": {
    }
}

List connected mapping alerts

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.

Responses

Response samples

Content type
application/json
{
  • "skus": [
    ],
  • "locations": [
    ]
}

Get connected sync status

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.

query Parameters
source
string
Enum: "shopify" "amazon"

Responses

Response samples

Content type
application/json
{
  • "configured": true,
  • "lastSyncedAt": "string",
  • "lastAttemptAt": "string",
  • "lastError": "string",
  • "backfill": {
    }
}

List connected SKU mappings

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.

query Parameters
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

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {
    },
  • "configured": true
}

Set connected SKU mapping

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.

Request Body schema: application/json
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}...

Responses

Request samples

Content type
application/json
{
  • "source": "shopify",
  • "variantId": "string",
  • "listingSku": "string",
  • "listingTitle": "string",
  • "skuId": "string"
}

Response samples

Content type
application/json
{
  • "variantId": "string",
  • "sku": {
    }
}

SalesReturns

Create sales return

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.

Request Body schema: application/json
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

Responses

Request samples

Content type
application/json
{
  • "status": "Planning",
  • "locationId": "string",
  • "returnDate": "2019-08-24",
  • "statusDates": {
    },
  • "notes": "string",
  • "salesReceiptId": "string",
  • "returnNumber": "string",
  • "items": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "salesReceipt": {
    },
  • "returnNumber": "string",
  • "status": "Planning",
  • "location": {
    },
  • "returnDate": "string",
  • "statusDates": {
    },
  • "notes": "string",
  • "createdAt": "string",
  • "updatedAt": "string",
  • "items": [
    ]
}

List sales returns

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.

query Parameters
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}$

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {
    }
}

Update sales return

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
Request Body schema: application/json
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

Responses

Request samples

Content type
application/json
{
  • "returnNumber": "string",
  • "status": "Planning",
  • "locationId": "string",
  • "returnDate": "2019-08-24",
  • "statusDates": {
    },
  • "notes": "string"
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "salesReceipt": {
    },
  • "returnNumber": "string",
  • "status": "Planning",
  • "location": {
    },
  • "returnDate": "string",
  • "statusDates": {
    },
  • "notes": "string",
  • "createdAt": "string",
  • "updatedAt": "string"
}

Delete sales return

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "deletedId": "string"
}

Get sales return detail

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "id": "string",
  • "salesReceipt": {
    },
  • "returnNumber": "string",
  • "status": "Planning",
  • "location": {
    },
  • "returnDate": "string",
  • "statusDates": {
    },
  • "notes": "string",
  • "createdAt": "string",
  • "updatedAt": "string",
  • "items": [
    ]
}

Add item to sales return

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.

Request Body schema: application/json
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}...

Responses

Request samples

Content type
application/json
{
  • "salesReceiptItemId": "string",
  • "quantity": 0,
  • "salesReturnId": "string"
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "salesReturnId": "string",
  • "salesReceiptItemId": "string",
  • "sku": {
    },
  • "quantity": "string"
}

List sales return items

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.

query Parameters
salesReturnId
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

Update sales return item

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
Request Body schema: application/json
quantity
number > 0

Responses

Request samples

Content type
application/json
{
  • "quantity": 0
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "salesReturnId": "string",
  • "salesReceiptItemId": "string",
  • "sku": {
    },
  • "quantity": "string"
}

Remove item from sales return

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "deletedId": "string"
}

OrderChannels

Create order channel

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.

Request Body schema: application/json
name
required
string [ 1 .. 100 ] characters

Responses

Request samples

Content type
application/json
{
  • "name": "string"
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "name": "string",
  • "sortOrder": 0,
  • "isActive": true,
  • "createdAt": "string"
}

List order channels

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

query Parameters
search
string

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

Update order channel

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
Request Body schema: application/json
name
string [ 1 .. 100 ] characters
sortOrder
integer
isActive
boolean

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "sortOrder": 0,
  • "isActive": true
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "name": "string",
  • "sortOrder": 0,
  • "isActive": true,
  • "createdAt": "string"
}

Delete order channel

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "deletedId": "string"
}

AcquisitionSources

Create acquisition source

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.

Request Body schema: application/json
name
required
string [ 1 .. 100 ] characters

Responses

Request samples

Content type
application/json
{
  • "name": "string"
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "name": "string",
  • "sortOrder": 0,
  • "isActive": true,
  • "createdAt": "string"
}

List acquisition sources

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

query Parameters
search
string

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

Update acquisition source

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
Request Body schema: application/json
name
string [ 1 .. 100 ] characters
sortOrder
integer
isActive
boolean

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "sortOrder": 0,
  • "isActive": true
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "name": "string",
  • "sortOrder": 0,
  • "isActive": true,
  • "createdAt": "string"
}

Delete acquisition source

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "deletedId": "string"
}

Connections

List connections

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.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

Create a Fivetran Connect Card

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.

path Parameters
provider
required
string
Enum: "shopify" "amazonSellerCentral" "metaAds" "googleAds" "tiktokAds" "shiphero" "quickbooksOnline"
Request Body schema: application/json
merchantId
string [ 4 .. 64 ] characters

Responses

Request samples

Content type
application/json
{
  • "merchantId": "string"
}

Response samples

Content type
application/json
{
  • "uri": "string",
  • "connection": {
    }
}

Get connection data status

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.

path Parameters
provider
required
string
Enum: "shopify" "shiphero" "amazonSellerCentral" "metaAds" "googleAds" "tiktokAds" "quickbooksOnline"

Responses

Response samples

Content type
application/json
{
  • "provider": "shopify",
  • "phase": "waiting",
  • "detail": "string"
}

Disconnect a provider

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.

path Parameters
provider
required
string
Enum: "shopify" "shiphero" "amazonSellerCentral" "metaAds" "googleAds" "tiktokAds" "quickbooksOnline"

Responses

Response samples

Content type
application/json
{
  • "deletedId": "string"
}

PurchaseOrders

Create PO

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.

Request Body schema: application/json
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

Responses

Request samples

Content type
application/json
{
  • "vendorId": "string",
  • "targetType": "materials",
  • "status": "Planning",
  • "shipToLocationId": "string",
  • "orderDate": "2019-08-24",
  • "shipDate": "2019-08-24",
  • "expectedDeliveryDate": "2019-08-24",
  • "statusDates": {
    },
  • "shippingCosts": 0,
  • "setupCosts": 0,
  • "otherCosts": 0,
  • "notes": "string",
  • "poNumber": "string",
  • "items": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "poNumber": "string",
  • "vendor": {
    },
  • "targetType": "materials",
  • "status": "Planning",
  • "shipToLocation": {
    },
  • "orderDate": "string",
  • "shipDate": "string",
  • "expectedDeliveryDate": "string",
  • "statusDates": {
    },
  • "financials": {
    },
  • "notes": "string",
  • "createdAt": "string",
  • "updatedAt": "string",
  • "items": [
    ],
  • "receipts": [
    ]
}

List POs

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.

query Parameters
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}$

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {
    }
}

Update PO

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
Request Body schema: application/json
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

Responses

Request samples

Content type
application/json
{
  • "poNumber": "string",
  • "vendorId": "string",
  • "shipToLocationId": "string",
  • "status": "Planning",
  • "orderDate": "2019-08-24",
  • "shipDate": "2019-08-24",
  • "expectedDeliveryDate": "2019-08-24",
  • "statusDates": {
    },
  • "shippingCosts": 0,
  • "setupCosts": 0,
  • "otherCosts": 0,
  • "notes": "string"
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "poNumber": "string",
  • "vendor": {
    },
  • "targetType": "materials",
  • "status": "Planning",
  • "shipToLocation": {
    },
  • "orderDate": "string",
  • "shipDate": "string",
  • "expectedDeliveryDate": "string",
  • "statusDates": {
    },
  • "financials": {
    },
  • "notes": "string",
  • "createdAt": "string",
  • "updatedAt": "string"
}

Delete PO

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "deletedId": "string"
}

Get PO detail

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "id": "string",
  • "poNumber": "string",
  • "vendor": {
    },
  • "targetType": "materials",
  • "status": "Planning",
  • "shipToLocation": {
    },
  • "orderDate": "string",
  • "shipDate": "string",
  • "expectedDeliveryDate": "string",
  • "statusDates": {
    },
  • "financials": {
    },
  • "notes": "string",
  • "createdAt": "string",
  • "updatedAt": "string",
  • "items": [
    ],
  • "receipts": [
    ]
}

Bulk-import POs

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.

Request Body schema: application/json
required
Array of objects
Array
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
required
Array of objects

Responses

Request samples

Content type
application/json
{
  • "rows": [
    ]
}

Response samples

Content type
application/json
{
  • "ids": [
    ]
}

Add item to PO

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.

Request Body schema: application/json
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}...

Responses

Request samples

Content type
application/json
{
  • "materialId": "string",
  • "skuId": "string",
  • "quantity": 0,
  • "unitCost": 0,
  • "purchaseOrderId": "string"
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "purchaseOrderId": "string",
  • "material": {
    },
  • "sku": {
    },
  • "quantity": "string",
  • "unitCost": "string"
}

List PO items

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.

query Parameters
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}...

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

Update PO item

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
Request Body schema: application/json
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

Responses

Request samples

Content type
application/json
{
  • "materialId": "string",
  • "skuId": "string",
  • "quantity": 0,
  • "unitCost": 0
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "purchaseOrderId": "string",
  • "material": {
    },
  • "sku": {
    },
  • "quantity": "string",
  • "unitCost": "string"
}

Remove PO item

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "deletedId": "string"
}

Create receipt against PO

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

Request Body schema: application/json
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

Responses

Request samples

Content type
application/json
{
  • "purchaseOrderId": "string",
  • "receiptDate": "2019-08-24",
  • "items": [
    ]
}

Response samples

Content type
application/json
{
  • "receipt": {
    },
  • "purchaseOrder": {
    }
}

List PO receipts

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.

query Parameters
purchaseOrderId
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

Update PO receipt

Update a goods receipt's date. Re-dating the receipt re-dates its RECEIVE ledger rows, but never changes the purchase order's status.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
Request Body schema: application/json
receiptDate
string <date>

Responses

Request samples

Content type
application/json
{
  • "receiptDate": "2019-08-24"
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "purchaseOrderId": "string",
  • "receiptDate": "string",
  • "createdAt": "string",
  • "updatedAt": "string"
}

Delete PO receipt

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "deletedId": "string"
}

Get PO receipt detail

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

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "id": "string",
  • "purchaseOrderId": "string",
  • "receiptDate": "string",
  • "createdAt": "string",
  • "updatedAt": "string",
  • "items": [
    ]
}

Update PO receipt item

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
Request Body schema: application/json
quantity
number > 0
unitCost
number >= 0
lotNumber
string or null <= 255 characters
expirationDate
string or null <date>

Responses

Request samples

Content type
application/json
{
  • "quantity": 0,
  • "unitCost": 0,
  • "lotNumber": "string",
  • "expirationDate": "2019-08-24"
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "receiptId": "string",
  • "material": {
    },
  • "sku": {
    },
  • "quantity": "string",
  • "unitCost": "string",
  • "lotNumber": "string",
  • "expirationDate": "string",
  • "createdAt": "string"
}

Generate PO PDF

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "error": {
    }
}

Generate PO PDF with overrides

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
Request Body schema: application/json
object
withPrices
boolean

Responses

Request samples

Content type
application/json
{
  • "overrides": {
    },
  • "withPrices": true
}

Response samples

Content type
application/json
{
  • "error": {
    }
}

Get PO PDF form values

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

List purchase order status types

List the purchase order status vocabulary (Planning, Placed, In Transit, Partial, Received, Completed, Cancelled) as a plain string array, for populating status pickers.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

WorkOrders

Create WO

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.

Request Body schema: application/json
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

Responses

Request samples

Content type
application/json
{
  • "status": "Planning",
  • "workSiteLocationId": "string",
  • "orderDate": "2019-08-24",
  • "expectedDeliveryDate": "2019-08-24",
  • "statusDates": {
    },
  • "inShippingCosts": 0,
  • "outShippingCosts": 0,
  • "otherCosts": 0,
  • "notes": "string",
  • "woNumber": "string",
  • "items": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "woNumber": "string",
  • "status": "Planning",
  • "workSite": {
    },
  • "orderDate": "string",
  • "expectedDeliveryDate": "string",
  • "statusDates": {
    },
  • "financials": {
    },
  • "notes": "string",
  • "createdAt": "string",
  • "updatedAt": "string",
  • "items": [
    ],
  • "receipts": [
    ]
}

List WOs

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.

query Parameters
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}$

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {
    }
}

Update WO

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
Request Body schema: application/json
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

Responses

Request samples

Content type
application/json
{
  • "woNumber": "string",
  • "workSiteLocationId": "string",
  • "status": "Planning",
  • "orderDate": "2019-08-24",
  • "expectedDeliveryDate": "2019-08-24",
  • "statusDates": {
    },
  • "inShippingCosts": 0,
  • "outShippingCosts": 0,
  • "otherCosts": 0,
  • "notes": "string"
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "woNumber": "string",
  • "status": "Planning",
  • "workSite": {
    },
  • "orderDate": "string",
  • "expectedDeliveryDate": "string",
  • "statusDates": {
    },
  • "financials": {
    },
  • "notes": "string",
  • "createdAt": "string",
  • "updatedAt": "string"
}

Delete WO

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "deletedId": "string"
}

Get WO detail

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "id": "string",
  • "woNumber": "string",
  • "status": "Planning",
  • "workSite": {
    },
  • "orderDate": "string",
  • "expectedDeliveryDate": "string",
  • "statusDates": {
    },
  • "financials": {
    },
  • "notes": "string",
  • "createdAt": "string",
  • "updatedAt": "string",
  • "items": [
    ],
  • "receipts": [
    ]
}

Bulk-import WOs

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.

Request Body schema: application/json
required
Array of objects
Array
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
required
Array of objects

Responses

Request samples

Content type
application/json
{
  • "rows": [
    ]
}

Response samples

Content type
application/json
{
  • "ids": [
    ]
}

Add item to WO

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

Request Body schema: application/json
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}...

Responses

Request samples

Content type
application/json
{
  • "skuId": "string",
  • "quantity": 1,
  • "conversionCost": 0,
  • "workOrderId": "string"
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "workOrderId": "string",
  • "sku": {
    },
  • "quantity": "string",
  • "conversionCost": "string",
  • "inputs": [
    ]
}

List WO items

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.

query Parameters
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}...

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

Update WO item

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
Request Body schema: application/json
quantity
integer >= 1
conversionCost
number >= 0

Responses

Request samples

Content type
application/json
{
  • "quantity": 1,
  • "conversionCost": 0
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "workOrderId": "string",
  • "sku": {
    },
  • "quantity": "string",
  • "conversionCost": "string",
  • "inputs": [
    ]
}

Remove WO item

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "deletedId": "string"
}

Create receipt against WO

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

Request Body schema: application/json
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

Responses

Request samples

Content type
application/json
{
  • "workOrderId": "string",
  • "receiptDate": "2019-08-24",
  • "items": [
    ]
}

Response samples

Content type
application/json
{
  • "receipt": {
    },
  • "workOrder": {
    }
}

List WO receipts

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.

query Parameters
workOrderId
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

Update WO receipt

Update a production receipt's date. Re-dating the receipt re-dates its ledger rows but never changes the work order's status.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
Request Body schema: application/json
receiptDate
string <date>

Responses

Request samples

Content type
application/json
{
  • "receiptDate": "2019-08-24"
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "workOrderId": "string",
  • "receiptDate": "string",
  • "createdAt": "string"
}

Delete WO receipt

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "deletedId": "string"
}

Get WO receipt detail

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

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "id": "string",
  • "workOrderId": "string",
  • "receiptDate": "string",
  • "createdAt": "string",
  • "items": [
    ]
}

Update WO receipt item

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
Request Body schema: application/json
quantity
integer >= 1
conversionCost
number >= 0
lotNumber
string or null <= 255 characters
expirationDate
string or null <date>

Responses

Request samples

Content type
application/json
{
  • "quantity": 1,
  • "conversionCost": 0,
  • "lotNumber": "string",
  • "expirationDate": "2019-08-24"
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "receiptId": "string",
  • "sku": {
    },
  • "quantity": "string",
  • "conversionCost": "string",
  • "lotNumber": "string",
  • "expirationDate": "string",
  • "createdAt": "string"
}

List work order status types

List the work order status vocabulary (Planning, Placed, Production, Partial, Received, Completed, Cancelled) as a plain string array, for populating status pickers.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

ProductionOrders

Create production order

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.

Request Body schema: application/json
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

Responses

Request samples

Content type
application/json
{
  • "status": "Planning",
  • "locationId": "string",
  • "orderDate": "2019-08-24",
  • "expectedDeliveryDate": "2019-08-24",
  • "statusDates": {
    },
  • "inShippingCosts": 0,
  • "outShippingCosts": 0,
  • "otherCosts": 0,
  • "notes": "string",
  • "prodNumber": "string",
  • "items": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "prodNumber": "string",
  • "status": "Planning",
  • "location": {
    },
  • "orderDate": "string",
  • "expectedDeliveryDate": "string",
  • "statusDates": {
    },
  • "financials": {
    },
  • "notes": "string",
  • "createdAt": "string",
  • "updatedAt": "string",
  • "items": [
    ],
  • "receipts": [
    ]
}

List production orders

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.

query Parameters
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}$

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {
    }
}

Update production order

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
Request Body schema: application/json
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

Responses

Request samples

Content type
application/json
{
  • "prodNumber": "string",
  • "locationId": "string",
  • "status": "Planning",
  • "orderDate": "2019-08-24",
  • "expectedDeliveryDate": "2019-08-24",
  • "statusDates": {
    },
  • "inShippingCosts": 0,
  • "outShippingCosts": 0,
  • "otherCosts": 0,
  • "notes": "string"
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "prodNumber": "string",
  • "status": "Planning",
  • "location": {
    },
  • "orderDate": "string",
  • "expectedDeliveryDate": "string",
  • "statusDates": {
    },
  • "financials": {
    },
  • "notes": "string",
  • "createdAt": "string",
  • "updatedAt": "string"
}

Delete production order

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "deletedId": "string"
}

Get production order detail

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "id": "string",
  • "prodNumber": "string",
  • "status": "Planning",
  • "location": {
    },
  • "orderDate": "string",
  • "expectedDeliveryDate": "string",
  • "statusDates": {
    },
  • "financials": {
    },
  • "notes": "string",
  • "createdAt": "string",
  • "updatedAt": "string",
  • "items": [
    ],
  • "receipts": [
    ]
}

Bulk-import production orders

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.

Request Body schema: application/json
required
Array of objects
Array
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
required
Array of objects

Responses

Request samples

Content type
application/json
{
  • "rows": [
    ]
}

Response samples

Content type
application/json
{
  • "ids": [
    ]
}

Add item to production order

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

Request Body schema: application/json
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}...

Responses

Request samples

Content type
application/json
{
  • "skuId": "string",
  • "quantity": 1,
  • "productionCost": 0,
  • "productionOrderId": "string"
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "productionOrderId": "string",
  • "sku": {
    },
  • "quantity": "string",
  • "productionCost": "string",
  • "inputs": [
    ]
}

List production order items

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.

query Parameters
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}...

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

Update production order item

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
Request Body schema: application/json
quantity
integer >= 1
productionCost
number or null >= 0

Responses

Request samples

Content type
application/json
{
  • "quantity": 1,
  • "productionCost": 0
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "productionOrderId": "string",
  • "sku": {
    },
  • "quantity": "string",
  • "productionCost": "string",
  • "inputs": [
    ]
}

Remove production order item

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "deletedId": "string"
}

Create receipt against production order

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

Request Body schema: application/json
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

Responses

Request samples

Content type
application/json
{
  • "productionOrderId": "string",
  • "receiptDate": "2019-08-24",
  • "items": [
    ]
}

Response samples

Content type
application/json
{
  • "receipt": {
    },
  • "productionOrder": {
    }
}

List production order receipts

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.

query Parameters
productionOrderId
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

Update production order receipt

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
Request Body schema: application/json
receiptDate
string <date>

Responses

Request samples

Content type
application/json
{
  • "receiptDate": "2019-08-24"
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "productionOrderId": "string",
  • "receiptDate": "string",
  • "createdAt": "string"
}

Delete production order receipt

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "deletedId": "string"
}

Get production order receipt detail

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

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "id": "string",
  • "productionOrderId": "string",
  • "receiptDate": "string",
  • "createdAt": "string",
  • "items": [
    ]
}

Update production order receipt item

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
Request Body schema: application/json
quantity
integer >= 1
productionCost
number >= 0
lotNumber
string or null <= 255 characters
expirationDate
string or null <date>

Responses

Request samples

Content type
application/json
{
  • "quantity": 1,
  • "productionCost": 0,
  • "lotNumber": "string",
  • "expirationDate": "2019-08-24"
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "receiptId": "string",
  • "sku": {
    },
  • "quantity": "string",
  • "productionCost": "string",
  • "lotNumber": "string",
  • "expirationDate": "string",
  • "createdAt": "string"
}

List production order status types

List the production order status vocabulary (Planning, Placed, Production, In Transit, Partial, Received, Completed, Cancelled) as a plain string array, for populating status pickers.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

Documents

Upload document

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.

Request Body schema: multipart/form-data
file
required
string <binary>
entityType
required
string
Enum: "sales_order" "purchase_order" "work_order"
entityId
required
string <uuid>

Responses

Response samples

Content type
application/json
{
  • "id": "string",
  • "entity": {
    },
  • "name": "string",
  • "mimeType": "string",
  • "sizeBytes": "string",
  • "createdAt": "string"
}

List documents for an entity

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.

query Parameters
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}...

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

Delete document

Permanently delete one document: its metadata row and the stored file. Irreversible, and there is no soft-delete or trash. Returns the deleted id.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "deletedId": "string"
}

Get document detail (signed download URL)

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "id": "string",
  • "entity": {
    },
  • "name": "string",
  • "mimeType": "string",
  • "sizeBytes": "string",
  • "createdAt": "string",
  • "signedUrl": "string"
}

TimelineNotes

Add timeline note

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.

Request Body schema: application/json
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

Responses

Request samples

Content type
application/json
{
  • "entityType": "sales_order",
  • "entityId": "string",
  • "note": "string"
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "entity": {
    },
  • "entityStatus": "string",
  • "note": "string",
  • "createdBy": {
    },
  • "createdAt": "string"
}

List timeline notes for an entity

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.

query Parameters
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}...

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

Delete timeline note

Permanently delete one timeline note by its own id (not the parent entity's id). Irreversible. Returns the deleted id.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "deletedId": "string"
}

Reporting

Get channel-segment contribution report

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.

query Parameters
window
string
Enum: "30d" "45d" "90d"

Responses

Response samples

Content type
application/json
{
  • "window": "30d",
  • "current": {
    },
  • "prior": {
    },
  • "data": [
    ],
  • "total": {
    },
  • "missingFreightCount": 0
}

Get committed outbound report

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.

query Parameters
horizonDays
integer [ 1 .. 90 ]

Responses

Response samples

Content type
application/json
{
  • "horizonDays": 0,
  • "purchaseOrders": {
    },
  • "workOrders": {
    },
  • "productionOrders": {
    },
  • "total": {
    }
}

Platform

Create user

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.

Request Body schema: application/json
displayName
string or null <= 255 characters

Responses

Request samples

Content type
application/json
{
  • "displayName": "string"
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "email": "string",
  • "displayName": "string",
  • "isActive": true,
  • "createdAt": "string",
  • "updatedAt": "string"
}

Get user

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.

Responses

Response samples

Content type
application/json
{
  • "user": {
    },
  • "organizations": [
    ]
}

Update user

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.

Request Body schema: application/json
displayName
string or null <= 255 characters

Responses

Request samples

Content type
application/json
{
  • "displayName": "string"
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "email": "string",
  • "displayName": "string",
  • "isActive": true,
  • "createdAt": "string",
  • "updatedAt": "string"
}

Delete user

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.

Responses

Response samples

Content type
application/json
{
  • "deletedId": "string"
}

Create API token (PAT)

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.

Request Body schema: application/json
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 ]

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "orgId": "string",
  • "expiresInDays": 1
}

Response samples

Content type
application/json
{
  • "token": {
    },
  • "plaintext": "string"
}

List my API tokens

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.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

Revoke API token

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "deletedId": "string"
}

List my pending invitations

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.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

Look up invitation by token (public)

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.

path Parameters
token
required
string

Responses

Response samples

Content type
application/json
{
  • "invitation": {
    }
}

Accept invitation

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.

Request Body schema: application/json
token
required
string
displayName
string or null <= 255 characters

Responses

Request samples

Content type
application/json
{
  • "token": "string",
  • "displayName": "string"
}

Response samples

Content type
application/json
{
  • "membership": {
    },
  • "organization": {
    }
}

Decline invitation

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.

Request Body schema: application/json
invitationId
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Request samples

Content type
application/json
{
  • "invitationId": "string"
}

Response samples

Content type
application/json
{
  • "declinedId": "string"
}

Create organization

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.

Request Body schema: application/json
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"

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "slug": "string",
  • "operatingModel": "Manufacturer",
  • "costingMethod": "FIFO"
}

Response samples

Content type
application/json
{
  • "organization": {
    },
  • "membership": {
    },
  • "settings": {
    }
}

Get current organization

Returns the current organization (selected by the X-Org-Id header) together with its settings. Control plane: requires a Firebase session, PATs are rejected.

Responses

Response samples

Content type
application/json
{
  • "organization": {
    },
  • "settings": {
    }
}

Update current organization

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.

Request Body schema: application/json
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}$

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "legalName": "string",
  • "address": {
    },
  • "logoUrl": "string",
  • "contact": {
    },
  • "operatingModel": "Manufacturer",
  • "costingMethod": "FIFO",
  • "timezone": "string",
  • "connectedLedgerFrom": "string"
}

Response samples

Content type
application/json
{
  • "organization": {
    },
  • "settings": {
    }
}

Delete current organization (purge)

Irreversibly purges the current organization: all business data, memberships, and invitations are deleted. Owner only. Control plane: requires a Firebase session, PATs are rejected.

Responses

Response samples

Content type
application/json
{
  • "deletedId": "string"
}

List organization members

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.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

Update user role

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.

path Parameters
userId
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
Request Body schema: application/json
role
required
string
Enum: "owner" "admin" "member" "viewer"

Responses

Request samples

Content type
application/json
{
  • "role": "owner"
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "userId": "string",
  • "orgId": "string",
  • "role": "owner",
  • "isActive": true,
  • "joinedAt": "string",
  • "lastActiveAt": "string",
  • "createdAt": "string",
  • "updatedAt": "string"
}

Remove user from org

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.

path Parameters
userId
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "deletedId": "string"
}

Leave organization

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.

Responses

Response samples

Content type
application/json
{
  • "deletedOrg": true
}

Send invitation

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.

Request Body schema: application/json
email
required
string <email>
role
required
string
Enum: "admin" "member" "viewer"

Responses

Request samples

Content type
application/json
{
  • "email": "user@example.com",
  • "role": "admin"
}

Response samples

Content type
application/json
{
  • "invitation": {
    },
  • "emailSent": true
}

List organization invitations

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.

query Parameters
status
string
Enum: "pending" "accepted" "revoked"

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

Resend invitation

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "invitation": {
    },
  • "emailSent": true
}

Revoke invitation

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "deletedId": "string"
}

Customers

Create customer

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

Request Body schema: application/json
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

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "typeId": "string",
  • "segmentId": "string",
  • "brokerId": "string",
  • "brokerFeePercent": 0,
  • "address": {
    },
  • "parentId": "string",
  • "qboId": "string",
  • "notes": "string",
  • "customFields": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "name": "string",
  • "isActive": true,
  • "type": {
    },
  • "segment": {
    },
  • "broker": {
    },
  • "parent": {
    },
  • "address": {
    },
  • "qboId": "string",
  • "notes": "string",
  • "customFields": [
    ],
  • "createdAt": "string",
  • "updatedAt": "string"
}

List customers

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.

query Parameters
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

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {
    }
}

Get customer detail

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "id": "string",
  • "name": "string",
  • "isActive": true,
  • "type": {
    },
  • "segment": {
    },
  • "broker": {
    },
  • "parent": {
    },
  • "address": {
    },
  • "qboId": "string",
  • "notes": "string",
  • "customFields": [
    ],
  • "createdAt": "string",
  • "updatedAt": "string",
  • "contacts": [
    ]
}

Update customer

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

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
Request Body schema: application/json
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

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "typeId": "string",
  • "segmentId": "string",
  • "brokerId": "string",
  • "brokerFeePercent": 0,
  • "address": {
    },
  • "parentId": "string",
  • "qboId": "string",
  • "notes": "string",
  • "customFields": [
    ],
  • "isActive": true
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "name": "string",
  • "isActive": true,
  • "type": {
    },
  • "segment": {
    },
  • "broker": {
    },
  • "parent": {
    },
  • "address": {
    },
  • "qboId": "string",
  • "notes": "string",
  • "customFields": [
    ],
  • "createdAt": "string",
  • "updatedAt": "string"
}

Delete customer

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "deletedId": "string"
}

Create customer type

Create a customer type, the required classification every customer carries. The name must be unique among the org's types (409 conflict otherwise).

Request Body schema: application/json
name
required
string [ 1 .. 100 ] characters

Responses

Request samples

Content type
application/json
{
  • "name": "string"
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "name": "string",
  • "sortOrder": 0,
  • "isActive": true,
  • "createdAt": "string"
}

List customer types

List the org's customer types in sort order, active and archived alike. Pass a free-text search to match names (case-insensitive contains).

query Parameters
search
string

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

Update customer type

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
Request Body schema: application/json
name
string [ 1 .. 100 ] characters
sortOrder
integer
isActive
boolean

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "sortOrder": 0,
  • "isActive": true
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "name": "string",
  • "sortOrder": 0,
  • "isActive": true,
  • "createdAt": "string"
}

Delete customer type

Permanently delete a customer type. Blocked with 409 in_use while any customer references it; the error details carry the blocking customerCount.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "deletedId": "string"
}

Create customer segment

Create a customer segment, an optional grouping (e.g. retail/wholesale). The name must be unique among the org's segments (409 conflict otherwise).

Request Body schema: application/json
name
required
string [ 1 .. 100 ] characters

Responses

Request samples

Content type
application/json
{
  • "name": "string"
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "name": "string",
  • "sortOrder": 0,
  • "isActive": true,
  • "createdAt": "string"
}

List customer segments

List the org's customer segments in sort order, active and archived alike. Pass a free-text search to match names (case-insensitive contains).

query Parameters
search
string

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

Update customer segment

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
Request Body schema: application/json
name
string [ 1 .. 100 ] characters
sortOrder
integer
isActive
boolean

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "sortOrder": 0,
  • "isActive": true
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "name": "string",
  • "sortOrder": 0,
  • "isActive": true,
  • "createdAt": "string"
}

Delete customer segment

Permanently delete a customer segment. Blocked with 409 in_use while any customer references it; the error details carry the blocking customerCount.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "deletedId": "string"
}

Bulk-import customers

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.

Request Body schema: application/json
required
Array of objects <= 1000 items
Array (<= 1000 items)
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

Responses

Request samples

Content type
application/json
{
  • "rows": [
    ]
}

Response samples

Content type
application/json
{
  • "ids": [
    ]
}

Brokers

Create broker

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

Request Body schema: application/json
name
required
string [ 1 .. 100 ] characters

Responses

Request samples

Content type
application/json
{
  • "name": "string"
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "name": "string",
  • "sortOrder": 0,
  • "isActive": true,
  • "createdAt": "string"
}

List brokers

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.

query Parameters
search
string

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

Update broker

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...
Request Body schema: application/json
name
string [ 1 .. 100 ] characters
sortOrder
integer
isActive
boolean

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "sortOrder": 0,
  • "isActive": true
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "name": "string",
  • "sortOrder": 0,
  • "isActive": true,
  • "createdAt": "string"
}

Delete broker

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.

path Parameters
id
required
string^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}...

Responses

Response samples

Content type
application/json
{
  • "deletedId": "string"
}