Invoices, payments, and receivables
This guide covers the money side of the sales and purchase documents: invoices, the payments that settle them, and the balances between the two. Read it after quote to cash, which shows the first conversion and payment calls.
ARC tracks the sub-ledger only. It does not post journals, keep a chart of accounts, or produce a general ledger. Your accounting connector reads the documents below and posts its own entries.
Permission strings follow module:resource:action. See tenant permissions for how roles map to them.
1. Learn the two directions
Invoices and payments each use one table with a type field. The direction is an enum value, not a separate object.
| Object | Type | Direction | Settled by | Create mutation |
|---|---|---|---|---|
| Invoice | SALES_INVOICE | Receivable (A/R): the customer owes you | COLLECTION payments | createInvoice (or see sales lifecycle) |
| Invoice | PURCHASE_INVOICE | Payable (A/P), also called a bill: you owe the supplier | PAYMENT payments | createInvoice |
| Payment | COLLECTION | Money in | A SALES_INVOICE | createCustomerCollection |
| Payment | PAYMENT | Money out | A PURCHASE_INVOICE | createSupplierPayment |
INCOMING and OUTGOING are not values of any type field. A filter on them matches nothing. Use SALES_INVOICE or PURCHASE_INVOICE for invoices, and COLLECTION or PAYMENT for payments.
createPayment also takes a type (COLLECTION or PAYMENT) and a partnerId. It is the same payment record with the direction chosen in the input.
Some points to plan for:
- A purchase invoice is available only when the tenant has the purchase-invoice feature enabled. On a tenant without it,
createInvoicewithtype: PURCHASE_INVOICEis refused with a feature error. - The payment method
CREDIT_NOTEcannot be recorded by hand. It is the settlement row created when a credit note is applied.createPayment,createCustomerCollectionandcreateSupplierPaymentall refuse it.
2. Read invoices
invoices(filter, skip, take) returns an InvoiceConnection with invoices and total. invoice(id) reads one invoice. Any authenticated user can call these. Rep-scoped users, meaning callers without analytics:organization:read, see only their customers' invoices.
The filter fields an integrator uses most:
type:SALES_INVOICEorPURCHASE_INVOICE.status: a persisted lifecycle value. See section 3.displayStatus: a derived status, used to filter byPAID,PARTIALLY_PAIDorOVERDUE. See section 3.partnerId: the customer or supplier.hasOutstanding:truereturns invoices with an open balance. Unless you also passstatus, it excludesDRAFT,CANCELLEDandVOIDEDinvoices.dueBeforeanddueOnOrAfter: date strings that filter on the due date.startDateandendDate: date strings that filter on the invoice date.search: matches invoice number, notes, order number and partner name.updatedAfter: returns rows changed after a timestamp. Used for sync in section 10.sortBy:DUE_DATE,INVOICE_DATE,OUTSTANDING_AMOUNT,TOTAL_AMOUNTorUPDATED_AT.sortOrder:ASCorDESC.
Always pass take. Do not rely on a default page size, and page with a stable sortBy.
query OpenSalesInvoices($skip: Int!, $take: Int!) {
invoices(
filter: {
type: SALES_INVOICE
hasOutstanding: true
sortBy: DUE_DATE
sortOrder: ASC
}
skip: $skip
take: $take
) {
total
invoices {
id
invoiceNumber
type
status
displayStatus
paymentStatus
currency
totalAmount
paidAmount
outstandingAmount
dueDate
isOverdue
partnerId
version
updatedAt
}
}
}
invoiceListSummary(filter) returns totals for the whole filtered set, not just the page. The amounts are in the organization's base currency. It returns draftCount, dueAmount, issuedCount, overdueAmount, overdueCount, paidAmount, totalAmount and totalCount.
invoiceAuditLogs(invoiceId) returns the audit entries for one invoice.
Cancelled and voided invoices are returned too. Use status or displayStatus to narrow the list to the states you need. Invoices are not deleted; they are cancelled and kept (see section 3 and What not to do).
3. Understand statuses, and what "paid" means
Lifecycle status and settlement are separate things. The persisted status is the document lifecycle, and it can be DRAFT, APPROVED, PENDING, SENT, CANCELLED or VOIDED. createInvoice starts an invoice as PENDING. How much has been paid lives in paidAmount and outstandingAmount. An invoice can be SENT and fully paid at the same time.
Three read-only fields are derived:
paymentStatus(a string):PAIDwhenpaidAmountis at leasttotalAmountandtotalAmountis above zero,PARTIALLY_PAIDwhen something but not all is paid, otherwiseUNPAID(including a zero-total invoice).displayStatus(a string): the status shown to people. It returnsCANCELLEDorVOIDEDfirst, thenPAID, thenOVERDUE, thenPARTIALLY_PAID, thenDRAFTorSENT. APENDINGorAPPROVEDinvoice with nothing paid readsAWAITING_PAYMENT.isOverdue:trueonly for an issued invoice (notDRAFT,CANCELLEDorVOIDED) that still owes money and whose due date's calendar day (UTC) has passed.
The displayStatus filter takes the InvoiceDisplayStatus enum: APPROVED, CANCELLED, DRAFT, OVERDUE, PAID, PARTIALLY_PAID, PENDING, SENT or VOIDED. The filter has no AWAITING_PAYMENT value. Filter on PENDING or APPROVED to find those rows.
PAID, PARTIALLY_PAID and OVERDUE are not valid values for status. Sending them fails validation. Use displayStatus to filter on them, or read paymentStatus.
Status changes follow these transitions:
| From | Allowed next statuses |
|---|---|
DRAFT | PENDING, APPROVED, SENT, CANCELLED, VOIDED |
PENDING | APPROVED, SENT, CANCELLED, VOIDED |
APPROVED | SENT, PENDING, CANCELLED, VOIDED |
SENT | CANCELLED, VOIDED |
CANCELLED, VOIDED | None (terminal) |
DRAFT, PENDING and APPROVED are pre-send flags. The API does not enforce that APPROVED comes before SENT.
Set a status with updateInvoice(id, input), passing status in input with the other editable fields. Requires payments:invoice:update. The one exception is CANCELLED, which must go through cancelInvoice.
Setting VOIDED through updateInvoice changes the status only. It does not issue a credit or debit note, restore order quantities or reverse stock the way cancelInvoice does. To undo an issued invoice, cancel it.
Cancel an invoice
Cancelling is a two-step call. Preview first, then cancel.
query CancelPreview($id: ID!) {
invoiceCancellationPreview(id: $id) {
invoiceId
invoiceNumber
canCancel
blockers
requiresReverseAllocationsFlag
totalAllocatedAmount
paidOnInvoiceAmount
estimatedCreditNoteAmount
estimatedDebitNoteAmount
projectedOrderStatus
}
}
Pass reverseAllocations from the preview's requiresReverseAllocationsFlag. Despite its name, the flag does not free the payment. It confirms that you accept the consequences the preview showed, and a cancel of an invoice with settled money is refused without it. Do not call cancelInvoice when canCancel is false or blockers is not empty.
Requires payments:invoice:cancel.
mutation CancelInvoice($id: ID!, $reverse: Boolean!) {
cancelInvoice(id: $id, reverseAllocations: $reverse) {
id
status
outstandingAmount
version
}
}
invoiceCancellationPreview needs an authenticated caller and no further permission string.
Do not cancel by setting status: CANCELLED on updateInvoice. It is refused. Use invoiceCancellationPreview, then cancelInvoice.
Cancelling is never a status flag on its own. It reverses what the invoice caused:
- Settled cash stays allocated to the cancelled invoice. A sales invoice gets one goodwill credit note for the cash part. A purchase invoice gets one goodwill debit note for the cash part.
- A credit note that paid a sales invoice is given back instead of being matched by a second note.
- An allocation that is kept cannot be reversed by hand afterwards, and a payment still applied to the cancelled invoice cannot be voided.
- A
DEBIT_NOTEsettlement on a bill blocks the cancel. So does a draft or unapplied credit note on a sales invoice. - Stock issued by the invoice is reversed, and cancelling a sales invoice recalculates the source order's invoice status. The preview shows the projected order status.
After a cancel, outstandingAmount reads as 0 for the invoice.
Rules for editing:
- A
CANCELLEDorVOIDEDinvoice cannot be edited at all. - A fully paid invoice cannot be updated, including its
status. - An illegal status jump is refused. Setting the status it already has is a no-op.
- An update that would take the total below
paidAmountis refused. Reverse the allocation first.
4. Track the outstanding balance
outstandingAmount is the authoritative open balance. When an invoice is created, outstandingAmount equals totalAmount. Each allocation raises paidAmount and lowers outstandingAmount. Use the returned values. Do not subtract payments on your side.
- A partly paid invoice has
paymentStatusPARTIALLY_PAID, withpaidAmountabove zero but belowtotalAmount. - An invoice is fully paid when
paidAmountis at leasttotalAmountandtotalAmountis above zero. - A foreign-currency invoice also exposes
baseOutstandingAmount,baseTotalAmountandexchangeRate. The exchange rate is frozen on the document. Reconcile in the invoice's owncurrency, and use the base fields only for roll-up. For tax and pricing rules, see money, pricing, and tax.
The server computes the aggregates. Use them rather than summing invoices in your client:
salesOutstandingSummaryandpurchaseOutstandingSummaryeach return anOutstandingSummary.outstandingSalesInvoices(customerId)andoutstandingSupplierInvoices(supplierId)return the open invoices for one partner, as[Invoice!]!.purchaseOutstandingSummaryandoutstandingSupplierInvoicesalso need the purchase-invoice feature on your plan. Without it they are refused with a plan error.
query ReceivablesPosition {
salesOutstandingSummary {
totalOutstanding
currentAmount
overdueAmount
totalInvoices
averageDaysOutstanding
aging { bucket count value }
}
}
OutstandingSummary also returns totalCustomers and partners. The aging buckets are computed by the server from the organization's aging policy. By default they are current, 1-30, 31-60, 61-90 and 90+ days by due date, but an organization can change the edges and the date they count from, so read the bucket labels rather than assuming them.
Aggregates cover invoices that still owe money and exclude DRAFT, CANCELLED and VOIDED invoices. They are computed in base currency, using each invoice's frozen rate. currentAmount plus overdueAmount equals totalOutstanding.
Overdue is calendar-day lateness, compared in UTC. An invoice due today is not overdue.
For organization-wide receivables analytics such as DSO, collection trend and top debtors, see analytics and search.
Rep-scoped callers see a narrowed invoice list, but purchaseOutstandingSummary is organization-wide. For A/P reconciliation, use an organization-level credential.
Allocation can never exceed the invoice's remaining outstanding. A clearly larger amount is refused with Allocation amount X exceeds outstanding amount Y for invoice N. A tiny overshoot is clamped to the outstanding amount.
5. Record a payment, and know where it starts
The three create mutations start in different states.
| Mutation | Starts as | Requires |
|---|---|---|
createCustomerCollection | DRAFT. With inline allocations it is committed at once and ends PARTIAL or PAID. | payments:collection:create |
createSupplierPayment | DRAFT. With inline allocations it is committed at once and ends PARTIAL or PAID. | payments:payment:create |
createPayment | PENDING, committed immediately. With inline allocations it ends PARTIAL or PAID. | payments:payment:create or payments:collection:create. A COLLECTION also needs payments:collection:create. A PAYMENT also needs payments:payment:create. |
A DRAFT payment has not moved money and does not change the partner's balance. Commit it with confirmPayment. It moves the payment to PENDING (nothing allocated), PARTIAL or PAID. Confirming a payment that is already committed returns it unchanged, and confirming a CANCELLED, REVERSED or REFUNDED payment is refused. Requires payments:payment:update. Confirm a payment before you allocate it.
mutation ConfirmPayment($paymentId: ID!, $expectedVersion: Int) {
confirmPayment(input: { paymentId: $paymentId, expectedVersion: $expectedVersion }) {
id
status
version
}
}
The direction is set by the mutation you call. Customer collections take customerId, supplier payments take supplierId, and createPayment takes type and partnerId.
Every create takes an idempotencyKey. Send the same key with the same request to get the recorded result back. Send the same key with a different payload and the call conflicts. Keep the key separate from referenceNumber, which is the bank or cheque reference. Records created without a key cannot prove that a payload matches, so a retry against one can fail with an error saying the outcome is unknown. Re-read the payment before retrying. See errors, pagination, and idempotency.
Always send an idempotencyKey. Without one, createCustomerCollection and createPayment fall back to referenceNumber for de-duplication. A collection whose referenceNumber is already stored on a payment returns that payment instead of creating a new one, and createPayment refuses it as an unknown outcome.
The payment status enum is:
APPROVED, CANCELLED, DRAFT, OVERDUE, PAID, PARTIAL, PENDING, PENDING_APPROVAL, PROCESSED, REFUNDED, REVERSED.
The enum mixes two vocabularies. Some values describe settlement, such as PARTIAL and PAID. Others describe the record's lifecycle, such as DRAFT, CANCELLED and REVERSED. The API has no payment approval step.
Payment reads:
payment(id)requirespayments:payment:read.payments(filter, skip, take)returns aPaymentConnectionwithpaymentsandtotal. Requirespayments:payment:list.paymentListSummary(filter)returns totals for today, the week, the month and all time, plusunallocatedAmount. Requirespayments:payment:list.
Payment.allocationState is derived at read time and is never stored. Its values are AWAITING_ALLOCATION, OVERPAID and FULLY_ALLOCATED. It is null for draft, cancelled, reversed and refunded payments.
unallocatedAmount is cash received that has not yet been applied to an invoice. It is sometimes called an advance or on-account balance.
mutation RecordCollection($input: CreateCustomerCollectionInput!) {
createCustomerCollection(input: $input) {
id
paymentNumber
type
status
currencyCode
amount
allocatedAmount
unallocatedAmount
allocationState
version
}
}
Variables use placeholders only:
{
"input": {
"customerId": "<customer-id>",
"amount": 100.0,
"currency": "<ISO-4217 code>",
"collectionDate": "<ISO-8601 date>",
"collectionMethod": "BANK_TRANSFER",
"bankAccountId": "<bank-account-id>",
"referenceNumber": "<bank-reference>",
"idempotencyKey": "payments/<external-payment-id>/create"
}
}
Then commit it with confirmPayment as shown above, passing expectedVersion from the version you read. 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). Otherwise the write proceeds. Always re-read after writing.
Rules for payments:
- A payment's currency is fixed when it is recorded. A funding bank account must use the same currency.
- On
createCustomerCollectionandcreateSupplierPayment, aBANK_TRANSFERorCHECKmust name thebankAccountIdit moved through. Without one the create is refused. voidPayment(input: VoidPaymentInput!)requirespayments:payment:void. ADRAFT,PENDINGorPENDING_APPROVALpayment with nothing allocated endsCANCELLED. Voiding a settled payment reverses what it paid. The invoices owe again, and the payment endsREVERSED. A collection captured through a payment gateway also produces one credit note when voided. Voiding an already voided, reversed or refunded payment returns it unchanged. A payment still applied to a cancelled invoice cannot be voided.- A buyer-registered EFT made through the customer portal settles nothing until staff confirm it. This is a portal flow, and this guide does not cover portal operations.
6. Allocate one payment across several invoices
allocatePayment assigns a payment's unallocated balance to one or more open invoices in a single call. reversePaymentAllocations undoes specific allocations.
AllocatePaymentInput takes a paymentId, an allocations array of invoiceId and amount pairs, and an optional expectedVersion. Requires payments:payment:allocate.
mutation AllocateAcrossInvoices($input: AllocatePaymentInput!) {
allocatePayment(input: $input) {
id
paymentNumber
status
amount
allocatedAmount
unallocatedAmount
allocationState
version
allocations {
id
invoiceId
invoiceNumber
amount
reversalOfId
reversedAt
}
}
}
{
"input": {
"paymentId": "<payment-id>",
"expectedVersion": 3,
"allocations": [
{ "invoiceId": "<invoice-id-1>", "amount": 60.0 },
{ "invoiceId": "<invoice-id-2>", "amount": 40.0 }
]
}
}
You can also pass the same allocations array inline on createCustomerCollection, createSupplierPayment or createPayment. The payment is recorded and allocated in one call.
Each guard below is enforced by the API as part of the allocation:
- Same currency. The payment and the invoice must share a currency. See section 7.
- Matching side. A
COLLECTIONcan settle only aSALES_INVOICE. APAYMENTcan settle only aPURCHASE_INVOICE. A collection aimed at a bill is refused with a message beginningCannot allocate a COLLECTION to invoice. - Same partner. The invoice must belong to the payment's partner.
- Open invoice. The invoice must not be
CANCELLEDorVOIDED. ADRAFTinvoice can still be allocated. - Within outstanding. Each allocation is at most the invoice's outstanding amount.
- Within unallocated. The sum of new allocations is at most the payment's unallocated amount. The refusal reads
Total new allocations ... exceed the payment's unallocated amount. - At least one. An empty
allocationsarray is refused withAt least one allocation is required. - Live payment. The payment must not be
CANCELLED,REVERSEDorREFUNDED, and it must not be the settlement row of a credit or debit note.
On the create mutations, the inline total is checked against the payment amount instead of the unallocated amount.
Topping up an invoice that the payment already settles is handled by ARC. Do not add a second row for the same invoice yourself, and do not reverse the earlier row first just to top it up.
Read status, allocationState and unallocatedAmount from the response. Do not predict the new status before the call returns.
To undo allocations, use reversePaymentAllocations(input: ReversePaymentAllocationsInput!). It requires payments:payment:reverse. Its input takes paymentId, allocationIds, an optional reason and an optional expectedVersion. A reversal does not delete the original row. It adds a negative counter-entry, so the original and its reversal net to zero. Each allocation exposes reversalOfId and reversedAt, so you can see which row reverses which. A replayed reversal is idempotent.
Allocations created by applying a credit or debit note cannot be reversed by hand.
7. Cross-currency allocation is refused
A payment and the invoice it settles must share a currency. ARC does not convert an amount during allocation. A payment amount is exactly the amount received in the payment's own currency.
When the currencies differ, the call fails with an error whose message has this shape:
{
"data": null,
"errors": [
{
"message": "Cannot allocate a <payment currency> payment to invoice <invoice-number> in <invoice currency>. Cross-currency allocation is not supported — use a payment in <invoice currency>.",
"path": ["allocatePayment"]
}
]
}
The check applies only when both sides carry a real three-letter currency code and the codes differ. An empty or legacy currency is treated as the organization's base currency and is allowed.
Design your integration around this rule:
- Group open invoices by
currency. - Read the payment's
currencyCodebefore allocating. - Offer or send only the invoices that match it.
- Never convert an amount yourself to make a match work.
Bank accounts follow the same rule. A funding bank account in a different currency from the payment is refused.
Branch on the GraphQL error and re-read the payment and invoices. Do not parse the message text, because it may change.
8. Payment terms and due dates
A due date is the invoice date plus the net days of the term. Net days come from the term the tenant has configured.
paymentTerms(includeInactive) returns a PaymentTerm list. The fields are:
code: tenant-authored, for exampleNET_30. It starts with an uppercase letter, then uses uppercase letters, digits or underscores, and is 2 to 32 characters long.label: the display text.netDays: the days added to the invoice date.kind:NET,CODorPREPAID.CODandPREPAIDboth havenetDaysof0.isDefault,isActiveandisPlatformDefault.
If the tenant has configured no terms, the platform default set is returned, so the list is never empty.
paymentTerms requires any one of sales:quote:read, sales:sales_order:read, sales:sales_invoice:read, payments:invoice:read, purchase:purchase_order:read, customers:customer:read, suppliers:supplier:read or settings:tenant:read.
query Terms {
paymentTerms {
code
label
netDays
kind
isDefault
isPlatformDefault
}
}
Send the typed term on createInvoice or updateInvoice as paymentTerms, using the code. The terms field is free text for terms and conditions. Do not put the code there.
dueDate is optional. When you omit it, ARC computes it from the term and the invoice date. Do not compute your own day count. When you omit paymentTerms too, ARC uses the source order's term, then the partner's default, then the organization's default.
ARC validates the code where it is entered. An unknown code is refused on partners, quotes, orders, purchase orders and invoices. A code is valid only if your organization offers it, and one organization cannot use a code another organization created.
Admin mutations manage the list: createPaymentTerm, updatePaymentTerm, setDefaultPaymentTerm, deactivatePaymentTerm and materializePaymentTerms. They require settings:payment-term:create, settings:payment-term:update or settings:payment-term:delete.
You can filter invoices by due window with dueBefore and dueOnOrAfter, as shown in section 2.
9. Bill supplier receipts (purchase invoices)
A bill is a createInvoice call with type: PURCHASE_INVOICE. There is no separate mutation that creates a bill from a goods receipt. Each bill line points at a receipt line with grnItemId. Create the receipt first, as described in purchasing and inventory.
Read the receipt before you bill it:
grn(id)requirespurchase:grn:read.grns(filter, skip, take)requirespurchase:grn:list. ItsGRNFilterInputacceptspurchaseOrderId, among other fields.
Each GRNItem exposes id, productId, variantId, receivedQuantity, acceptedQuantity, rejectedQuantity, invoicedQuantity, unitCost, taxRate and remainingQuantity.
To link the bill to its purchase order, pass the purchase order's id as orderId.
Billing rules:
- A purchase invoice never bills more than was received. A receipt-linked line is capped at the receipt item's accepted quantity, less what other live bills already invoice and less units returned to the supplier.
- Lines against a purchase order are capped at the order's accepted quantity for each product and variant.
- The same received units cannot be billed twice. That covers billing once from the purchase order and again from the receipt, and two lines on one receipt item.
- The cap is checked on create and again on edit. A cancelled bill frees what it had billed.
- A bill against a cancelled purchase order is not refused. It is capped at what the receipts received.
- A bill takes its currency from the purchase order or receipt it bills. A
currencythat differs from that source is refused. - A receipt-linked line keeps the receipt line's frozen tax rate and discount, so a tax change made after the goods arrived does not reprice the bill. Send a different
taxRateonly when you mean to override it. - A standalone bill, one with no linked purchase order or sales order, runs the supplier credit-limit check. A refusal rolls back the creation. Bills linked to an order skip that check.
Requires payments:invoice:create.
mutation CreatePurchaseBill($input: CreateInvoiceInput!) {
createInvoice(input: $input) {
id
invoiceNumber
type
status
currency
totalAmount
outstandingAmount
dueDate
paymentTerms
version
}
}
{
"input": {
"type": "PURCHASE_INVOICE",
"partnerId": "<supplier-id>",
"invoiceDate": "<ISO-8601 date>",
"currency": "<ISO-4217 code>",
"paymentTerms": "<term-code>",
"supplierName": "<supplier name>",
"items": [
{
"productId": "<product-id>",
"variantId": "<variant-id>",
"grnItemId": "<grn-item-id>",
"description": "<line description>",
"quantity": 10,
"unitPrice": 5.0
}
]
}
}
createInvoice has no idempotencyKey, so see What not to do before retrying after a timeout. Leave invoiceNumber out so ARC generates it, and store the returned id.
Pay the bill with a PAYMENT. Call createSupplierPayment with supplierId, amount, currency, paymentDate, paymentMethod and allocations (each invoiceId with an amount), plus an idempotencyKey. A BANK_TRANSFER or CHECK also needs bankAccountId. The partner must be a supplier. Use outstandingSupplierInvoices(supplierId) to list the bills that are still open.
Cancelling a purchase invoice that has cash allocations keeps the allocation and issues one goodwill debit note. The full cancel flow is in section 3.
10. Sync incrementally to an accounting connector
ARC is the sub-ledger. Your connector reads documents and posts its own journals. The pattern below pulls changes from ARC and leaves the posting to you.
Pull invoice changes
Use updatedAfter with sortBy: UPDATED_AT. updatedAfter returns rows changed strictly after the value you pass, so a row stamped exactly at the watermark is not returned again.
query InvoiceDelta($since: String!, $skip: Int!, $take: Int!) {
invoices(
filter: { updatedAfter: $since, sortBy: UPDATED_AT, sortOrder: ASC }
skip: $skip
take: $take
) {
total
invoices {
id
invoiceNumber
type
status
currency
totalAmount
paidAmount
outstandingAmount
exchangeRate
baseOutstandingAmount
dueDate
version
updatedAt
allocations {
id
paymentId
paymentNumber
amount
reversalOfId
reversedAt
}
}
}
}
Every change that matters bumps updatedAt. That covers creation, edits, status changes, cancellation, allocations and allocation reversals.
Checkpoint your progress. Store the updatedAt of the last row you fully processed, for each tenant and each invoice type. Page with skip and take while keeping the sort stable.
Recommended practice, not an API guarantee: re-read a small overlap window before the stored watermark, and upsert by invoice id. Compare version to skip stale copies. Rows written while you are paging can otherwise be missed. Rows that share one updatedAt can also straddle a page boundary. The overlap window covers both cases.
Pull payment changes
PaymentFilterInput has no updatedAfter, so use two sources.
- Read settlement from invoices. Each changed invoice's
allocationsshow which payment settled it, and whether an allocation was reversed. - For payment headers, page
payments(filter: { type: COLLECTION, sortBy: UPDATED_AT, sortOrder: DESC }, skip, take), or usePAYMENTfor supplier payments. Stop once rows are older than your overlap window. Recommended practice, not an API guarantee. Then callpayment(id)for each one to read itsstatus,versionandallocations.
Voids and reversals change the payment's status (to REVERSED or CANCELLED) and its allocation rows. Re-read the payment itself, not just the rows in your date window.
Map ids and handle statuses
- Use the ARC
idas your key. Treat it as opaque. - Store
invoiceNumberandpaymentNumberfor display only. Do not use them as keys. - Skip
DRAFTinvoices and payments, since they are not issued and have not moved money. - Treat
CANCELLEDandVOIDEDinvoices as reversals. A cancelled invoice readsoutstandingAmountas0. - A
DRAFTcollection or supplier payment has no effect until it is confirmed. Do not post journals for it.
Reconcile with the aggregates
After a sync run, compare your open balance with salesOutstandingSummary.totalOutstanding and purchaseOutstandingSummary.totalOutstanding. Both are in base currency. Remember that they exclude DRAFT, CANCELLED and VOIDED invoices.
Use an organization-level credential for the connector. A rep-scoped credential sees only its customers' invoices, so it will give you a partial view.
For limits and retry guidance, see errors, pagination, and idempotency. To be told when a sales invoice is generated or fully paid instead of polling, subscribe to the invoice.generated and invoice.paid webhooks. Purchase invoices do not fire them. Treat a notification as a prompt to re-read the invoice, not as the balance itself.
What not to do
- Do not filter on
INCOMINGorOUTGOING. UseSALES_INVOICEorPURCHASE_INVOICEfor invoices, andCOLLECTIONorPAYMENTfor payments. - Do not send
PAID,PARTIALLY_PAIDorOVERDUEas an invoicestatus. UsedisplayStatusto filter on them, or readpaymentStatus. - Do not compute balances by subtracting payments, and do not sum invoice totals in your client. Use
outstandingAmount,paidAmountand the server aggregates. - Do not allocate across currencies, beyond an invoice's outstanding amount, beyond a payment's unallocated amount, to a
CANCELLEDorVOIDEDinvoice, or to an invoice of the wrong type or partner. Each of these is refused. Filter your open-invoice list by currency, side and partner before you allocate. - Do not cancel through
updateInvoicewithstatus: CANCELLED, and do not usestatus: VOIDEDto undo an issued invoice. UseinvoiceCancellationPreview, thencancelInvoice. - Do not call
deleteInvoice. It is deprecated and always refuses. Cancel the invoice instead. Cancelled and voided invoices are kept for audit and cannot be edited. - Do not record
CREDIT_NOTEpayments yourself. They come from applying credit and debit notes. - Do not omit
takeoninvoicesorpayments. Always pass it, and page with a stable sort. - Do not read lateness as "due date is before now". Overdue is calendar-day based, and an invoice due today is not overdue.
- Do not use a rep-scoped credential for connector sync or A/P reads. The invoice list is narrowed, and
purchaseOutstandingSummaryis organization-wide. - Do not retry
createInvoiceblindly after a timeout. It has noidempotencyKey. Before retrying, search withinvoices(filter: { search: "<your reference>" }). Thesearchfilter matches invoice number, notes, order number and partner name, so put your own external reference innoteswhen you create the invoice. - Do not assume a payment has moved money until you have confirmed it. Without inline allocations, a collection or supplier payment stays
DRAFTuntilconfirmPayment. - Do not create a collection or payment without an
idempotencyKey. Without one, a repeatedreferenceNumberis treated as a duplicate. - Pass
expectedVersiononupdateInvoice,allocatePayment,confirmPayment,voidPaymentandreversePaymentAllocations. Readversionfirst. A stale version is refused only when the server enforces concurrency checks; otherwise the write proceeds. Always re-read the record after writing, and re-read it before retrying if the call reports a conflict.
See also the API reference for the generated types, and analytics and search for organization-wide receivables views.