Skip to main content

Returns, credit notes, and debit notes

A return is goods coming back. A sales return comes back from a customer and produces a credit note, which reduces what the customer owes you. A purchase return goes back to a supplier and produces a debit note, which reduces what you owe the supplier.

Both directions share one object, ProductReturn. Its type field tells you which direction a return is. A return also moves through one of two separate lifecycles, depending on how it was created. Choose the path first, because the two paths share no state machine.

Money, tax and currency rules are on money, pricing, and tax. The examples below use 0 for every amount. That zero stands for your own amount. The server calculates the real figures, so read them back from the response. Use "<ISO-8601 date>" for dates. For the exact field list of every type, see the API reference.

1. Choose the return path​

Two flows create returns. Each has its own statuses and its own steps. Advance a return only with the operations of the flow that created it.

Order-sourced flowInvoice or RMA flow
CreatecreateReturn, with a sales order or purchase order as the source and an optional grnIdcreateSalesReturn (source: a sales invoice), or createPurchaseReturn (source: a GRN or a purchase order)
Lands inPENDINGAPPROVED for a sales return; APPROVED or PENDING_APPROVAL for a purchase return
Advance withprocessReturn, which moves it to PROCESSED, or to REJECTED with action: REJECTSales return: markReturnReceived, then processReturnInspection, which moves it to COMPLETED. Purchase return: posted at creation, or by approveRMA when it waits for approval
Who may callPermission strings (see section 8)The tenant role, not a permission string

Return statuses are not guarded by a transition table.

The API writes these statuses: PENDING, PROCESSED, APPROVED, PENDING_APPROVAL, IN_TRANSIT, RECEIVED, INSPECTING, COMPLETED, REJECTED and PROCESSING. The status enum also declares DRAFT, CANCELLED and PARTIAL. Nothing writes those three, so a filter on them returns nothing.

caution

Some cross-flow calls are refused and some are not. processReturn accepts only a PENDING return, so it refuses an APPROVED RMA return with Return has already been processed. processReturnInspection accepts only a RECEIVED return and refuses others with Return must be received before inspection. But markReturnReceived sets RECEIVED whatever the current status is, and approveRMA and rejectRMA on a sales return overwrite its status whatever it is. Never call the RMA operations on a return the order-sourced flow created, or on a return that is already finished.

2. Create a return from an order​

Use createReturn when the goods are returned against a sales order or a purchase order.

Requires sales:credit_note:create. A PURCHASE_RETURN also requires purchase:debit_note:create.

mutation CreateReturn($input: CreateReturnInput!) {
createReturn(input: $input) {
id
returnNumber
status
type
currency
subtotal
taxAmount
totalAmount
items {
id
productId
variantId
quantity
unitPrice
totalAmount
condition
restockable
}
}
}

Variables:

{
"input": {
"type": "SALES_RETURN",
"originalOrderId": "<sales-order-id>",
"warehouseId": "<warehouse-id>",
"reason": "DAMAGED",
"reasonDescription": "<reason text>",
"items": [
{
"productId": "<product-id>",
"variantId": "<variant-id>",
"quantity": 2,
"unitPrice": 0,
"condition": "DAMAGED"
}
]
}
}

Rules for this step:

  • Required input: type, originalOrderId, reason, warehouseId and items. Each item needs productId, variantId, quantity, unitPrice and condition. Returns are tracked per variant, so variantId is always required.
  • This mutation accepts SALES_RETURN and PURCHASE_RETURN. The ReturnType enum lists more values, but this mutation does not accept them.
  • reason takes DAMAGED, DEFECTIVE, EXPIRED, INCORRECT, NOT_REQUIRED or OTHER. Item condition takes DAMAGED, DEFECTIVE, EXPIRED or GOOD.
  • A sales return needs an order that has shipped or been invoiced. Otherwise the call is refused with a message that starts Sales order <order-number> is <STATUS>: a return can only be created once the order has been shipped, delivered or invoiced.
  • Each item is matched to the first line of the source order for that variant that still has room to return. The input type declares originalOrderItemId, but createReturn does not use it today, so you cannot pick a specific line when the order lists the same variant twice.
  • The returnable quantity is capped per source line. Earlier returns count against the cap unless they are CANCELLED or REJECTED. A sales return cannot exceed the ordered quantity. A purchase return cannot exceed the received quantity on its purchase order line. A refused call starts with Cannot return <quantity> units for product <product>. Only <available> available.
  • The refund value comes from the original order line. It is net of the line discount and of the order discount for that line, and it carries tax at the source document's own rate and method. The unitPrice you send is required, but it is for your reference. Do not expect the server to use it, and do not compute the refund yourself. Read totalAmount on the return and on the note it produces.
  • Currency and exchange rate are inherited from the source document and are never re-rated.
  • A purchase return needs the purchase returns feature on your plan. Otherwise the call is refused with a plan error.
  • Visibility follows permissions in each direction. A caller who holds only sales permissions cannot list or read purchase returns, and the reverse is also true.

