Build a quote-to-cash integration
The sales document pipeline is:
Quote → SalesOrder → Invoice → Payment → Payment allocation
Each transition is a business command. It may check permissions, status, inventory, credit, tax method, idempotency, and document version. Do not model a transition as a direct database-style status update.
1. Create a quote
Use the operation's current input definition from the reference:
mutation CreateQuote($input: CreateQuoteInput!) {
createQuote(input: $input) {
id
quoteNumber
status
currency
subtotal
discountAmount
taxAmount
totalAmount
validUntil
version
items {
productId
variantId
quantity
unitPrice
discountAmount
taxAmount
totalPrice
}
}
}
The input contains line and header fields because ARC supports multiple clients. The server persists authoritative calculated totals and currency data. Resolve the product/variant/UOM first and do not turn a stale catalog price into a quote without a pricing check.
Use idempotencyKey when the input exposes it. A timeout after a successful create must be recovered by replaying the same logical request or searching/re-reading the external reference before attempting another create.
2. Convert the quote
A quote becomes a sales order through the explicit conversion mutation:
mutation ConvertQuote($quoteId: ID!, $idempotencyKey: String) {
convertQuoteToSalesOrder(
quoteId: $quoteId
warehouseId: "<warehouse-id>"
idempotencyKey: $idempotencyKey
) {
id
orderNumber
status
currency
totalAmount
paymentStatus
}
}
Conversion rules to design for:
- the quote must be in a convertible state and not expired;
- a quote with taxed lines converts only when its tax method is
EXCLUSIVE, orINCLUSIVEon a legacy quote; a taxed quote with any other or no method is refused. New documents areEXCLUSIVE-only, soINCLUSIVEappears only on legacy quotes (see money, pricing, and tax); - the accepted quote price is honoured, while current-price drift may be detected and audited;
- the created order is always
DRAFT; confirm it withconfirmSalesOrder(section 3); - use the warehouse and inventory rules returned by the API; do not reserve stock locally.
3. Confirm and fulfil
Creating an order without a quote
You can also create a sales order directly with createSalesOrder. Two input differences from
quotes matter:
uomIdis required on every order line (optional on quote lines);unitPriceis optional on order lines — when omitted, the server resolves the line price through the canonical pricing pipeline. Quote lines requireunitPricebecause the quote is the priced commitment.
Order status transitions are governed by the document state machine. The common confirmation shape is:
mutation ConfirmOrder($id: ID!, $expectedVersion: Int) {
confirmSalesOrder(id: $id, expectedVersion: $expectedVersion) {
id
orderNumber
status
version
paymentStatus
}
}
Read the order immediately before a transition and pass version as expectedVersion where available. Whether a stale version is refused depends on the server's concurrency mode: when enforcement is on, you get a version-conflict error (HTTP 409 on REST; on GraphQL the message says the document changed since you last loaded it), and you should re-read and decide again. Otherwise the write proceeds. Always re-read after writing.
Shipment/fulfilment changes inventory through ARC's stock movement path. Do not create a parallel inventory adjustment to make your external system appear consistent.
4. Invoice the order
ARC supports invoice creation and quote-to-invoice conversion. Which path is correct depends on the source document and tenant workflow:
mutation ConvertQuoteToInvoice($quoteId: ID!) {
convertQuoteToInvoice(quoteId: $quoteId) {
id
invoiceNumber
status
currency
totalAmount
outstandingAmount
dueDate
}
}
Use the invoice's returned outstandingAmount, paymentStatus, dueDate, and status for reconciliation. Do not calculate outstanding balances by subtracting locally recorded payments.
5. Record and allocate a payment
A payment and its allocations are separate operations:
mutation CreatePayment($input: CreatePaymentInput!) {
createPayment(input: $input) {
id
paymentNumber
status
amount
currencyCode
unallocatedAmount
version
}
}
mutation AllocatePayment($input: AllocatePaymentInput!) {
allocatePayment(input: $input) {
id
paymentNumber
allocatedAmount
unallocatedAmount
allocationState
version
}
}
An allocation input identifies the invoice and amount. Pass expectedVersion when the input supports it; as with orders, a stale version is refused only when the server enforces concurrency checks, and otherwise the write proceeds. After allocation, re-read the invoice and payment if your external ledger needs an exact balance.
State and error handling
Expect domain errors when:
- a quote is expired or already converted;
- the quote tax method is ambiguous;
- a document version is stale (when the server enforces concurrency checks);
- the customer is over credit limit;
- stock is unavailable;
- the caller lacks the operation's permission;
- the requested currency has no valid price.
These are not transient network failures. Handle them as reconciliation or operator-action states.
External idempotency mapping
Use the external system's immutable ids:
external quote id → CreateQuoteInput.idempotencyKey
external order id → CreateSalesOrderInput.idempotencyKey
external payment id → CreatePaymentInput.idempotencyKey
Keep ARC ids, document numbers, statuses, totals, and timestamps in your mapping table. The document number is for display; the ARC id is the opaque resource identifier.
Next: Purchasing and inventory.