data-model / data-model-f1f5c163d194Markdown ↓

Explore these examples

Edit the published examples in place. Running an example sends a real request to the instance you choose. Nothing runs when you open the page.

Checking sign-in…

Choose existing records

Read records from the selected instance. Nothing is created. Select a record to use it in the walkthrough.

Sign in and choose an instance to browse records.

Selected records survive page navigation in this tab for this sign-in and instance. Switching instances or signing out clears them. Responses and editor drafts stay on this page.

Working with the Data Model — ERP

Use ERP to work with customers, agree what they are buying, record orders, arrange purchasing, raise invoices and keep track of payments. It brings the commercial records together so you can follow a sale from the first conversation to its account balance, then compare the printing job’s actual costs with the price agreed.

Use IaC for infrastructure and provisioning. Use the API for runtime data. Customer records, estimates, orders, invoices and payments are runtime data. Provision users, teams, permissions, integrations, business units, cost centres, suppliers, products, tax codes, price brackets, cost categories, cost rates and operation cost mappings through IaC. You can read configured business units and cost centres when working with commercial records.

This guide brings CRM, all commercial operations and costing together across the commerce and costing topics. Some also belong to production or logistics: creating jobs from an order and checking fulfilment are part of the commercial handover. Shared discovery and help operations are listed at the end so you can find them without confusing them with the steps of a sale.

Choose where to start

What you need to do Start here
Register a customer, contact or address CRM
Record an agreed purchase from a customer Orders
Check the configured costs before quoting Cost configuration
Record and review costs incurred on a job Actual job costs
Prepare a price before the customer commits Estimates
Turn an order into work to produce Production handover
Bill a customer and record payment Invoices and payments
Buy materials or services from a supplier Purchasing
Maintain products, prices and tax codes Products, pricing and tax
Read configured business units and cost centres Business structure
Check whether an order can be completed Fulfilment
Record a conversation or operational instruction Notes

The ERP API reference lists arguments and return types for every operation, followed by record fields, inputs and enums. The workflow sections below explain when to use them.

Read and change records safely

A query reads information. A mutation changes it. Send the operation and its variables separately to your environment's GraphQL API using an authenticated account. The permissions for each resource still apply, including when you request related records inside another result.

The examples use illustrative names, dates, amounts and identifiers. Replace identifiers with those returned by your own requests. The examples are recipes, not one script: creating an order directly and converting an estimate are alternative ways to obtain an order.

Check top-level GraphQL errors and any errors { path message } returned inside the mutation result. Continue only when the expected record is present and the errors are empty. Keep the returned IDs for later requests. A successful HTTP response alone does not confirm that the requested change succeeded.

Start with create… for a new record and update… for changes to an existing record. These are the recommended operations for everyday work: the request makes your intention clear, and an update lets you supply the fields you want to change. Check each input’s treatment of omitted fields, nulls and nested values.

Treat upsert… as an advanced option. Use it when you deliberately need its create-or-update behaviour and understand how it identifies records, which fields it requires and whether it replaces child collections. It is not a general shortcut for a partial update or a promise to match an external reference.

Some resources expose only an upsert operation. Where that is the available route, this guide explains how to use it; do not invent a create or update operation that the API does not provide.

Separate mutations are separate requests. Use a combined operation when its behaviour matches the task, such as createOrderGraph for an order and its lines or recordPayment for a payment and the associated balances. If a response is lost, read the relevant records before retrying a creation or payment; a reference string is not a general promise of retry safety.

Money values belong with their currency. Check line amounts, document totals, discounts and tax together. A required amount in an input must be supplied; do not assume that storing a line recalculates every related document. The amounts below simplify the arithmetic and do not prescribe a tax treatment.

CRM — working with customers, contacts and addresses

Use the CRM API to keep customer information current as your business works with people: register a new customer, add the person you speak to, record where goods should go and update details when they change.

Use IaC for infrastructure and provisioning. Use the API for runtime data. Customer records, contact details and delivery addresses are operational data. The users, teams, permissions and integrations that support this work belong in your IaC configuration.

This chapter follows one customer, Northstar Packaging, from an initial lookup to a customer record with a contact and shipping address. It then shows how to read the information together, make a partial update and browse active customers. A query reads information; a mutation changes it.

Understand the records

Record GraphQL type What it represents
Customer CustomerState A business or individual you work with
Contact ContactState A person associated with a customer
Address AddressState An address used for billing, shipping or both

Each record has its own identifier. A contact's customerId connects the person to a customer. For a customer's address, supply customerId; you can also supply contactId when it is associated with a particular person.

A contact is a business contact, not a CoCoCo user account. Creating one does not provision login access.

id is the identifier assigned by CoCoCo. externalId is an optional reference you supply, such as a customer number from another business system. Keep both when integrating systems: use externalId to find the corresponding record and the returned id to read or update it.

Before you begin

Send the examples to your environment's GraphQL API using an authenticated account. Requests operate in your authenticated tenant. Supply variables separately from the operation.

Task Required permission
Read a customer by ID or external ID customer:read
Browse customers customer:list
Create or update a customer customer:write
Read a contact by ID or lookup contact:read
Browse contacts, including CustomerState.contacts contact:list
Create or update a contact contact:write
Read an address by ID or external ID address:read
Browse addresses, including CustomerState.addresses address:list
Create or update an address address:write

The examples use illustrative identifiers and responses. Replace them with the values returned by your own calls. Run each step only after the previous step has succeeded; the separate mutations are separate requests, not one transaction.

Find a customer before creating one

Suppose Northstar Packaging is known as CRM-1042 in your customer system. Check whether that reference already exists in CoCoCo:

query FindCustomer($externalId: String!) {
  getCustomerBy(externalId: $externalId) {
    id
    externalId
    name
    email
    isActive
  }
}

Variables:

{
  "externalId": "CRM-1042"
}

If the customer exists, an illustrative response is:

Illustrative response — not a live result
{
  "data": {
    "getCustomerBy": {
      "id": "cus_01h455vb4pex5vsknk084sn02q",
      "externalId": "CRM-1042",
      "name": "Northstar Packaging",
      "email": "orders@northstar.example",
      "isActive": true
    }
  }
}

Keep the returned id and continue with that customer. A null result with no GraphQL errors means no customer matched the external ID in this tenant. Omitting the selector also returns null, so always supply the reference you intend to find.

If you do not have an external ID, use listCustomers with a name or email filter, then inspect the matches. A name search can return several records; do not treat the first match as an exact identity lookup.

Register a new customer

Use this step when the lookup found no existing customer:

mutation RegisterCustomer($input: CreateCustomerInput!) {
  createCustomer(input: $input) {
    customer { id externalId name email isActive }
    errors { path message }
  }
}

Variables:

{
  "input": {
    "name": "Northstar Packaging",
    "externalId": "CRM-1042",
    "email": "orders@northstar.example",
    "isActive": true
  }
}

A successful result contains the new customer, including its assigned id, and errors: []. Only name is required. A new customer is active by default when isActive is omitted.

A supplied external ID must be unique among customers in the tenant. If creation reports that it already exists, look up that reference again and inspect the record. Creating a customer does not automatically create a contact or an address.

Check both the top-level GraphQL errors and the mutation's errors list. An HTTP response alone does not establish success. Continue only when there are no errors and the expected customer is present.

Add a customer contact

Add Maya Chen as the person who coordinates orders for Northstar Packaging:

mutation AddCustomerContact($input: CreateContactInput!) {
  createContact(input: $input) {
    contact { id customerId firstName lastName email jobTitle isPrimary }
    errors { path message }
  }
}

Variables:

{
  "input": {
    "customerId": "cus_01h455vb4pex5vsknk084sn02q",
    "firstName": "Maya",
    "lastName": "Chen",
    "email": "maya.chen@northstar.example",
    "jobTitle": "Operations Manager",
    "isPrimary": true
  }
}

Illustrative response:

Illustrative response — not a live result
{
  "data": {
    "createContact": {
      "contact": {
        "id": "con_01h455vb4pex5vsknk084sn02r",
        "customerId": "cus_01h455vb4pex5vsknk084sn02q",
        "firstName": "Maya",
        "lastName": "Chen",
        "email": "maya.chen@northstar.example",
        "jobTitle": "Operations Manager",
        "isPrimary": true
      },
      "errors": []
    }
  }
}

The customer must already exist in your tenant. Keep the contact ID if you need to update Maya's details or associate an address with her.

isPrimary records whether this is a primary contact. Setting it to true does not automatically unset the flag on another contact. When changing the designated primary contact, read the current contacts and update the relevant flags explicitly. The default for a new contact is false.

Record a shipping address

Create an address associated with the customer and Maya:

mutation AddShippingAddress($input: CreateAddressInput!) {
  createAddress(input: $input) {
    address {
      id customerId contactId addressType
      street1 city postalCode country timezone isDefault
    }
    errors { path message }
  }
}

Variables:

{
  "input": {
    "customerId": "cus_01h455vb4pex5vsknk084sn02q",
    "contactId": "con_01h455vb4pex5vsknk084sn02r",
    "addressType": "SHIPPING",
    "street1": "18 Example Street",
    "city": "Berlin",
    "postalCode": "10115",
    "country": "DE",
    "timezone": "Europe/Berlin",
    "isDefault": true
  }
}

A successful result returns the saved address with its assigned AddressID and an empty errors list. Read back the customer and contact IDs as well as the address details to confirm the association.

addressType is BILLING, SHIPPING or BOTH. Use a two-letter country code, such as DE. timezone is an optional IANA timezone name; null in a returned address means the timezone is unknown.

Choose a contact belonging to the intended customer. isDefault records the default flag for this address; setting it does not automatically clear other addresses' flags. A new address defaults to false when you omit it.

Read the customer, contacts and addresses together

Once the records exist, retrieve the customer and the related details in one query:

query ReadCustomerOverview($id: CustomerID!) {
  getCustomer(id: $id) {
    id name email phone notes isActive
    contacts {
      id firstName lastName email phone jobTitle isPrimary
    }
    addresses {
      id contactId addressType street1 street2
      city state postalCode country timezone isDefault
    }
  }
}

Variables:

{
  "id": "cus_01h455vb4pex5vsknk084sn02q"
}

The result contains getCustomer.contacts and getCustomer.addresses as lists. Maya should appear in the contact list, and the shipping address should carry her contact ID.

This selection requires customer:read, contact:list and address:list. Permission to read the customer does not by itself grant access to the related lists. If you only need the customer's details, leave the relationship fields out of the selection.

For separate, paginated lists, use listContacts or listAddresses with filter: {customerId: {eq: ...}}. The nested lists above do not accept pagination arguments.

Update details after a customer conversation

Suppose Northstar asks your team to use a new order email address and to call before delivery. Update those details without resending the rest of the customer record:

mutation UpdateCustomerDetails($id: CustomerID!, $input: UpdateCustomerInput!) {
  updateCustomer(id: $id, input: $input) {
    customer { id email notes updatedAt }
    errors { path message }
  }
}

Variables:

{
  "id": "cus_01h455vb4pex5vsknk084sn02q",
  "input": {
    "email": "purchasing@northstar.example",
    "notes": "Call the receiving contact before delivery."
  }
}

Confirm that the returned customer contains the new email and note. Omitted business fields retain their current values. The update refreshes updatedAt unless you supply an explicit timestamp override.

notes is one general text field. Supplying it replaces that text; it does not append a dated conversation entry. Read the existing note first when you want to preserve information already recorded there.

For updateCustomer, updateContact and updateAddress, an explicit null is treated like an omitted business field: it leaves the current value unchanged. Do not send null expecting to clear an optional value. false, however, is a supplied Boolean value and changes a flag to false.

Use updateContact(id:, input:) for Maya's personal email or phone number. Changing the customer's email does not change the contact's email. Similarly, use updateAddress to change the delivery location.

Browse active customers

Find active customers whose names contain “Northstar”:

query BrowseCustomers($filter: CustomerFilterInput!, $after: String) {
  listCustomers(
    first: 20
    after: $after
    filter: $filter
    sort: [{field: NAME, direction: ASC}]
  ) {
    edges {
      node { id name externalId email isActive }
      cursor
    }
    pageInfo { hasNextPage endCursor }
    totalCount
  }
}

Variables for the first page:

{
  "filter": {
    "name": {
      "contains": "Northstar"
    },
    "isActive": {
      "eq": true
    }
  },
  "after": null
}

Read each customer from edges[].node. If pageInfo.hasNextPage is true, repeat the same query with after set to the returned endCursor. Keep the filter and sort unchanged. Cursors are opaque strings; pass them back unchanged.

To browse all active customers, remove the name condition from the variables. To include inactive customers too, omit the isActive condition. totalCount is the number matching the filter, not the number on the current page.

Mark a customer inactive

When the customer relationship is no longer active, retain the record and change its status:

mutation MarkCustomerInactive($id: CustomerID!) {
  updateCustomer(id: $id, input: {isActive: false}) {
    customer { id name isActive }
    errors { path message }
  }
}

Variables:

{
  "id": "cus_01h455vb4pex5vsknk084sn02q"
}

Confirm isActive: false with no errors. The customer remains readable by ID, and the active-customer query above excludes it. This flag does not delete the customer or its related records. Set isActive: true through updateCustomer to reactivate it.

Query reference

! means an input is required or an output cannot be null. Brackets describe a list. Nullable single-record queries can return null when no matching record is found.

Query Arguments Result Use it to
getCustomer id: CustomerID! CustomerState Read one customer and optionally its related records
getCustomerBy externalId: String CustomerState Find a customer by an external reference; supply the selector
listCustomers first: Int, last: Int, after: String, before: String, filter: CustomerFilterInput, sort: [CustomerSortInput!] CustomerConnection! Browse matching customers
getContact id: ContactID! ContactState Read a contact by its own identifier
getContactBy externalId: String, email: String ContactState Look up a contact using a supplied selector
listContacts first: Int, last: Int, after: String, before: String, filter: ContactFilterInput, sort: [ContactSortInput!] ContactConnection! Browse contacts, optionally within a customer
getAddress id: AddressID! AddressState Read one address
getAddressBy externalId: String AddressState Find an address by an external reference
listAddresses first: Int, last: Int, after: String, before: String, filter: AddressFilterInput, sort: [AddressSortInput!] AddressConnection! Browse addresses, optionally within a customer or contact

The list queries accept first: Int, after: String, last: Int and before: String. For forward pagination, use first and after as shown above. Use getContact(id:) when you already know which contact you want; use a customer-filtered listContacts when an email could identify more than one person.

Mutation reference