To read a return back, use returnById(id: String!) or returns(filters: ReturnFilterInput, skip: Int, take: Int). returnById requires sales:credit_note:read or purchase:debit_note:read, depending on the direction of the return. returns requires sales:credit_note:list or purchase:debit_note:list. returns is offset-paginated with skip and take, as described in errors, pagination, and idempotency.

The fields you will usually select on ProductReturn are id, returnNumber, status, type, reason, currency, subtotal, taxAmount, totalAmount, items, creditNotes and debitNotes.

3. Process a return​

processReturn is the only step in the order-sourced flow that restocks goods and mints a note.

Requires sales:credit_note:create. A purchase return also requires purchase:debit_note:create.

mutation ProcessReturn($input: ProcessReturnInput!) {
processReturn(input: $input) {
message
creditNoteId
debitNoteId
updatedReturn {
id
returnNumber
status
processedAt
items {
id
acceptedQuantity
rejectedQuantity
restockable
}
creditNotes {
id
creditNoteNumber
status
totalAmount
}
debitNotes {
id
debitNoteNumber
status
totalAmount
}
}
}
}

Variables:

{
"input": {
"returnId": "<return-id>",
"action": "PROCESS",
"processedItems": [
{
"itemId": "<return-item-id>",
"acceptedQuantity": 1,
"rejectedQuantity": 1,
"restockable": false,
"disposalReason": "<disposal reason>"
}
],
"creditNoteDetails": {
"amount": 0,
"taxAmount": 0,
"notes": "<notes text>"
}
}
}

Rules for this step:

  • Restocking happens only through ARC's stock movement path, in the same transaction as the status change. Do not post your own stock adjustment for a returned item.
  • Direction is signed. On a sales return, accepted quantity comes back into stock. On a purchase return, accepted quantity leaves stock for the supplier.
  • Quantities are in the document's unit of measure, but inventory moves in base units. Each movement is the accepted quantity multiplied by the source line's conversion factor. The factor is taken from the source line when the return is created, and you cannot send it. For example, a return of 1 case with a factor of 12 restocks 12 base units.
  • Only items with restockable: true and an acceptedQuantity above zero move stock. An item with no variant is skipped.
  • On a sales return, restockable: true is a request, not the decision. A line whose recorded condition is DAMAGED, DEFECTIVE or EXPIRED is never put back into sellable stock, and the stored restockable reads false. On a purchase return the flag you send is used, because the goods leave stock for the supplier.
  • For each item, accepted plus rejected must not exceed the quantity returned on that line, and no count may be negative. A refused line moves no stock and creates no note. The message starts Return <return-number> line <n> (<name>): accepted.
  • Every processedItems[].itemId must belong to this return. Otherwise the call is refused with Return item <item-id> does not belong to return <return-number>. All item checks run before any write, so one bad id fails the whole call.
  • Two concurrent calls cannot restock twice. Only one claims the PENDING return. The other is refused with Return has already been processed or Return changed or has already been processed. Refresh it before continuing.
  • action is required and accepts APPROVE, PROCESS or REJECT. PROCESS and APPROVE both process the return to PROCESSED. REJECT closes the return as REJECTED: every line is recorded as fully rejected and not restockable, no stock moves and no note is minted, even if you pass note details. To reject only part of a return, send PROCESS with rejectedQuantity on the lines concerned.
  • A note is minted only when you pass creditNoteDetails (sales) or debitNoteDetails (purchase). The response then carries creditNoteId or debitNoteId. Each takes amount and taxAmount, plus optional adjustments and notes. A credit note minted here starts PENDING.
  • The note's value is capped by the value of the accepted lines, including their tax and any shipping on the return. A larger note is refused with Credit note total <amount> exceeds the value of the accepted return lines (<amount>). Adjust the amount, or ask an approver with the 'returns:return:override-note-amount' permission to issue it. For a purchase return the message says Debit note total. Only callers holding returns:return:override-note-amount can exceed the cap. The tenant ADMIN role does not grant it. Overrides are audited.
  • A debit note minted here is always written at the value of the accepted lines. The amount and taxAmount you send are not used for it, and a purchase return can have only one live debit note.
  • If you pass no note details, the return still restocks and no note is created. Create the note separately if you need one: a credit note as in section 5, or a debit note as in section 7.
  • Customer-portal returns create no stock movement. Only staff processing touches inventory.

