Skip to main content
Version: v1.0.0

Tool Reference

The Pharus MCP server exposes 189 tools covering the full data-plane API. This page is generated from the same definitions the server uses, so it always matches the live tool surface.

Legend: ✏️ write · 🔴 destructive (MCP hosts ask for confirmation) · unmarked = read-only. Arguments marked * are required. Every tool also accepts an optional orgId — only relevant for tokens not bound to an organization (today all PATs are org-bound, so you can ignore it).

Locations

create_location ✏️

Create a warehouse/site location. Only name is required (must be unique in the org); address fields are optional. New locations are active by default. customFields accepts opaque ordered key/value string pairs the platform never interprets (e.g. warehouse / external codes).

Arguments: name* (string), address (object,null), notes (string,null), customFields (array)

delete_location 🔴

Permanently delete a location. Attempt the delete directly: if any PO, WO, SO, transfer, or inventory ledger references it, it fails with 409 in_use and the error details carry the per-source blocking counts (purchaseOrderCount, workOrderCount, salesOrderCount, inventoryLedgerCount, transferCount). Prefer archiving via update_location { isActive: false } for routine cleanup.

Arguments: id* (string)

get_location

Fetch a single location by id, including its address (a single address object), notes, active flag, custom fields, and the contacts linked to it (each with its methods). 404 if the id does not exist in the org.

Arguments: id* (string)

list_locations

List one page of locations. Pass active to show only active or only archived, a free-text search (matches the name and notes), territory filters (city/state/country, case-insensitive exact), or customFieldKey/customFieldValue to find a location by a custom-field code (combine both to match the same pair). Results are paged: limit (default 50, max 200) and offset, with meta.total giving the filtered count; sort with sort (name, createdAt, updatedAt) and order. For a stable full walk while data changes, sort by createdAt ascending and page forward.

Arguments: sort (string), order (string), limit (integer), offset (integer), active (string), search (string), city (string), state (string), country (string), customFieldKey (string), customFieldValue (string)

update_location ✏️

Update a location's fields. Archiving and reactivating are done here too: pass isActive: false to archive, true to reactivate (there is no separate endpoint). Sending customFields replaces the whole set of opaque key/value pairs (e.g. warehouse codes).

Arguments: id* (string), name (string), address (object,null), notes (string,null), customFields (array), sortOrder (integer), isActive (boolean)

Vendors

bulk_import_vendors ✏️

Create many vendors in one atomic call from an array of rows. Use for initial data loads. The batch is all-or-nothing: the first row whose name or QBO id collides aborts the whole import with 409 conflict and nothing is inserted, so fix the row and resend the full set. Returns the created ids in row order. Each row may carry customFields (opaque key/value pairs, e.g. distributor / external account codes).

Arguments: rows* (array)

create_vendor ✏️

Create a vendor (materials supplier). Vendors are referenced by purchase orders. Name is unique per org (case-insensitive), as is QBO id when set; a collision is rejected with 409 conflict. customFields accepts opaque ordered key/value string pairs the platform never interprets (e.g. distributor / external account codes). Returns the created vendor including its id, which you pass to create_purchase_order.

Arguments: name* (string), address (object,null), term (string,null), qboId (string,null), notes (string,null), customFields (array)

delete_vendor 🔴

Permanently delete a vendor. Fails with 409 in_use when any purchase order references it; archive it instead (update_vendor with isActive=false) to retire a supplier while keeping history. The id comes from list_vendors.

Arguments: id* (string)

get_vendor

Get full detail for one vendor by id, including its linked contacts (each with the person, their email and phone methods, and the link role). The id comes from list_vendors.

Arguments: id* (string)

list_vendors

List one page of vendors. To resolve one vendor, pass search (matches the name, notes, and payment term, case-insensitive) rather than listing everything and filtering. Pass active to show only active or only archived vendors, territory filters (city/state/country, case-insensitive exact), term (exact, e.g. Net 30), or customFieldKey/customFieldValue to find a vendor by a custom-field code (combine both to match the same pair). Results are paged: limit (default 50, max 200) and offset, with meta.total giving the filtered count; sort with sort (name, term, createdAt, updatedAt) and order. For a stable full walk while data changes, sort by createdAt ascending and page forward.

Arguments: sort (string), order (string), limit (integer), offset (integer), active (string), search (string), city (string), state (string), country (string), term (string), customFieldKey (string), customFieldValue (string)

update_vendor ✏️

Update a vendor's fields (partial update, only send what changes). Set isActive false to archive the vendor or true to reactivate it (there is no separate archive tool). A name or QBO id that collides with another vendor is rejected with 409 conflict. Sending customFields replaces the whole set of opaque key/value pairs (e.g. distributor codes). The id comes from list_vendors.

Arguments: id* (string), name (string), address (object,null), term (string,null), qboId (string,null), notes (string,null), customFields (array), isActive (boolean)

Materials

bulk_import_materials ✏️

Create many materials in one atomic call from an array of rows. Use for initial data loads. The whole batch is one transaction: the first invalid row (unknown type, or a duplicate code) rolls back all of them. Returns the created ids in row order. Each row may carry customFields (opaque key/value pairs, e.g. UPC / distributor codes). At most 1000 rows per call.

Arguments: rows* (array)

create_material ✏️

Create a raw material. Materials are consumed by work orders (via BOMs) and tracked in the materials inventory ledger. The typeId comes from list_material_types. customFields accepts opaque ordered key/value string pairs the platform never interprets (e.g. UPC, distributor codes). The code must be unique in the org (409 conflict otherwise).

Arguments: code* (string), name* (string), typeId* (string), notes (string,null), customFields (array)

create_material_type ✏️

Create a material type (org-defined category used to classify materials). The returned id feeds the REQUIRED typeId on create_material. Check list_material_types first and reuse an existing type rather than creating a near-duplicate; a name already in use is rejected with 409 conflict.

Arguments: name* (string)

delete_material 🔴

Permanently delete a material. Fails with 409 in_use when a BOM, PO item, ledger entry, or transfer references it (a blocking transfer surfaces its transferCount in the error details). Archive it instead (update_material with isActive=false) to keep the history.

Arguments: id* (string)

delete_material_type 🔴

Delete a material type. Attempt the delete directly: while materials still use the type it fails with 409 in_use and the error details carry the blocking materialCount. Archive it instead (update_material_type with isActive=false) to retire a type without deleting it.

Arguments: id* (string)

get_material

Get full detail for one material by id, with the material type embedded as a type object with its id and name. The id comes from list_materials.

Arguments: id* (string)

list_material_types

List the org's material types, active and archived alike (per the isActive flag on each row). Use it to resolve the typeId for create_material or update_material, and check it before create_material_type to avoid duplicates. Pass search to match names instead of listing everything.

Arguments: search (string)

list_materials

List one page of materials. Pass active, a CSV type id list, or a free-text search (matches material code, name, type name, or notes). Pass customFieldKey and/or customFieldValue to find a material by a custom-field code (case-insensitive; combine both to match the same pair). Each row embeds the material type as a type object with its id and name. Results are paged: limit (default 50, max 200) and offset, with meta.total giving the filtered count; sort with sort (code, name, type, createdAt, updatedAt) and order. For a stable full walk while data changes, sort by createdAt ascending and page forward.

Arguments: sort (string), order (string), limit (integer), offset (integer), active (string), type (string), search (string), customFieldKey (string), customFieldValue (string)

update_material ✏️

Update a material's fields (partial update, only send what changes). Set isActive false to archive or true to reactivate. Sending customFields replaces the whole set of opaque key/value pairs (e.g. UPC, distributor codes). Changing the code to one already in use returns 409 conflict.

Arguments: id* (string), code (string), name (string), typeId (string), notes (string,null), customFields (array), isActive (boolean)

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 tool). Renaming to a name another type already uses returns 409 conflict. The id comes from list_material_types.

Arguments: id* (string), name (string), sortOrder (integer), isActive (boolean)

Products (SKUs)

bulk_import_skus ✏️

Create many SKUs in one atomic call from an array of rows. Use for initial data loads. The whole batch is one transaction: the first invalid row (unknown type, or a duplicate code) rolls back all of them. Returns the created ids in row order. Each row may carry customFields (opaque key/value pairs, e.g. UPC / distributor codes). At most 1000 rows per call.

Arguments: rows* (array)

create_sku ✏️

Create a SKU (finished-goods product). SKUs are produced by work orders, sold on sales orders, and tracked in the finished-goods inventory ledger. The typeId comes from list_sku_types. customFields accepts opaque ordered key/value string pairs the platform never interprets (e.g. UPC, distributor codes). The code must be unique in the org (409 conflict otherwise). itemsPerUnit defaults to 1 when omitted.

Arguments: code* (string), name* (string), typeId* (string), itemsPerUnit (integer), ozPerItem (number,null), qboId (string,null), notes (string,null), customFields (array)

create_sku_type ✏️

Create a SKU type (org-defined category used to classify SKUs). The returned id feeds the REQUIRED typeId on create_sku. Check list_sku_types first and reuse an existing type rather than creating a near-duplicate; a name already in use is rejected with 409 conflict.

Arguments: name* (string)

delete_sku 🔴

Permanently delete a SKU. Fails with 409 in_use when a BOM, order item, ledger entry, or transfer references it (a blocking transfer surfaces its transferCount in the error details). Archive it instead (update_sku with isActive=false) to keep the history.

Arguments: id* (string)

delete_sku_type 🔴

Delete a SKU type. Attempt the delete directly: while SKUs still use the type it fails with 409 in_use and the error details carry the blocking skuCount. Archive it instead (update_sku_type with isActive=false) to retire a type without deleting it.

Arguments: id* (string)

get_sku

Get full detail for one SKU by id, including pack size and custom fields, with the SKU type embedded as a type object with its id and name. The id comes from list_skus.

Arguments: id* (string)

list_sku_types

List the org's SKU types, active and archived alike (per the isActive flag on each row). Use it to resolve the typeId for create_sku or update_sku, and check it before create_sku_type to avoid duplicates. Pass search to match names instead of listing everything.

Arguments: search (string)

list_skus

List one page of SKUs. Pass active, a CSV type id list, the itemsPerUnitMin/Max or total-oz totalOzMin/Max ranges, or a free-text search (matches SKU code, name, type name, or notes). Pass customFieldKey and/or customFieldValue to find a SKU by a custom-field code (case-insensitive; combine both to match the same pair). Each row embeds the SKU type as a type object with its id and name. Results are paged: limit (default 50, max 200) and offset, with meta.total giving the filtered count; sort with sort (code, name, type, itemsPerUnit, totalOz, createdAt, updatedAt) and order. For a stable full walk while data changes, sort by createdAt ascending and page forward.

Arguments: sort (string), order (string), limit (integer), offset (integer), active (string), type (string), itemsPerUnitMin (number), itemsPerUnitMax (number), totalOzMin (number), totalOzMax (number), search (string), customFieldKey (string), customFieldValue (string)

update_sku ✏️

Update a SKU's fields (partial update, only send what changes). Set isActive false to archive or true to reactivate. Sending customFields replaces the whole set of opaque key/value pairs (e.g. UPC, distributor codes). Changing the code to one already in use returns 409 conflict.

Arguments: id* (string), code (string), name (string), typeId (string), itemsPerUnit (integer), ozPerItem (number,null), qboId (string,null), notes (string,null), customFields (array), isActive (boolean)

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 tool). Renaming to a name another type already uses returns 409 conflict. The id comes from list_sku_types.