Mutation Arguments Result Use it to
createCustomer input: CreateCustomerInput! UpsertCustomerPayload! Register a customer
updateCustomer id: CustomerID!, input: UpdateCustomerInput! UpsertCustomerPayload! Change selected customer fields
createContact input: CreateContactInput! UpsertContactPayload! Add a person to a customer
updateContact id: ContactID!, input: UpdateContactInput! UpsertContactPayload! Change selected contact fields
createAddress input: CreateAddressInput! UpsertAddressPayload! Record an address
updateAddress id: AddressID!, input: UpdateAddressInput! UpsertAddressPayload! Change selected address fields
deleteCustomer id: CustomerID! DeletePayload! Request customer deletion; requires customer:delete
deleteContact id: ContactID! DeleteContactPayload! Request contact deletion; requires contact:delete
deleteAddress id: AddressID! DeleteAddressPayload! Request address deletion; requires address:delete
upsertCustomer input: UpsertCustomerInput! UpsertCustomerPayload! Advanced: Create or update a customer using a complete record
upsertContact input: UpsertContactInput! UpsertContactPayload! Advanced: Create or update a contact using a complete record
upsertAddress input: UpsertAddressInput! UpsertAddressPayload! Advanced: Create or update an address using a complete record

Deletion is different from marking a customer inactive. Existing references can prevent deletion; inspect the result rather than assuming it succeeded.

Advanced: upsert. Prefer the explicit create and update operations above for everyday CRM work. upsertCustomer, upsertContact and upsertAddress are also available, each taking its corresponding Upsert…Input! and returning the same payload type as creation. Their inputs describe a complete record and can replace optional values with defaults or empty values. Use the explicit create and update operations above for the tasks in this chapter. An upsert is not a request to find a record by matching its external ID.

Type reference

Customer fields

Fields Types Meaning
id CustomerID! Assigned CoCoCo identifier
name String! Business or individual name
externalId String Reference from another system
email, phone, notes String General contact details and customer note
isActive Boolean! Whether the customer is active
creditLimit, creditCurrency Float, CurrencyISO Recorded credit limit and its currency
paymentTermsDays Int Recorded net payment terms; 0 means prepayment, null means not set
isCreditHold, creditHoldReason Boolean!, String Credit-hold flag and explanation
defaultTaxCodeId TaxCodeID Reference to an existing tax code
isTaxExempt, taxExemptReference Boolean!, String Tax-exemption flag and reference
createdAt, updatedAt String! ISO 8601 timestamps
contacts, addresses [ContactState!]!, [AddressState!]! Related records, with their own read permissions

Credit and tax fields record customer-specific values. This chapter does not configure tax codes or define how downstream ordering and invoicing processes apply those values.

Contact and address fields

Type Fields
ContactState identity and relationship id: ContactID!, customerId: CustomerID!, externalId: String
Contact name and details firstName: String!, lastName: String!, email: String, phone: String, jobTitle: String, isPrimary: Boolean!
AddressState identity and relationships id: AddressID!, customerId: CustomerID, contactId: ContactID, supplierId: SupplierID, externalId: String
Address location addressType: AddressType!, street1: String!, street2: String, city: String!, state: String, postalCode: String, country: String!, timezone: String, isDefault: Boolean!
Timestamps on both types createdAt: ISO8601!, updatedAt: ISO8601!

AddressType has values BILLING, SHIPPING and BOTH. The shared address type also supports supplier addresses; for the customer tasks here, supply the customer relationship. Identifiers are opaque strings: retain the complete value, including its prefix.

Inputs for these tasks

Input Required fields Optional fields
CreateCustomerInput name: String! externalId: String, email: String, phone: String, notes: String, isActive: Boolean, creditLimit: Float, creditCurrency: CurrencyISO, paymentTermsDays: Int, isCreditHold: Boolean, creditHoldReason: String, defaultTaxCodeId: TaxCodeID, isTaxExempt: Boolean, taxExemptReference: String, createdAt: ISO8601, updatedAt: ISO8601
UpdateCustomerInput None inside input; the mutation requires id name: String, externalId: String, email: String, phone: String, notes: String, isActive: Boolean, creditLimit: Float, creditCurrency: CurrencyISO, paymentTermsDays: Int, isCreditHold: Boolean, creditHoldReason: String, defaultTaxCodeId: TaxCodeID, isTaxExempt: Boolean, taxExemptReference: String, updatedAt: ISO8601
CreateContactInput customerId: CustomerID!, firstName: String!, lastName: String! externalId: String, email: String, phone: String, jobTitle: String, isPrimary: Boolean
UpdateContactInput None inside input; the mutation requires id customerId: CustomerID, externalId: String, firstName: String, lastName: String, email: String, phone: String, jobTitle: String, isPrimary: Boolean
CreateAddressInput addressType: AddressType!, street1: String!, city: String!, country: String! timezone: String, customerId: CustomerID, contactId: ContactID, supplierId: SupplierID, externalId: String, street2: String, state: String, postalCode: String, isDefault: Boolean
UpdateAddressInput None inside input; the mutation requires id timezone: String, customerId: CustomerID, contactId: ContactID, supplierId: SupplierID, externalId: String, addressType: AddressType, street1: String, street2: String, city: String, state: String, postalCode: String, country: String, isDefault: Boolean

Optional input fields use the corresponding record field's type without !. Customer timestamp overrides use ISO8601, rather than the returned timestamp's String type. Omit timestamp overrides for ordinary day-to-day edits. Customer creation defaults isActive to true, isCreditHold to false and isTaxExempt to false.

Filters and sorting

List Filter fields Sort fields
Customers name, externalId, email, phone, isActive, createdAt, updatedAt NAME, CREATED_AT, UPDATED_AT
Contacts customerId, firstName, lastName, email, isPrimary, createdAt, updatedAt LAST_NAME, FIRST_NAME, CREATED_AT, UPDATED_AT
Addresses customerId, contactId, externalId, addressType, city, country, isDefault, createdAt, updatedAt CITY, COUNTRY, CREATED_AT, UPDATED_AT

Each sort entry supplies field and direction, with ASC or DESC as the direction. String filters support eq, neq, contains, startsWith, endsWith, in and notIn. Relationship filters use an ID filter, for example {customerId: {eq: "cus_..."}}. Boolean filters use {eq: true} or {eq: false}. The address-type filter is a string filter even though the record field is an enum; use the enum's spelling, such as {addressType: {eq: "SHIPPING"}}.

Results and errors

UpsertCustomerPayload contains customer and errors. Contact and address payloads contain contact or address and errors. Deletion payloads contain success and errors.

Each FieldError supplies a path and message; a stable code may also be available. Correct the indicated field before retrying. Top-level GraphQL errors can also report permission failures or other request problems.

Paginated results contain edges, pageInfo and totalCount. Each edge holds a node and cursor. Use pageInfo.hasNextPage and pageInfo.endCursor to continue reading forward.

How this fits the schema

These operations belong to the schema's commerce topic, declared through @topic(names: ["commerce"]). CRM is the customer-facing part of that broader topic covered here.

A topic helps you discover related operations. It does not decide whether an operation belongs in a runtime workflow: this guide uses customer, contact and address data operations, while infrastructure and provisioning remain in the IaC configuration.

Review the customer's commercial account

Once the customer has estimates, orders or invoices, customerAccountSummary brings them together:

query ReadCommerceCustomerAccount($customerId: CustomerID!) {
  customerAccountSummary(customerId: $customerId) {
    customer { id name }
    orders { id orderNumber totalAmount currency }
    estimates { id estimateNumber totalAmount currency }
    invoices { id invoiceNumber totalAmount paidAmount currency }
    totalInvoiced totalPaid totalOutstanding
    errors { path message }
  }
}