4. The invoice and RMA flow​

Use this flow for goods returned against a sales invoice, and for goods sent back to a supplier when you want a receive-and-inspect cycle. Its operations check the tenant role, not permission strings.

caution

The RMA operations require the tenant role itself, matched exactly against me.role. A user whose role is not listed for an operation is refused (OWNER is not listed for any of them), even when it holds the credit-note or debit-note permissions that let it run processReturn. Confirm the integration's user holds the role.

Operations and their requirements:

  • createSalesReturn(input: CreateSalesReturnInput!): ReturnResponse! requires the tenant role ADMIN or MANAGER. Its input takes customerId, invoiceId, items, reason (DAMAGED, INCORRECT, DEFECTIVE, EXPIRED, NOT_REQUIRED or OTHER), refundMethod and warehouseId. Optional fields are customerRequest, requiresPickup and pickupAddress. Each item takes productId, variantId, quantity (a whole number), and condition (free text). refundMethod takes BANK_TRANSFER, CASH, CREDIT_CARD, CREDIT_NOTE, DEBIT_NOTE, REPAIR, REPLACEMENT or STORE_CREDIT.
  • createPurchaseReturn(input: CreatePurchaseReturnInput!): ReturnResponse! requires the tenant role ADMIN, MANAGER or WAREHOUSE_STAFF, and the purchase returns feature on your plan. Its input takes supplierId, warehouseId, reason (the same six codes), items, and either grnId or purchaseOrderId. The optional requiresApproval flag sends the return to approval first.
  • approveRMA(returnId: String!, terms: [String!]!): ReturnResponse! requires the tenant role ADMIN or MANAGER.
  • rejectRMA(returnId: String!, reason: String!): ReturnResponse! requires the tenant role ADMIN or MANAGER.
  • markReturnReceived(returnId: String!, receivedItems: [PartialReturnItemInput!]): ReturnResponse! requires the tenant role ADMIN, MANAGER or WAREHOUSE_STAFF. It moves the return to RECEIVED.
  • processReturnInspection(returnId: String!, input: InspectionInput!): ReturnActionResult! requires the tenant role ADMIN, MANAGER or WAREHOUSE_STAFF. It is the step that restocks and issues the credit note. The example below shows it.

Pickup and tracking:

  • scheduleReturnPickup(returnId: String!, input: SchedulePickupInput!): ReturnResponse! requires the tenant role ADMIN or MANAGER. Without input.delivery it only records the pickup. With input.delivery it books a courier, and then it also requires sales:shipment:create.
  • bookReturnPickup(productReturnId: ID!, delivery: ShipmentBookingInput!): ShipmentGql! requires sales:shipment:create and the delivery partners feature on your plan. When the carrier reports the parcel as collected, the return moves from APPROVED to IN_TRANSIT. Receiving and the credit note stay manual.
  • updateReturnTracking(returnId: String!, courierService: String!, trackingNumber: String!): ReturnResponse! requires the tenant role ADMIN or MANAGER.

Two more RMA operations exist. createWarrantyReturn requires the tenant role ADMIN or MANAGER and is a separate warranty flow that this page does not cover. createPartialReturn requires the tenant role ADMIN or MANAGER and lands in PENDING_APPROVAL.

The RMA resolver also has list reads, such as getReturn, which requires the tenant role ADMIN, MANAGER or WAREHOUSE_STAFF. For new code, prefer returnById and returns from section 2, because they follow the permission scoping described there.