Arguments: id* (string), name (string), sortOrder (integer), isActive (boolean)

Bills of Materials

delete_sku_bom 🔴

Delete a SKU's BOM, removing all its composition lines. The id is the output SKU id. Existing work orders are unaffected (their inputs are snapshotted at creation). Returns 404 when the SKU has no BOM.

Arguments: id* (string)

get_sku_bom

Get one SKU's bill of materials: its input lines, each an input object with kind ('material' or 'sku'), id, code, and name, plus a quantity. The id argument is the output SKU id (from list_skus). Returns 404 when the SKU has no BOM defined.

Arguments: id* (string)

list_boms

List one page of the output SKUs that have a BOM defined, each carrying the output SKU as a sku object (id, code, name) and the count of its input items. Filters AND together: containsMaterialId and containsSkuId are CSVs of ids (each OR-ed within itself) that keep only the BOMs whose input lines include one of those materials / input SKUs ('which recipes use material X'); search substring-matches the output SKU's code or name, and also the input lines' content (the codes and names of input materials and input SKUs). Results are paged: limit (default 50, max 200) and offset, with meta.total giving the filtered count of BOMs; sort with sort (code, name, itemCount; default code ascending; the summary carries no timestamps) and order. For a stable full walk while data changes, keep the default code ascending sort and page forward. Use get_sku_bom for one SKU's full composition.

Arguments: sort (string), order (string), limit (integer), offset (integer), containsMaterialId (string), containsSkuId (string), search (string)

set_sku_bom ✏️

Create or replace a SKU's bill of materials in one idempotent call: the items you send become the SKU's entire composition (a full replace, not a partial merge). The id is the output SKU id (from list_skus). Each item sets exactly one of materialId (from list_materials) or inputSkuId (from list_skus) with a positive quantity, and at least one item is required (to clear a BOM, use delete_sku_bom). Returns 404 if the output SKU or any referenced material or input SKU does not exist.

Arguments: id* (string), items* (array)

Contacts

add_contact_method ✏️

Add an email or phone method to a contact (pass contactId in the body). Marking it primary demotes the contact's current primary of that type. An email-typed method must be a valid email. A duplicate (same type and value on the contact) is rejected with 409 conflict.

Arguments: contactId* (string), type* (string), value* (string), label (string,null), isPrimary (boolean)

create_contact ✏️

Create a standalone contact (person) in the shared address book. It starts with no methods and no entity links. Before creating, search list_contacts for the person: the pool is shared across entities, so reuse an existing contact (link it with link_contact) rather than creating a duplicate. To attach a new contact to a customer/vendor/location, link it afterwards with link_contact.

Arguments: firstName* (string), lastName (string,null), title (string,null), notes (string,null), displayName (string)

delete_contact 🔴

Permanently delete a contact, including its links and contact methods. Archive it instead (update_contact with isActive=false) to keep its links. The id comes from list_contacts.

Arguments: id* (string)

delete_contact_method 🔴

Delete a contact method. The id is the method id, not the contact id.

Arguments: id* (string)

get_contact

Get one contact: its header fields at the top level, with its email/phone methods and entity links nested (each link carries an entity object with the target's type, id, and name). The id comes from list_contacts.

Arguments: id* (string)

Link an existing contact to an entity (customer/vendor/location) with an optional role. Pass contactId (from list_contacts) plus entityType and entityId in the body. Reuse an existing contact from list_contacts rather than creating a new one when the same person is already in the pool. Both ends must exist (404 otherwise), and a duplicate link (same contact, entity, role) is rejected with 409 conflict.

Arguments: contactId* (string), entityType* (string), entityId* (string), role (string,null), notes (string,null), isPrimary (boolean)

list_contact_methods

List a contact's email and phone methods, primary first. Pass contactId as a query arg.

Arguments: contactId* (string)

list_contacts

List one page of contacts, each with its entity links hydrated: every link carries an entity object with the linked entity's type, id, and resolved name. Pass search to match a name, title, notes, or a method value (find a contact by an email or phone fragment, case-insensitive), active (true/false) for active vs archived, linkedEntityType (customer/vendor/location) for contacts attached to that entity kind, hasLinks=false for orphan contacts, and methodType (email/phone) for contacts with at least one method of that kind. Results are paged: limit (default 50, max 200) and offset, with meta.total giving the filtered count; sort with sort (name, title, createdAt, updatedAt) and order. For a stable full walk while data changes, sort by createdAt ascending and page forward. Search here first before create_contact to avoid duplicating a person already in the pool.

Arguments: sort (string), order (string), limit (integer), offset (integer), search (string), active (string), linkedEntityType (string), hasLinks (string), methodType (string)

list_contacts_linked_to

List the contacts linked to one entity, passing entityType (customer/vendor/location) and entityId as query args. Each row carries the link, the contact, and the contact's methods, so use this to resolve an entity's people rather than listing all contacts.

Arguments: entityType* (string), entityId* (string)

Remove a contact-entity link (the contact itself is kept). The id is the link id, not the contact id.

Arguments: id* (string)

update_contact ✏️

Update a contact's fields (partial update, only send what changes). Set isActive false to archive the contact or true to reactivate it (there is no separate archive tool). The id comes from list_contacts.

Arguments: id* (string), firstName (string), lastName (string,null), title (string,null), notes (string,null), displayName (string), isActive (boolean)

Update a contact-entity link's role, notes, or primary flag. A role change that collides with another link on the same contact and entity returns 409 conflict. The id is the link id (from list_contacts_linked_to or the contact detail), not the contact id.

Arguments: id* (string), role (string,null), notes (string,null), isPrimary (boolean), isActive (boolean)

update_contact_method ✏️

Update a contact method's value, label, or primary flag. Promoting it to primary demotes the current primary of its type; a collision with an existing method returns 409 conflict. The id is the method id, not the contact id.

Arguments: id* (string), type (string), value (string), label (string,null), isPrimary (boolean), isActive (boolean)

Customers

bulk_import_customers ✏️

Create many customers in one atomic call from an array of rows. Use for initial data loads. The whole batch is one transaction: the first invalid row rolls back all of them. Returns the created ids in row order. Each row may carry customFields (opaque key/value pairs, e.g. distributor / external account codes).

Arguments: rows* (array)

create_customer ✏️

Create a customer, the entity sales orders are placed against. typeId is REQUIRED and comes from list_customer_types; segmentId (from list_customer_segments) and brokerId (from list_brokers) are optional. A duplicate name returns 409 conflict. address is one nested value object ({ line1, city, state, postalCode, country }), not flat fields. customFields accepts opaque ordered key/value string pairs the platform never interprets (e.g. distributor / external account codes).

Arguments: name* (string), typeId* (string), segmentId (string,null), brokerId (string,null), brokerFeePercent (number,null), address (object,null), parentId (string,null), qboId (string,null), notes (string,null), customFields (array)

create_customer_segment ✏️

Create a customer segment (org-defined grouping, e.g. retail/wholesale). The returned id feeds segmentId on create_customer. A duplicate name returns 409 conflict.

Arguments: name* (string)

create_customer_type ✏️

Create a customer type (org-defined classification). The returned id feeds the REQUIRED typeId on create_customer. A duplicate name returns 409 conflict.

Arguments: name* (string)

delete_customer 🔴

Permanently delete a customer. Fails with 409 in_use when a sales order references it, or when it is the parent of another customer.

Arguments: id* (string)

delete_customer_segment 🔴

Delete a customer segment. Attempt the delete directly: while customers use the segment it fails with 409 in_use and the error details carry the blocking customerCount. Archive it instead (update_customer_segment with isActive=false) to retire it while keeping history.

Arguments: id* (string)

delete_customer_type 🔴

Delete a customer type. Attempt the delete directly: while customers use the type it fails with 409 in_use and the error details carry the blocking customerCount. Archive it instead (update_customer_type with isActive=false) to retire it while keeping history.

Arguments: id* (string)

get_customer

Get full detail for one customer by id, including the contacts linked to it (each with its methods). The id comes from list_customers.

Arguments: id* (string)

list_customer_segments

List the org's customer segments. Pass search to match names instead of listing everything.

Arguments: search (string)

list_customer_types

List the org's customer types. Pass search to match names instead of listing everything.

Arguments: search (string)

list_customers

List one page of customers. Filter instead of paging through everything: active, type, segment, broker, and parentCustomer (each a CSV of ids for a multi-select), territory (city/state/country, case-insensitive exact), hasParent (true = subsidiaries only), inclusive createdFrom/createdTo dates, a free-text search over the name, classifier names and notes, and customFieldKey / customFieldValue to find a customer by a custom-field code (combine both to match the same pair). Results are paged: limit (default 50, max 200) and offset, with meta.total giving the filtered count; sort with sort (name, type, segment, broker, parent, brokerFeePercent, createdAt, updatedAt) and order. For a stable full walk while data changes, sort by createdAt ascending and page forward.

Arguments: sort (string), order (string), limit (integer), offset (integer), active (string), type (string), segment (string), broker (string), parentCustomer (string), city (string), state (string), country (string), hasParent (string), createdFrom (string), createdTo (string), search (string), customFieldKey (string), customFieldValue (string)

update_customer ✏️

Update a customer's fields (partial update, only send what changes). Set isActive false to archive or true to reactivate. address is one nested value object ({ line1, city, state, postalCode, country }); omitting it leaves the address untouched, sending it sets each field (a null field clears it). Sending customFields replaces the whole set of opaque key/value pairs (e.g. distributor codes). Renaming to a name already in use returns 409 conflict.

Arguments: id* (string), name (string), typeId (string), segmentId (string,null), brokerId (string,null), brokerFeePercent (number,null), address (object,null), parentId (string,null), qboId (string,null), notes (string,null), customFields (array), isActive (boolean)

update_customer_segment ✏️

Rename/update a customer segment. A duplicate name returns 409 conflict. Archive a segment by passing isActive: false (reactivate with true); there is no separate delete-to-archive.

Arguments: id* (string), name (string), sortOrder (integer), isActive (boolean)

update_customer_type ✏️

Rename/update a customer type. A duplicate name returns 409 conflict. Archive a type by passing isActive: false (reactivate with true); there is no separate delete-to-archive.

Arguments: id* (string), name (string), sortOrder (integer), isActive (boolean)

Brokers

create_broker ✏️

Create a broker, a third-party sales intermediary that can be attached to customers and sales orders. Check list_brokers first and reuse an existing broker rather than creating a near-duplicate; a name already in use is rejected with 409 conflict.

Arguments: name* (string)

delete_broker 🔴

Permanently delete a broker. Attempt the delete directly: when customers or sales orders still reference the broker it fails with 409 in_use and the error details carry the blocking counts (customerCount, salesOrderCount). Archive it instead (update_broker with isActive=false) to retire a broker while keeping history.

Arguments: id* (string)

list_brokers

List the org's brokers, active and archived alike (per the isActive flag on each row). Pass search to match broker names instead of listing everything. Use it to resolve the broker id to set on a customer or sales order, and check it before create_broker to avoid duplicates.

Arguments: search (string)

update_broker ✏️

Update a broker's name or sort order (partial update, only send what changes). Set isActive false to archive the broker or true to reactivate it (there is no separate archive tool). Renaming to a name another broker already uses returns 409 conflict. The id comes from list_brokers.

Arguments: id* (string), name (string), sortOrder (integer), isActive (boolean)

Materials Inventory

adjust_materials_inventory ✏️

Record a manual stock count correction for a material at a location. The quantity is signed: a positive value raises on-hand (found stock), a negative value lowers it (a count entered too high); zero is rejected. Use transfers for moves and disposals for write-offs (damage, loss, expiry) that carry waste semantics.

Arguments: materialId* (string), locationId* (string), quantity* (number), notes (string,null), eventDate (string)

dispose_materials_inventory 🔴

Write off material stock (damage, expiry, loss). This permanently removes quantity from stock via a DISPOSE ledger entry.

Arguments: materialId* (string), locationId* (string), quantity* (number), notes (string,null), eventDate (string)

get_materials_inventory_summary

Complete materials inventory position by material and location, derived 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, excluding transfers between your own locations and disposals, so you can compute weeks of cover). The window defaults to 4 whole weeks; pass demandWindowWeeks (1..52, e.g. 13 for a trailing quarter) when you want a steadier rate that is less swayed by one unusual month. Each row also carries openSalesDemand and openJobDemand, the active unfulfilled commitment split by source (customer sales vs production inputs), which is the number to use for 'how much customer demand cannot be filled'. weeklyDemandDtc and weeklyDemandWholesale split the trailing rate by channel. inTransitProduction separates production orders on the way in from the rest of on-order. Each row embeds its material ({ id, code, name }) and location ({ id, name }). Filters AND together: materialId and locationId are CSVs of ids (each OR-ed within itself), and search substring-matches (case-insensitive) the material's code and name. Pass includeOnHandValue=true when you need the MONEY tied up in stock: it adds onHandValue, the costed value of the on-hand units under the org's costing method. It replays the ledger per row, so ask for it only when the question is about value, and narrow with the filters first. This is the go-to tool for 'how much of X do we have (and where)'.