Variables:

{
  "customerId": "cus_01h455vb4pex5vsknk084sn02q"
}

Inspect each document's currency when interpreting the totals. The summary's total fields do not themselves carry a currency; use the individual invoices when you need a breakdown by currency. Optional date arguments can narrow the review.

The complete customer operation list, including this account view, is:

Operation Use it to
Query.getCustomer Read customer by identifier
Query.getCustomerBy Read customer using a supplied lookup value
Query.listCustomers Browse customers
Query.listContacts Browse contacts
Query.getContact Read contact by identifier
Query.getContactBy Read contact using a supplied lookup value
Query.getAddress Read address by identifier
Query.getAddressBy Read address using a supplied lookup value
Query.listAddresses Browse addresses
Query.customerAccountSummary Review a customer’s orders, estimates, invoices and account totals
Mutation.upsertCustomer Advanced: Create or update a customer. Prefer create/update for routine changes.
Mutation.createCustomer Create a new customer
Mutation.updateCustomer Change selected customer fields
Mutation.deleteCustomer Delete a customer
Mutation.upsertContact Advanced: Create or update a contact. Prefer create/update for routine changes.
Mutation.createContact Create a new contact
Mutation.updateContact Change selected contact fields
Mutation.deleteContact Delete a contact
Mutation.upsertAddress Advanced: Create or update a address. Prefer create/update for routine changes.
Mutation.createAddress Create a new address
Mutation.updateAddress Change selected address fields
Mutation.deleteAddress Delete a address

Orders — record what the customer is buying

An order is the customer's commercial commitment. Its header records the customer, order number, dates, amounts and overall status. Its lines describe the products or services, quantities and prices. OrderState is the header; OrderLineState is an individual line.

An order is different from a purchase order: the order records what your customer buys from you; a purchase order records what you buy from a supplier.

Find an existing order

When another system supplies a stable reference, look for it before creating a second order:

query FindCommerceOrder($externalId: String!) {
  getOrderBy(externalId: $externalId) { id orderNumber status paymentStatus customerId }
}

Variables:

{
  "externalId": "SALES-2042"
}

Keep the returned id. If you already have that ID, use getOrder. Use listOrders to browse by customer, order number or status; inspect matches rather than treating a partial number search as a unique lookup.

Create an order with its lines

createOrderGraph creates the header and lines together. It can also create a customer and billing or shipping addresses in the same request. Here the customer already exists:

mutation CreateCommerceOrder($input: CreateOrderGraphInput!) {
  createOrderGraph(input: $input) {
    order { id orderNumber status totalAmount currency }
    lines { id lineNumber productName quantity lineTotal }
    errors { path message }
  }
}

Variables:

{
  "input": {
    "order": {
      "orderNumber": "ORD-2042",
      "externalId": "SALES-2042",
      "customerId": "cus_01h455vb4pex5vsknk084sn02q",
      "currency": "EUR",
      "subtotalAmount": 1000,
      "taxAmount": 0,
      "totalAmount": 1000
    },
    "lines": [
      {
        "lineNumber": 1,
        "productName": "Packaging sleeves",
        "quantity": 500,
        "unitPrice": 2,
        "lineTotal": 1000,
        "currency": "EUR"
      }
    ]
  }
}

This request requires order:write and, because it contains lines, orderline:write. Creating a nested customer requires customer:write; creating nested addresses requires address:write. Attaching files requires the relevant file permissions as well.

Use either an existing customer ID or a nested customer, never both. Apply the same choice to each address. Although the nested input types have names beginning with Upsert, this operation creates new records: omit nested id fields and each line's orderId. The new order supplies the line relationship.

Read the returned order and line IDs before proceeding. For separate steps, use createOrder and createOrderLine; those requests do not provide the same all-together creation boundary. Use updateOrder and updateOrderLine to change existing records.

Follow the order and browse work

Use orderLifecycle when you want the order in context:

query ReadCommerceOrderLifecycle($orderId: OrderID!) {
  orderLifecycle(orderId: $orderId) {
    order { id orderNumber status paymentStatus }
    orderLines { id lineNumber quantity }
    estimate { id estimateNumber }
    invoices { id invoiceNumber totalAmount paidAmount currency }
    jobs { id name }
    errors { path message }
  }
}

Variables:

{
  "orderId": "ord_01h455vb4pex5vsknk084sn02q"
}

You choose which related records to request. This example reads the estimate, invoices and production jobs; the same result also offers shipment information.

To browse confirmed orders for one customer:

query BrowseCommerceOrders($customerId: ID!, $after: String) {
  listOrders(first: 20, after: $after, filter: {customerId: {eq: $customerId}, status: {eq: CONFIRMED}}, sort: [{field: ORDER_DATE, direction: DESC}]) {
    edges { cursor node { id orderNumber status totalAmount currency } }
    pageInfo { endCursor hasNextPage }
  }
}

Variables:

{
  "customerId": "cus_01h455vb4pex5vsknk084sn02q",
  "after": null
}

For the next page, pass endCursor as after while keeping the same filter and sort. Stop when hasNextPage is false.

The available order status values are DRAFT, CONFIRMED, IN_PRODUCTION, PARTIALLY_SHIPPED, SHIPPED, DELIVERED, COMPLETED, CANCELLED, ON_HOLD. These values describe the supported states; they do not make every transition appropriate. bulkUpdateOrderStatuses changes several orders in one request. Inspect its result for the orders changed and any reported errors. Use the fulfilment check below when completing an order; changing a status does not itself dispatch or deliver goods.

Operation Use it to
Query.getOrder Read order by identifier
Query.getOrderBy Read order using a supplied lookup value
Query.listOrders Browse orders
Query.getOrderLine Read order line by identifier
Query.listOrderLines Browse order lines
Query.orderLifecycle Read an order together with its lines, estimate, invoices, shipments and jobs
Mutation.upsertOrder Advanced: Create or update a order. Prefer create/update for routine changes.
Mutation.createOrder Create a new order
Mutation.updateOrder Change selected order fields
Mutation.createOrderGraph Create an order, its lines and optional customer and addresses together
Mutation.deleteOrder Delete a order
Mutation.upsertOrderLine Advanced: Create or update a order line. Prefer create/update for routine changes.
Mutation.createOrderLine Create a new order line
Mutation.updateOrderLine Change selected order line fields
Mutation.deleteOrderLine Delete a order line
Mutation.bulkUpdateOrderStatuses Change the status of several orders in one request

Read configured costs and check quoting readiness

Before pricing a printing job, establish what its production work costs. A cost category describes the kind of cost and the unit you measure. A cost rate supplies an amount and currency for that category. An operation cost mapping connects a production operation to the category used when calculating an estimate.

For example, press time might be measured in hours, while a material category uses sheets. The quantity and rate must use the same unit: 2.5 hours at EUR 75 per hour is EUR 187.50. Finished copies, press sheets and hours are different quantities, even when they belong to the same job.

Provision cost categories, cost rates and operation cost mappings through IaC. Read those configured records through the API when preparing estimates or recording actual costs. Keep the configuration review separate from entering the costs of an individual job.

Find the category and rate

Find a configured category by its agreed code:

query FindERPCostCategory($code: String!) {
  getCostCategoryBy(code: $code) { id code name unit isActive }
}

Variables:

{
  "code": "PRESS_TIME"
}