Rules for this flow:

  • createSalesReturn lands in APPROVED and needs inspection. It does not restock anything and does not issue a credit note, because the goods have not come back yet. The response gives id, returnNumber, rmaNumber and status.
  • A sales return must be raised within 30 days of the invoice date. A later attempt is refused with Return window of 30 days has expired. Setting customerRequest to true bypasses the window. The window is fixed and is not a tenant setting.
  • Item quantities on createSalesReturn, createPurchaseReturn and processReturnInspection are whole numbers.
  • A sales return line is restockable only when the condition you send on createSalesReturn is NEW, LIKE_NEW or GOOD (any letter case). Any other condition is stored as not restockable, and inspection never restocks it.
  • markReturnReceived moves the return to RECEIVED. The receivedItems you send are recorded for the audit trail only. Per-line counts are given at inspection.
  • processReturnInspection requires the return to be RECEIVED. It claims the return atomically, so a second inspection that runs at the same time is refused with Return changed or has already been processed. Refresh it before continuing. It restocks only the restockQuantity, and only for lines whose stored restockable flag is true. The quantity moved is the restock quantity multiplied by the invoice line's conversion factor. For each line, accepted plus rejected must not exceed returned, and restock must not exceed accepted. An itemId that is not a line of this return is ignored, not refused, so check your ids. The return finishes in COMPLETED.
  • When refundMethod is CREDIT_NOTE, the credit note is issued during inspection, in the same transaction as the restock. It covers the accepted quantity only, so nothing accepted means no note. When only part of the goods is accepted, the note's figures are scaled by accepted divided by returned. creditNoteId in the result identifies the note. Its lines are then locked (linesLocked is true) and cannot be changed.
  • A purchase return without requiresApproval lands in APPROVED and posts its stock-out and supplier debit note when it is created. One with requiresApproval set lands in PENDING_APPROVAL. While it waits, it posts nothing: no stock movement and no debit note. approveRMA on that return posts the stock-out, at the quantity multiplied by the line's factor, and the debit note, exactly once. Approving an already APPROVED purchase return returns it unchanged, and any other status is refused. rejectRMA is refused on a purchase return once stock or a debit note has been posted, and on any APPROVED purchase return. A purchase return does not go through receive and inspect.
  • A purchase return whose unit cost cannot be resolved is refused with Cannot resolve a unit cost for product <product> on this purchase return; supply a GRN with cost, a purchase order, or set the variant cost.
  • The debit note for a purchase return is capped at the quantity accepted on the goods receipt, and its tax follows the source line's rate.
  • Scrap and repair quantities are recorded on each item, but nothing acts on them yet.

Example, inspecting a received return:

mutation InspectReturn($returnId: String!, $input: InspectionInput!) {
processReturnInspection(returnId: $returnId, input: $input) {
success
message
creditNoteId
}
}

Variables:

{
"returnId": "<return-id>",
"input": {
"overallResult": "PARTIAL",
"items": [
{
"itemId": "<return-item-id>",
"inspectionResult": "RESELLABLE",
"acceptedQuantity": 3,
"rejectedQuantity": 1,
"restockQuantity": 3,
"scrapQuantity": 0,
"repairQuantity": 0,
"qualityScore": 8
}
]
}
}

Every item needs itemId, inspectionResult, acceptedQuantity, rejectedQuantity, restockQuantity, scrapQuantity, repairQuantity and qualityScore. overallResult takes FAIL, PARTIAL or PASS. inspectionResult takes DONATE, RECYCLE, REPAIRABLE, RESELLABLE, RETURN_TO_VENDOR or SCRAP.

5. Credit notes​

A credit note reduces what a customer owes you. Use one to amend an invoice, or to settle goods that came back and were processed through the order-sourced flow. Amending a confirmed order is covered at the end of this section.

OperationRequiresPurpose
createCreditNote(input: CreateCreditNoteInput!)sales:credit_note:createCreate a note
creditNote(id: ID!)sales:credit_note:readRead one note
creditNotes(filters: CreditNoteFiltersInput, skip: Int, take: Int)sales:credit_note:listList notes. creditNotesCount(filters:) returns the matching count
updateCreditNote(id: ID!, input: UpdateCreditNoteInput!)sales:credit_note:updateEdit a note while it is unapplied
applyCreditNote and voidCreditNotesales:credit_note:updateApply to an invoice, or void. See section 6
removeCreditNote(id: ID!)sales:credit_note:deleteDelete a draft or pending note

