Credit notes
A credit note documents a correction to an issued invoice. It preserves the original lines, discounts, and tax amounts. Apply its credit to an unpaid balance, or use it to create a linked refund on a paid invoice.
Issuing the document alone does not move money. A refund returns money to the buyer's original payment methods and keeps the credit note and invoice linked to that refund.
How a credit note works#
Every credit note belongs to one invoice, and that invoice has to be past draft and not void (INVOICE_NOT_CREDITABLE). Paid, uncollectible, and already-credited invoices all accept a credit note. They reject allocation, because there is no balance left to draw down. On a paid invoice, refund the note instead.
Credit-note responses include version. Issue, void, allocation, and allocation reversal accept optional expected_version to reject changes based on an older note. Use the version returned by the latest successful write for the next edit.
The note itself moves through three states:
- draft moves to issued on issue
- draft on edit: lines can change freely
- issued moves to void on void
- issued on allocate: the invoice balance falls
- issued on refund: money returns on a paid invoice
Issuing is the line in the sand. Before it, the note is a working document you can edit as many times as you like. After it, the lines and totals are frozen, the note has a credit_note_number, the PDF exists, and the only remaining moves are allocating credit, reversing those allocations, and refunding the credit on a paid invoice.
Four money fields track the note:
total_moneyis the face value, the sum of its lines.unallocated_moneyis what is left to allocate or refund. It falls as you allocate or request a refund, and rises again when you reverse an allocation or a refund fails.pending_refund_moneyis credit reserved by refunds that have not resolved yet.refunded_moneyis credit consumed by refunds that succeeded.
Create a draft#
POST /v1/credit-notes takes the invoice, a reason, and an optional memo.
curl -X POST https://api.withflintpay.com/v1/credit-notes \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: credit-note-inv-1042-001" \
-d '{
"invoice_id": "inv_1kmn0aExample",
"reason": "returned_goods",
"memo": "Two of the six chairs came back damaged."
}'
{
"data": {
"credit_note_id": "cn_1kmn0aExample",
"invoice_id": "inv_1kmn0aExample",
"status": "draft",
"reason": "returned_goods",
"memo": "Two of the six chairs came back damaged.",
"credit_note_lines": [],
"total_money": {"amount": 0, "currency": "USD"},
"unallocated_money": {"amount": 0, "currency": "USD"}
}
}
reason is one of returned_goods, order_adjustment, billing_error, goodwill, or other (INVALID_CREDIT_NOTE_REASON). It prints on the credit note, so pick the one the customer would recognize. memo runs to 4096 characters (INVALID_CREDIT_NOTE_MEMO).
The note uses the invoice's currency. Include credit_note_lines on create to add corrections, or omit it for an empty draft. credit_note_number is absent on drafts.
PATCH /v1/credit-notes/{credit_note_id} edits reason, memo, and credit_note_lines. Omitted fields stay unchanged. Send "memo": null to clear it. Sending no mutable fields returns EMPTY_UPDATE, and unknown fields are rejected rather than ignored.
Credit specific lines#
PATCH /v1/credit-notes/{credit_note_id} replaces the complete credit_note_lines array when supplied. Send the note's last-read version as expected_version. Include credit_note_line_id to retain an existing line, omit the ID to create a line, and omit a previous line to remove it. Send [] to clear the draft. Null is invalid. A stale version returns CREDIT_NOTE_CHANGED; retrieve the note before retrying.
Each entry points at an invoice_line_item_id from the invoice's frozen snapshot and carries one correction:
curl -X PATCH https://api.withflintpay.com/v1/credit-notes/cn_1kmn0aExample \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"expected_version": 1,
"credit_note_lines": [
{
"invoice_line_item_id": "invli_1kmn0aExample",
"correction": {"type": "quantity", "quantity": 2}
},
{
"invoice_line_item_id": "invli_2bqr7dExample",
"correction": {"type": "amount", "amount_money": {"amount": 4000, "currency": "USD"}}
}
]
}'
{
"data": {
"credit_note_id": "cn_1kmn0aExample",
"status": "draft",
"credit_note_lines": [
{
"credit_note_line_id": "cnli_1kmn0aExample",
"invoice_line_item_id": "invli_1kmn0aExample",
"quantity": 2,
"description": "Oak side chair",
"subtotal_money": {"amount": 30000, "currency": "USD"},
"discount_money": {"amount": 3000, "currency": "USD"},
"tax_money": {"amount": 2228, "currency": "USD"},
"total_money": {"amount": 29228, "currency": "USD"}
}
],
"total_money": {"amount": 33228, "currency": "USD"},
"unallocated_money": {"amount": 0, "currency": "USD"}
}
}
Two correction types, and they are mutually exclusive:
quantitycredits whole units. Passquantityand nothing else. It cannot exceed the units on the source line.amountcredits a figure you name. Passamount_moneyand nothing else. It has to be positive, in the invoice currency, and no more than what that line is worth.
Sending the wrong pairing returns INVALID_CREDIT_NOTE_LINE; an unrecognized type returns INVALID_CREDIT_NOTE_CORRECTION_TYPE.
You do not compute the money. Flint derives subtotal_money, discount_money, and tax_money from the source line's own proportions, so crediting two of six chairs credits a third of that line's discount and a third of its tax. For an amount correction, the rounding residual lands in tax_money so total_money comes out to exactly the figure you asked for.
Each invoice_line_item_id can appear once per note (DUPLICATE_INVOICE_LINE). To credit the same line twice for different reasons, combine it into one line here or issue a second note.
Issue it#
curl -X POST https://api.withflintpay.com/v1/credit-notes/cn_1kmn0aExample/issue \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: issue-cn-1kmn0a-001"
Issue assigns credit_note_number, stamps issued_at, renders the PDF, and sets unallocated_money to the note's total. To return money on a paid invoice in the same call, add a refund body as shown in Refund a paid invoice. Numbering is sequential per merchant, starts at 1001, and carries the prefix from your invoice settings (CN- unless you changed it). Test mode and live mode number independently.
The note needs at least one line and a total above zero. Editing anything after issue returns 409 Conflict with CREDIT_NOTE_NOT_DRAFT.
The over-credit check runs at issue, not when you edit lines. Two drafts against the same invoice line can each look fine on their own and the second one fails at issue with CREDIT_NOTE_AMOUNT_EXCEEDS_CREDITABLE, because across every issued note a line can only be credited down to zero. If you keep more than one draft open against an invoice, expect to handle that error rather than treating issue as a formality.
Two merchants' notes never collide, but two of your own issues racing for the next number do: the second returns retryable CREDIT_NOTE_ISSUE_IN_PROGRESS and names the note holding the number. Retry with the same idempotency key.
Apply the credit#
Issuing a note does not change the invoice. Allocating it does.
curl -X POST https://api.withflintpay.com/v1/credit-notes/cn_1kmn0aExample/allocations \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: allocate-cn-1kmn0a-001" \
-d '{"amount_money": {"amount": 33228, "currency": "USD"}}'
{
"data": {
"credit_note_allocation": {
"credit_note_allocation_id": "cna_1kmn0aExample",
"credit_note_id": "cn_1kmn0aExample",
"invoice_id": "inv_1kmn0aExample",
"amount_money": {"amount": 33228, "currency": "USD"},
"allocated_at": "2026-07-20T16:04:11Z",
"idempotency_key": "allocate-cn-1kmn0a-001"
},
"credit_note": {
"credit_note_id": "cn_1kmn0aExample",
"status": "issued",
"total_money": {"amount": 33228, "currency": "USD"},
"unallocated_money": {"amount": 0, "currency": "USD"}
},
"invoice": {
"invoice_id": "inv_1kmn0aExample",
"status": "open",
"outstanding_money": {"amount": 216772, "currency": "USD"}
}
}
}
Allocation requires an Idempotency-Key (IDEMPOTENCY_KEY_REQUIRED), as do the refund calls. The key is the allocation's identity: it comes back on the allocation, it is queryable with GET /v1/credit-notes/{credit_note_id}/allocations?idempotency_key=..., replaying it with the same amount returns the original allocation, and reusing it with a different amount returns IDEMPOTENCY_KEY_REUSED. That is what makes a timed-out request safe to repeat.
Unlike every other credit-note endpoint, allocate and reverse return three objects: the allocation, the note with its new unallocated_money, and the recomputed invoice. You do not need a follow-up fetch to see the new balance.
The amount has to clear two ceilings, and both report as CREDIT_NOTE_ALLOCATION_EXCEEDS_BALANCE: the note's unallocated_money, and the invoice's outstanding balance. An invoice that is paid, void, uncollectible, or already credited has no outstanding balance, so any allocation against it fails. A note that is still draft returns CREDIT_NOTE_NOT_ISSUED.
Allocation is also rejected with retryable INVOICE_PAYMENT_RESOLVING while an online payment is resolving, for the same reason recording an offline payment is: the customer may be mid-payment for a balance you are about to change.
When allocations take the balance to zero and no money was collected, the invoice becomes credited, closed_at is stamped, and invoice.credited fires.
Reverse an allocation#
Applied the credit to the wrong invoice, or applied too much? Reverse it.
curl -X POST https://api.withflintpay.com/v1/credit-notes/cn_1kmn0aExample/allocations/cna_1kmn0aExample/reverse \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: reverse-cna-1kmn0a-001"
Reversal is append-only: the original allocation keeps its row and gains reversed_at, so the history reads as what happened rather than what it ended up looking like. The credit returns to unallocated_money and the invoice balance reopens. A credited invoice that reopens goes back to open or partially_paid and emits invoice.updated. Handle that event: credited is not a terminal status the way paid and void are.
Reversing with the same idempotency key twice returns the same reversal. Reversing an already-reversed allocation under a different key returns CREDIT_NOTE_ALLOCATION_ALREADY_REVERSED.
Refund a paid invoice#
Use both commerce.credit_notes.write and commerce.refunds.write for either refund workflow. The source invoice must be paid and have eligible payments collected through Flint. Manually recorded payments cannot be refunded through these actions.
To issue a draft and request a refund together:
POST /v1/credit-notes/cn_123/issue
Idempotency-Key: correction-1042-refund
Content-Type: application/json
{
"refund": {
"reason": "other",
"reason_message": "Correcting an overcharge",
"amount_money": { "amount": 1000, "currency": "USD" }
}
}
The response keeps the issued credit note at data and includes the new refund at data.refund. The refund has credit_note_id, invoice_id, and idempotency_key. Its initial status can be pending; acceptance does not mean the money has arrived.
For an already-issued note, use:
POST /v1/credit-notes/cn_123/refunds
Idempotency-Key: correction-1042-remainder
Content-Type: application/json
{ "reason": "other" }
This returns the Refund resource at data. Omit amount_money to refund all available credit, or specify a partial amount. Flint resolves an omitted amount once when accepting the request. If the invoice's eligible payments cannot cover the requested amount, the request fails without creating a smaller refund.
pending_refund_money reserves credit while a refund is unresolved. refunded_money records succeeded amounts. unallocated_money excludes both, along with existing invoice allocations. A refund leaves the invoice paid with no outstanding balance. You cannot void a credit note while a refund is pending or after any of its refunds succeed.
SDK and CLI#
With the Node SDK, pass the saved command key in request options. For combined issuance:
const result = await flint.creditNotes.issue(
draftNoteId,
{ refund: { reason: "other" } },
{ idempotencyKey: savedIssueKey },
);
const refund = result.refund;
For a note that is already issued:
const refund = await flint.creditNotes.createRefund(
issuedNoteId,
{ reason: "other", amount_money: { amount: "1000", currency: "USD" } },
{ idempotencyKey: savedRefundKey },
);
const recovered = await flint.creditNotes.listRefunds(issuedNoteId, {
idempotency_key: savedRefundKey,
});
These SDK methods generate a key before dispatch if you omit one. Persist and supply your own key when you need to recover after a process restart or retry through another SDK call.
The CLI accepts the same JSON request bodies through --input. Save the combined issuance body above as issue-refund.json, or the issued-note refund body as refund.json. Use separate saved keys for separate actions:
flint credit-notes issue cn_123 --input issue-refund.json --idempotency-key "$ISSUE_KEY"
flint credit-notes refunds create cn_456 --input refund.json --idempotency-key "$REFUND_KEY"
flint credit-notes refunds list cn_456 --filter-idempotency-key "$REFUND_KEY"
Use --dry-run=client --output json on either mutation to inspect its request and generated key without sending it.
Recover and retry#
Save your idempotency key with the business action. Reuse the identical request and key after a timeout; changing the input with the same key returns a conflict. These refund command identities remain retained for the lifetime of their financial effects.
Use GET /v1/credit-notes/cn_123/refunds?idempotency_key=correction-1042-refund to recover a refund after losing the response. Listing requires both credit-note and refund read scopes. Results include failed attempts and use the standard page_size, page_token, and next_page_token pagination fields.
Track the existing refund.* events or read the refund for its current status. A failed or canceled payment refund releases its unsuccessful amount for a new request with a new key. A partially succeeded refund keeps the successful amount consumed. The credit note stays issued, and its PDF stays unchanged. Do not start another request while the previous outcome is uncertain.
This workflow creates new refunds. It does not attach existing refunds, record money returned outside Flint, or create a customer credit balance.
Void a credit note#
Void retires a note that should not have been issued.
curl -X POST https://api.withflintpay.com/v1/credit-notes/cn_1kmn0aExample/void \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: void-cn-1kmn0a-001"
Every allocation has to be reversed first, and no refund on the note can be pending or succeeded. Both report as CREDIT_NOTE_HAS_ALLOCATIONS. Read details[].blocking_resources for the active credit_note_allocation and public refund IDs, and blocking_resource_count for the total. Only issued notes void (CREDIT_NOTE_NOT_ISSUED). Void is terminal: there is no un-void, and there is no way to delete a draft. A draft you no longer want can simply be left, or emptied with PATCH /v1/credit-notes/{credit_note_id}, sending its current expected_version and credit_note_lines: [].
Issuing a credit note also locks the invoice against voiding: an invoice with an issued credit note against it returns INVOICE_HAS_ISSUED_CREDIT_NOTE. Void the issued credit notes listed in details[].blocking_resources first, then the invoice.
Download the PDF#
curl https://api.withflintpay.com/v1/credit-notes/cn_1kmn0aExample/pdf \
-H "Authorization: Bearer YOUR_API_KEY" \
-o credit-note-CN-1001.pdf
The PDF carries your branding, the credited lines, and the original invoice number. It exists from issue onward; asking for a draft's PDF returns CREDIT_NOTE_NOT_ISSUED. The endpoint returns application/pdf directly, so pipe it to a file or proxy it to your own back office.
Show it to the customer#
Three read endpoints work from a customer session or from the invoice access token in a hosted invoice link, so a buyer looking at their invoice can see what was credited:
GET /v1/me/invoices/{invoice_id}/credit-notesGET /v1/me/invoices/{invoice_id}/credit-notes/{credit_note_id}GET /v1/me/invoices/{invoice_id}/credit-notes/{credit_note_id}/pdf
These return issued and void notes, newest first. Drafts are not visible to the buyer and return 404. The buyer shape drops unallocated_money and the merchant identifiers, because how much credit is left to apply is your bookkeeping, not theirs. It keeps pending_refund_money, refunded_money, and latest_refund_status, so the buyer can see when a refund on the note is on its way and when it arrived.
Query credit notes#
GET /v1/credit-notes returns your notes newest first, filterable by invoice_id and by status (draft, issued, or void; anything else returns INVALID_CREDIT_NOTE_STATUS).
curl -G https://api.withflintpay.com/v1/credit-notes \
-H "Authorization: Bearer YOUR_API_KEY" \
--data-urlencode "invoice_id=inv_1kmn0aExample" \
--data-urlencode "status=issued"
GET /v1/credit-notes/{credit_note_id}/allocations does the same for one note's allocations, including the reversed ones. All list endpoints paginate with page_token; see Pagination.
Handle webhook events#
curl -X POST https://api.withflintpay.com/v1/webhook-endpoints \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: webhook-credit-notes-001" \
-d '{
"url": "https://example.com/webhooks/flint",
"enabled_events": [
"credit_note.issued",
"credit_note.allocation_created",
"credit_note.allocation_reversed",
"credit_note.voided",
"invoice.credited"
],
"description": "Credit note lifecycle"
}'
Replacing lines arrives as credit_note.updated, not as its own event, so a handler that syncs credit notes into your ledger should re-read the lines on every updated.
Both credit_note.* families need commerce.credit_notes.read on the endpoint's key. See the Webhooks guide for registration, signature verification, and retry behavior.
Errors you will encounter#
Credit note state conflicts return 409 Conflict with type: "conflict_error". Invalid request inputs return 400 Bad Request with type: "validation_error".
All errors use the standard error envelope.
Related docs#
- Credit Notes API Reference: every field on every endpoint.
- Invoicing: the invoice a credit note corrects.
- Refunds: returning money that was already collected.
- Idempotency: safe retries and command keys.
- Money & currency: minor units and currency handling.