Check its unit and active flag before using the returned ID. The example code is illustrative; it does not create a press-time category. Use listCostCategories when browsing, and listCostRates or getCostRate to inspect the selected rate, currency, version and effective dates. A cost centre can distinguish where the cost applies.

Rate versions matter when comparing work priced at different times. The reference exposes both in-place updates and version-producing upserts; their behaviour is different. Manage rate changes through IaC with the intended history in mind, rather than assuming every change creates a new version.

Check the configuration before automated quoting

Run the readiness check before relying on automated estimates:

query CheckERPQuotingReadiness {
  quotingReadinessCheck {
    isReady summary configuredOperationKinds unconfiguredOperationKinds
    unsupportedOperationKinds categoriesWithoutRates warnings
  }
}

Variables:

{}

Read the summary and warnings as well as isReady. Missing operation mappings, categories without current rates and missing work-centre capabilities can leave the configuration incomplete. unsupportedOperationKinds identifies registered kinds that no configured work centre supports; that list is informational rather than a readiness failure by itself.

Readiness checks the costing configuration. It does not promise that a particular brochure can be produced, that its specification is complete or that a proposed delivery date is achievable. Review the individual estimate's feasibility and diagnostics as well.

exportCostConfiguration provides a CSV view for reviewing configured costs. importCostConfiguration remains an advanced bulk-import tool in the reference; use IaC for routine provisioning. An import can skip invalid rows while applying valid ones, and replacement mode deactivates omitted configuration. Its presence is not an invitation to maintain a second, competing source of configuration.

Operation Use it to
Query.getCostCategory Read a configured cost category and its unit.
Query.getCostCategoryBy Find a configured cost category by code or external reference.
Query.listCostCategories Browse configured cost categories.
Query.getCostRate Read a configured rate, currency and version.
Query.listCostRates Browse configured rates and their effective dates.
Query.getOperationCostMapping Read the cost mapping for a production operation.
Query.listOperationCostMappings Browse configured operation cost mappings.
Query.quotingReadinessCheck Check for gaps in the configuration used for automated quoting.
Mutation.createCostCategory Manage through IaC: create a cost category.
Mutation.updateCostCategory Manage through IaC: change a cost category.
Mutation.upsertCostCategory Manage through IaC: create or update a cost category.
Mutation.deleteCostCategory Manage through IaC: delete a cost category.
Mutation.upsertCostRate Manage through IaC: create or update a cost rate.
Mutation.createCostRate Manage through IaC: create a cost rate.
Mutation.updateCostRate Manage through IaC: change a cost rate.
Mutation.upsertOperationCostMapping Manage through IaC: create or update an operation cost mapping.
Mutation.createOperationCostMapping Manage through IaC: create an operation cost mapping.
Mutation.updateOperationCostMapping Manage through IaC: change an operation cost mapping.
Mutation.deleteOperationCostMapping Manage through IaC: delete an operation cost mapping.
Mutation.exportCostConfiguration Export configured costs and mappings for review.
Mutation.importCostConfiguration Advanced configuration import; manage routine provisioning through IaC.
Mutation.deleteCostRate Manage through IaC: delete an unused cost rate; dependent costs or later versions prevent deletion.

Estimates — prepare and revise an offer

An estimate records proposed quantities, prices and totals before an order is agreed. EstimateState is the offer; EstimateLineState describes one proposed item. Cost breakdowns explain the calculation behind a line, while the estimate itself records the amount offered to the customer.

Prepare an estimate and a line

When supplying your own commercial amounts, create the estimate first:

mutation CreateCommerceEstimate($input: CreateEstimateInput!) {
  createEstimate(input: $input) {
    estimate { id estimateNumber status totalAmount currency }
    errors { path message }
  }
}

Variables:

{
  "input": {
    "estimateNumber": "EST-2042",
    "customerId": "cus_01h455vb4pex5vsknk084sn02q",
    "issueDate": "2026-10-21T09:00:00.000Z",
    "subtotalAmount": 1000,
    "taxAmount": 0,
    "totalAmount": 1000,
    "currency": "EUR"
  }
}

Use the returned estimate ID when adding its line:

mutation AddCommerceEstimateLine($input: CreateEstimateLineInput!) {
  createEstimateLine(input: $input) {
    estimateLine { id lineNumber productName quantity lineTotal }
    errors { path message }
  }
}

Variables:

{
  "input": {
    "estimateId": "est_01h455vb4pex5vsknk084sn02q",
    "lineNumber": 1,
    "productName": "Packaging sleeves",
    "quantity": 500,
    "unitPrice": 2,
    "lineTotal": 1000,
    "currency": "EUR"
  }
}

These are two requests. If the second fails, the estimate header still exists. Correct the error and continue with that estimate rather than creating another header. Keep the header totals consistent with the lines you intend to offer.

Use generateEstimate when you want an estimate calculated from production requirements instead of supplying all amounts yourself. Inspect its input and result in the reference; the calculated offer depends on the available production and costing information.

Convert the agreed offer into an order

After reviewing the offer with the customer, convert it with:

mutation ConvertCommerceEstimate($input: ConvertEstimateToOrderInput!) {
  convertEstimateToOrder(input: $input) {
    order { id orderNumber status }
    orderLines { id lineNumber quantity }
    updatedEstimate { id status convertedToOrderId }
    errors { path message }
  }
}

Variables:

{
  "input": {
    "estimateId": "est_01h455vb4pex5vsknk084sn02q",
    "orderNumber": "ORD-2043"
  }
}

Conversion creates an order and copies the estimate lines. It marks the estimate accepted and connects it to the new order. The estimate must not already be converted, rejected or expired. Keep the returned order ID; do not also run the direct order-creation example for the same sale.

For a changed offer, use reviseEstimate: it makes a new draft revision with copied lines and marks the original revised. An already accepted estimate cannot be revised this way. For a different customer, use duplicateEstimateForCustomer, reviewing any requested price adjustment before submitting it.

Estimate status values are DRAFT, SENT, ACCEPTED, REJECTED, EXPIRED, REVISED. Use getEstimate, getEstimateBy and listEstimates to find offers, and listEstimateCostBreakdowns to inspect the costs behind a line.

Operation Use it to
Query.getEstimate Read estimate by identifier
Query.getEstimateBy Read estimate using a supplied lookup value
Query.listEstimates Browse estimates
Query.getEstimateLine Read estimate line by identifier
Query.listEstimateLines Browse estimate lines
Query.listEstimateCostBreakdowns Read the cost breakdowns behind an estimate line
Mutation.upsertEstimate Advanced: Create or update a estimate. Prefer create/update for routine changes.
Mutation.createEstimate Create a new estimate
Mutation.updateEstimate Change selected estimate fields
Mutation.deleteEstimate Delete a estimate
Mutation.upsertEstimateLine Advanced: Create or update a estimate line. Prefer create/update for routine changes.
Mutation.createEstimateLine Create a new estimate line
Mutation.updateEstimateLine Change selected estimate line fields
Mutation.deleteEstimateLine Delete a estimate line
Mutation.convertEstimateToOrder Create an order and its lines from an estimate
Mutation.reviseEstimate Create a new draft revision with the original estimate’s lines
Mutation.generateEstimate Prepare an estimate from production requirements and calculated costs
Mutation.duplicateEstimateForCustomer Copy an estimate for another customer, optionally adjusting prices

From a sale to production

Use createJobsFromOrder when an existing order is ready to become production work. Each job entry identifies a line belonging to that order:

mutation CreateCommerceProductionJobs($input: CreateJobsFromOrderInput!) {
  createJobsFromOrder(input: $input) {
    order { id status }
    jobs { id name }
    errors { path message }
  }
}

Variables:

{
  "input": {
    "orderId": "ord_01h455vb4pex5vsknk084sn02q",
    "jobs": [
      {
        "orderLineId": "oln_01h455vb4pex5vsknk084sn02q",
        "jobName": "Northstar packaging sleeves",
        "quantity": 500
      }
    ]
  }
}

This operation requires job:write. Read the returned job IDs and order status before handing work to production. Product-template information on a line can determine how its production work is created. The manual example supplies a job name and quantity for one line; select the lines and quantities appropriate to the actual order.

If you are starting with an estimate and want to accept it, create its order and create production jobs in one operation, use acceptEstimateAndCreateJobs. Choose this combined path or the separate conversion and job-creation path for a sale; do not run both as successive steps.

The creation of jobs is the handover. Scheduling, production progress and machine operation belong to the Production guide. Artwork preparation belongs to Prepress.

Operation Use it to
Mutation.createJobsFromOrder Create production jobs from selected order lines
Mutation.acceptEstimateAndCreateJobs Accept an estimate and create its order and production jobs

Record and review actual job costs

An estimate records what you expected to charge. A recorded cost captures a cost actually incurred. An invoice records what you billed. Keeping these separate lets you compare the printing job's outcome with the offer without rewriting the original commercial agreement.

Use RecordedCostState for an actual cost entry. Link it to the job with jobId, choose a configured cost category and record the quantity, rate, currency and time incurred. A description and source reference help explain where the entry came from.

Record a cost incurred

Suppose the autumn brochure used 2.5 hours of press time at EUR 75 per hour. After checking that the configured category uses hours, record the entry:

mutation RecordERPJobCost($input: CreateRecordedCostInput!) {
  createRecordedCost(input: $input) {
    recordedCost {
      id jobId costCategoryId quantity unitRate totalCost currency recordedAt
    }
    errors { path message }
  }
}

Variables:

{
  "input": {
    "costCategoryId": "cca_00000000008008000000000201",
    "jobId": "job_000000000080080000000000r1",
    "quantity": 2.5,
    "unitRate": 75,
    "currency": "EUR",
    "description": "Autumn brochure — recorded press time",
    "referenceType": "timesheet",
    "referenceId": "TS-2026-1042",
    "recordedAt": "2026-10-22T13:00:00Z"
  }
}

Replace the category and job IDs with the IDs you have read. The returned totalCost is quantity multiplied by unit rate: EUR 187.50 in this example. Read the record and any errors before continuing.

This example supplies the rate explicitly. Alternatively, costRateId can supply the unit rate when unitRate is omitted. Check that the selected rate belongs to the intended category and currency; the input currency is still your responsibility. Do not omit both rate sources and assume the current rate will be found automatically.

The source reference identifies the timesheet entry for a reader or integration. It does not make repeated creates safe. If a response is lost, inspect existing entries before recording the same cost again.

Correct an entry

Keep the returned recorded-cost ID. If the time was entered incorrectly, update that entry:

mutation CorrectERPJobCost($id: RecordedCostID!, $input: UpdateRecordedCostInput!) {
  updateRecordedCost(id: $id, input: $input) {
    recordedCost { id jobId quantity unitRate totalCost currency description }
    errors { path message }
  }
}

Variables:

{
  "id": "rco_00000000008008000000000202",
  "input": {
    "quantity": 2.75,
    "description": "Autumn brochure — corrected press time"
  }
}

Here the unit rate remains EUR 75 and the corrected total becomes EUR 206.25. The operation recalculates the total from the resulting quantity and rate. A correction changes the existing cost record; it does not create a separate adjustment entry.

Use deleteRecordedCost only for a specific entry that should not exist. Deleting a cost record does not reverse a supplier payment, warehouse movement or production report. upsertRecordedCost is available for deliberate create-or-update workflows; prefer create/update for routine entry and corrections.

Browse the costs recorded against the job

query BrowseERPJobCosts($first: Int!, $filter: RecordedCostFilterInput!, $after: String) {
  listRecordedCosts(first: $first, after: $after, filter: $filter) {
    edges {
      node {
        id jobId costCategoryId quantity unitRate totalCost currency recordedAt
        referenceType referenceId
      }
    }
    pageInfo { hasNextPage endCursor }
  }
}

Variables:

{
  "first": 20,
  "filter": {
    "jobId": {
      "eq": "job_000000000080080000000000r1"
    }
  }
}

Follow endCursor while hasNextPage is true, keeping the filter unchanged. You can also filter by category, cost centre, currency and dates. Review the entries and their source references before deciding that the recorded costs are complete.

Compare recorded costs with the job's commercial outcome

The same job summary used in Production brings the cost views together:

query ReviewPrintingCosts($jobId: JobID!) {
  jobCostingSummary(jobId: $jobId) {
    job { id name }
    totalMaterialCost totalOperationCost totalCost
    recordedCostsAvailable totalRecordedCost recordedCostCurrency
    sellingPrice sellingPriceCurrency recordedCostMargin
    errors { path message }
  }
}

Variables:

{
  "jobId": "job_0h248h248h848r248h248h248h"
}

Keep the purchased or planned material and operation costs separate from recorded actual costs. They can describe the same work, so adding the two views together would double count it. Recording shop-floor progress does not by itself create entries in this actual-cost view.

Check recordedCostsAvailable: job access alone does not grant access to recorded costs. When access is unavailable, hidden totals are not zero costs. With access, use recordedCostTotals for the currency breakdown. totalRecordedCost is present only when recorded entries exist in one currency; mixed currencies must remain separate.

recordedCostMargin compares the price of the single linked order line with costs recorded so far, when their currencies agree. It is not final profitability. Missing costs, multiple linked order lines or mismatched currencies prevent a meaningful single margin. Reconcile the entries before using the result to review the job.

Operation Use it to
Query.getRecordedCost Read an actual cost entry.
Query.listRecordedCosts Browse actual costs by job, category, currency or date.
Query.jobCostingSummary Compare a job’s material and operation costs with recorded actual costs.
Mutation.upsertRecordedCost Advanced: create or replace an actual cost entry. Prefer create/update for routine changes.
Mutation.createRecordedCost Record a new actual cost incurred.
Mutation.updateRecordedCost Correct selected fields on an actual cost entry.
Mutation.deleteRecordedCost Delete an incorrectly recorded cost.

Invoices, credit notes and payments

An invoice records the amount billed to a customer. Its lines describe the charges. A payment records money received against an invoice. These are separate records: recording an order does not itself establish that the customer has been billed or has paid.

Invoice an order

invoiceOrder creates an invoice and lines from an existing order:

mutation InvoiceCommerceOrder($input: InvoiceOrderInput!) {
  invoiceOrder(input: $input) {
    invoice { id invoiceNumber status totalAmount paidAmount currency }
    invoiceLines { id }
    errors { path message }
  }
}

Variables:

{
  "input": {
    "orderId": "ord_01h455vb4pex5vsknk084sn02q",
    "invoiceNumber": "INV-2042",
    "issueDate": "2026-10-21T09:00:00.000Z",
    "dueDate": "2026-11-20T09:00:00.000Z"
  }
}

Supply your invoice number and the intended dates. Keep the returned invoice ID and check its amounts and currency. The operation rejects an order that already has an invoice, including concurrent attempts; if a request's result is uncertain, inspect the order's invoices before trying again.