creditNotes returns a plain list with skip and take. Use creditNotesCount for totals, as described in errors, pagination, and idempotency.

mutation CreateCreditNote($input: CreateCreditNoteInput!) {
createCreditNote(input: $input) {
id
creditNoteNumber
status
type
reason
currency
totalAmount
appliedAmount
remainingAmount
originalInvoiceId
linesLocked
items {
description
quantity
unitPrice
amount
taxAmount
totalAmount
}
}
}

Variables:

{
"input": {
"type": "<type-label>",
"reason": "<reason text>",
"partnerId": "<customer-id>",
"originalInvoiceId": "<invoice-id>",
"amount": 0,
"items": [
{
"description": "<line description>",
"quantity": 1,
"unitPrice": 0,
"amount": 0,
"taxRate": 0
}
]
}
}

Rules for this step:

  • A new note is always PENDING. status is a plain string, not an enum. The values are DRAFT, PENDING, APPROVED, PARTIALLY_APPLIED, FULLY_APPLIED and CANCELLED. applyCreditNote moves a note to PARTIALLY_APPLIED or FULLY_APPLIED, and voidCreditNote moves it to CANCELLED. Cancelling the invoice a note was applied to can give the note back (to PENDING or PARTIALLY_APPLIED) or cancel it with the invoice. No mutation moves a note to APPROVED today. This is current behavior, and it may change.
  • type is a free-text label. Use the labels your tenant already uses. Nothing checks it against a fixed list.
  • Required inputs are type, reason, partnerId, amount and items. Optional inputs include originalInvoiceId, returnId, currency, taxAmount, adjustments, expiryDate, notes and internalNotes.
  • Each item needs description, quantity, unitPrice and amount. Optional item fields include productId, variantId, taxRate, taxAmount, discountPercent, discountAmount, discountAmountExact and taxBreakdown.
  • The server recalculates line and header totals from the item identities and rates. The amount and taxAmount you send are accepted, but where they disagree with the server's figures, the server's figures are stored. Read totalAmount from the response instead of assuming your own.
  • A note raised against an invoice, or against a return that came from an invoice, takes that invoice's currency and exchange rate. Any other note is in your organization's base currency. The currency input is not used to choose the currency.
  • Credit on an invoice is capped at the invoice total, not its unpaid amount. All live credit against an invoice must stay within that total. A note tied to a return counts against the return's invoice when the return came from an invoice. A note with no invoice, or tied to a return from the order-sourced flow, is not capped by this rule. The refusals are:
    • Credit note amount <amount> exceeds the amount left to credit on invoice <invoice-number> (the message then gives the invoice total and the amount already credited);
    • Invoice <invoice-number> is still a draft. Edit the draft instead of raising a credit note against it.;
    • Invoice <invoice-number> is <status>. A credit note cannot be raised against it.
  • Credit notes are not gated by a plan feature.
caution

Read the return's creditNotes before you create a note against it with returnId. Do not rely on the server to refuse a second credit note for the same return. The invoice credit cap is the limit you can rely on.

Editing and deleting:

  • updateCreditNote works only while the note is DRAFT or PENDING and nothing has been applied to it. Otherwise the call is refused with Cannot update a credit note with status <status>…void it and issue a new one instead. The message suggests voiding, but voiding needs an APPROVED or PARTIALLY_APPLIED note (see section 6). Current behavior, and it may change.
  • A note issued by a return's inspection has linesLocked set to true. Its lines cannot be changed.
  • removeCreditNote works only for DRAFT or PENDING notes. Otherwise the call is refused with Only draft or pending credit notes can be deleted. The delete is a soft delete.

Amending a confirmed order:

Staff can edit an order's lines, shipping and order-level discount while the order is a draft, awaiting payment, or confirmed. Those edits are refused once anything has shipped or been short-closed, once the order is partly or fully invoiced, once any payment is recorded, for an order paid online, and while a live invoice references the order. For an order in that state, amend it by cancelling and re-creating it, or by issuing a credit note. If goods physically came back, also create a return. Staff edit an order with updateSalesOrder(id, input), which requires sales:sales_order:update. For the order lifecycle, see sales lifecycle.