Arguments: materialId (string), locationId (string), search (string), includeOnHandValue (string), demandWindowWeeks (integer)

get_materials_ledger_entry

Get one materials ledger entry by id (from list_materials_ledger_entries), with its embedded material ({ id, code, name }) and location ({ id, name }). The single read is not expanded per lot.

Arguments: id* (string)

list_material_open_requirements

The work and production orders still consuming one material: one row per order with its reference, status, required quantity, and what is still open after consumption and settles. Pass the materialId from list_materials or the materials summary. Use this to answer 'which jobs need this material' or to drill into the summary's openJobDemand figure; do not reconstruct it from the raw ledger, the netting rules live here.

Arguments: materialId* (string)

list_materials_ledger_entries

List one page of materials inventory ledger entries (append-only event log: RECEIVE, ORDER, CONSUME, ADJUST, DISPOSE). Each row embeds its material ({ id, code, name }) and location ({ id, name }). Filter instead of paging through everything: materialId, locationId, eventType and sourceType are CSV multi-selects (each OR-ed within itself, filters AND together); sourceType selects rows written by one kind of source document (e.g. sourceType=transfer for every transfer leg). ref is a single exact match on the row's correlator: the PO number for PO events, or the transfer's TFR code for a transfer's CONSUME (source) and RECEIVE (destination) legs. lotNumber is a case-insensitive exact match; the date filters are one inclusive From/To pair per field (eventDateFrom/eventDateTo on the business event date, expirationFrom/expirationTo on the lot expiration date; YYYY-MM-DD). search substring-matches (case-insensitive) the row code (MIL-...), ref, the material's code and name, the location name, the lot number, and notes. Results are paged: limit (default 50, max 200) and offset, with meta.total giving the filtered count of stored rows (a row that splits into per-lot lines keeps its one page slot); sort with sort (code, material, location, eventDate, quantity, createdAt; default eventDate descending, newest first) and order. For a stable full walk while data changes, sort by createdAt ascending and page forward.

Arguments: sort (string), order (string), limit (integer), offset (integer), materialId (string), locationId (string), eventType (string), sourceType (string), ref (string), lotNumber (string), search (string), eventDateFrom (string), eventDateTo (string), expirationFrom (string), expirationTo (string)

Finished-Goods Inventory

adjust_finished_goods_inventory ✏️

Record a manual stock count correction for a SKU at a location. The quantity is signed: a positive value raises on-hand (found stock), a negative value lowers it (a count entered too high); zero is rejected. Use transfers for moves and disposals for write-offs (damage, loss, expiry) that carry waste semantics.

Arguments: skuId* (string), locationId* (string), quantity* (number), notes (string,null), eventDate (string)

dispose_finished_goods_inventory 🔴

Write off finished-goods stock (damage, expiry, loss). This permanently removes quantity from stock via a DISPOSE ledger entry.

Arguments: skuId* (string), locationId* (string), quantity* (number), notes (string,null), eventDate (string)

get_finished_goods_inventory_summary

Complete finished-goods inventory position by SKU and location, derived 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, excluding transfers between your own locations and disposals, so you can compute weeks of cover). The window defaults to 4 whole weeks; pass demandWindowWeeks (1..52, e.g. 13 for a trailing quarter) when you want a steadier rate that is less swayed by one unusual month. Each row also carries openSalesDemand and openJobDemand, the active unfulfilled commitment split by source (customer sales vs production inputs), which is the number to use for 'how much customer demand cannot be filled'. weeklyDemandDtc and weeklyDemandWholesale split the trailing rate by channel. inTransitProduction separates production orders on the way in from the rest of on-order. Each row embeds its sku ({ id, code, name }) and location ({ id, name }). Filters AND together: skuId and locationId are CSVs of ids (each OR-ed within itself), and search substring-matches (case-insensitive) the SKU's code and name. Pass includeOnHandValue=true when you need the MONEY tied up in stock: it adds onHandValue, the costed value of the on-hand units under the org's costing method. It replays the ledger per row, so ask for it only when the question is about value, and narrow with the filters first. This is the go-to tool for 'how many units of SKU X do we have (and where)'.

Arguments: skuId (string), locationId (string), search (string), includeOnHandValue (string), demandWindowWeeks (integer)

get_finished_goods_ledger_entry

Get one finished-goods ledger entry by id (from list_finished_goods_ledger_entries), with its embedded sku ({ id, code, name }) and location ({ id, name }). The single read is not expanded per lot.

Arguments: id* (string)

list_finished_goods_ledger_entries

List one page of finished-goods inventory ledger entries (append-only event log: RECEIVE, CONSUME, ADJUST, DISPOSE). Produced and bought goods are both a RECEIVE, told apart by source_type. Each row embeds its sku ({ id, code, name }) and location ({ id, name }). Filter instead of paging through everything: skuId, locationId, eventType and sourceType are CSV multi-selects (each OR-ed within itself, filters AND together); sourceType selects rows written by one kind of source document (e.g. sourceType=transfer for every transfer leg). ref is a single exact match on the row's correlator: the order number for order events, or the transfer's TFR code for a transfer's CONSUME (source) and RECEIVE (destination) legs. lotNumber is a case-insensitive exact match; the date filters are one inclusive From/To pair per field (eventDateFrom/eventDateTo on the business event date, expirationFrom/expirationTo on the lot expiration date; YYYY-MM-DD). search substring-matches (case-insensitive) the row code (FIL-...), ref, the SKU's code and name, the location name, the lot number, and notes. Results are paged: limit (default 50, max 200) and offset, with meta.total giving the filtered count of stored rows (a row that splits into per-lot lines keeps its one page slot); sort with sort (code, sku, location, eventDate, quantity, createdAt; default eventDate descending, newest first) and order. For a stable full walk while data changes, sort by createdAt ascending and page forward.

Arguments: sort (string), order (string), limit (integer), offset (integer), skuId (string), locationId (string), eventType (string), sourceType (string), ref (string), lotNumber (string), search (string), eventDateFrom (string), eventDateTo (string), expirationFrom (string), expirationTo (string)

Transfers

create_transfer ✏️

Create a stock transfer between two different locations. The TFR-xxx code is server-assigned; never supply one. The movement is the items array, one entry per moved item: itemKind picks that line's inventory (material | sku) and itemId is the material's id (from list_materials) or the SKU's id (from list_skus), plus a positive quantity. One transfer can mix materials and SKUs, so a whole shipment is ONE transfer, not one per item. At least one line is required and the same item may not appear twice. The location ids come from list_locations. Opens in Planning by default, which touches no inventory; pass status 'In Transit' to also write every line's outbound CONSUME leg at the source immediately, or 'Completed' to write both legs of every line. Per-status dates auto-stamp from the opening status; override via the optional statusDates object (keys planning, inTransit, completed; YYYY-MM-DD). Rejected with 404 when an item or a location does not exist, 409 when an item is repeated across lines, and 422 when an item is inactive or source equals destination.

Arguments: sourceLocationId* (string), destinationLocationId* (string), items* (array), status (string), statusDates (object), notes (string,null)

delete_transfer 🔴