The API also provides individual invoice and invoice-line operations for workflows that need direct control. Creating or editing those records should reflect the document you actually intend to issue.

Record a payment

After confirming that a payment was received, use the invoice ID:

mutation RecordCommercePayment($input: OTCRecordPaymentInput!) {
  recordPayment(input: $input) {
    payment { id amount currency method reference }
    invoice { id status totalAmount paidAmount }
    order { id paymentStatus }
    errors { path message }
  }
}

Variables:

{
  "input": {
    "invoiceId": "inv_01h455vb4pex5vsknk084sn02q",
    "amount": 1000,
    "currency": "EUR",
    "method": "BANK_TRANSFER",
    "reference": "BANK-2042",
    "receivedAt": "2026-10-22T09:00:00.000Z"
  }
}

Use the actual amount, currency, date received and reference. recordPayment stores the payment and updates the invoice's paid amount and payment state, together with the associated order's payment status. Read all three results. For a partial payment, send only the amount received.

The payment methods are BANK_TRANSFER, CHEQUE, CARD, DIRECT_DEBIT, CREDIT_NOTE, CASH, OTHER. A bank reference helps you identify a payment; check existing payments before retrying a request whose outcome you do not know.

For a credit against an invoice, use issueCreditNote and inspect the resulting credit document. To remove an incorrectly recorded payment, deletePayment removes that payment and recalculates the associated payment totals. It requires payment:delete; confirm the specific payment ID before using it. Deleting a payment record does not move money through a bank.

Operation Use it to
Query.getInvoice Read invoice by identifier
Query.listInvoices Browse invoices
Query.getInvoiceLine Read invoice line by identifier
Query.listInvoiceLines Browse invoice lines
Query.getPayment Read payment by identifier
Query.listPayments Browse payments
Mutation.upsertInvoice Advanced: Create or update a invoice. Prefer create/update for routine changes.
Mutation.createInvoice Create a new invoice
Mutation.updateInvoice Change selected invoice fields
Mutation.deleteInvoice Delete a invoice
Mutation.upsertInvoiceLine Advanced: Create or update a invoice line. Prefer create/update for routine changes.
Mutation.createInvoiceLine Create a new invoice line
Mutation.updateInvoiceLine Change selected invoice line fields
Mutation.deleteInvoiceLine Delete a invoice line
Mutation.recordPayment Record an invoice payment and update the invoice and order payment totals together
Mutation.deletePayment Remove a payment and recalculate the invoice and order payment totals
Mutation.invoiceOrder Generate one invoice and its lines from an order
Mutation.issueCreditNote Issue a credit note against an invoice

Suppliers and purchase orders

A supplier is a business you buy from. A purchase order records the materials or services you request from that supplier, and its lines record the quantities and prices. Supplier records are independent of customer records, even when the same business fills both roles.

Find the configured supplier

Manage supplier records through IaC. For purchasing, browse the configured suppliers and choose the intended business:

query BrowseCommerceSuppliers($name: String!) {
  listSuppliers(first: 20, filter: {name: {contains: $name}}) {
    edges { node { id name code currency isActive } }
    pageInfo { endCursor hasNextPage }
  }
}

Variables:

{
  "name": "Riverbend"
}

Inspect the returned name, code, active flag and currency; a partial name search can match several suppliers. Keep the selected supplier ID. If the supplier is missing or its details need changing, update your IaC configuration before placing the purchase order.

Create a purchase order and a line

Purchase order headers expose upsertPurchaseOrder, without a separate create/update pair. This is an exception to the usual create/update approach. Omit id to create a new header:

mutation CreateCommercePurchaseOrder($input: UpsertPurchaseOrderInput!) {
  upsertPurchaseOrder(input: $input) {
    purchaseOrder { id poNumber status totalAmount currency }
    errors { path message }
  }
}

Variables:

{
  "input": {
    "poNumber": "PO-2042",
    "supplierId": "sup_01h455vb4pex5vsknk084sn02q",
    "orderDate": "2026-10-21T09:00:00.000Z",
    "totalAmount": 500,
    "currency": "EUR"
  }
}

Use the returned purchase order ID to add the material being ordered:

mutation AddCommercePurchaseOrderLine($input: CreatePurchaseOrderLineInput!) {
  createPurchaseOrderLine(input: $input) {
    purchaseOrderLine { id description quantity unitPrice lineTotal }
    errors { path message }
  }
}

Variables:

{
  "input": {
    "purchaseOrderId": "pur_01h455vb4pex5vsknk084sn02q",
    "lineNumber": 1,
    "description": "Material for packaging sleeves",
    "quantity": 250,
    "unitPrice": 2,
    "lineTotal": 500,
    "currency": "EUR"
  }
}

The header and line are separate requests. Check both results and keep amounts and currency consistent. Use updatePurchaseOrder and updatePurchaseOrderLine for subsequent changes, including the supported fields for received quantities when goods actually arrive.

Purchase order status values are DRAFT, SUBMITTED, ACKNOWLEDGED, PARTIALLY_RECEIVED, RECEIVED, CANCELLED, DISPUTED. Recording a status or received quantity is a change to the purchasing record; do not assume that it also performs every stock or logistics action required by your workflow.

Operation Use it to
Query.getSupplier Read supplier by identifier
Query.listSuppliers Browse suppliers
Query.getPurchaseOrder Read purchase order by identifier
Query.listPurchaseOrders Browse purchase orders
Query.getPurchaseOrderLine Read purchase order line by identifier
Query.listPurchaseOrderLines Browse purchase order lines
Mutation.upsertSupplier Manage through IaC: create or update a supplier
Mutation.createSupplier Manage through IaC: create a new supplier
Mutation.updateSupplier Manage through IaC: change selected supplier fields
Mutation.deleteSupplier Manage through IaC: delete a supplier
Mutation.upsertPurchaseOrder Create or update a purchase order
Mutation.updatePurchaseOrder Change selected purchase order fields
Mutation.deletePurchaseOrder Delete a purchase order
Mutation.upsertPurchaseOrderLine Advanced: Create or update a purchase order line. Prefer create/update for routine changes.
Mutation.createPurchaseOrderLine Create a new purchase order line
Mutation.updatePurchaseOrderLine Change selected purchase order line fields
Mutation.deletePurchaseOrderLine Delete a purchase order line

Products, pricing and tax

Products describe what you sell. Price brackets express quantity-based rates for a scope. Customer price overrides record agreed exceptions. Tax codes record rates and related tax information. Manage products, price brackets and tax codes through IaC. Read those configured records through the API when preparing a sale. Customer price overrides have runtime API operations for agreed customer-specific exceptions.

Record Type What to consider
Product ProductState SKU, description, active flag, base price and currency
Price bracket PriceBracketState Scope, quantity range, rate type, currency and validity dates
Customer price override CustomerPriceOverrideState Customer, scope, value, priority and effective dates
Tax code TaxCodeState Code, rate, jurisdiction and active/default flags

Browse active products to choose the product record for an order or estimate line:

query BrowseCommerceProducts($first: Int!) {
  listProducts(first: $first, filter: {isActive: {eq: true}}) {
    edges { node { id sku name productType basePrice currency } }
    pageInfo { endCursor hasNextPage }
  }
}

Variables:

{
  "first": 20
}

Use the returned product ID when linking a line to a catalogue product. Inspect the line's own price, tax, discount and total fields: a product reference alone is not a promise that every amount will be filled in or recalculated.