To correct an invoiced price, issue a credit note against the invoice with originalInvoiceId. To correct quantities for goods that came back, create a return.

6. Apply and void credit notes​

applyCreditNote settles an outstanding sales invoice from a credit note.

Requires sales:credit_note:update.

mutation ApplyCreditNote($id: ID!, $invoiceId: ID!, $amount: Float!) {
applyCreditNote(id: $id, invoiceId: $invoiceId, amount: $amount) {
id
creditNoteNumber
status
totalAmount
appliedAmount
remainingAmount
}
}

After the call, re-read the invoice with the invoice query to get its outstandingAmount. See invoices and payments for invoice reconciliation.

Rules for this step:

  • The note's status must be PENDING, APPROVED or PARTIALLY_APPLIED. Otherwise the call is refused with Credit note must be approved to apply.
  • amount must be above zero (Applied amount must be positive) and no more than the note's remainingAmount (Applied amount cannot exceed remaining amount).
  • The target must be a sales invoice for the same customer as the note, and it must be issued. Refusals include A credit note can only be applied to a sales invoice, Invoice and credit note must belong to the same partner, and Cannot apply a credit note to a <status> invoice.
  • The note and the invoice must share a currency. A mismatch is refused with Cannot apply a <ISO-4217 code> credit note to invoice <invoice-number> in <ISO-4217 code>. Cross-currency application is not supported.
  • amount cannot exceed the invoice's outstanding amount (Applied amount <amount> exceeds invoice outstanding amount <amount>). An invoice that is already paid is refused with Invoice <invoice-number> is already fully paid.
  • One note can settle several invoices. Its status moves to PARTIALLY_APPLIED, and then to FULLY_APPLIED once its remaining amount reaches zero within a rounding tolerance. Re-read remainingAmount and the invoice's outstandingAmount. Do not subtract the amounts yourself.
  • Concurrent applications against the same note or invoice are serialized, so one balance cannot be spent twice.
  • Applying a credit note also records a settlement in the payment ledger, in the same transaction: a COLLECTION payment with method CREDIT_NOTE, status APPROVED and the note's number as referenceNumber, plus a payment allocation to the invoice. Treat it as a non-cash settlement. It cannot be reversed, re-allocated or voided as a payment; voidCreditNote is the only way to undo it. See invoices and payments.
caution

The refusal Credit note must be approved to apply is misleading. PENDING notes are accepted. Because no mutation approves a note, do not wait for an approval step before applying one.

Voiding:

voidCreditNote(id: ID!, reason: String!) requires sales:credit_note:update. It accepts only APPROVED or PARTIALLY_APPLIED notes, and refuses others with Only approved or partially applied credit notes can be voided. Voiding reverses every application the note made against invoices and sets the note to CANCELLED.

A PENDING note cannot be voided, because no operation moves it to APPROVED. Edit it, apply it or delete it while it is unapplied. A FULLY_APPLIED note cannot be voided either. This is current behavior, and it may change.

7. Debit notes​

A debit note records what a supplier owes you back. You raise it against a supplier and apply it to a purchase invoice.

Debit notes need the debit notes feature on your plan, for reads as well as writes. Credit notes do not.

Operations:

OperationRequiresPurpose
createDebitNote(input: CreateDebitNoteInput!)purchase:debit_note:create, and the debit notes featureCreate a note. Returns DebitNoteResponse
applyDebitNote(id: ID!, invoiceId: ID!, amount: Float!)purchase:debit_note:update, and the debit notes featureApply to a purchase invoice
cancelDebitNote(id: ID!)purchase:debit_note:update, and the debit notes featureCancel a note that has not been applied
updateDebitNote(id: ID!, input: UpdateDebitNoteInput!)purchase:debit_note:update, and the debit notes featureEdit a pending note
removeDebitNote(id: ID!)purchase:debit_note:delete, and the debit notes featureDelete a note that has not been applied
debitNote(id: ID!)purchase:debit_note:read, and the debit notes featureRead one note
debitNotes(filters: DebitNoteFiltersInput, partnerId: ID, skip: Int, take: Int)purchase:debit_note:list, and the debit notes featureList notes. debitNotesCount(filters:) returns the matching count and has the same gates
mutation CreateDebitNote($input: CreateDebitNoteInput!) {
createDebitNote(input: $input) {
success
message
debitNote {
id
debitNoteNumber
status
type
partnerId
currency
amount
taxAmount
totalAmount
appliedAmount
remainingAmount
returnId
originalInvoiceId
issueDate
}
}
}