Hard-delete a transfer: removes the document, its moved items, and the ledger legs they projected (each line's outbound CONSUME and inbound RECEIVE, on whichever ledgers they landed). On-hand at both locations recomputes as if the transfer never happened. To merely undo the movement while keeping the document, step the status back with update_transfer instead.

Arguments: id* (string)

get_transfer

Get one transfer by id (from list_transfers), with its sourceLocation / destinationLocation ({ 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. Same shape as a list row.

Arguments: id* (string)

list_transfer_status_types

List the transfer status vocabulary in lifecycle order: Planning, In Transit, Completed. Use it to populate a status picker; transitions only move one step at a time along this line (see update_transfer).

Arguments: none

list_transfers

List one page of inventory transfers, newest first by default. A transfer moves one or more items between two locations; each row embeds its sourceLocation / destinationLocation ({ id, name }), nests its moved items (each { id, item: { kind, id, code, name }, quantity }), and groups the per-status timestamps in statusDates (keys planning, inTransit, completed). Narrow with the filters instead of listing everything (they AND together): pass itemId to see every transfer that moved one material or SKU, itemKind (material | sku) for the transfers carrying a line of that kind (a transfer mixing both matches either), status as a CSV of statuses (Planning, In Transit, Completed; OR-ed within itself), sourceLocationId and destinationLocationId each as a CSV of location ids, and the per-status date filters, one inclusive From/To pair per status key (planningFrom/planningTo, inTransitFrom/inTransitTo, completedFrom/completedTo; YYYY-MM-DD), each bounding that status's own date. search substring-matches (case-insensitive) the transfer's TFR code, any line's item code and name, both location names, and notes. Paging counts transfers, not lines: limit (default 50, max 200) and offset, with meta.total giving the filtered document count; sort with sort (number, item, sourceLocation, destinationLocation, status, createdAt, updatedAt; item sorts by the alphabetically first line's code; default createdAt descending) and order. For a stable full walk while data changes, sort by createdAt ascending and page forward.

Arguments: sort (string), order (string), limit (integer), offset (integer), itemKind (string), itemId (string), status (string), sourceLocationId (string), destinationLocationId (string), search (string), planningFrom (string), planningTo (string), inTransitFrom (string), inTransitTo (string), completedFrom (string), completedTo (string)

update_transfer ✏️

Update a transfer. Set status to move it one step along Planning <-> In Transit <-> Completed (both directions, adjacent steps only; a jump such as Planning -> Completed is rejected with 409 invalid_transition). Reaching In Transit writes every line's outbound CONSUME at the source, reaching Completed writes every line's RECEIVE at the destination, and stepping back removes the legs it added. Date edits go through the statusDates object (keys planning, inTransit, completed), which merges PER KEY (only the keys you send change) and re-dates the matching ledger leg of every line; a reached status's date cannot be cleared to null (step the status back instead). 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); once it has left Planning they are edit-locked and rejected with 409 invalid_transition. Careful with items: unlike statusDates it does NOT merge, a sent array replaces the entire line set, so include every line you want to keep (and never repeat an item, which is 409 conflict).

Arguments: id* (string), sourceLocationId (string), destinationLocationId (string), items (array), status (string), statusDates (object), notes (string,null)

Purchase Orders

add_purchase_order_item ✏️

Add a line item to an existing PO. Pass purchaseOrderId in the body (the PO id from list_purchase_orders), plus exactly one of materialId or skuId (matching the order's targetType), quantity, and unitCost (up to 6 decimals). Adding to an ordered PO books the line's ORDER ledger entry immediately. A missing PO, material, or SKU is rejected 404; rejected 409 once any receipt exists against the PO: received orders lock their line items.

Arguments: materialId (string,null), skuId (string,null), quantity* (number), unitCost (number), purchaseOrderId* (string)

bulk_import_purchase_orders ✏️

Import many purchase orders in ONE atomic call (admins/owners only): every row creates a PO with an auto-generated number, its lines, and the projected ORDER ledger entries for its status, and the whole batch rolls back if any row fails. Use for initial data loads; for day-to-day creation prefer create_purchase_order. Returns the created ids in row order.

Arguments: rows* (array)

create_purchase_order ✏️

Create a purchase order against a vendor (vendorId from list_vendors). targetType picks the inventory the order buys: materials (each line names a materialId from list_materials) or finished_goods (each line names a skuId from list_skus). Items can be included here or added afterwards with add_purchase_order_item; unit costs accept up to 6 decimals. Opening in any ordered status writes ORDER entries to the matching inventory ledger (ref = PO number); Planning has no inventory effect. orderDate is the user-owned order date (YYYY-MM-DD, defaults to today). Pass poNumber to set a custom number (unique per org; a clash is rejected 409), or omit it to auto-generate the next one. Per-status dates auto-stamp from the opening status; override via the optional statusDates object (keys planning, placed, inTransit, partial, received, completed, cancelled; YYYY-MM-DD). The 201 is the same composite get_purchase_order serves: header fields at top level with the vendor, ship-to location, and each line's material/SKU embedded as references, the timestamps grouped in statusDates, items nested, and receipts empty. A missing vendor, location, material, or SKU is rejected 404.

Arguments: vendorId* (string), targetType* (string), status* (string), shipToLocationId (string,null), orderDate (string,null), shipDate (string,null), expectedDeliveryDate (string,null), statusDates (object), shippingCosts (number), setupCosts (number), otherCosts (number), notes (string,null), poNumber (string), items* (array)

create_purchase_order_receipt ✏️

Record goods received against a PO (full or partial). Pass purchaseOrderId in the body (the PO id from list_purchase_orders); each line names the material/SKU received, the quantity, a unitCost (up to 6 decimals, read live by the costing engine), and optionally a lotNumber and expirationDate. Writes one RECEIVE entry per line to the matching inventory ledger (ref = the PO number), increasing stock at the ship-to location, and moves the PO to Partial (receiving against a Received PO reopens it). The response is two-keyed: the created receipt with its lines nested under items, plus the updated PO header. A missing PO, material, or SKU is rejected 404; rejected 409 while the PO is not open for receiving (Planning, Completed, or Cancelled).

Arguments: purchaseOrderId* (string), receiptDate (string), items* (array)

delete_purchase_order 🔴

Permanently delete a purchase order and its items/receipts, removing its projected ledger rows so on-hand recomputes. There is no in-use gate and no undo; cancel the order (status Cancelled via update_purchase_order) to keep the audit trail instead.

Arguments: id* (string)

delete_purchase_order_receipt 🔴

Delete a PO receipt and its lines, as if the goods never arrived. The id is the receipt id, not the PO id. Its RECEIVE ledger rows are removed (on-hand recomputes) and the PO status walks back to what the surviving receipts support. Rejected 409 while the PO is Completed: reopen it first via update_purchase_order.

Arguments: id* (string)

get_purchase_order

Get one purchase order as a composite: the header's fields at top level (no wrapper key), with the per-status timestamps grouped in statusDates and a financials block of the stored operational costs plus a derived goodsCost (the received-goods value from the costing engine) and totalCost, its items nested (material/SKU embedded as { id, code, name }), and its receipts nested with each receipt's received lines under its own items. goodsCost is recomputed on every read, so it can change as historical inventory data changes.

Arguments: id* (string)

get_purchase_order_receipt

Get one PO receipt: the receipt header's fields at top level with its received lines nested under items, each line's material or SKU embedded as { id, code, name }. The id is the receipt id from list_purchase_order_receipts, not the PO id.

Arguments: id* (string)

list_purchase_order_items

List PO line items, each with its material or SKU embedded as { id, code, name }. Pass purchaseOrderId for one order's items, or materialId / skuId for questions like 'which POs order material X'; do not pull everything and filter client-side. Each row carries its purchaseOrderId so you can fetch the order with get_purchase_order.

Arguments: purchaseOrderId (string), materialId (string), skuId (string)

list_purchase_order_receipts

List PO goods receipts (headers only) in receipt-date order. Pass purchaseOrderId for one order's receipts; omit it to query receipts across every PO. Fetch a receipt's lines with get_purchase_order_receipt.

Arguments: purchaseOrderId (string)

list_purchase_order_status_types

List the PO status vocabulary (Planning, Placed, In Transit, Partial, Received, Completed, Cancelled) as plain strings. Use before setting a status with update_purchase_order; note Partial is receipt-driven, not set manually.

Arguments: none

list_purchase_orders

List one page of purchase orders, newest first by default, each with its vendor and ship-to location embedded as { id, name }, the per-status timestamps grouped in statusDates, and the stored operational costs in financials. Prefer the filters over pulling everything: status, vendorId (from list_vendors), and shipToLocationId (from list_locations) each accept a CSV list (values OR within a filter, filters AND together), targetType (materials | finished_goods), containsMaterialId / containsSkuId keep only orders with a line for that material or SKU, and every date filter is an inclusive From/To pair (YYYY-MM-DD): orderDateFrom/To, shipDateFrom/To, expectedDeliveryFrom/To, plus one pair per status key (e.g. placedFrom/placedTo, receivedFrom/receivedTo). 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. Results are paged: limit (default 50, max 200) and offset, with meta.total giving the filtered count; sort with sort (number, vendor, shipTo, status, orderDate, shipDate, expectedDeliveryDate, createdAt, updatedAt; default orderDate descending) and order. For a stable full walk while data changes, sort by createdAt ascending and page forward. Fetch one order's lines and receipts with get_purchase_order. Filter by expected total cost with costMin/costMax and sort with sort=totalCost (the order's own lines plus operational costs).

Arguments: sort (string), order (string), limit (integer), offset (integer), status (string), vendorId (string), shipToLocationId (string), targetType (string), containsMaterialId (string), containsSkuId (string), costMin (number), costMax (number), search (string), orderDateFrom (string), orderDateTo (string), shipDateFrom (string), shipDateTo (string), expectedDeliveryFrom (string), expectedDeliveryTo (string), planningFrom (string), planningTo (string), placedFrom (string), placedTo (string), inTransitFrom (string), inTransitTo (string), partialFrom (string), partialTo (string), receivedFrom (string), receivedTo (string), completedFrom (string), completedTo (string), cancelledFrom (string), cancelledTo (string)

remove_purchase_order_item 🔴

Remove a line item from a PO, unwinding the ORDER ledger entry the line had projected. The id is the item id, not the PO id. Rejected 409 once any receipt exists against the PO.

Arguments: id* (string)

update_purchase_order ✏️

Update a PO's header fields, including its status. Use list_purchase_order_status_types for valid status values; moving the status projects or unwinds the matching ORDER ledger entries, and closing to Received writes a settle that trues on-order to zero. orderDate is the user-owned order date and is freely settable (YYYY-MM-DD). Per-status lifecycle dates auto-stamp when the status changes and can also be set through the statusDates object (keys planning, placed, inTransit, partial, received, completed, cancelled), which merges PER KEY: only the keys you send change, and null clears a date where allowed. partial is receipt-derived (from the earliest receipt's date); dates must stay in lifecycle order or the call is rejected. Editing statusDates.placed re-dates the PO's ORDER ledger rows. vendorId and shipToLocationId are editable only while the PO is still in Planning (rejected 409 invalid_transition afterwards); targetType is immutable after create. The PO number can be renamed via poNumber (unique in the org; a collision is rejected 409) and the rename cascades to the ledger refs.

Arguments: id* (string), poNumber (string), vendorId (string), shipToLocationId (string,null), status (string), orderDate (string,null), shipDate (string,null), expectedDeliveryDate (string,null), statusDates (object), shippingCosts (number), setupCosts (number), otherCosts (number), notes (string,null)

update_purchase_order_item ✏️

Update a PO line item's quantity, unitCost (up to 6 decimals), or referenced material/SKU. The id is the item id from list_purchase_order_items, not the PO id. A quantity or target change re-projects the line's ORDER ledger entry; retargeting to a material or SKU that does not exist is rejected 404. Rejected 409 once any receipt exists against the PO: received orders lock their line items.

Arguments: id* (string), materialId (string,null), skuId (string,null), quantity (number), unitCost (number)

update_purchase_order_receipt ✏️

Update a PO receipt's receiptDate (YYYY-MM-DD; the only editable field). The id is the receipt id, not the PO id. Re-dating the receipt re-dates its RECEIVE ledger rows and the PO's receipt-derived partialAt, but never changes the PO status. Edit lines with update_purchase_order_receipt_item.

Arguments: id* (string), receiptDate (string)

update_purchase_order_receipt_item ✏️

Edit a receipt line in place (quantity, unitCost, lotNumber, expirationDate). The id is the receipt-item id from get_purchase_order_receipt. Quantity/lot/expiration propagate to the line's RECEIVE ledger row and a quantity change recomputes the PO status; unitCost (up to 6 decimals) needs no propagation, the costing engine reads it live. Rejected 409 while the PO is Completed: reopen it first via update_purchase_order.

Arguments: id* (string), quantity (number), unitCost (number), lotNumber (string,null), expirationDate (string,null)

Work Orders

add_work_order_item ✏️

Add an output SKU (+ quantity to produce) to an existing WO; its BOM inputs are snapshotted at add time. Pass workOrderId in the body (the WO id from list_work_orders), the skuId (from list_skus), and conversionCost (up to 6 decimals). Adding to an ordered WO projects the line's commitment ledger events. The response is the line with its output SKU embedded and recipe inputs nested. A missing WO is rejected 404; rejected 409 once a receipt exists against the WO (invalid_transition, its items are locked) or the SKU is already a line on it (conflict).

Arguments: skuId* (string), quantity* (integer), conversionCost (number), workOrderId* (string)

bulk_import_work_orders ✏️

Import many work orders in ONE atomic call (admins/owners only): every row creates a WO with an auto-generated number, its items, the recipe snapshots, and the projected commitment ledger events for its status, and the whole batch rolls back if any row fails. Use for initial data loads; for day-to-day creation prefer create_work_order. Returns the created ids in row order.

Arguments: rows* (array)

create_work_order ✏️

Create a work order (in-house production run). Items each name an output SKU (skuId from list_skus) and a quantity to produce; each SKU's BOM inputs are snapshotted at creation (later BOM edits do not change the WO), and conversionCost accepts up to 6 decimals. workSiteLocationId is the producing location (from list_locations). Opening in an ordered status projects the commitment ledger events. orderDate is the user-owned order date (YYYY-MM-DD, defaults to today). Pass woNumber to set a custom number (unique per org; a clash is rejected 409), or omit it to auto-generate the next one. Per-status dates auto-stamp from the opening status; override via the optional statusDates object (keys planning, placed, production, partial, received, completed, cancelled; YYYY-MM-DD). The 201 is the same composite get_work_order serves: header fields at top level with the work-site location embedded, the timestamps grouped in statusDates, items nested (each output SKU embedded, its recipe inputs nested with their material/component SKU embedded), and receipts empty. A missing work-site location or output SKU is rejected 404.

Arguments: status* (string), workSiteLocationId* (string), orderDate (string,null), expectedDeliveryDate (string,null), statusDates (object), inShippingCosts (number), outShippingCosts (number), otherCosts (number), notes (string,null), woNumber (string), items* (array)

create_work_order_receipt ✏️

Record production output against a WO (full or partial). Pass workOrderId in the body (the WO id from list_work_orders); each line names the produced skuId (which must be an output line on the WO), the quantity, and optionally a lotNumber and expirationDate. Writes BOTH ledger legs, ref = the WO number: a RECEIVE per produced SKU on the finished-goods ledger (read back with the finished-goods ledger tools) and a CONSUME per snapshotted input on the materials and finished-goods ledgers, then advances the WO to Partial (receiving against a Received WO reopens it). Conversion cost is read live by the costing engine, never rounded. The response is two-keyed: the created receipt with its produced lines nested under items, plus the updated WO header. A missing WO is rejected 404; rejected 409 invalid_transition while the WO is not open for receiving (Planning, Placed, Completed, or Cancelled).

Arguments: workOrderId* (string), receiptDate (string), items* (array)

delete_work_order 🔴

Permanently delete a work order and its items/recipe inputs/receipts, removing its projected ledger rows so on-hand recomputes. There is no in-use gate and no undo; cancel the order (status Cancelled via update_work_order) to keep the audit trail instead.

Arguments: id* (string)

delete_work_order_receipt 🔴

Delete a WO receipt and its lines, as if the production never happened. The id is the receipt id, not the WO id. Its CONSUME and RECEIVE ledger rows on both ledgers are removed (on-hand recomputes) and the WO status walks back to what the surviving receipts support. Rejected 409 invalid_transition while the WO is Completed: reopen it first via update_work_order.

Arguments: id* (string)

get_work_order

Get one work order as a composite: the header's fields at top level (no wrapper key), with the per-status timestamps grouped in statusDates and a financials block of the stored operational costs plus a derived goodsCost (the produced-goods value from the costing engine) and totalCost, its items nested (each output SKU embedded as { id, code, name }, the snapshotted recipe inputs nested under inputs with their material/component SKU embedded), and its receipts nested with each receipt's produced lines under its own items. goodsCost is recomputed on every read, so it can change as historical inventory data changes.

Arguments: id* (string)

get_work_order_receipt

Get one WO receipt: the receipt header's fields at top level with its produced lines nested under items, each line's SKU embedded as { id, code, name }. The id is the receipt id from list_work_order_receipts, not the WO id.

Arguments: id* (string)

list_work_order_items

List WO output line items, each with its produced SKU embedded as { id, code, name } and its snapshotted recipe inputs nested under inputs. Pass workOrderId for one order's items; omit it to query lines across every WO in one flat view, for questions like 'all orders producing SKU X' (skuId narrows by the produced SKU). Each row carries its workOrderId so you can fetch the order.

Arguments: workOrderId (string), skuId (string)

list_work_order_receipts

List WO production receipts (headers only) in receipt-date order. Pass workOrderId for one order's receipts; omit it to query receipts across every WO. Fetch a receipt's produced lines with get_work_order_receipt.

Arguments: workOrderId (string)

list_work_order_status_types

List the WO status vocabulary (Planning, Placed, Production, Partial, Received, Completed, Cancelled) as plain strings. Use before setting a status with update_work_order; note Partial is receipt-driven, not set manually.

Arguments: none

list_work_orders

List one page of work orders, newest first by default, each with its work-site location embedded as { id, name }, the per-status timestamps grouped in statusDates, and the stored operational costs in financials. Prefer the filters over pulling everything: status and workSiteId (from list_locations) each accept a CSV list (values OR within a filter, filters AND together), containsSkuId keeps only orders with an output line for that SKU, and every date filter is an inclusive From/To pair (YYYY-MM-DD): orderDateFrom/To, expectedDeliveryFrom/To, plus one pair per status key (e.g. placedFrom/placedTo, productionFrom/productionTo). search is a case-insensitive substring over the WO number, the work-site name, notes, and the output SKUs' codes and names. Results are paged: limit (default 50, max 200) and offset, with meta.total giving the filtered count; sort with sort (number, workSite, status, orderDate, expectedDeliveryDate, createdAt, updatedAt; default orderDate descending) and order. For a stable full walk while data changes, sort by createdAt ascending and page forward. Fetch one order's items, recipe inputs, and receipts with get_work_order.

Arguments: sort (string), order (string), limit (integer), offset (integer), status (string), workSiteId (string), containsSkuId (string), search (string), orderDateFrom (string), orderDateTo (string), expectedDeliveryFrom (string), expectedDeliveryTo (string), planningFrom (string), planningTo (string), placedFrom (string), placedTo (string), productionFrom (string), productionTo (string), partialFrom (string), partialTo (string), receivedFrom (string), receivedTo (string), completedFrom (string), completedTo (string), cancelledFrom (string), cancelledTo (string)

remove_work_order_item 🔴

Remove a line item from a WO, unwinding the commitment ledger events it had projected; its recipe inputs cascade away. The id is the item id, not the WO id. Rejected 409 invalid_transition once any receipt exists against the WO.

Arguments: id* (string)

update_work_order ✏️

Update a WO's header fields, including its status. Use list_work_order_status_types for valid status values; moving the status projects the commitment ledger events, and closing to Received settles the open commitments. orderDate is the user-owned order date and is freely settable (YYYY-MM-DD). Per-status lifecycle dates auto-stamp when the status changes and can also be set through the statusDates object (keys planning, placed, production, partial, received, completed, cancelled), which merges PER KEY: only the keys you send change, and null clears a date where allowed. partial is receipt-derived (the earliest receipt's date); dates must stay in lifecycle order or the call is rejected. workSiteLocationId is editable only while the WO is still in Planning (rejected 409 invalid_transition afterwards). The WO number can be renamed via woNumber (unique in the org; a collision is rejected 409) and the rename cascades to the ledger refs.

