Tool Reference
The Pharus MCP server exposes 220 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). pickAndPack ({ perOrder, perItem }) is what the location charges to pick and pack an order, the 3PL's rate card: a fee per order plus a fee per item, where a receipt's items are what it takes from stock (each line's product units, a kit's components). It feeds the serving costs of every sales receipt shipped from there.
Arguments: name* (string), address (object,null), notes (string,null), customFields (array), pickAndPack (object,null)
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). pickAndPack ({ perOrder, perItem }) sets the location's pick and pack rate, a fee per order plus a fee per item, null clears it; every sales receipt shipped from the location re-prices its serving costs on the next read. Set it to zero for a location that charges nothing (e.g. Amazon FBA, whose fulfillment is in Amazon's fees) so its receipts stop reading as estimates.
Arguments: id* (string), name (string), address (object,null), notes (string,null), customFields (array), pickAndPack (object,null), 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)
get_vendor_parts
Get a vendor's parts: every item bought from the vendor on any of its purchase orders, plus any item given a code without being bought yet, each with the vendor's code (null when it has none) and the latest order date. The fastest way to find which items still need a vendor part number before calling set_vendor_part_number. The id comes from list_vendors.
Arguments: id* (string)
list_vendor_part_numbers
List vendor part numbers: what each vendor calls a material or product bought from it, one row per vendor and item. Pass vendorId (from list_vendors) to get one vendor's codes, or materialId/productId (from list_materials / list_products) to see what every vendor calls one item; pass search to find a code or item rather than listing everything. Paged with limit (default 50, max 200) and offset. To see which bought items still lack a code, use get_vendor_parts instead.
Arguments: sort (string), order (string), limit (integer), offset (integer), vendorId (string), materialId (string), productId (string), search (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)
set_vendor_part_number ✏️
Set what one vendor calls one item: pass vendorId (from list_vendors), exactly one of materialId (list_materials) or productId (list_products), and vendorPartNumber. Upserts, so setting it again replaces the code; pass null to remove it. To load a vendor's price list, call get_vendor_parts first to see what was bought from them, then set each code. Rejected with 409 when the organization does not keep vendor part numbers (an admin turns them on in Settings, Vendors). The purchase order PDF prints the code on each line for that PO's vendor.
Arguments: vendorId* (string), materialId (string), productId (string), vendorPartNumber* (string,null)
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), description (string,null), 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), description (string,null), 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), isActive (boolean)
Products
bulk_import_products ✏️
Create many products 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 category, 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. distributor codes) and the same logistics fields as create_product (gtin, dimensions, weightLb). At most 1000 rows per call.
Arguments: rows* (array)
create_product ✏️
Create a product (finished good). Products are produced by work orders, sold on sales orders, and tracked in the finished-goods inventory ledger. type is the closed system-owned kind (base, case, or kit; defaults to base), distinct from categoryId, the org-owned organizational category from list_product_categories. A case may declare its base: pass baseProductId (a base-typed product) and baseFactor (integer, how many bases are inside one case) together. Only a case carries the pair. customFields accepts opaque ordered key/value string pairs the platform never interprets (e.g. distributor codes). Logistics fields are optional: gtin (a UPC-A or GTIN-8/13/14, digits only; a wrong length or check digit is rejected with 422, leading zeros are kept), dimensions ({ lengthIn, widthIn, heightIn } in inches) and weightLb (packed shipping weight in pounds). The code must be unique in the org (409 conflict otherwise).
Arguments: code* (string), name* (string), type (string), categoryId* (string), baseProductId (string,null), baseFactor (integer,null), description (string,null), gtin (string,null), dimensions (object,null), weightLb (number,null), notes (string,null), customFields (array)
create_product_category ✏️
Create a product category (org-defined category used to classify products). The returned id feeds the REQUIRED categoryId on create_product. Check list_product_categories first and reuse an existing category rather than creating a near-duplicate; a name already in use is rejected with 409 conflict.
Arguments: name* (string)
delete_product 🔴
Permanently delete a product. 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_product with isActive=false) to keep the history.
Arguments: id* (string)
delete_product_category 🔴
Delete a product category. Attempt the delete directly: while products still use it the delete fails with 409 in_use and the error details carry the blocking productCount. Archive it instead (update_product_category with isActive=false) to retire a category without deleting it.
Arguments: id* (string)
get_product
Get full detail for one product by id, including the closed type (base, case, kit), the case-to-base relationship (base as an { id, code, name } reference plus the integer baseFactor, null on non-cases), the logistics fields (gtin, dimensions in inches, and weightLb) and custom fields, with the product category embedded as a category object with its id and name. The id comes from list_products.
Arguments: id* (string)
list_product_categories
List the org's product categories, active and archived alike (per the isActive flag on each row). Use it to resolve the categoryId for create_product or update_product, and check it before create_product_category to avoid duplicates. Pass search to match names instead of listing everything.
Arguments: search (string)
list_products
List one page of products. Pass active, a CSV type list (base, case, kit), a CSV category id list, a baseUnitsMin/Max range (1 for a base, the factor for a case; kits never match), code (one product's exact code; use it to find a product by code instead of listing and filtering), or a free-text search (matches product code, name, category name, or notes). Pass customFieldKey and/or customFieldValue to find a product by a custom-field code (case-insensitive; combine both to match the same pair). Each row carries the closed type (base, case, kit) and embeds the product category as a category 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, category, baseUnits, 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), category (string), baseUnitsMin (number), baseUnitsMax (number), code (string), search (string), customFieldKey (string), customFieldValue (string)
update_product ✏️
Update a product's fields (partial update, only send what changes), including its type (base, case, or kit) and a case's baseProductId/baseFactor pair (send both null to clear). Deactivating a base fails with 409 in_use while cases or kit BOMs point at it. dimensions is written as a group: send every field you want kept (an omitted one clears), or null to clear the group; an invalid gtin is rejected with 422. Set isActive false to archive or true to reactivate. Sending customFields replaces the whole set of opaque key/value pairs (e.g. distributor codes). Changing the code to one already in use returns 409 conflict.
Arguments: id* (string), code (string), name (string), type (string), categoryId (string), baseProductId (string,null), baseFactor (integer,null), description (string,null), gtin (string,null), dimensions (object,null), weightLb (number,null), notes (string,null), customFields (array), isActive (boolean)
update_product_category ✏️
Update a product category'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 category already uses returns 409 conflict. The id comes from list_product_categories.
Arguments: id* (string), name (string), isActive (boolean)
Bills of Materials
delete_product_bom 🔴
Delete a product's BOM, removing all its composition lines. The id is the output product id. Existing work orders are unaffected (their inputs are snapshotted at creation). Returns 404 when the product has no BOM.
Arguments: id* (string)
get_product_bom
Get one product's bill of materials: its input lines, each an input object with kind ('material' or 'product'), id, code, and name, plus a quantity. The id argument is the output product id (from list_products). Returns 404 when the product has no BOM defined.
Arguments: id* (string)
list_boms
List one page of the output products that have a BOM defined, each carrying the output product as a product object (id, code, name) and the count of its input items. Filters AND together: containsMaterialId and containsProductId are CSVs of ids (each OR-ed within itself) that keep only the BOMs whose input lines include one of those materials / input products ('which recipes use material X'); search substring-matches the output product's code or name, and also the input lines' content (the codes and names of input materials and input products). 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_product_bom for one product's full composition.
Arguments: sort (string), order (string), limit (integer), offset (integer), containsMaterialId (string), containsProductId (string), search (string)
set_product_bom ✏️
Create or replace a product's bill of materials in one idempotent call: the items you send become the product's entire composition (a full replace, not a partial merge). The id is the output product id (from list_products). Each item sets exactly one of materialId (from list_materials) or inputProductId (from list_products) with a positive quantity, and at least one item is required (to clear a BOM, use delete_product_bom). Returns 404 if the output product or any referenced material or input product 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_contact ✏️
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)
unlink_contact 🔴
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_contact_link ✏️
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)
get_customer_item_numbers
Get a customer's item numbers: every product with a code at the customer or at its parent, each with the customer's own code, the parent's code, and source saying which applies (customer or parent). The fastest way to see what a distribution center inherits from its purchaser and what it overrides before calling set_customer_item_number. The id comes from list_customers.
Arguments: id* (string)
list_customer_item_numbers
List customer item numbers: what each customer calls a product sold to it (Costco's item 2049444), one row per customer and product. Pass customerId (from list_customers) to get one customer's codes, productId (from list_products) to see what every customer calls one product, or customerItemNumber to find the product behind a code; do not list everything and filter. Codes usually sit on the purchaser (the parent customer) and its distribution centers inherit them: to match an incoming order line for a distribution center, pass its customerId, the line's customerItemNumber and includeParent=true, which resolves the center's own code first and then its parent's in one call. Paged with limit (default 50, max 200) and offset. For one customer's full card with own and inherited codes side by side, use get_customer_item_numbers instead.
Arguments: sort (string), order (string), limit (integer), offset (integer), customerId (string), productId (string), customerItemNumber (string), includeParent (string), search (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)
set_customer_item_number ✏️
Set what one customer calls one product: pass customerId (from list_customers), productId (from list_products) and customerItemNumber. Upserts, so setting it again replaces the code; pass null to remove it. Set the code on the purchaser (the parent customer) so every distribution center under it uses it, and on one distribution center only when it calls the product something else; removing a center's own code falls back to its parent's. Products only, never materials. Rejected with 409 when the organization does not keep customer item numbers (an admin turns them on in Settings, Customers), and with 409 when the code already belongs to another product at this customer, its parent or one of its distribution centers (one code, one product). The sales order packing slip prints the code that applies on each line.
Arguments: customerId* (string), productId* (string), customerItemNumber* (string,null)
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), 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), 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), 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. Pass eventDate (YYYY-MM-DD, default today) to post the correction as of its real business date, e.g. a month-end count entered days later. On a positive adjustment unitCost optionally declares the cost of the added stock (stamped on the entry and used by costing; without it the stock takes the current pool cost), and lotNumber/expirationDate optionally declare its lot identity, seeded onto the cost layer like a receipt's. Negative adjustments take neither. 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), unitCost (number), lotNumber (string,null), expirationDate (string,null)
dispose_materials_inventory 🔴
Write off material stock (damage, expiry, loss). This permanently removes quantity from stock via a DISPOSE ledger entry. Pass eventDate (YYYY-MM-DD, default today) to post the write-off as of its real business date.
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. The same flag adds unbackedQuantity (units the cost layers hold beyond the ledger's on-hand: stock consumed before it was received, so a later receipt left value with no stock behind it; 0 on a healthy row), unbackedValue (those units at the layers' average cost; an ESTIMATE, always report it as one) and firstShortfallDate (the business day the ledger first went short there). A row with unbackedValue > 0 is overvalued: open its ledger filtered by item and location and look for the first negative balance. 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). Stock events: RECEIVE, CONSUME, ADJUST, DISPOSE. Commitment events ride the same ledger: ORDER (purchase orders, the on-order side), SETTLE_* close-outs, and REVERSE_* rows that net an earlier event to zero. All are legal eventType filter values. 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. Each stock row carries balance, the on-hand at its location right after the row over the item's full history in costing order (event date, then insertion order), the same figure under any paging or sort. A negative balance is a consumption that drew stock the location did not hold yet: that row is where value gets stranded (#1263), so to audit a location filter by item and location, sort eventDate ascending, and look for the first negative balance. Planning rows carry null. Each stock row also carries paddedQuantity and estimated (#1424): units of the row no cost layer covered (a consumption beyond what the location held, or the remainder a transfer or return could not vouch for) are priced at an ESTIMATE, the item's average receipt cost over the org's estimate window ending on the row's date, never at zero; paddedQuantity is how many, and estimated is true when the row's unitCost rests on padding, its own or that of the stock it drew or was built from. Always report an estimated unitCost as an estimate; it re-derives, and clears, once the missing receipt is entered. Each row also names the document that wrote it ({ kind, id }: purchase_order, sales_order, sales_receipt, sales_return, work_order, production_order, transfer or conversion; null on manual rows), so the row that went short leads straight to the document whose date, quantity or location needs fixing.
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)
update_materials_adjustment ✏️
Correct a manual materials adjustment in place instead of posting a compensating one: fix a mistyped quantity, move a count to its real business date, or change its notes, declared unitCost or lot identity. Find the row with list_materials_ledger_entries (eventType=ADJUST, filtered by materialId and locationId) and pass its id. Send only the fields to change; the material and location cannot be changed. eventDate (YYYY-MM-DD) moves the row to that business day and keeps its time of day. The edited row must still pass the adjust_materials_inventory rules: a nonzero quantity, and unitCost, lotNumber and expirationDate only on a positive quantity (send them as null to clear them when turning an adjustment negative). Costs and balances derive from the ledger, so every later read reflects the edit. Only manual adjustments are editable: a disposal or a row written by a document returns 404.
Arguments: id* (string), quantity (number), notes (string,null), eventDate (string), unitCost (number,null), lotNumber (string,null), expirationDate (string,null)
Finished-Goods Inventory
adjust_finished_goods_inventory ✏️
Record a manual stock count correction for a product 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. Pass eventDate (YYYY-MM-DD, default today) to post the correction as of its real business date, e.g. a month-end count entered days later. On a positive adjustment unitCost optionally declares the cost of the added stock (stamped on the entry and used by costing; without it the stock takes the current pool cost), and lotNumber/expirationDate optionally declare its lot identity, seeded onto the cost layer like a receipt's. Negative adjustments take neither. Use transfers for moves and disposals for write-offs (damage, loss, expiry) that carry waste semantics.
Arguments: productId* (string), locationId* (string), quantity* (number), notes (string,null), eventDate (string), unitCost (number), lotNumber (string,null), expirationDate (string,null)
dispose_finished_goods_inventory 🔴
Write off finished-goods stock (damage, expiry, loss). This permanently removes quantity from stock via a DISPOSE ledger entry. Pass eventDate (YYYY-MM-DD, default today) to post the write-off as of its real business date.
Arguments: productId* (string), locationId* (string), quantity* (number), notes (string,null), eventDate (string)
get_finished_goods_inventory_summary
Complete finished-goods inventory position by product 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 product ({ id, code, name }) and location ({ id, name }). Filters AND together: productId and locationId are CSVs of ids (each OR-ed within itself), and search substring-matches (case-insensitive) the product'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. The same flag adds unbackedQuantity (units the cost layers hold beyond the ledger's on-hand: stock consumed before it was received, so a later receipt left value with no stock behind it; 0 on a healthy row), unbackedValue (those units at the layers' average cost; an ESTIMATE, always report it as one) and firstShortfallDate (the business day the ledger first went short there). A row with unbackedValue > 0 is overvalued: open its ledger filtered by item and location and look for the first negative balance. This is the go-to tool for 'how many units of product X do we have (and where)'.
Arguments: productId (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 product ({ 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). Stock events: RECEIVE, CONSUME, ADJUST, DISPOSE. Commitment events ride the same ledger: ORDER (purchase orders and jobs, the on-order side), DEMAND and ALLOCATE (sales orders and receipts; a kit line posts one per component), SETTLE_* close-outs, and REVERSE_* rows that net an earlier event to zero. All are legal eventType filter values. Produced and bought goods are both a RECEIVE, told apart by source_type. Each row embeds its product ({ id, code, name }) and location ({ id, name }). Filter instead of paging through everything: productId, 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 product'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, product, 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. Each stock row carries balance, the on-hand at its location right after the row over the item's full history in costing order (event date, then insertion order), the same figure under any paging or sort. A negative balance is a consumption that drew stock the location did not hold yet: that row is where value gets stranded (#1263), so to audit a location filter by item and location, sort eventDate ascending, and look for the first negative balance. Planning rows carry null. Each stock row also carries paddedQuantity and estimated (#1424): units of the row no cost layer covered (a consumption beyond what the location held, or the remainder a transfer or return could not vouch for) are priced at an ESTIMATE, the product's average receipt cost over the org's estimate window ending on the row's date, never at zero; paddedQuantity is how many, and estimated is true when the row's unitCost rests on padding, its own or that of the stock it drew or was built from. Always report an estimated unitCost as an estimate; it re-derives, and clears, once the missing receipt is entered. Each row also names the document that wrote it ({ kind, id }: purchase_order, sales_order, sales_receipt, sales_return, work_order, production_order, transfer or conversion; null on manual rows), so the row that went short leads straight to the document whose date, quantity or location needs fixing.
Arguments: sort (string), order (string), limit (integer), offset (integer), productId (string), locationId (string), eventType (string), sourceType (string), ref (string), lotNumber (string), search (string), eventDateFrom (string), eventDateTo (string), expirationFrom (string), expirationTo (string)
update_finished_goods_adjustment ✏️
Correct a manual finished-goods adjustment in place instead of posting a compensating one: fix a mistyped quantity, move a count to its real business date, or change its notes, declared unitCost or lot identity. Find the row with list_finished_goods_ledger_entries (eventType=ADJUST, filtered by productId and locationId) and pass its id. Send only the fields to change; the product and location cannot be changed. eventDate (YYYY-MM-DD) moves the row to that business day and keeps its time of day. The edited row must still pass the adjust_finished_goods_inventory rules: a nonzero quantity, and unitCost, lotNumber and expirationDate only on a positive quantity (send them as null to clear them when turning an adjustment negative). Costs and balances derive from the ledger, so every later read reflects the edit. Only manual adjustments are editable: a disposal or a row written by a document returns 404.
Arguments: id* (string), quantity (number), notes (string,null), eventDate (string), unitCost (number,null), lotNumber (string,null), expirationDate (string,null)
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 | product) and itemId is the material's id (from list_materials) or the product's id (from list_products), plus a positive quantity. One transfer can mix materials and products, 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 (in item-code order), status, the per-status timestamps grouped in statusDates, and what the move is worth. A transfer enters no price: every figure is derived on read from the source location's cost layers. Each line carries available (inventory's Available for the item at the source right now, on hand minus allocated, signed; negative means the source is oversold), unitCost (the costing engine's price of the line's CONSUME leg at departure, the cost the destination inherits), value (quantity times that), paddedQuantity (units no source layer covered, priced at the item's estimate) and estimated (the unit cost rests on padding, the line's own or inherited from the stock it drew; report it as an estimate). While the transfer is in Planning no leg exists, so each line is priced as the engine would price a consume of that quantity at the source today and preview is true. The value block sums the lines: products, materials, total, estimatedValue (the part of the total priced from padding), shortUnits (the padded units behind it), estimated and preview. A padded line means the source's ledger is missing a receipt: open the materials or finished-goods ledger for that item at the source to find the gap.
Arguments: id* (string)
get_transfer_ledger
The inventory ledger rows one transfer posted, grouped by ledger (materials and finishedGoods, each { data, total }; a ledger the transfer did not touch is empty). Each moved line writes to the ledger its kind picks: a CONSUME at the source when the transfer reaches In Transit and the paired RECEIVE at the destination when it reaches Completed, both carrying the transfer code as ref. Rows are in the ledger list's shape (code, item, location, quantity, the engine's unitCost, balance, the document behind the row) and oldest first, so this is the tool for 'what did this transfer move and at what cost': reach for it before list_materials_ledger_entries / list_finished_goods_ledger_entries with ref = the transfer code, which need the ledger and the code up front. Take the transfer id from list_transfers; an unknown id is rejected 404.
Arguments: id* (string)
get_transfer_pdf_link
Get a download link to the transfer's packing slip as a PDF, when someone wants the document itself (to print, file or send on). Give the person the url: it opens the PDF without signing in, rendered from the record as it is now with the organization's PDF settings, and expires after about 15 minutes (expiresAt), so call again for a fresh link rather than reusing an old one. Nothing is saved to the record. Find the id via list_transfers; to read the document's contents use get_transfer instead.
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, value, estimated }), and groups the per-status timestamps in statusDates (keys planning, inTransit, completed). A line's value is what it was worth as it left the source (quantity times the costing engine's unit cost for its CONSUME leg, the cost the destination inherits); it is null while the transfer is in Planning because nothing has moved. estimated true means the value rests on padding (the source could not cover the full quantity, or drew stock that was itself padded): always report it as an estimate. Narrow with the filters instead of listing everything (they AND together): pass itemId to see every transfer that moved one material or product, itemKind (material | product) 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)
Conversions
create_conversion ✏️
Create a conversion at one location (locationId from list_locations). Exactly one input, { productId, quantity } with the id from list_products, and one output, { productId } only: the output's quantity is always derived through the case factors and must never be sent. The output's product must be in the input's family, meaning the same product, the input's base product, or a case that packs that same base; converting eaches into cases must fill whole cases, or the call is rejected with 422. The materials consumed are derived automatically from the output's bill of materials minus the input's; extraMaterials adds units on top of that, each { materialId, quantity } with the id from list_materials, and each must already be on the output's own bill of materials (422 otherwise) with no material repeated (409). The CNV code is server-assigned. status is the opening stage, Planning by default (never Partial, which is reached by receiving); statusDates back-dates it. Planning posts nothing. Placed puts the output on order, reserves the input and demands the consumed materials; Production reserves the materials; the stock moves as it is received, one create_conversion_receipt at a time, each a whole multiple of the returned step; Received settles the open figures. Create in Received to record a repack already done: it gets one reception for its full output on the received date. The response is the detail with its receptions. conversionCost is the cost of converting one unit of output (the co-packer's fee or the labor), zero by default; each reception snapshots it, and its output receives at the cost of what that reception consumed (the value drawn for the input plus the value of the consumed materials, spread over the units received) plus that snapshot per unit, minus the loss, what the reception threw away valued from the order that built the input (nothing when the input was not built in Pharus). conversionDate is the document's business date and defaults to today. Nothing checks stock on hand. Kits are rejected. Rejected with 404 when the input product, the output product, an extra material, or the location does not exist.
Arguments: locationId* (string), status (string), statusDates (object), input* (object), output* (object), extraMaterials (array), conversionDate (string), conversionCost (number), notes (string,null)
create_conversion_receipt ✏️
Record output that came back from a conversion (conversionId from list_conversions; the conversion must be in Production, Partial or Received). quantity is in output units and must be a whole multiple of the conversion's step (read it off get_conversion), so every reception uses up whole units of the input: 422 otherwise, naming the step. The reception writes its own rows, dated receptionDate (YYYY-MM-DD, defaults to today): the input CONSUME, one materials CONSUME per consumed material scaled by received over planned, the output RECEIVE with lotNumber and expirationDate. It snapshots the conversion's conversionCost per unit unless you send your own. The first reception moves the conversion to Partial; receiving against a Received conversion reopens it. Over-receipt is allowed in steps. Returns the reception and the conversion detail it landed on.
Arguments: conversionId* (string), receptionDate (string), quantity* (number), conversionCost (number), lotNumber (string,null), expirationDate (string,null), notes (string,null)
delete_conversion 🔴
Hard-delete a conversion that has no receptions, together with every ledger row it wrote: the commitments, their reversals and the settle. A conversion with receptions is refused with 409 conflict: delete them first with delete_conversion_receipt.
Arguments: id* (string)
delete_conversion_receipt 🔴
Delete a reception and the rows it wrote, as if that output never came back. The conversion drops to Production when no reception remains, and a Received conversion reopens. Rejected with 409 while the conversion is Completed: reopen it first.
Arguments: id* (string)
get_conversion
Get one conversion by id (from list_conversions): a list row plus its receptions, oldest first, each with its date, quantity (output units), lot, expiration, notes and loss, and the conversion's loss summed over them. The loss is what a reception threw away (the input's materials the output does not keep, plus extras that replaced them), valued from the order that built the input: per material the quantity, the unit cost that build gave it and the value, plus a total. An input that was not built in Pharus books no loss and keeps its whole value in the output. Computed on read, never stored. The detail also carries the engine's finals: the output's finalAvgUnitCost, the produced value over the units produced across the receptions (null until the first reception, finalEstimated true while any consumed line behind it drew more than its layers held), and on the input and each consumed material inventory's available at the location (on hand minus allocated, signed, negative when oversold), read live. Each reception carries its cost read: produced (unitCost, value, estimated), consumed (the input units used up as an inputProduct line, then each material the output's recipe adds, with quantity, unitCost, value, paddedQuantity and estimated), conversion (units received times the reception's fee), loss extended with quantity (discarded material units), unitCost, value (the deduction, negative) and estimated, and total (consumed plus conversion plus the loss value, equal to the produced value). financials.final holds the same rows as the plan from the receptions (input, materials, conversionCost, loss, goodsCost, total, estimated), null until the first reception.
Arguments: id* (string)
get_conversion_ledger
The inventory ledger rows one conversion posted, grouped by ledger (materials and finishedGoods, each { data, total }). Placing the conversion posts an ORDER on the output product and a DEMAND per added material; starting production posts an ALLOCATE on the input product and per material; each reception posts the CONSUME of the input, the RECEIVE of the output and a CONSUME per material, all carrying the conversion code as ref. Rows are in the ledger list's shape (code, item, location, quantity, the engine's unitCost, balance, the document behind the row) and oldest first, so this is the tool for 'what did this conversion do to stock and at what cost': reach for it before list_materials_ledger_entries / list_finished_goods_ledger_entries with ref = the conversion code, which need the ledger and the code up front. Take the id from list_conversions; an unknown id is rejected 404.
Arguments: id* (string)
get_conversion_pdf_link
Get a download link to the conversion as a PDF, when someone wants the document itself (to print, file or send on). Give the person the url: it opens the PDF without signing in, rendered from the record as it is now with the organization's PDF settings, and expires after about 15 minutes (expiresAt), so call again for a fresh link rather than reusing an old one. Nothing is saved to the record. Find the id via list_conversions; to read the document's contents use get_conversion instead.
Arguments: id* (string)
get_conversion_receipt
Get one reception by id: its conversionId, date, quantity, lot, expiration, notes and its loss (what it threw away, valued from the order that built its input).
Arguments: id* (string)
list_conversion_receipts
List conversion receptions, oldest first. Pass conversionId for one conversion's; omit it to read receptions across every conversion. Each carries its date, quantity (output units), lot, expiration and notes.
Arguments: conversionId (string)
list_conversion_status_types
List the conversion status vocabulary (Planning, Placed, Production, Partial, Received, Completed, Cancelled) as plain strings. Use before setting a status with update_conversion; Partial is reached by receiving, not set by hand.
Arguments: none
list_conversions
List one page of conversions, newest first by default. A conversion repacks finished goods at one location: input (the product consumed) and output (the product received, from the input's family, its quantity derived through the case factors) each resolve to { id, code, name } plus a quantity; consumed lists the materials used up (each flagged derived when the bill-of-materials diff wrote it, false for a caller-added extra); waste reports the materials the output does not keep from the input, plus the extras, unpriced. Rows carry status (Planning, Placed, Production, Partial, Received, Completed, Cancelled), statusDates (the seven stage dates keyed planning, placed, production, partial, received, completed, cancelled), the embedded location ({ id, name }), conversionDate, the document's business date, step (the output units a reception must be a multiple of) and received (output units received so far). The placement estimate rides on every row, derived on read and anchored on the placement date: the input's estimatedUnitCost (its inventory cost as of that date at the location) and estimatedValue, each consumed material's estimatedUnitCost (its purchase-receipt average over the conversions' estimate window ending that day) and estimatedValue, the output's estimatedUnitCost, and financials with estimatedTotal and estimated (input, materials, conversion, loss, goodsCost); null before placement, while an input has nothing to price it on, and on a Received or Completed conversion that has a finalTotal (get_conversion carries its plan). financials.finalTotal is the engine's produced value once the conversion is Received or Completed (null while open), finalEstimated true while it rests on a padded consume. Narrow instead of listing everything: pass status as a CSV of statuses, productId to see every conversion whose input or output is one product, materialId for one consumed material, locationId as a CSV of location ids, and dateFrom/dateTo (YYYY-MM-DD) to bound the conversion date. search substring-matches the CNV code, the location name, the input or output's item code or name, and notes. Page with limit (default 50, max 200) and offset; sort with sort (number, status, location, date, createdAt, updatedAt) and order.
Arguments: sort (string), order (string), limit (integer), offset (integer), status (string), locationId (string), productId (string), materialId (string), search (string), dateFrom (string), dateTo (string)
update_conversion ✏️
Update a conversion. Move it with status (from list_conversion_status_types): Planning, Placed and Production move freely among themselves and to Cancelled; Partial is reached by the first reception (never set by hand) and goes to Received, the manual close; Received reopens to Partial or goes on to Completed; Completed steps back to Received; Cancelled reactivates to Planning, Placed or Production; any other move is rejected with 409 invalid_transition. Advancing posts the stage's commitments, stepping back reverses them, entering Received settles, reopening unsettles, Cancelled reverses whatever is open. Each stage stamps its date; statusDates merges per key and may back-date a stage, in lifecycle order. The location is editable only in Planning and the input, output and extraMaterials only until a reception exists (409 invalid_transition afterwards; delete the receptions to change them). While editable, a sent input or output replaces its side (an omitted one is what is stored) and re-derives the output's quantity and the consumed materials; a sent extraMaterials list replaces the extras, validated as on create_conversion, and an extra whose material is no longer on the output's bill of materials is dropped rather than rejected. conversionDate, conversionCost and notes are editable in any status; a new conversionCost applies to receptions recorded after it. Returns the detail.
Arguments: id* (string), status (string), statusDates (object), locationId (string), conversionDate (string), conversionCost (number), input (object), output (object), extraMaterials (array), notes (string,null)
update_conversion_receipt ✏️
Edit a reception in place: receptionDate (moves every row it wrote), quantity (still a multiple of the step; rescales its rows), lotNumber and expirationDate (on the RECEIVE), notes, conversionCost (read live by the costing engine). Never changes the conversion's status. Rejected with 409 while the conversion is Completed: reopen it first.
Arguments: id* (string), receptionDate (string), quantity (number), conversionCost (number), lotNumber (string,null), expirationDate (string,null), 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 productId (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 product is rejected 404; rejected 409 once any receipt exists against the PO: received orders lock their line items.
Arguments: materialId (string,null), productId (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 productId from list_products). 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/product embedded as references, the timestamps grouped in statusDates, items nested, and receipts empty. A missing vendor, location, material, or product is rejected 404.
Arguments: vendorId* (string), targetType* (string), status* (string), shipToLocationId (string,null), orderDate (string,null), shipDate (string,null), expectedDeliveryDate (string,null), shipVia (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/product 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 product 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 that keeps the two cost figures apart: estimated is the ordered lines (qty x unit cost) plus the stored shipping, setup and other costs, what the buyer expected; final is the receipt lines plus the same overheads, what was paid, and is null until the first receipt line exists. Each carries items, shipping, setup, other and total, computed from the order itself (no costing engine). Its items are nested (material/product embedded as { id, code, name }), and its receipts nested with each receipt's received lines under its own items.
Arguments: id* (string)
get_purchase_order_ledger
The inventory ledger rows one purchase order posted, grouped by ledger (materials and finishedGoods, each { data, total }; a PO writes only its target ledger, the other group is empty). Placing the order posts ORDER rows, each receipt line posts a RECEIVE row, all carrying the PO number as ref. Rows are in the ledger list's shape (code, item, location, quantity, the engine's unitCost, balance, the document behind the row) and oldest first, so this is the tool for 'what did this PO do to stock and at what cost': reach for it before list_materials_ledger_entries / list_finished_goods_ledger_entries with ref = the PO number, which need the ledger and the number up front. Take the PO id from list_purchase_orders; an unknown id is rejected 404.
Arguments: id* (string)
get_purchase_order_pdf_link
Get a download link to the purchase order as a PDF, when someone wants the document itself (to print, file or send on). Give the person the url: it opens the PDF without signing in, rendered from the record as it is now with the organization's PDF settings, and expires after about 15 minutes (expiresAt), so call again for a fresh link rather than reusing an old one. Nothing is saved to the record. Find the id via list_purchase_orders; to read the document's contents use get_purchase_order instead.
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 product 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 product embedded as { id, code, name }. Pass purchaseOrderId for one order's items, or materialId / productId 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), productId (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 same financials block get_purchase_order returns (estimated and final, each with items, shipping, setup, other and total; final is null until a receipt line exists). 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 / containsProductId keep only orders with a line for that material or product, 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/product 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 the estimated total with costMin/costMax and sort with sort=totalCost (also the estimated total: the order's own lines plus shipping, setup and other; it exists for every order, received or not).
Arguments: sort (string), order (string), limit (integer), offset (integer), status (string), vendorId (string), shipToLocationId (string), targetType (string), containsMaterialId (string), containsProductId (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), shipVia (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/product. 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 product 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), productId (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 product (+ 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 productId (from list_products), 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 product 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 product is already a line on it (conflict).
Arguments: productId* (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 product (productId from list_products) and a quantity to produce; each product'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 product embedded, its recipe inputs nested with their material/component product embedded), and receipts empty. A missing work-site location or output product 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 productId (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 product 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 product embedded as { id, code, name }, the snapshotted recipe inputs nested under inputs with their material/component product 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. The placement estimate rides along, derived on read and anchored on the placement date: each item's estimatedUnitCost, each input's estimatedUnitCost and estimatedValue, and financials.estimated (materials, conversion, goodsCost) beside financials.estimatedTotal, all null before placement or while an input has no purchase receipt in the estimate window. The engine's finals ride beside it: each item's finalAvgUnitCost (the produced value over the units produced across its receipts, null until the first receipt) with finalEstimated true while any consumed line behind it drew more than its layers held (the figure rests on the padded estimate and heals once the missing receipt is entered), and each input's available, inventory's Available for it at the work site right now (on hand minus allocated, signed, negative when oversold). financials.estimated and financials.final carry the same rows (materials, conversion, goodsCost, inShipping, outShipping, other; final adds total, the plan's total is estimatedTotal): the plan against the engine's figure from the receipts, final null until the first receipt and its estimated flag true while it rests on padding; finalTotal beside estimatedTotal follows the list's rule. Each receipt shows its cost read: every produced line's unitCost, value and estimated, the consumed lines beneath (the input as a material or inputProduct reference, quantity, unitCost, value, the paddedQuantity no layer covered, estimated), the conversion line and the receipt total, the consumed value plus the conversion, which equals the produced total.
Arguments: id* (string)
get_work_order_ledger
The inventory ledger rows one work order posted, grouped by ledger (materials and finishedGoods, each { data, total }). Placing the order posts an ORDER per output product on finished goods and a DEMAND per recipe input on the ledger its kind routes to (materials, or finished goods for a component product); starting production posts an ALLOCATE per input; each receipt line posts a RECEIVE for the output and a CONSUME per input, all carrying the WO number as ref. Rows are in the ledger list's shape (code, item, location, quantity, the engine's unitCost, balance, the document behind the row) and oldest first, so this is the tool for 'what did this work order do to stock and at what cost': reach for it before list_materials_ledger_entries / list_finished_goods_ledger_entries with ref = the WO number, which need the ledger and the number up front. Take the WO id from list_work_orders; an unknown id is rejected 404.
Arguments: id* (string)
get_work_order_pdf_link
Get a download link to the work order as a PDF, when someone wants the document itself (to print, file or send on). Give the person the url: it opens the PDF without signing in, rendered from the record as it is now with the organization's PDF settings, and expires after about 15 minutes (expiresAt), so call again for a fresh link rather than reusing an old one. Nothing is saved to the record. Find the id via list_work_orders; to read the document's contents use get_work_order instead.
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 product 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 product 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 product X' (productId narrows by the produced product). Each row carries its workOrderId so you can fetch the order.
Arguments: workOrderId (string), productId (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 with estimatedTotal, the placement estimate's total (the recipe priced at each input's purchase-receipt average over the work orders' estimate window ending on the placement date, plus the conversion cost and the overheads; null before placement, when an input has no receipt in the window, or on a Received or Completed order that has a finalTotal; get_work_order carries its plan) and finalTotal, the engine's final (the produced value plus the overheads) once the order is Received or Completed and null while it is open, with finalEstimated true while any consumed line behind it drew more than its layers held (one batched engine pass per page, nothing persisted). 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), containsProductId keeps only orders with an output line for that product, 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 products' 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), containsProductId (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 product 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 product (+ 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 productId (from list_products). Adding to an ordered order projects the line's commitment ledger events. The response is the line with its output product 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 product is already a line on it (conflict).
Arguments: productId* (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 product (productId from list_products) and a quantity to produce; each product'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 product 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 product embedded, its recipe inputs nested with their material embedded), and receipts empty. A missing location or output product 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 productId (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 product 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 product'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 product 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. The placement estimate rides along, derived on read and anchored on the placement date: each item's estimatedUnitCost, each input's estimatedUnitCost and estimatedValue, and financials.estimated (materials, productionCost, goodsCost) beside financials.estimatedTotal, all null before placement, while the line's production cost is unset, or while a material has no purchase receipt in the window. The engine's finals ride beside it: each item's finalAvgUnitCost (the produced value over the units produced across its receipts, null until the first receipt) with finalEstimated true while any consumed line behind it drew more than its layers held (the figure rests on the padded estimate and heals once the missing receipt is entered), and each input's available, inventory's Available for it at the order's location right now (on hand minus allocated, signed, negative when oversold). financials.estimated and financials.final carry the same rows (materials, productionCost, goodsCost, inShipping, outShipping, other; final adds total, the plan's total is estimatedTotal): the plan against the engine's figure from the receipts, the productionCost row being the ordered units at the quoted price against what the receipts recorded; final is null until the first receipt and its estimated flag true while it rests on padding; finalTotal beside estimatedTotal follows the list's rule. Each receipt shows its cost read: every produced line's unitCost, value and estimated, the consumed lines beneath (the material reference, quantity, unitCost, value, the paddedQuantity no layer covered, estimated), the productionCost line at the receipt lines' own figure and the receipt total, the consumed value plus the production cost, which equals the produced total.
Arguments: id* (string)
get_production_order_ledger
The inventory ledger rows one production order posted, grouped by ledger (materials and finishedGoods, each { data, total }). Placing the order posts an ORDER per output product on finished goods and a DEMAND per exploded-BOM material input on materials; starting production posts an ALLOCATE per input; each receipt line posts a RECEIVE for the output and a CONSUME per input, all carrying the order number as ref. Rows are in the ledger list's shape (code, item, location, quantity, the engine's unitCost, balance, the document behind the row) and oldest first, so this is the tool for 'what did this production order do to stock and at what cost': reach for it before list_materials_ledger_entries / list_finished_goods_ledger_entries with ref = the order number, which need the ledger and the number up front. Take the id from list_production_orders; an unknown id is rejected 404.
Arguments: id* (string)
get_production_order_pdf_link
Get a download link to the production order as a PDF, when someone wants the document itself (to print, file or send on). Give the person the url: it opens the PDF without signing in, rendered from the record as it is now with the organization's PDF settings, and expires after about 15 minutes (expiresAt), so call again for a fresh link rather than reusing an old one. Nothing is saved to the record. Find the id via list_production_orders; to read the document's contents use get_production_order instead.
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 product 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 product 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 product X' (productId narrows by the produced product). Each row carries its productionOrderId so you can fetch the order.
Arguments: productionOrderId (string), productId (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 with estimatedTotal, the placement estimate's total (the exploded recipe priced at each material's purchase-receipt average over the production orders' estimate window ending on the placement date, plus the quoted production cost and the overheads; null before placement, while a line's production cost is unset, when a material has no receipt in the window, or on a Received or Completed order that has a finalTotal; get_production_order carries its plan) and finalTotal, the engine's final (the produced value plus the overheads) once the order is Received or Completed and null while it is open, with finalEstimated true while any consumed line behind it drew more than its layers held (one batched engine pass per page, nothing persisted). 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), containsProductId keeps only orders with an output line for that product, 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 products' 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), containsProductId (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 product 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 (product + quantity + price) to an existing sales order. Pass salesOrderId in the body (the SO id from list_sales_orders). Rejected with 409 if the product is already a line on the order.
Arguments: productId* (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), shipByDate (string,null), 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 product embedded), and a financials block with the stored costs plus derived netRevenue (what the goods sold for), inventoryCost (COGS), grossProfit (net revenue minus the cost of goods), totalCost, netProfit and netMargin. These are the same names sales receipts use. Total cost is the inventory cost plus the five entered fees, netProfit is net revenue minus it and netMargin is netProfit over net revenue. The inventory-cost figures are recomputed on every read, so they can change as historical inventory data changes. The financials block is the order's lines added up, so it carries the same three markers they do: financials.live, financials.paddedQuantity and financials.estimated. Report the inventory cost, the total cost, the net profit and the net margin as estimates whenever financials.estimated is true, and say why: the order has not shipped, so the cost is a live reading of the shelf (live true), or it shipped short of stock (paddedQuantity units drawn beyond what the location held). Every line also says what it cost and what it earned, derived the same way from the fulfillment location's cost layers: available (inventory's Available for the product there right now, on hand minus allocated, signed; on a kit line, how many kits the location could build, set by the limiting component), unitCost, cost (quantity times it), grossProfit (the line's total price minus that cost) and grossMargin (grossProfit over the price), all null when nothing prices the line. live true means the goods have not shipped, so the unit cost is what the location's stock is worth today and settles on the ship date: always report it as an estimate. live false means the cost is what the engine priced the line's CONSUME legs at and no longer moves; report it as an estimate only when estimated is true, which happens when paddedQuantity is above zero (the line drew more units than the location held, so those are priced at the product's average receipt cost) or when the stock it drew was itself padded. A padded line means the location's ledger is missing a receipt: open the finished-goods ledger for that product there to find the gap. A rejected order never shipped, so its lines carry no cost at all. A kit line has no cost and no stock of its own: it posts one ledger leg per component, so its figures are a rollup and components holds what it actually consumed, each with the product, quantityPerKit, the quantity this line draws, available there, unitCost on the same reading as the line, and the paddedQuantity and estimated that mark the kit line above it. Components carry no price and no profit, because the customer is charged for the kit; every other line has an empty components.
Arguments: id* (string)
get_sales_order_ledger
The inventory ledger rows one sales order posted, grouped by ledger (materials and finishedGoods, each { data, total }); a sales order only sells finished goods, so materials is always empty. Per line it posts three rows at the fulfillment location: a DEMAND in Planning, an ALLOCATE once Placed and a CONSUME once In Transit, the only one that moves stock and carries cost; a kit line posts one of each per component, so the rows read per product rather than per line, and a Rejected order has reversed all three. Every row carries the SO number as ref. Rows are in the ledger list's shape (code, product, location, quantity, the engine's unitCost, balance, the document behind the row) and oldest first, so this is the tool for 'what did this order take out of stock and at what cost': reach for it before list_finished_goods_ledger_entries with ref = the SO number, which needs the ledger and the number up front. Take the order id from list_sales_orders; an unknown id is rejected 404.
Arguments: id* (string)
get_sales_order_pdf_link
Get a download link to the sales order's packing slip as a PDF, when someone wants the document itself (to print, file or send on). Give the person the url: it opens the PDF without signing in, rendered from the record as it is now with the organization's PDF settings, and expires after about 15 minutes (expiresAt), so call again for a fresh link rather than reusing an old one. Nothing is saved to the record. Find the id via list_sales_orders; to read the document's contents use get_sales_order instead.
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 product X' (productId narrows by the sold product). Each row carries its salesOrderId so you can fetch the order.
Arguments: salesOrderId (string), productId (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, containsProductId keeps orders with a line item for that product, 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 product 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, netRevenue, 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 net revenue (the line prices summed) with netRevenueMin/netRevenueMax and sort with sort=netRevenue.
Arguments: sort (string), order (string), limit (integer), offset (integer), status (string), fulfillmentStatus (string), customerId (string), channelId (string), fulfillmentLocationId (string), brokerId (string), containsProductId (string), netRevenueMin (number), netRevenueMax (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), shipByDate (string,null), 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. Moving a line onto a product another line of the order already carries is rejected with 409: edit that line instead.
Arguments: id* (string), productId (string), quantity (integer), unitPrice (number)
Sales Receipts
add_sales_receipt_item ✏️
Add a line (productId + quantity + optional unitPrice and lineDiscount) to an existing manual receipt. Pass salesReceiptId in the body (the receipt id from list_sales_receipts). Rejected with 409 if the product is already a line on the receipt or the receipt is synced.
Arguments: productId* (string), quantity* (number), unitPrice (number), lineDiscount (number,null), salesReceiptId* (string)
create_sales_receipt ✏️
Record a sales receipt by hand (a manual receipt). Marketplace orders from Shopify and Amazon arrive on their own through the sync; use this only for sales no connected feed carries. channelId (the sales channel it ran through) is optional. 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 default to false. Each line carries a productId (from list_products), a quantity (decimals allowed), an optional unitPrice and an optional lineDiscount (never more than quantity × unitPrice); unit cost is derived from inventory, not set here. shippingCharged, tax and tip record what the customer paid on top of the goods (default 0; the tip is what they added for the business, counted as other income, not revenue); the receipt's subtotal and total are derived from these. postage (what the label cost) and channelFee (what a sales channel or affiliate took, e.g. a RepRally fee) are optional typed costs; leave them out when there were none. Pass locationId (from list_locations) for the ship-from location: without it the sale never moves inventory and carries no cost. Pass srNumber to set a custom number (unique per org), or omit it to auto-generate the next one. Status defaults to Placed. Returns the receipt as get_sales_receipt serves it.
Arguments: customerName (string,null), customerRef (string,null), isSubscription (boolean), isRecurring (boolean), channelId (string,null), status (string), locationId (string,null), orderDate (string), statusDates (object), shippingCharged (number), tax (number), tip (number), postage (number,null), channelFee (number,null), notes (string,null), srNumber (string), items* (array)
delete_sales_receipt 🔴
Permanently delete a manual sales receipt and its line items, removing its ledger rows. Synced receipts cannot be deleted (409).
Arguments: id* (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 each synced receipt from list_sales_receipts and get_sales_receipt.
Arguments: source (string)
get_sales_receipt
Get one sales receipt (any source; the id from list_sales_receipts) as a composite: identity (source, number, and for a synced receipt the marketplace order id and number, store and admin deep link), the lifecycle (status, statusDates, and for a synced receipt the marketplace's raw sourceStatus; Delivered is dated by the delivery scan, else an Amazon order's must-arrive-by date (assumed met; merchant-fulfilled orders carry it), else an FBA order's estimated arrival, else 7 days after shipping, and deliveredDateSource says which; Completed means delivered and settled, dated on the later of the two: a Shopify order settles at payment, so it is Delivered and Completed the same day, while an Amazon order settles when Amazon posts its fees (awaitingSettlement until then, at most 30 days after shipping, when it completes with the fees at zero); a synced order that left in parts is Partial, statusDates.partial its first shipment and statusDates.shipped the day the last line left (an order that left in one go carries its shipped date as partial too); Amazon charges at shipping, so an Amazon order goes from Placed straight to Partial or Shipped, statusDates.paid its payment date; when Amazon says an order shipped but its shipments report has no row, Shipped is the order's ship-by date and shippedDateSource says "shipBy"), the order's tags (Shopify's, sorted; empty on Amazon and manual receipts), the customer (Amazon anonymizes buyers, so its customer is often null), channel, destination and carrier with a trackingUrl to the carrier's tracking page, the Pharus location and the marketplace's own fulfillment location, the buyer-side money (list subtotal, discounts with codes, net subtotal, shipping charged, tax, tip (null on Amazon), total paid, a reconciliation flag, paidByGiftCard (Shopify only, informational), refunded with refundedParts: goods, shipping and tax), notes, the lines as sold (a product can repeat across a synced receipt's lines when bundles explode) each with its mapped product, the units it moved in inventory and what it cost (unitCost per unit sold, cost, grossProfit, grossMargin; a kit line also lists its components, the products it consumes with quantityPerKit, quantity, available and unitCost, as on a sales order), and the refunds (each with its kind: refund, chargeback or a_to_z_claim, and its amount in goods, shipping and tax parts, null on one stored before refunds were split), and events, the timeline oldest first: things that happened without changing the status (line_shipped groups the lines that left on one day, only when the order left in parts; refunded and chargeback carry the kind, the money in parts and fullyRefunded on the one that gave everything back; reimbursed is Amazon paying the seller back for a lost or damaged order (lost true) or another reason, with the reason code and amount, and never changes the receipt's money; cancelled its day). Read events instead of rebuilding the story from lines and refunds. Costs are recomputed on every read and never stored: while the receipt is Placed or Paid they read the location's stock today (live, always estimated), once shipped they are what the engine priced the consumed stock at (estimated only where a line drew more than the location held); while Partial, the lines that left price as shipped and the rest live. financials carries the receipt's economics: grossRevenue (total paid less refunded) less shippingKept (shipping charged less shipping refunded), taxKept (tax less tax refunded) and tip gives netRevenue (what the goods sold for, net of the goods refunded); inventoryCost (cost of goods, the lines added up, with the same marks), grossProfit and grossMargin; servingCosts with its parts (the fees by kind: Shopify's cardProcessing and disputeFee, Amazon's referralCommission, refundFee and marketplaceFulfillment, a manual receipt's typed channelFee, null where it does not apply, plus unsplit on a receipt stored before the split; postage, the 3PL's label cost or the one typed on a manual receipt; the location's pick and pack rate; a part that applies but is unknown is null and listed in missing, which marks the groups as estimates) and its groups (sellingFees, which is card processing, dispute fee, channel fee, referral commission and refund fee; fulfillment, which is postage plus pick and pack plus marketplace fulfillment less shippingKept; otherCosts; total); otherIncome (the tip plus other income); and totalCost, netProfit and netMargin (net profit over net revenue). Its cost fields are null while any line or the location is unmapped, so complete the mapping first with set_connected_product_mapping and set_connected_location_mapping. For the ledger rows behind the cost use get_sales_receipt_ledger. An Amazon Multi-Channel Fulfillment order (isMcf) is not a sale: it carries no revenue or cost, and its mcf block gives the seller's reference and, when that names a Shopify order, the receipt it fulfills (fulfills, open it with this tool). 404 when the id is unknown or names a synced receipt before the org's connected go-live date.
Arguments: id* (string)
get_sales_receipt_ledger
The inventory ledger rows one sales receipt posted, grouped by ledger (materials and finishedGoods, each { data, total }); a receipt only sells finished goods, so materials is always empty. Per line it posts three rows at its location: a DEMAND once Placed, an ALLOCATE once Paid and a CONSUME once Shipped (on a Partial synced receipt, only for the lines that already left, each on its own ship day), the only one that moves stock and carries cost; a kit line posts one of each per component, a synced line posts its product units (quantity times the listing factor), an unmapped line or a receipt without a location posts nothing, and a Cancelled receipt has reversed all three on the lines that hadn't left (#1782: a synced order cancelled after shipping keeps its shipped lines' rows). Every row carries the receipt number as ref. Rows are in the ledger list's shape (code, product, location, quantity, the engine's unitCost, balance, the document) and oldest first, so this is the tool for 'what did this sale take out of stock and at what cost'. Take the receipt id from list_sales_receipts; an unknown id is rejected 404.
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_product_mapping and set_connected_location_mapping, which backfill the ledger immediately. When the org records Amazon orders copied into Shopify once, it also lists the Shopify copies whose Amazon order is still missing days later: those sales are recorded nowhere until the Amazon feed delivers them.
Arguments: none
list_connected_product_mappings
The listing-to-product 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 product code and titles, its sold volume, and the Pharus product it currently maps to (null = unmapped). Listings are keyed by their store's stable identity, the variant id on Shopify or the seller product on Amazon (both travel in variantId). Shopify lines that point at no store listing (a custom item written by an app like Faire, or a deleted variant) gather into one row per SKU, keyed "sku:<SKU>" and flagged unlinked, or, with no SKU either, one row per title keyed "title:<title>": map it like any other row and every such line follows. A row with notAProduct true is settled with no product (it sells no goods). Pass mapped=false to see exactly what still needs mapping instead of listing everything, search to find one listing by its product 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_product_mapping. Mapped products appear on the lines get_sales_receipt serves; each row reports unitsPerListing.
Arguments: source (string), search (string), mapped (string), sort (string), order (string), limit (integer), offset (integer)
list_connected_receipt_locations
The distinct fulfillment-location names on the org's synced sales receipts (the mapped Pharus location, else the Shopify assigned location or the Amazon channel label like Amazon FBA), sorted. Pass them verbatim to list_sales_receipts' fulfillmentLocation filter; to filter by a Pharus location, pass its id (from list_locations) as locationId.
Arguments: none
list_sales_receipt_items
List one page of receipt lines across every source, newest receipt first. Pass salesReceiptId for one receipt's lines; omit it to query lines across every receipt, for questions like 'every sale of product X' (productId narrows by the mapped product). Each row is the line as get_sales_receipt serves it plus its salesReceiptId. Page with limit (default 50, max 200) and offset; meta.total counts the filtered set.
Arguments: salesReceiptId (string), productId (string), limit (integer), offset (integer)
list_sales_receipts
List one page of the org's sales receipts, every source together: the Shopify and Amazon orders the sync keeps in Pharus and the receipts recorded by hand (each row's source says which: manual, shopify or amazon). Rows carry the number, status and statusDates, customer name, channel and location references, the marketplace's own fulfillmentLocation on synced rows, netRevenue (merchandise net of discounts and refunds, excluding shipping and tax), financials (what the goods cost, gross profit and margin, the same figures get_sales_receipt adds up), dataAsOf (the sync watermark), isSample, isMcf (an Amazon Multi-Channel Fulfillment order: Amazon shipping stock for a sale made elsewhere, never revenue and never on the ledger; leave these out when totalling sales), and tags (the Shopify order's tags, sorted; empty on Amazon and manual receipts). Narrow in SQL instead of pulling everything: status (Placed, Paid, Partial, Shipped, Delivered, Completed, Cancelled; Partial is a synced order that left in parts), source, channelId, locationId and fulfillmentLocation accept comma-separated values (OR within a filter, filters AND together); containsProductId keeps receipts with a line for that product; netRevenueMin/netRevenueMax bound the net revenue; isSample="true" lists only the free samples sent through Samples (they stay out of revenue and order counts), "false" leaves them out; tag keeps receipts whose order carries that tag (whole tag, any case: pass tag="wholesale" rather than listing everything and reading tags); srNumber finds one receipt by its number, exactly, with or without Shopify's leading # (srNumber="1042" finds #1042); the date filters are inclusive From/To pairs (orderDateFrom/orderDateTo plus one pair per statusDates key, e.g. shippedFrom/shippedTo). search matches the number, the marketplace order id, the customer name and ref, notes, the tags, channel and location names, and the lines' listings and products. Synced receipts before the org's connected go-live date are not served. Page with limit (default 50, max 200) and offset (meta.total is the filtered count); sort with sort (srNumber, orderDate, status, source, customerName, channel, fulfillmentLocation, netRevenue, cost, netMargin, createdAt, updatedAt; default orderDate newest first; cost and netMargin price every filtered receipt first, so narrow by date before using them on a large org) and order. For a stable full walk while data changes, sort by createdAt ascending and page forward. Fetch one receipt's money and costed lines with get_sales_receipt.
Arguments: sort (string), order (string), limit (integer), offset (integer), status (string), source (string), channelId (string), locationId (string), fulfillmentLocation (string), containsProductId (string), netRevenueMin (number), netRevenueMax (number), isSample (string), tag (string), srNumber (string), search (string), orderDateFrom (string), orderDateTo (string), placedFrom (string), placedTo (string), paidFrom (string), paidTo (string), partialFrom (string), partialTo (string), shippedFrom (string), shippedTo (string), deliveredFrom (string), deliveredTo (string), completedFrom (string), completedTo (string), cancelledFrom (string), cancelledTo (string)
remove_sales_receipt_item 🔴
Remove a line from a manual receipt. The id is the item id, not the receipt id. Synced receipts' lines are read-only (409).
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, and the synced receipts at that location post to (or leave) inventory at once.
Arguments: source (string), externalKey* (string), externalName (string,null), locationId* (string,null)
set_connected_product_mapping ✏️
Map one marketplace listing to a Pharus product: pass the variantId from list_connected_product_mappings (with the same source, shopify by default or amazon) and the productId from list_products (pass productId null to clear a mapping). Upserts: re-mapping replaces the previous mapping. For a listing that sells no goods (return protection, a fee line), pass notAProduct true with productId null instead: the listing leaves the pending list, its lines move no stock, and they no longer hold back a receipt's cost of goods. 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 product on Amazon (both travel in variantId), or an unlinked row's "sku:<SKU>" or "title:<title>" key passed exactly as the worksheet lists it. Pass unitsPerListing (a whole number from 1 to 100000) when the listing is a multiple of the product, for example 8 for an 8-pack listing of a single unit product; inventory then consumes quantity × unitsPerListing per line while revenue stays as sold. Omitting it keeps an existing mapping's factor and defaults to 1 for a new mapping, so a save that only changes the product never resets the listing's factor. Use a real case product instead when the multiple exists physically in inventory. The factor also sets the items pick and pack charges for: a receipt's items are the product units its lines take from stock (a kit line counts its components), times the location's perItem fee.
Arguments: source (string), variantId* (string), listingSku (string,null), listingTitle (string,null), productId* (string,null), unitsPerListing (integer), notAProduct (boolean)
update_sales_receipt ✏️
Update a manual receipt's header fields, including status (Placed, Paid, Shipped, Completed, Cancelled; one step at a time either way). Status moves are validated: cancel is only allowed before Shipped. Partial belongs to synced receipts and is rejected here: a manual receipt ships whole, and Shipped dates every line. 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 and locationId stay editable throughout (a receipt has no draft stage); shippingCharged, tax and tip restate what the customer paid on top of the goods, and postage and channelFee restate its typed costs (null clears one). The receipt number can be renamed via srNumber (unique per org). A synced receipt (source shopify or amazon) takes notes only: anything else is rejected with 409, since the sync owns it.
Arguments: id* (string), srNumber (string,null), customerName (string,null), customerRef (string,null), isSubscription (boolean), isRecurring (boolean), channelId (string,null), status (string), locationId (string,null), orderDate (string,null), statusDates (object), shippingCharged (number), tax (number), tip (number), postage (number,null), channelFee (number,null), notes (string,null)
update_sales_receipt_item ✏️
Update a manual receipt's line (productId, quantity, unitPrice or lineDiscount). The id is the item id, not the receipt id. Moving a line onto a product another line already carries is rejected with 409 (edit that line instead), and synced receipts' lines are read-only (409).
Arguments: id* (string), productId (string), quantity (number), unitPrice (number), lineDiscount (number,null)
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 product 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 product 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), containsProductId keeps returns with a line whose receipt line references that product, 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 product 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), containsProductId (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)
Samples
create_sample_request ✏️
Request free product for an event, a partner or a buyer, the same as the Samples form. Send purpose, neededBy (YYYY-MM-DD, required), eventDate (optional), the recipient (name, optional company and phone, residential flag, and address line1, line2, city, state, postalCode; US addresses), liftGate, notes, and items: each line is a Shopify listing's variantId with a whole quantity counted in that listing (a 24-pack counts 1). Only listings mapped to a product can be sent: find them with list_connected_product_mappings (source=shopify, mapped=true), whose rows carry the variantId and the product; a listing the org hides from samples in its settings is refused. delivery is one of the org's delivery option labels, or omit it for no preference (the warehouse default applies). The request gets the org's next number and each line an estimated cost (its product's average receipt cost over the year before it was submitted, by default); the request's cost is them added up. If the org requires approval it waits for an approver in Pharus, unless the caller is an approver or the org only requires approval above an amount and the cost is at or under it with every line priced; otherwise Pharus creates it in the org's Shopify store right away as a draft or an order with a 100% discount, tagged with the sample tag and the request number. That fails without saving anything when the store's write access is not connected (409) or Shopify refuses (502).
Arguments: purpose* (string), eventDate (string,null), neededBy* (string), recipient* (object), delivery (string,null), liftGate (boolean), notes (string,null), items* (array)
get_sample_request
Get one sample request by id (from list_sample_requests or create_sample_request): the same header as a list row (number, purpose, dates, recipient, requester, status, the cost, the decision with who, when and a rejection's reason, and the Shopify link once it was created there) plus its lines under items, each with the Shopify listing it sends (variantId, listingTitle, listingSku), the Pharus product it is mapped to (null if that product was deleted since), the quantity counted in that listing, and unitCost (per listing) and cost (the line), estimates while estimated and final once the receipt drew the stock. viewer says whether the caller may edit, delete or decide it right now; those actions are done in the web client.
Arguments: id* (string)
list_sample_requests
List one page of the org's sample requests, every requester's, newest first. Each row has its number, purpose, dates, recipient, requester, status (Waiting, Approved, Rejected, or Sent when it needed no approval), the decision (who, when, and the reason for a rejection), its cost (the lines added up, estimated until the receipt its Shopify order came back as has drawn the stock, with the count of lines that had no cost), the receipt once synced (its id for get_sales_receipt, number and externalOrderId), its lines in short (each labeled with the listing's code and our product's name, e.g. 600008PP - Cherry Berry 4-Pack, and its quantity), and the Shopify link once it was created there. Narrow instead of listing everything: status and requestedById (user ids) take comma-separated values, requestedFrom/requestedTo and neededByFrom/neededByTo are inclusive date pairs, and search matches the number, purpose, recipient, company, city and notes. Page with limit (default 50, max 200) and offset; meta.total counts the filtered set. Sort with sort (number, requested, requestedBy, purpose, neededBy, status) and order.
Arguments: sort (string), order (string), limit (integer), offset (integer), status (string), requestedById (string), search (string), requestedFrom (string), requestedTo (string), neededByFrom (string), neededByTo (string)
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 by name, 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), 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_amazon_performance
Is Amazon making money, and why: the Amazon channel's P&L for a date range (from, to as YYYY-MM-DD), from gross sales down to contribution after inventory cost, Amazon's referral and FBA fees, merchant-fulfilled postage, ad spend, storage and account fees. Use it for any question about Amazon revenue, margins, fees, TACoS, ad efficiency, FBA versus merchant-fulfilled economics, which products make or lose money on Amazon, or which ad campaigns spend without selling; do not rebuild it from sales receipts, the fees, ads and postage are not stored on receipts. Pass grain=week or month (default month) for the bucket series and historyFrom to extend it back for trends; totals.range and totals.previous compare the range with the previous period of the same length. Every bucket and total has total, fba and mfn views; ads, account fees and contribution exist only on total. products gives per-product contribution with an estimated ad share; campaigns lists Sponsored Products and Sponsored Brands campaigns with spend, clicks, attributed orders and sales (ACoS = spend / attributedSales). placements shows where Sponsored Products ads ran and how each placement converted; searchTerms flags the search terms spending without selling ($5 or more with no attributed sale) and the terms selling best, which is where to add negative keywords or raise bids. Attributed sales flagged provisional are still growing (14-day attribution). Before quoting fees, ads or postage check available: a false group reads zero or null because its feed is not connected, not fully synced or failed (errors says which), and must not be read as zero cost. Fees and refunds are dated by Amazon's posting, not the order, so a recent period's fees lag its orders.
Arguments: from* (string), to* (string), grain (string), historyFrom (string)
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. Sales receipts marked as samples (free product sent through the Samples page) count in neither orders nor revenue, while their operational costs and COGS still count.
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)
get_shopify_performance
Is the Shopify store making money, and why: the store's direct-to-consumer P&L for a date range (from, to as YYYY-MM-DD), from gross sales down to contribution after inventory cost, payment fees, disputes, 3PL postage and Meta and Google ad spend. Use it for any question about Shopify or DTC revenue, margins, refunds, processing fees, chargebacks, shipping cost, ad efficiency (MER = netRevenue / adSpend on the dtc view; blended CAC = adSpend / newCustomers), new versus returning customers, which products make or lose money in the store, which sales channels feed it, or which Meta and Google campaigns spend without converting; do not rebuild it from sales receipts, the fees, refunds, disputes, ads, postage and channels are not stored on receipts. Pass grain=week or month (default month) for the bucket series and historyFrom to extend it back for trends; totals.range and totals.previous compare the range with the previous period of the same length. Every bucket and total has views.dtc (the headline: the store's checkout, subscriptions, Shop app, social and point of sale; the only view with adSpend and contribution), views.all and views.byChannel keyed by the channels list, which also says which channels are left out of dtc and why (marketplaces and wholesale apps like TikTok Shop or Faire, draft orders, free samples). products gives per-product contribution in the dtc scope with fees and ads split by revenue (estimates); campaigns lists Meta and Google campaigns with spend, clicks, platform-reported conversions and, for Google, conversion value. Platform-reported conversions overlap and overclaim; MER against all dtc revenue is the honest efficiency figure. Before quoting a figure check available: a false group reads zero or null because its feed is not connected, not synced or failed (errors says which), and must not be read as zero cost; ads.partial means a platform's history starts inside the period or stops before its end, ads.provisional that conversions are still being credited. estimatedPaymentFees is the part of paymentFees estimated at 2.9% + $0.30 for card payments outside Shopify Payments.
Arguments: from* (string), to* (string), grain (string), historyFrom (string)