Variables:

{
"input": {
"type": "ADJUSTMENT",
"partnerId": "<supplier-id>",
"originalInvoiceId": "<purchase-invoice-id>",
"amount": 0,
"taxAmount": 0,
"issueDate": "<ISO-8601 date>",
"notes": "<notes text>"
}
}

Rules for this step:

  • type must be one of PURCHASE_RETURN, ADJUSTMENT, PENALTY or OTHER. Every debit note is raised on a supplier. A sales return's note is a credit note, not a debit note. type cannot be changed after creation.
  • Required inputs are type, partnerId, amount and issueDate. Optional inputs include taxAmount and adjustments (both default to 0), dueDate, originalInvoiceId, returnId, notes and internalNotes.
  • A standalone note keeps the header figures you send. It has no lines.
  • A debit note raised with returnId takes the return's figures, and the figures you send may be overridden. A purchase return can have only one live debit note.
  • createDebitNote returns a DebitNoteResponse with debitNote, message and success. A failure arrives as a GraphQL error whose message starts with Failed to create debit note:.
  • A new note is PENDING. Its statuses are PENDING, APPROVED, PARTIALLY_APPLIED, APPLIED and CANCELLED. Debit notes use APPLIED where credit notes use FULLY_APPLIED.
  • updateDebitNote works only while the note is PENDING (Cannot update debit note that is not in PENDING status). For a note raised from a return, the amount fields can only be set to the return's figures.
  • The note must be PENDING, APPROVED or PARTIALLY_APPLIED to be applied. amount must be above zero (Amount must be positive.) and no more than the note's remaining balance (Amount <amount> exceeds the debit note's remaining balance <amount>.). The invoice must be a purchase invoice in your organization and not cancelled, otherwise the call is refused with Invoice not found or not eligible for debit note application. The invoice must belong to the same supplier (Invoice and debit note must belong to the same partner) and share the note's currency. amount cannot exceed the invoice's outstanding amount.
  • Applying a debit note creates a PAYMENT-type payment with method DEBIT_NOTE and status PAID, plus a payment allocation, in the same transaction. The application therefore appears in your payment ledger as a non-cash settlement that cannot be reversed or voided as a payment. See invoices and payments.
  • An apply refusal arrives with its message prefixed Failed to apply debit note:.
  • cancelDebitNote and removeDebitNote are refused once the note has any application: Cannot cancel a debit note that has already been applied and Cannot delete a debit note that has already been applied. Debit notes have no void or reversal operation.

Applying a debit note:

mutation ApplyDebitNote($id: ID!, $invoiceId: ID!, $amount: Float!) {
applyDebitNote(id: $id, invoiceId: $invoiceId, amount: $amount) {
success
message
debitNote {
id
status
appliedAmount
remainingAmount
}
}
}

8. Permissions and roles at a glance​

Operation groupRequires
createReturn, processReturnsales:credit_note:create. A purchase return also needs purchase:debit_note:create
returns, returnsSummary, returnById, returnAuditLogslist or read on sales:credit_note or purchase:debit_note, for each direction you need. A grant in one direction does not open the other
processReturn above the note capreturns:return:override-note-amount. The tenant ADMIN role does not grant it
createSalesReturn, approveRMA, rejectRMA, createWarrantyReturn, createPartialReturn, scheduleReturnPickup, updateReturnTrackingThe tenant role ADMIN or MANAGER. scheduleReturnPickup with input.delivery also needs sales:shipment:create
createPurchaseReturn, markReturnReceived, processReturnInspection, getReturnThe tenant role ADMIN, MANAGER or WAREHOUSE_STAFF. createPurchaseReturn also needs the purchase returns feature
bookReturnPickupsales:shipment:create, and the delivery partners feature
Credit notessales:credit_note:create, read, list, update (apply, void, edit) and delete
Debit notespurchase:debit_note:create, read, list, update (apply, cancel, edit) and delete. Every debit-note operation, reads included, also needs the debit notes feature