Before changing a customer price override, read the existing entries for that customer or scope. Check overlapping effective dates and priorities. For product, bracket or tax-code changes, use IaC. The operation list below marks those writes explicitly; their presence in the API does not change the recommended provisioning route.

Operation Use it to
Query.listCustomerPriceOverrides Browse customer price overrides
Query.getTaxCode Read tax code by identifier
Query.listTaxCodes Browse tax codes
Query.getPriceBracket Read price bracket by identifier
Query.listPriceBrackets Browse price brackets
Query.getProduct Read product by identifier
Query.listProducts Browse products
Mutation.upsertCustomerPriceOverride Advanced: Create or update a customer price override. Prefer create/update for routine changes.
Mutation.createCustomerPriceOverride Create a new customer price override
Mutation.updateCustomerPriceOverride Change selected customer price override fields
Mutation.deleteCustomerPriceOverride Delete a customer price override
Mutation.upsertTaxCode Manage through IaC: create or update a tax code
Mutation.createTaxCode Manage through IaC: create a new tax code
Mutation.updateTaxCode Manage through IaC: change selected tax code fields
Mutation.deleteTaxCode Manage through IaC: delete a tax code
Mutation.upsertPriceBracket Manage through IaC: create or update a price bracket
Mutation.createPriceBracket Manage through IaC: create a new price bracket
Mutation.updatePriceBracket Manage through IaC: change selected price bracket fields
Mutation.deletePriceBracket Manage through IaC: delete a price bracket
Mutation.upsertProduct Manage through IaC: create or update a product
Mutation.createProduct Manage through IaC: create a new product
Mutation.updateProduct Manage through IaC: change selected product fields
Mutation.deleteProduct Manage through IaC: delete a product

Business units and cost centres

Business units and cost centres describe your configured organisational structure. Manage their creation, changes and deletion through IaC. Read them through the API when you need their IDs, codes or details during commercial work.

The operations below are included because they belong to the commercial topics. The write operations are listed for discoverability, with the recommended IaC route made explicit; they are not steps in an API provisioning walkthrough.

Operation Use it to
Query.getBusinessUnit Read business unit by identifier
Query.getBusinessUnitBy Read business unit using a supplied lookup value
Query.listBusinessUnits Browse business units
Query.getCostCenter Read cost center by identifier
Query.getCostCenterBy Read cost center using a supplied lookup value
Query.listCostCenters Browse cost centers
Mutation.upsertBusinessUnit Manage through IaC: create or update a business unit
Mutation.createBusinessUnit Manage through IaC: create a new business unit
Mutation.updateBusinessUnit Manage through IaC: change selected business unit fields
Mutation.deleteBusinessUnit Manage through IaC: delete a business unit
Mutation.upsertCostCenter Manage through IaC: create or update a cost center
Mutation.createCostCenter Manage through IaC: create a new cost center
Mutation.updateCostCenter Manage through IaC: change selected cost center fields
Mutation.deleteCostCenter Manage through IaC: delete a cost center

Order fulfilment

An order's commercial status is useful only when it reflects the work actually completed. Use orderFulfillmentCompletion to inspect the current completion point and the assessment for an order:

query CheckCommerceFulfilment($orderId: OrderID!) {
  orderFulfillmentCompletion(orderId: $orderId) {
    orderId completionPoint outcome diagnosticSummary diagnostics
  }
}

Variables:

{
  "orderId": "ord_01h455vb4pex5vsknk084sn02q"
}

Read the outcome and diagnostics. The completion point is DISPATCH, DELIVERY. You can also request the per-item results when you need to understand which planned quantities are preventing completion.

When the order satisfies the required milestone, use completeOrderFulfillment with its order ID and the expectedCompletionPoint you just reviewed. That expectation protects the meaning of your request if the completion policy changes. The operation checks the current conditions; it does not invent dispatch or delivery activity to make the order complete.

Planning deliveries, recording dispatch and recording delivery are covered in Warehousing and Logistics. This section covers the commercial assessment and completion of the order.

Operation Use it to
Query.orderFulfillmentCompletion Check whether every order line meets the current completion milestone
Mutation.completeOrderFulfillment Complete an order after checking its fulfilment milestone

Notes on commercial records

Use a note to preserve a conversation, instruction or explanation attached to a record. For example, attach a delivery instruction to an order:

mutation AddCommerceOrderNote($input: CreateEntityNoteInput!) {
  createEntityNote(input: $input) {
    entityNote { id targetId body version }
    errors { path message }
  }
}

Variables:

{
  "input": {
    "targetId": "ord_01h455vb4pex5vsknk084sn02q",
    "body": "Customer requested delivery after 10:00. Confirm the delivery appointment before dispatch."
  }
}

Keep the returned note ID and version. To edit or delete a note, provide its current version as expectedVersion. If someone else has changed it, read the latest note before deciding how to proceed.

Use entityNotes with a target filter to browse a record's notes, or searchEntityNotes to search the notes you may access. Source integration and key lookups help integrations find their own notes. Access follows the target record's permissions. Saving a note and making it available in search are separate concerns; a note can be saved even when its search indexing needs attention. reindexEntityNotes requests refreshed indexing for a bounded selection.

Operation Use it to
Query.getEntityNote Read a note attached to a record you can access
Query.getEntityNoteBySource Find a note by its source integration and key
Query.entityNotes Browse notes on a record
Query.searchEntityNotes Search notes you are allowed to read
Mutation.createEntityNote Add a note to a record
Mutation.updateEntityNote Edit a note using its current version
Mutation.deleteEntityNote Delete a note using its current version
Mutation.reindexEntityNotes Request refreshed search indexing for a selected set of notes

Examples and shared API tools

Each recipe has a stable name. Use topicExamples(names: [...]) to retrieve its GraphQL document, variables and any illustrative response. For example, commerce.create-order is the order-and-lines recipe and crm.find-customer is the customer lookup. Fetching an example does not run it. Supply the returned variables with the returned document only when you are ready to perform that task.

The Commerce topic also exposes help, schema discovery, compact named queries and change subscriptions. These support working with the API; they are not additional commercial records. The IaC discovery and validation tools support infrastructure authoring, while dataChangeEvents allows a client to follow record changes subject to its access.

Operation Use it to
Query.topicExamples Fetch named operations, example variables and illustrative responses
Query.magicQuery Read data with a compact named query
Query.getMagicQuerySchema Inspect the inputs and result of a compact named query
Query.listMagicQueries Discover the available compact named queries
Query.getMagicQueryGraphQL Inspect the GraphQL operation for a compact named query
Query.listDataChangeEventTopics Discover record-change event topics
Query.generateDefaultDataQuery Prepare a query for a record-change event
Query.ping Check whether the API responds
Query.iacSchema Read the infrastructure schema for IaC tooling
Query.iacValidate Validate an IaC source document
Query.iacGuides Read IaC authoring guides
Query.helpChapters Discover help chapters
Query.helpChapter Read a help chapter
Query.searchHelp Find help by search term
Query.schemaAuthoringCatalog Discover supported schema authoring features
Query.compileSchemaXML Convert an authored XML schema to JSON Schema
Query.renderSchemaXML Convert JSON Schema to authored XML
Query.iacSchemaDocument Read a schema document used by IaC tooling
Subscription.dataChangeEvents Subscribe to record-change events

For the precise arguments, return types, input fields and enum values, continue to the ERP API reference.

Published platform manual · revision e0e7964628ec