Arguments: id* (string), woNumber (string), workSiteLocationId (string), status (string), orderDate (string,null), expectedDeliveryDate (string,null), statusDates (object), inShippingCosts (number), outShippingCosts (number), otherCosts (number), notes (string,null)

update_work_order_item ✏️

Update a WO line item's quantity or conversionCost (up to 6 decimals). The id is the item id from list_work_order_items, not the WO id. A quantity change rescales the item's snapshotted BOM inputs and re-projects its commitment ledger events. The response is the line with its output SKU embedded and recipe inputs nested. Rejected 409 invalid_transition once any receipt exists against the WO: its line items are locked.

Arguments: id* (string), quantity (integer), conversionCost (number)

update_work_order_receipt ✏️

Update a WO receipt's receiptDate (YYYY-MM-DD; the only editable field). The id is the receipt id, not the WO id. Re-dating the receipt re-dates its CONSUME/RECEIVE ledger rows and the WO's receipt-derived partial date, but never changes the WO status. Edit lines with update_work_order_receipt_item.

Arguments: id* (string), receiptDate (string)

update_work_order_receipt_item ✏️

Edit a produced line in place (quantity, conversionCost, lotNumber, expirationDate). The id is the receipt-item id from get_work_order_receipt. Quantity propagates to the RECEIVE ledger row and rescales the line's input CONSUME rows; lot/expiration update the RECEIVE row; conversionCost (up to 6 decimals) needs no propagation, the costing engine reads it live. Rejected 409 invalid_transition while the WO is Completed: reopen it first via update_work_order.

Arguments: id* (string), quantity (integer), conversionCost (number), lotNumber (string,null), expirationDate (string,null)

Production Orders

add_production_order_item ✏️

Add an output SKU (+ quantity to produce, optional per-unit productionCost up to 6 decimals) to an existing production order; its exploded BOM is snapshotted at add time. Pass productionOrderId in the body (the order id from list_production_orders) and the skuId (from list_skus). Adding to an ordered order projects the line's commitment ledger events. The response is the line with its output SKU embedded and recipe inputs nested. A missing order is rejected 404; rejected 409 once a receipt exists against the order (invalid_transition, its items are locked) or the SKU is already a line on it (conflict).

Arguments: skuId* (string), quantity* (integer), productionCost (number,null), productionOrderId* (string)

bulk_import_production_orders ✏️

Import many production orders in ONE atomic call (admins/owners only): every row creates an order with an auto-generated number, its items, the recipe snapshots, and the projected commitment ledger events for its status, and the whole batch rolls back if any row fails. Use for initial data loads; for day-to-day creation prefer create_production_order. Returns the created ids in row order.

Arguments: rows* (array)

create_production_order ✏️

Create a production order (subcontracted / toll manufacturing). Items each name an output SKU (skuId from list_skus) and a quantity to produce; each SKU's BOM is exploded to raw materials and snapshotted at creation (later BOM edits do not change the order). productionCost is the optional per-unit production cost (up to 6 decimals): optional here but REQUIRED before that SKU can be received. locationId is the producing/receiving location (from list_locations). Opening in an ordered status projects the commitment ledger events. orderDate is the user-owned order date (YYYY-MM-DD, defaults to today). Pass prodNumber to set a custom number (unique per org; a clash is rejected 409), or omit it to auto-generate the next one. Per-status dates auto-stamp from the opening status; override via the optional statusDates object (keys planning, placed, production, inTransit, partial, received, completed, cancelled; YYYY-MM-DD). The 201 is the same composite get_production_order serves: header fields at top level with the location embedded, the timestamps grouped in statusDates, items nested (each output SKU embedded, its recipe inputs nested with their material embedded), and receipts empty. A missing location or output SKU is rejected 404.

Arguments: status* (string), locationId* (string), orderDate (string,null), expectedDeliveryDate (string,null), statusDates (object), inShippingCosts (number), outShippingCosts (number), otherCosts (number), notes (string,null), prodNumber (string), items* (array)

create_production_order_receipt ✏️