Customer-initiated returns (customerCreateReturn) are a buyer-portal operation that runs under the customer's own sign-in. They feed the order-sourced flow and create no stock movement. This page does not document that contract.

9. The deprecated createInternalReturn​

createInternalReturn is deprecated. Every call is refused with Internal returns are not supported: move stock between warehouses with a stock transfer (transferInventory from <from-warehouse-id> to <to-warehouse-id>).

Use transferInventory(input: TransferInventoryInput!): StockMovement! instead. It requires inventory:stock_transfer:create and the stock transfers feature on your plan. See purchasing and inventory for the transfer contract.

What not to do​

  • Do not mix the two flows. Advance a return created by createReturn only with processReturn. Advance a return created by createSalesReturn or createPurchaseReturn only with the RMA operations. Not every cross-flow call is refused, so the server will not always stop you.
  • Do not post your own stock adjustment for returned goods. The return restocks through the stock movement path. A second adjustment double-counts. Restock is signed (positive for sales, negative for purchases) and equals accepted quantity multiplied by the source line's conversion factor.
  • Do not compute the refund yourself, and do not rely on the unitPrice you send. The server derives the value from the original order, invoice or goods receipt line. Read totalAmount on the return and on the note.
  • Do not rely on restockable: true to restock a sales return line recorded as DAMAGED, DEFECTIVE or EXPIRED. The server refuses to put it back into sellable stock. On a purchase return the flag you send is used.
  • Do not send action: REJECT to reject part of a return. REJECT rejects every line, moves no stock and mints no note. Use PROCESS with rejectedQuantity for a partial rejection.
  • Do not expect a credit note from createSalesReturn. It is issued during inspection, for the accepted quantity only. Do not expect processReturn to mint a note without creditNoteDetails or debitNoteDetails.
  • Do not create a second credit note for a return that already has one. Read the return's creditNotes first, because the server does not refuse a second credit note for the same return.
  • None of createReturn, createSalesReturn, createPurchaseReturn, createCreditNote, createDebitNote or any apply mutation takes an idempotencyKey. A retry after a timeout can create a duplicate. Before you retry, read returns, creditNotes or debitNotes filtered by partner and date, or read the return's creditNotes and debitNotes. The caps on returnable quantity, invoice credit and note balance are the only backstops. transferInventory is the exception: it does take an idempotencyKey. For the retry rules in general, see errors, pagination, and idempotency.
  • Do not apply a credit note across currencies, partners, invoice types or non-issued invoices. All of these are hard refusals. A credit note settles a sales invoice of the same customer. A debit note settles a purchase invoice of the same supplier.
  • Do not treat note settlements in the payment ledger as cash. Applying a credit note or a debit note writes a payment with method CREDIT_NOTE or DEBIT_NOTE. It cannot be reversed or voided as a payment.
  • Do not plan to void a PENDING or FULLY_APPLIED credit note. Only APPROVED or PARTIALLY_APPLIED notes can be voided, and no mutation moves a note to APPROVED. Edit, apply or delete it while it is unapplied.
  • Do not edit a credit note's lines after an inspection has issued it (linesLocked is true). Do not edit or cancel an applied debit note.
  • Do not expect the 30-day window for RMA sales returns to be a tenant setting. It is fixed. customerRequest: true bypasses it.
  • Do not call createInternalReturn. It is deprecated and always refuses. Use transferInventory.
  • Do not filter returns by DRAFT, CANCELLED or PARTIAL. Nothing writes those statuses.
  • Plan the returns:return:override-note-amount permission before you build an automatic credit flow. The tenant ADMIN role does not grant it.
  • Purchase-side features are gated by plan. The purchase returns, debit notes and stock transfer features each return a plan error. A plan error is not a problem with your payload.
  • The RMA operations check the tenant role. A user without the listed role is refused, even when it holds the credit-note permissions. Verify the role with your tenant admin.
  • Use placeholders such as <customer-id>, <supplier-id>, <invoice-id>, <warehouse-id> and <ISO-4217 code> in your own mapping. Zeros in the examples stand for your own amounts.