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