Record finished goods received back from the subcontractor against a production order (full or partial). Pass productionOrderId in the body (the order id from list_production_orders); each line names the produced skuId (which must be an output line on the order), the quantity, and optionally a lotNumber and expirationDate. Writes BOTH ledger legs, ref = the order number: a RECEIVE per produced SKU on the finished-goods ledger (read back with the finished-goods ledger tools) and a CONSUME per snapshotted material on the materials ledger, then advances the order to Partial (receiving against a Received order reopens it). The produced unit cost (materials + production cost) is derived on read by the costing engine, never rounded. The response is two-keyed: the created receipt with its produced lines nested under items, plus the updated order header. A missing order is rejected 404; rejected 422 if a produced SKU's per-unit production cost is still unset; rejected 409 invalid_transition while the order is not open for receiving (Planning, Placed, Completed, or Cancelled).

Arguments: productionOrderId* (string), receiptDate (string), items* (array)

delete_production_order 🔴

Permanently delete a production order and its items/recipe inputs/receipts, removing its projected ledger rows so on-hand recomputes. There is no in-use gate and no undo; cancel the order (status Cancelled via update_production_order) to keep the audit trail instead.

Arguments: id* (string)

delete_production_order_receipt 🔴

Delete a production order receipt and its lines, as if the production never happened. The id is the receipt id, not the order id. Its CONSUME and RECEIVE ledger rows on both ledgers are removed (on-hand recomputes) and the order status walks back to what the surviving receipts support. Rejected 409 invalid_transition while the order is Completed: reopen it first via update_production_order.

Arguments: id* (string)

get_production_order

Get one production order as a composite: the header's fields at top level (no wrapper key), with the per-status timestamps grouped in statusDates and a financials block of the stored operational costs plus a derived goodsCost (the produced-goods value from the costing engine, materials + production cost) and totalCost, its items nested (each output SKU embedded as { id, code, name }, the snapshotted recipe inputs nested under inputs with their material embedded), and its receipts nested with each receipt's produced lines under its own items. goodsCost is recomputed on every read, so it can change as historical inventory data changes.

Arguments: id* (string)

get_production_order_receipt

Get one production order receipt: the receipt header's fields at top level with its produced lines nested under items, each line's SKU embedded as { id, code, name }. The id is the receipt id from list_production_order_receipts, not the order id.

Arguments: id* (string)

list_production_order_items

List production order output line items, each with its produced SKU embedded as { id, code, name } and its snapshotted recipe inputs nested under inputs. Pass productionOrderId for one order's items; omit it to query lines across every order in one flat view, for questions like 'all orders producing SKU X' (skuId narrows by the produced SKU). Each row carries its productionOrderId so you can fetch the order.

Arguments: productionOrderId (string), skuId (string)

list_production_order_receipts

List production order receipts (headers only) in receipt-date order. Pass productionOrderId for one order's receipts; omit it to query receipts across every order. Fetch a receipt's produced lines with get_production_order_receipt.

Arguments: productionOrderId (string)

list_production_order_status_types

List the production order status vocabulary (Planning, Placed, Production, In Transit, Partial, Received, Completed, Cancelled) as plain strings. Use before setting a status with update_production_order; note Partial is receipt-driven, not set manually.

Arguments: none

list_production_orders

List one page of production orders, newest first by default, each with its location embedded as { id, name }, the per-status timestamps grouped in statusDates, and the stored operational costs in financials. Prefer the filters over pulling everything: status and locationId (from list_locations) each accept a CSV list (values OR within a filter, filters AND together), containsSkuId keeps only orders with an output line for that SKU, and every date filter is an inclusive From/To pair (YYYY-MM-DD): orderDateFrom/To, expectedDeliveryFrom/To, plus one pair per status key (e.g. placedFrom/placedTo, inTransitFrom/inTransitTo). search is a case-insensitive substring over the order number, the location name, notes, and the output SKUs' codes and names. Results are paged: limit (default 50, max 200) and offset, with meta.total giving the filtered count; sort with sort (number, location, status, orderDate, expectedDeliveryDate, createdAt, updatedAt; default orderDate descending) and order. For a stable full walk while data changes, sort by createdAt ascending and page forward. Fetch one order's items, recipe inputs, and receipts with get_production_order.

Arguments: sort (string), order (string), limit (integer), offset (integer), status (string), locationId (string), containsSkuId (string), search (string), orderDateFrom (string), orderDateTo (string), expectedDeliveryFrom (string), expectedDeliveryTo (string), planningFrom (string), planningTo (string), placedFrom (string), placedTo (string), productionFrom (string), productionTo (string), inTransitFrom (string), inTransitTo (string), partialFrom (string), partialTo (string), receivedFrom (string), receivedTo (string), completedFrom (string), completedTo (string), cancelledFrom (string), cancelledTo (string)

remove_production_order_item 🔴

Remove a line item from a production order, unwinding the commitment ledger events it had projected; its recipe inputs cascade away. The id is the item id, not the order id. Rejected 409 invalid_transition once any receipt exists against the order.

Arguments: id* (string)

update_production_order ✏️

Update a production order's header fields, including its status. Use list_production_order_status_types for valid status values; moving the status projects the commitment ledger events, and closing to Received settles the open commitments. orderDate is the user-owned order date and is freely settable (YYYY-MM-DD). Per-status lifecycle dates auto-stamp when the status changes and can also be set through the statusDates object (keys planning, placed, production, inTransit, partial, received, completed, cancelled), which merges PER KEY: only the keys you send change, and null clears a date where allowed. partial is receipt-derived (the earliest receipt's date); dates must stay in lifecycle order or the call is rejected. locationId is editable only while the order is still in Planning (rejected 409 invalid_transition afterwards). The order number can be renamed via prodNumber (unique in the org; a collision is rejected 409) and the rename cascades to the ledger refs.

Arguments: id* (string), prodNumber (string), locationId (string), status (string), orderDate (string,null), expectedDeliveryDate (string,null), statusDates (object), inShippingCosts (number), outShippingCosts (number), otherCosts (number), notes (string,null)

update_production_order_item ✏️

Update a production order line item's quantity or per-unit productionCost (up to 6 decimals). The id is the item id from list_production_order_items, not the order id. A quantity change rescales the item's snapshotted exploded-BOM inputs and re-projects its 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 is never blocked. The response is the line with its output SKU embedded and recipe inputs nested. A quantity change is rejected 409 invalid_transition once any receipt exists against the order.

Arguments: id* (string), quantity (integer), productionCost (number,null)

update_production_order_receipt ✏️

Update a production order receipt's receiptDate (YYYY-MM-DD; the only editable field). The id is the receipt id, not the order id. Re-dating the receipt re-dates its CONSUME/RECEIVE ledger rows on both ledgers and the order's receipt-derived partial date, but never changes the order status. Edit lines with update_production_order_receipt_item.

Arguments: id* (string), receiptDate (string)

update_production_order_receipt_item ✏️

Edit a produced line in place (quantity, productionCost, lotNumber, expirationDate). The id is the receipt-item id from get_production_order_receipt. Quantity propagates to the RECEIVE ledger row and rescales the line's materials CONSUME rows; lot/expiration update the RECEIVE row; productionCost (up to 6 decimals) needs no propagation, the costing engine reads it live. Rejected 409 invalid_transition while the order is Completed: reopen it first via update_production_order.

Arguments: id* (string), quantity (integer), productionCost (number), lotNumber (string,null), expirationDate (string,null)

Sales Orders

add_sales_order_item ✏️

Add a line item (SKU + quantity + price) to an existing sales order. Pass salesOrderId in the body (the SO id from list_sales_orders). Rejected with 409 if the SKU is already a line on the order.

Arguments: skuId* (string), quantity* (integer), unitPrice (number), salesOrderId* (string)

bulk_import_sales_orders ✏️

Create many SOs in one call from an array of rows. Use for initial data loads; the call is atomic (one bad row rolls back the whole batch). Each order's number is auto-generated.

Arguments: rows* (array)

create_sales_order ✏️

Create a sales order for a customer. Requires mustArriveBy (the must-arrive-by date, YYYY-MM-DD). Items can be included or added afterwards with add_sales_order_item. Pass soNumber to set a custom number (often the customer's order reference; unique per org, a clash is rejected), or omit it to auto-generate the next one.

Arguments: customerId* (string), channelId* (string), status* (string), fulfillmentStatus* (string), fulfillmentLocationId (string,null), brokerId (string,null), brokerFeePercent (number,null), brokerFees (number), fulfillmentCosts (number), freightCosts (number), promoCosts (number), otherCosts (number), mustArriveBy* (string), orderDate (string), statusDates (object), notes (string,null), qboId (string,null), soNumber (string), items* (array)

delete_sales_order 🔴

Permanently delete a sales order and its items/notes, removing its projected ledger rows so on-hand recomputes.

Arguments: id* (string)

get_sales_order

Get one sales order as a composite: the header (customer, channel, fulfillment location and broker embedded as references, per-status dates in statusDates), its line items nested under items (each with the sold SKU embedded), and a financials block with the stored costs plus derived revenue, inventory cost (COGS), gross profit, profit and margin. The inventory-cost figures are recomputed on every read, so they can change as historical inventory data changes.

Arguments: id* (string)

list_sales_order_fulfillment_status_types

List the org's valid sales order fulfillment status values.

Arguments: none

list_sales_order_items

List SO line items. Pass salesOrderId for one order's items; omit it to query lines across every SO in one flat view, for questions like 'all orders selling SKU X' (skuId narrows by the sold SKU). Each row carries its salesOrderId so you can fetch the order.

Arguments: salesOrderId (string), skuId (string)

list_sales_order_status_types

List the org's valid sales order status values.

Arguments: none

list_sales_orders

List one page of sales orders, each with the customer, channel, fulfillment location and broker embedded as { id, name } references, the per-status dates in statusDates, and the derived financials rollup. Narrow in SQL instead of pulling everything: status, customerId, channelId, fulfillmentLocationId and brokerId accept comma-separated values (OR within a filter, filters AND together), fulfillmentStatus is single-valued, containsSkuId keeps orders with a line item for that SKU, and the date filters are inclusive From/To pairs (orderDateFrom/orderDateTo, mustArriveByFrom/mustArriveByTo, plus one pair per statusDates key, e.g. placedFrom/placedTo). search matches the SO number, customer name, channel name, fulfillment location name, notes, and line-item SKU codes and names. Results are paged: limit (default 50, max 200) and offset, with meta.total giving the filtered count; sort with sort (number, customer, channel, fulfillmentLocation, status, orderDate, mustArriveBy, createdAt, updatedAt; default orderDate descending, newest first) and order. For a stable full walk while data changes, sort by createdAt ascending and page forward. Fetch one order's line items with get_sales_order. Filter by order value with revenueMin/revenueMax and sort with sort=revenue.

Arguments: sort (string), order (string), limit (integer), offset (integer), status (string), fulfillmentStatus (string), customerId (string), channelId (string), fulfillmentLocationId (string), brokerId (string), containsSkuId (string), revenueMin (number), revenueMax (number), search (string), orderDateFrom (string), orderDateTo (string), mustArriveByFrom (string), mustArriveByTo (string), planningFrom (string), planningTo (string), placedFrom (string), placedTo (string), inTransitFrom (string), inTransitTo (string), invoicedFrom (string), invoicedTo (string), paidFrom (string), paidTo (string), rejectedFrom (string), rejectedTo (string), inDisputeFrom (string), inDisputeTo (string), completedFrom (string), completedTo (string)

remove_sales_order_item 🔴

Remove a line item from a sales order. The id is the item id, not the SO id.

Arguments: id* (string)

update_sales_order ✏️

Update an SO's header fields, including order/fulfillment status. Use list_sales_order_status_types and list_sales_order_fulfillment_status_types for valid values. orderDate and mustArriveBy are user-owned dates, freely settable (YYYY-MM-DD). Per-status lifecycle dates travel in the statusDates object keyed by status name (planning, placed, inTransit, invoiced, paid, completed, inDispute, rejected); they auto-stamp on a status change and merge per key when set directly, and must stay in lifecycle order or the call is rejected. customerId, channelId and fulfillmentLocationId can only be changed while the order is in Planning (rejected with 409 afterwards). The SO number can be renamed via soNumber (unique per org; a collision is rejected). A status change reconciles the finished-goods ledger to the new status (forward transitions add DEMAND/ALLOCATE/CONSUME, backward or terminal ones reverse them), and a pure date edit re-dates the active ledger rows.

Arguments: id* (string), soNumber (string,null), customerId (string), channelId (string), fulfillmentLocationId (string,null), status (string), fulfillmentStatus (string), orderDate (string,null), mustArriveBy (string), statusDates (object), brokerId (string,null), brokerFeePercent (number,null), brokerFees (number), fulfillmentCosts (number), freightCosts (number), promoCosts (number), otherCosts (number), notes (string,null), qboId (string,null)

update_sales_order_item ✏️

Update an SO line item. The id is the item id, not the SO id.

Arguments: id* (string), skuId (string), quantity (integer), unitPrice (number)

Sales Receipts

add_sales_receipt_item ✏️

Add a line item (SKU + quantity in cases + optional unit price) to an existing receipt. Pass salesReceiptId in the body (the receipt id from list_sales_receipts). Rejected with 409 if the SKU is already a line on the receipt.

Arguments: skuId* (string), quantity* (number), unitPrice (number), salesReceiptId* (string)

create_sales_receipt ✏️

Create a direct-to-consumer sales receipt. channelId (the sales channel it ran through) is optional, but acquisitionSourceId (how the customer was acquired; see list_acquisition_sources) is required. The customer is typed text, not a linked entity: an optional customerName plus an optional customerRef (the customer's identifier in the source system). Set isSubscription true to mark a subscription sale, and isRecurring true to mark a repeat customer; both are separate axes and default to false. Counts in cases and allows decimals (e.g. quantity 0.5 of a case). Each line carries a SKU, a quantity, and an optional user-entered unit price; unit cost is derived from inventory, not set here. Pass srNumber to set a custom number (unique per org, a clash is rejected), or omit it to auto-generate the next one. Status defaults to Placed.

Arguments: customerName (string,null), customerRef (string,null), isSubscription (boolean), isRecurring (boolean), channelId (string,null), acquisitionSourceId* (string), status (string), locationId (string,null), fulfillmentCosts (number), shippingCosts (number), merchantCosts (number), otherCosts (number), orderDate (string), statusDates (object), notes (string,null), srNumber (string), items* (array)

delete_sales_receipt 🔴

Permanently delete a sales receipt and its line items, removing its ledger rows.

Arguments: id* (string)

get_connected_sales_receipt

Get one connected sales receipt by the marketplace order id from list_connected_sales_receipts (Shopify or Amazon; the id's shape routes the read). Returns the full view model from the materialized rows: external identity and an admin deep link, the derived lifecycle (statusDates plus the marketplace's raw sourceStatus and a completedByFallback flag), customer identity with the retention join key (customer.ref; Amazon anonymizes buyers, so its customer block is null for now), destination and carrier, the buyer-side money picture (subtotal, discounts with codes, shipping charged, tax, total paid, a reconciliation flag, refunds, processor fees when present), the source's line items exactly as sold (a SKU can repeat across lines when bundles explode), and refund events with restock linkage. When every line is mapped to a Pharus SKU, financials carries the engine-computed inventory cost, gross profit, and margin (read-time replay, never stored); it is null while any line is unmapped, so use set_connected_sku_mapping to complete the mapping first. 404 when the id is unknown, the receipt predates the org's connected go-live date (not served anywhere), or the org has no connected feed.

Arguments: externalId* (string)

get_connected_sync_status

Check how fresh a connected feed's data is before trusting it: last successful sync, last attempt, the failure message when the latest run errored, and whether the initial history copy is complete (with the oldest stored order day) or still walking (with the day reached). Pass source=shopify (default) or amazon. Pairs with the dataAsOf stamp on individual receipts from get_connected_sales_receipt.

Arguments: source (string)

get_sales_receipt

Get one sales receipt as a composite: the header (channel, acquisition source and location embedded as references, per-status dates in statusDates), its line items nested under items (each with the sold SKU embedded), and a financials block with the stored costs plus derived revenue, inventory cost (COGS), gross profit, profit and margin. The inventory-cost figures are recomputed on every read, so they can change as historical inventory data changes.

Arguments: id* (string)

list_connected_location_mappings

The location-mapping worksheet for connected sales receipts: each feed's fulfillment locations referenced on or after the connected go-live date, by their stable key (Shopify location id, Amazon channel code) with display names, and the Pharus location each maps to (null = unmapped). Pass source=shopify (the default) or source=amazon. Map rows with set_connected_location_mapping; mapped locations appear on connected receipts and make the fulfillmentLocation filter match them.

Arguments: source (string)

list_connected_mapping_alerts

Summarize what is blocked from inventory by missing mappings: per feed, how many marketplace listings and fulfillment locations are unmapped and how many stored receipt lines or receipts they hold out of the ledger (scoped to the org's go-live window). Use it to decide whether mapping work is pending before trusting inventory numbers; fix the entries via set_connected_sku_mapping and set_connected_location_mapping, which backfill the ledger immediately.

Arguments: none

list_connected_receipt_locations

The distinct fulfillment-location names on the org's connected sales receipts (Shopify assigned locations, Amazon channel labels like Amazon FBA), sorted. These are the valid values for list_connected_sales_receipts' fulfillmentLocation filter; pass them verbatim. Manual receipts filter by location id via list_locations instead.

Arguments: none

list_connected_sales_receipts

List the org's connected sales receipts: marketplace orders (Shopify and Amazon Seller Central, whichever feeds the org has connected, each row's source naming its feed) served from the rows the sync engine keeps in Pharus, newest first. These are NOT the manually entered receipts of list_sales_receipts, and they are read-only here (the sync owns the writes; mapped lines post to inventory). Receipts before the org's connected go-live date are stored but not served anywhere, so display and inventory always agree. Filter by status, order date, source, fulfillmentLocation (values from list_connected_receipt_locations), and netRevenueMin/netRevenueMax; search matches order numbers, buyer names (Shopify), and line SKUs/titles. Rows are addressed by the marketplace's own order id (the id field); pass it to get_connected_sales_receipt for the full detail. search matches the order number and buyer name; page with limit (default 50, max 200) and offset. When configured is false the org has no data-plane connection and this surface does not apply; use list_sales_receipts instead.

Arguments: search (string), status (string), source (string), fulfillmentLocation (string), netRevenueMin (number), netRevenueMax (number), sort (string), order (string), limit (integer), offset (integer), orderDateFrom (string), orderDateTo (string)

list_connected_sku_mappings

The listing-to-SKU worksheet for connected sales receipts: one page of the distinct marketplace listings found on the org's connected order lines on or after the connected go-live date (retired pre-go-live products are not pending work) for one feed (pass source=shopify, the default, or source=amazon), each with the listing's own sku code and titles, its sold volume, and the Pharus SKU it currently maps to (null = unmapped). Listings are keyed by their store's stable identity, the variant id on Shopify or the seller SKU on Amazon (both travel in variantId). Pass mapped=false to see exactly what still needs mapping instead of listing everything, search to find one listing by its sku or title, and page with limit/offset (meta.total counts the filtered set; sort: unitsSold default, lineCount, listingSku, listingTitle). Then map rows with set_connected_sku_mapping. Mapped SKUs appear on connected receipt detail lines.

Arguments: source (string), search (string), mapped (string), sort (string), order (string), limit (integer), offset (integer)

list_sales_receipt_items

List MANUAL receipt line items. Pass salesReceiptId for one receipt's items; omit it to query lines across every manual receipt in one flat view, for questions like 'all manually entered sales of SKU X' (skuId narrows by the sold SKU). Synced marketplace lines are excluded, like the manual header list: read those via list_connected_sales_receipts and get_connected_sales_receipt. Each row carries its salesReceiptId so you can fetch the receipt.

Arguments: salesReceiptId (string), skuId (string)

list_sales_receipts

List one page of sales receipts, each with the channel, acquisition source and location embedded as { id, name } references, the per-status dates in statusDates, and the derived financials rollup. Narrow in SQL instead of pulling everything: status, channelId, acquisitionSourceId and locationId accept comma-separated values (OR within a filter, filters AND together), containsSkuId keeps receipts with a line item for that SKU, and the date filters are inclusive From/To pairs (orderDateFrom/orderDateTo plus one pair per statusDates key, e.g. placedFrom/placedTo). search matches 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. Results are paged: limit (default 50, max 200) and offset, with meta.total giving the filtered count; sort with sort (number, customerName, channel, acquisitionSource, location, status, orderDate, createdAt, updatedAt; default orderDate descending, newest first) and order. For a stable full walk while data changes, sort by createdAt ascending and page forward. Fetch one receipt's line items with get_sales_receipt.

Arguments: sort (string), order (string), limit (integer), offset (integer), status (string), channelId (string), acquisitionSourceId (string), locationId (string), containsSkuId (string), netRevenueMin (number), netRevenueMax (number), search (string), orderDateFrom (string), orderDateTo (string), placedFrom (string), placedTo (string), paidFrom (string), paidTo (string), shippedFrom (string), shippedTo (string), completedFrom (string), completedTo (string), cancelledFrom (string), cancelledTo (string)

remove_sales_receipt_item 🔴

Remove a line item from a receipt. The id is the item id, not the receipt id.

Arguments: id* (string)

set_connected_location_mapping ✏️

Map one connected fulfillment location to a Pharus location: pass the externalKey from list_connected_location_mappings (with the same source) and a locationId from list_locations, or locationId null to clear. Upserts: re-mapping replaces the previous mapping.

Arguments: source (string), externalKey* (string), externalName (string,null), locationId* (string,null)

set_connected_sku_mapping ✏️

Map one marketplace listing to a Pharus SKU: pass the variantId from list_connected_sku_mappings (with the same source, shopify by default or amazon) and the skuId from list_skus (pass skuId null to clear a mapping). Upserts: re-mapping replaces the previous mapping. Optionally pass listingSku and listingTitle as display snapshots. The mapping keys on the store's stable listing identity: the variant id on Shopify, the seller SKU on Amazon (both travel in variantId).

Arguments: source (string), variantId* (string), listingSku (string,null), listingTitle (string,null), skuId* (string,null)

update_sales_receipt ✏️

Update a receipt's header fields, including status (Placed, Paid, Shipped, Completed, Cancelled). Status moves are validated: cancel is only allowed before Shipped. orderDate is the user-owned order date (YYYY-MM-DD). Per-status lifecycle dates travel in the statusDates object keyed by status name (placed, paid, shipped, completed, cancelled); they auto-stamp on a status change and merge per key when set directly, and must stay in lifecycle order. channelId, acquisitionSourceId and locationId stay editable throughout (a receipt has no draft stage). The receipt number can be renamed via srNumber (unique per org).

Arguments: id* (string), srNumber (string,null), customerName (string,null), customerRef (string,null), isSubscription (boolean), isRecurring (boolean), channelId (string,null), acquisitionSourceId (string), status (string), locationId (string,null), orderDate (string,null), statusDates (object), fulfillmentCosts (number), shippingCosts (number), merchantCosts (number), otherCosts (number), notes (string,null)

update_sales_receipt_item ✏️

Update a receipt line item. The id is the item id, not the receipt id.

Arguments: id* (string), skuId (string), quantity (number), unitPrice (number)

Sales Returns

add_sales_return_item ✏️

Add a line item to an existing return. Pass salesReturnId in the body (the return id from list_sales_returns) plus a receipt line (salesReceiptItemId, must belong to the return's receipt; see list_sales_receipt_items) and a quantity in cases. One line per receipt line per return; the over-return cap applies.

Arguments: salesReceiptItemId* (string), quantity* (number), salesReturnId* (string)

create_sales_return ✏️

Create a sales return against a COMPLETED sales receipt (salesReceiptId; the create is rejected while the receipt is in any other status). Each line targets one of that receipt's line items (salesReceiptItemId, see list_sales_receipt_items) with a quantity in cases (decimals allowed); across all live returns the cumulative returned quantity per receipt line can never exceed what it received. Returns are restock-only: when the return reaches Completed one RECEIVE per line posts into finished-goods inventory at locationId, valued on read at the cost the receipt line's shipment carried (never locked). Pass returnNumber to set a custom number (unique per org, a clash is rejected), or omit it to auto-generate the next one. Status defaults to Planning.

Arguments: status (string), locationId (string,null), returnDate (string), statusDates (object), notes (string,null), salesReceiptId* (string), returnNumber (string), items* (array)

delete_sales_return 🔴

Permanently delete a sales return and its line items, removing its ledger rows.

Arguments: id* (string)

get_sales_return

Get one sales return as a composite: the header (the originating receipt embedded as { id, number }, the restock location as a reference, per-status dates in statusDates) and its line items nested under items, each carrying the receipt line it returns against (salesReceiptItemId) and the SKU embedded. A return stores no money; the restock value is derived by the costing engine on inventory reads.

Arguments: id* (string)

list_sales_return_items

List return line items, each carrying the SKU read through its receipt line. Pass salesReturnId for one return's items; omit it to query lines across every return in one flat view. Each row carries its salesReturnId so you can fetch the return.

Arguments: salesReturnId (string)

list_sales_returns

List one page of sales returns, each with the originating receipt embedded as { id, number }, the restock location as an { id, name } reference, and the per-status dates in statusDates. Narrow in SQL instead of pulling everything: status, salesReceiptId and locationId accept comma-separated values (OR within a filter, filters AND together), containsSkuId keeps returns with a line whose receipt line references that SKU, and the date filters are inclusive From/To pairs (returnDateFrom/returnDateTo plus one pair per statusDates key, e.g. placedFrom/placedTo). search matches the return number, the originating receipt's number, customer name and external ref, notes, and line-item SKU codes and names. Results are paged: limit (default 50, max 200) and offset, with meta.total giving the filtered count; sort with sort (number, receiptNumber, customerName, restockLocation, status, returnDate, createdAt, updatedAt; default returnDate descending, newest first) and order. For a stable full walk while data changes, sort by createdAt ascending and page forward. Fetch one return's line items with get_sales_return.

Arguments: sort (string), order (string), limit (integer), offset (integer), status (string), salesReceiptId (string), locationId (string), containsSkuId (string), search (string), returnDateFrom (string), returnDateTo (string), planningFrom (string), planningTo (string), placedFrom (string), placedTo (string), inTransitFrom (string), inTransitTo (string), completedFrom (string), completedTo (string), cancelledFrom (string), cancelledTo (string)

remove_sales_return_item 🔴

Remove a line item from a return. The id is the item id, not the return id.

Arguments: id* (string)

update_sales_return ✏️

Update a return's header fields, including status (Planning, Placed, In Transit, Completed, Cancelled). Status moves are validated; reaching Completed posts the restock RECEIVE rows and leaving it (or cancelling) reverses them. Un-cancelling re-checks the over-return cap. returnDate is the user-owned business date (YYYY-MM-DD). Per-status lifecycle dates travel in the statusDates object keyed by status name (planning, placed, inTransit, completed, cancelled); they auto-stamp on a status change and merge per key when set directly, and must stay in lifecycle order. The originating receipt is fixed for life. The return number can be renamed via returnNumber (unique per org).

Arguments: id* (string), returnNumber (string), status (string), locationId (string,null), returnDate (string,null), statusDates (object), notes (string,null)

update_sales_return_item ✏️

Update a return line's quantity (the over-return cap applies; the receipt-line link is fixed — re-pointing is a remove + add). The id is the item id, not the return id.

Arguments: id* (string), quantity (number)

Order Channels

create_order_channel ✏️

Create an order channel (where an order originates, e.g. Shopify, wholesale, direct). The returned id feeds the optional channelId on create_sales_order and create_sales_receipt. Check list_order_channels first and reuse an existing channel rather than creating a near-duplicate; a name already in use is rejected with 409 conflict. Admins/owners only.

Arguments: name* (string)

delete_order_channel 🔴

Delete an order channel. Attempt the delete directly: while sales orders or sales receipts reference the channel it fails with 409 in_use and the error details carry the blocking salesOrderCount and salesReceiptCount. Deactivate it instead (update_order_channel with isActive=false) to retire it while keeping history.

Arguments: id* (string)

list_order_channels

List the org's order channels in sort order, active and deactivated alike (per the isActive flag on each row). Use it to resolve the channelId for create_sales_order or create_sales_receipt, or to translate a channel id on an order into its name. Pass search to match names instead of listing everything.

Arguments: search (string)

update_order_channel ✏️

Rename, reorder, or toggle an order channel's active flag: isActive=false is the soft-delete path and isActive=true reactivates it (there is no separate reactivate tool). Admins/owners only. Rejected with 409 if the new name collides with another channel.

Arguments: id* (string), name (string), sortOrder (integer), isActive (boolean)

Acquisition Sources

create_acquisition_source ✏️

Create an acquisition source (how a DTC sale was acquired, e.g. a TikTok ad or a Meta ad). The returned id feeds the REQUIRED acquisitionSourceId on create_sales_receipt. Check list_acquisition_sources first and reuse an existing source rather than creating a near-duplicate; a name already in use is rejected with 409 conflict. Admins/owners only.

Arguments: name* (string)

delete_acquisition_source 🔴

Delete an acquisition source. Attempt the delete directly: while sales receipts reference the source it fails with 409 in_use and the error details carry the blocking salesReceiptCount. Deactivate it instead (update_acquisition_source with isActive=false) to retire it while keeping history.

Arguments: id* (string)

list_acquisition_sources

List the org's acquisition sources in sort order, active and deactivated alike (per the isActive flag on each row). Use it to resolve the acquisitionSourceId required by create_sales_receipt, or to translate a source id on a receipt into its name. Pass search to match names instead of listing everything.

Arguments: search (string)

update_acquisition_source ✏️

Rename, reorder, or toggle an acquisition source's active flag: isActive=false is the soft-delete path and isActive=true reactivates it (there is no separate reactivate tool). Admins/owners only. Rejected with 409 if the new name collides with another source.

Arguments: id* (string), name (string), sortOrder (integer), isActive (boolean)

Documents

delete_document 🔴

Permanently delete a document and its stored file.

Arguments: id* (string)

get_document

Get one document's metadata plus a freshly minted, short-lived (about 15 minutes) signed URL for downloading its file. The URL is generated per call and never stored, so fetch it again if it expires. The document carries its parent as an entity object (type, id, and the parent's order number under number). Find the id via list_documents.

Arguments: id* (string)

list_documents

List the documents attached to one entity, newest first. Both query args are required: entityType (one of sales_order, purchase_order, work_order) and entityId (the parent's id, from list_purchase_orders / list_sales_orders / list_work_orders). There is no list-everything mode; scope every call to a single parent. Returns metadata rows only. Each row carries its parent as an entity object (type, id, and the parent's order number as name). Use get_document to obtain a download URL for one.

Arguments: entityType* (string), entityId* (string)

Timeline Notes

add_timeline_note ✏️

Append a free-text note to one entity's timeline (an audit trail of human/agent commentary, e.g. "customer called to confirm delivery"). Name the parent with entityType and entityId: sales_order (id from list_sales_orders), purchase_order (list_purchase_orders), work_order (list_work_orders), production_order (list_production_orders), sales_receipt (list_sales_receipts), sales_return (list_sales_returns), or transfer (list_transfers). The note text goes in note. The server stamps entityStatus, a snapshot of the parent's status at write time, and createdBy, the author from the session user; neither is accepted as input and neither updates afterward. Notes are permanent history: there is no update, only add and delete_timeline_note. An unknown parent is rejected with a 404.

Arguments: entityType* (string), entityId* (string), note* (string)

delete_timeline_note 🔴

Permanently delete a timeline note. The id is the note id (from list_timeline_notes), not the parent entity's id.

Arguments: id* (string)

list_timeline_notes

List the timeline notes attached to one entity, newest first. Both query args are required: entityType (one of sales_order, purchase_order, work_order, production_order, sales_receipt, sales_return, transfer) and entityId (the parent's id from the matching list tool). There is no list-everything mode; scope every call to a single parent. Each row carries its parent as an entity object (type, id, and the parent's document number under number), entityStatus, the snapshot of the parent's status when the note was written (it never updates afterward), and createdBy, the author as { id, name } (null on notes migrated from before authorship existed).

Arguments: entityType* (string), entityId* (string)

Reporting

get_channel_segment_contribution

Contribution margin by channel-segment pair: completed sales orders and sales receipts grouped by (order channel, customer segment) over a trailing window, each row carrying orders, revenue, operational costs, COGS, contribution margin and margin ratio for the current window and the equal-length prior window. A sales order's row pairs its channel with its customer's segment read at report time; a null segment means the customer is unsegmented yet, a data-quality signal. Sales receipts carry only an inline customer and can never have a segment: their rows use the fixed DTC segment dimension paired with the receipt's channel (null channel means the receipt has none). Rows appear only where a window had activity; idle channels and segments are not listed. Use this to answer which channel-segment combinations make money and how margins are trending; do not rebuild it by listing orders and summing client-side, the COGS here comes from the costing engine and cannot be derived from list reads. Pass window=30d, 45d or 90d (default 45d); all windows are trailing and compare against the equal-length prior span. A nonzero missingFreightCount means some completed orders have no freight recorded and the margin reads high; sales returns are not netted out.

Arguments: window (string)

get_committed_outbound

How much cash is committed to inbound goods, and how much of it lands within a horizon: open purchase orders (Placed, In Transit, Partial), work orders (Placed, Production, Partial) and production orders (those plus In Transit). Committed means unreceived remainders for purchase orders (expected total minus received value, clamped at zero) and unconsumed input quantities valued at the engine's current unit cost plus stored fees for work/production orders. Pass horizonDays (default 14, bounded 1..90) to change the arriving-soon window; a null expected delivery date never counts as arriving soon, but orders already past their expected delivery date count as arriving soon. Do not rebuild this from list reads, the engine-derived unit costs and remainders are not on any list.

Arguments: horizonDays (integer)