Invoicing

An invoice records that a specific customer owes you a specific amount, then does the work of collecting it. Flint emails the customer a hosted payment page, tracks the outstanding balance as card, check, and wire payments land, follows up with reminders when you ask it to, and renders a PDF that accounts-payable teams can file. Through all of it you keep one object that can answer the question no other payment surface can: how much is still owed?

That makes invoices the right tool for consulting work billed net 30, B2B sales that route through an approval chain, and any balance that might arrive late, in pieces, or by check. They are the wrong tool when the money is due right now (that is a checkout session) or when many buyers should share one URL (that is a payment link).

Rule of thumb: payment links share, checkout sessions collect, invoices chase. If you are not sure which you need, see Payment Links vs Checkout Sessions vs Invoices.

How an Invoice Works#

Every invoice is backed by an order, the durable record of what was sold. You either point the invoice at an order you already have, or pass quick_pay and Flint creates the backing order for you. The invoice adds the receivable layer on top: who owes, how much, by when, and what has been collected so far.

An invoice starts as a draft you can freely edit. Sending it issues the invoice: the billing content freezes into a snapshot, a hosted payment link is minted, and the first email goes out. From there, payments move it forward until the balance reaches zero:

text
draft ── send ──▶ open ─┬─ full payment ─────────────────────▶ paid
  │                     ├─ partial payment ─▶ partially_paid ─▶ paid
  │                     └─ void   (unpaid invoices only)
  └─ void

Three fields do the receivables math for you, all integers in the currency's minor unit:

  • amount_due_money is what is still owed. It starts at the billed total and runs down as payments apply.
  • paid_money is what has been collected, across card payments and recorded offline payments.
  • refunded_money tracks money returned after collection. Refunds live on a separate axis, refund_status (none, partially_refunded, refunded), so a paid invoice stays paid even when later refunded.

Two ownership rules keep the books unambiguous:

  • One invoice per order. An order can have at most one non-void invoice. Creating a second draft against the same order fails with ORDER_ALREADY_HAS_ACTIVE_INVOICE until the first is voided.
  • The invoice owns collection. Once sent, the invoice is the only way to collect on its order. Generic checkout sessions and payment intents against the backing order are rejected, and the order cannot be edited, until the invoice is paid or voided. This is what keeps amount_due_money truthful.

is_overdue is computed, never stored: it flips to true when due_at passes while a balance remains. Nothing is emailed automatically when that happens; see Remind the Customer for how to run your own follow-up cadence.

Create an Invoice Draft#

POST /v1/invoices takes exactly one source: order_id or quick_pay.

From an Existing Order#

Use order_id when the order already exists, for example a quote your app assembled line by line. The order must still be open with no payments or refunds recorded, and no other active invoice:

Bash
curl -X POST https://api.withflintpay.com/v1/invoices \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: invoice-po-1042-001" \
  -d '{
    "order_id": "ord_1kmn0aExample",
    "recipient_email": "ap@example.com",
    "due_at": "2026-08-01T00:00:00Z",
    "reference": "PO-1042",
    "memo": "Net 30. Thank you for your business."
  }'

Quick Pay#

Use quick_pay when there is no order yet and the invoice is the sale, the common case for services billing. Flint creates the backing order internally:

Bash
curl -X POST https://api.withflintpay.com/v1/invoices \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: invoice-consulting-june-001" \
  -d '{
    "quick_pay": {
      "customer_id": "cus_1kmn0aExample",
      "line_items": [{
        "name": "Consulting, June 2026",
        "quantity": 1,
        "unit_price_money": {"amount": 250000, "currency": "USD"}
      }]
    },
    "recipient_email": "ap@example.com",
    "due_at": "2026-08-01T00:00:00Z",
    "memo": "Net 30. Thank you for your business.",
    "metadata": {"engagement": "acme-q2"}
  }'
JSON
{
  "data": {
    "invoice_id": "inv_1kmn0aExample",
    "order_id": "ord_1kmn0aExample",
    "customer_id": "cus_1kmn0aExample",
    "invoice_number": "1042",
    "status": "draft",
    "refund_status": "none",
    "collection_block_status": "none",
    "due_at": "2026-08-01T00:00:00Z",
    "recipient_email": "ap@example.com",
    "amount_due_money": {"amount": 250000, "currency": "USD"},
    "paid_money": {"amount": 0, "currency": "USD"},
    "refunded_money": {"amount": 0, "currency": "USD"},
    "is_overdue": false,
    "metadata": {"engagement": "acme-q2"}
  }
}

Beyond line_items, quick pay accepts discounts, a requested_tip, a buyer_note shown to the customer, and an internal_note for your own team. customer_id links the invoice to a customer so the invoice document carries their name and billing details, and so GET /v1/invoices?customer_id=... finds it later. There is no top-level customer_id on the create request; it lives inside quick_pay.

The response includes snapshot, the customer-facing billing content (line items, totals, notes). It is drafted now and frozen at send.

invoice_number is assigned at creation: sequential per merchant, with test mode and live mode numbered independently. Every invoice also gets your metadata and an optional external_reference_id for correlating with your own billing system; both come back on every fetch, and external_reference_id is an exact-match list filter.

Billing Fields#

All of these are optional at create time and editable while the invoice is a draft:

FieldWhat it does
recipient_emailWhere the invoice email goes. Required before you can send.
cc_emailsAdditional recipients copied on the initial send and every reminder.
due_atWhen payment is due. Drives is_overdue and the due_before/due_after list filters.
referenceA customer-facing reference such as a PO number, shown on the invoice and PDF.
service_atThe service date shown on the document, for billing that trails delivery.
memoA message to the customer, shown on the invoice.
footerFine print at the bottom of the invoice and PDF.
scheduled_send_atHands the send to Flint at a future time. See Schedule the send.
metadata, external_reference_idYour own correlation tags, returned on every fetch and (for external_reference_id) filterable.

Edit a Draft#

PATCH /v1/invoices/{invoice_id} updates any of the fields above with sparse semantics: omitted fields are unchanged, present fields are applied.

Bash
curl -X PATCH https://api.withflintpay.com/v1/invoices/inv_1kmn0aExample \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{
    "cc_emails": ["accounting@example.com"],
    "footer": "Questions? Contact billing@yourcompany.com"
  }'

Drafts only. Once sent, an invoice is immutable except for delivery actions (reminders and link regeneration); edits return INVOICE_NOT_DRAFT. If a sent invoice is wrong, void it and issue a corrected one. To change the line items on an order-backed draft, edit the order; the snapshot refreezes from the order at send.

Send the Invoice#

Bash
curl -X POST https://api.withflintpay.com/v1/invoices/inv_1kmn0aExample/send \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: send-inv-1kmn0a-001"
JSON
{
  "data": {
    "invoice": {
      "invoice_id": "inv_1kmn0aExample",
      "status": "open",
      "sent_at": "2026-07-02T17:20:04Z",
      "amount_due_money": {"amount": 250000, "currency": "USD"}
    },
    "public_url": "https://checkout.withflintpay.com/i/ivat_1kmn0aExample#invoice_token=...",
    "delivery_attempt": {
      "invoice_delivery_attempt_id": "indel_1kmn0aExample",
      "delivery_type": "send",
      "channel": "email",
      "to_email": "ap@example.com",
      "status": "sent",
      "sent_at": "2026-07-02T17:20:05Z"
    }
  }
}

Send is the moment the invoice becomes real. In one operation, Flint:

  1. Issues the invoice: draft becomes open and sent_at is stamped.
  2. Freezes the billing snapshot. If tax is enabled on the order, tax is recalculated first, then frozen. From here on, nothing about the underlying order changes what the customer was billed.
  3. Mints the hosted payment link and returns it as public_url.
  4. Emails the recipient, copying cc_emails, and reports the outcome in delivery_attempt.
  5. Takes ownership of collection on the backing order.

Issuance is authoritative even if the email fails. The invoice is open and collectible the moment send returns; check delivery_attempt.status (sent, pending, or failed) to confirm the email went out. A failed delivery is retried by calling send-reminder, not by sending again.

Send validates more strictly than draft creation, because it is the last moment to catch a conflict. It requires recipient_email (RECIPIENT_EMAIL_REQUIRED) and rejects when the backing order is no longer billable: closed or canceled (ORDER_NOT_OPEN), already paid or refunded (ORDER_ALREADY_HAS_PAYMENTS, ORDER_ALREADY_HAS_REFUNDS), mid-collection through an open checkout session or active payment intent (ORDER_HAS_OPEN_CHECKOUT, ORDER_HAS_ACTIVE_PAYMENT_INTENT), or carrying manual payments recorded directly on the order (ORDER_HAS_MANUAL_PAYMENTS). Resolve the conflict, then send again.

Schedule the Send#

Set scheduled_send_at on a draft and Flint issues and delivers it for you at that time, exactly as if you had called send:

Bash
curl -X PATCH https://api.withflintpay.com/v1/invoices/inv_1kmn0aExample \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{"scheduled_send_at": "2026-07-15T13:00:00Z"}'

The timestamp must be in the future (SCHEDULED_SEND_IN_PAST) and the draft needs a recipient_email at scheduling time. The send preconditions above are checked when you schedule and again when the moment arrives, so a conflict that appears in between surfaces then rather than silently billing a broken order.

To cancel, set "scheduled_send_at": null. Sending manually or voiding the draft also clears the schedule.

What the Customer Receives#

The email is titled "New invoice" and carries the invoice number, amount due, due date, your memo, and a "Pay invoice" button that opens the hosted page. Reminders reuse the same layout under "Invoice reminder".

The hosted page at public_url is the invoice itself: line items, totals, due date, your reference and notes, and the merchant name your account displays. While the invoice is open or partially_paid, the page shows a "Pay invoice" button that starts a card checkout for the remaining balance; a partially paid invoice shows how much of the total has been collected. A paid invoice shows its receipt state, and a voided one explains there is nothing to pay. The first time the customer opens the page, Flint stamps viewed_at on the invoice and records a viewed entry in the audit trail, so you can tell "never saw it" from "saw it and has not paid".

The URL is a capability: anyone who has it can view the invoice and pay. It stays valid well past the due date, but it is revocable. If a link leaks, or an email was forwarded somewhere it should not have been, rotate it:

Bash
curl -X POST https://api.withflintpay.com/v1/invoices/inv_1kmn0aExample/regenerate-public-link \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: regen-inv-1kmn0a-001"

The old link stops working immediately and the response carries the new public_url. Regeneration only exists after send (INVOICE_NOT_SENT before that), and reminder emails always carry the currently active link. If a reminder ever fails with INVOICE_LINK_UNAVAILABLE, the link has lapsed; regenerate and remind again.

Collect Payment#

Money reaches an invoice on two rails. Online card payment always collects the full remaining balance through hosted checkout. Offline recording accepts any amount up to the balance, which is how partial payments happen. Both rails move status and amount_due_money the same way, and every event lands in the audit trail.

Online Card Payment#

Customers normally self-serve from the email or hosted page and you never touch this endpoint. Call it yourself when your own surface needs to start the payment, for example a "Pay now" button inside your customer portal:

Bash
curl -X POST https://api.withflintpay.com/v1/invoices/inv_1kmn0aExample/checkout-session \
  -H "Authorization: Bearer YOUR_API_KEY"
JSON
{
  "data": {
    "invoice_payment_attempt": {
      "invoice_payment_attempt_id": "invpa_1kmn0aExample",
      "rail": "card",
      "status": "open",
      "expected_amount_money": {"amount": 250000, "currency": "USD"},
      "checkout_session_id": "cs_1kmn0aExample",
      "expires_at": "2026-07-03T17:20:04Z"
    },
    "checkout_session": {
      "checkout_session_id": "cs_1kmn0aExample",
      "status": "open",
      "invoice_id": "inv_1kmn0aExample"
    },
    "hosted_checkout": {
      "url": "https://checkout.withflintpay.com/cs/cs_1kmn0aExample",
      "checkout_auth_token": "..."
    },
    "reused_existing": false
  }
}

The endpoint ensures a viable session rather than blindly creating one. If an invoice checkout session is already open with more than 30 minutes before expiry, you get it back with reused_existing: true and a 200; otherwise the stale session is retired and a fresh one returns with a 201. Sessions last 24 hours. It only works while the invoice is open or partially_paid with a balance remaining (INVOICE_NOT_COLLECTIBLE otherwise).

The invoice_payment_attempt is the invoice's record of the collection try: its expected_amount_money targets the balance at creation, and its status ends at settled, failed, canceled, or expired. Attempts expire with their session, so an abandoned checkout cleans itself up.

Always collect an invoiced order through this endpoint, never through a generic order checkout session or payment intent. The invoice owns collection while it is open, so generic attempts are rejected, and this endpoint is what keeps the payment attached to the invoice's balance, events, and webhooks.

Record an Offline Payment#

When a check clears, a wire lands, or cash changes hands, record it so the receivable reflects reality:

Bash
curl -X POST https://api.withflintpay.com/v1/invoices/inv_1kmn0aExample/manual-payments \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: manual-pay-check-4521" \
  -d '{
    "amount_money": {"amount": 150000, "currency": "USD"},
    "received_at": "2026-07-20T00:00:00Z",
    "external_reference_id": "check-4521",
    "note": "Check #4521, received by mail"
  }'

The amount must be positive, in the invoice currency (CURRENCY_MISMATCH), and no more than the remaining balance (AMOUNT_EXCEEDS_BALANCE). A payment that covers part of the balance moves the invoice to partially_paid; covering the rest moves it to paid and stamps paid_at. received_at backdates the payment for your records, and external_reference_id is the natural home for the check or wire number. Each recording emits invoice.manual_payment_recorded.

Recording is rejected while online collection is in flight, because the customer might be typing their card number for a balance your check just changed. Three conditions block it, each with its own code: an open checkout session (INVOICE_HAS_OPEN_CHECKOUT), an active payment intent (INVOICE_HAS_ACTIVE_PAYMENT_INTENT), or a live card payment attempt (INVOICE_HAS_OPEN_PAYMENT_ATTEMPT). Close the session or cancel the intent and retry; attempts expire on their own with their session.

Reverse a Manual Payment#

If a recorded payment was mis-keyed or the check bounces, reverse it:

Bash
curl -X POST https://api.withflintpay.com/v1/invoices/inv_1kmn0aExample/manual-payments/reverse \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: reverse-check-4521" \
  -d '{
    "amount_money": {"amount": 150000, "currency": "USD"},
    "note": "Check #4521 returned NSF"
  }'

A reversal is a bookkeeping correction, not a refund: it can only undo offline amounts, never card money. The reversal cannot exceed what manual payments currently have applied (REVERSAL_EXCEEDS_MANUAL_PAYMENTS), and it moves status backward as the balance reopens, paid back to partially_paid or open. Each reversal emits invoice.manual_payment_reversed. To return card money, use a refund instead.

Collection Blocks#

One rare state pauses both rails: an invoice generated by a subscription renewal whose items ran out of inventory carries collection_block_status: "inventory_blocked", and collection attempts return INVOICE_COLLECTION_BLOCKED until the shortfall is resolved. The hosted page tells the customer payment is temporarily unavailable rather than failing at pay time. Invoices you create yourself are never blocked; the field reads none.

Remind the Customer#

Flint never emails your customers on its own schedule. due_at passing flips is_overdue and nothing else, so reminders are always an explicit act:

Bash
curl -X POST https://api.withflintpay.com/v1/invoices/inv_1kmn0aExample/send-reminder \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: reminder-inv-1kmn0a-001"

Reminders work for any collectible invoice, open or partially_paid with a balance remaining (INVOICE_NOT_COLLECTIBLE otherwise), and each one is a fresh delivery attempt with its own outcome in the response. There is no built-in cadence or cap, so the polite pacing is yours to design. A simple daily job covers most needs:

Bash
curl -G https://api.withflintpay.com/v1/invoices \
  -H "Authorization: Bearer YOUR_API_KEY" \
  --data-urlencode "is_overdue=true" \
  --data-urlencode "status=open,partially_paid" \
  --data-urlencode "sort_by=due_at" \
  --data-urlencode "sort_direction=asc"

Then call send-reminder for each result on whatever schedule fits your customer relationships, and use each invoice's audit trail (reminder_sent entries) to avoid over-mailing.

Void an Invoice#

Void cancels an invoice that should never be collected: a duplicate, a bad amount, a deal that fell through.

Bash
curl -X POST https://api.withflintpay.com/v1/invoices/inv_1kmn0aExample/void \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: void-inv-1kmn0a-001"

Void is terminal and only reaches unpaid invoices: drafts and open invoices with nothing collected (INVOICE_NOT_VOIDABLE otherwise). It zeroes amount_due_money, stamps voided_at, cancels any scheduled send, emits invoice.voided, and releases the backing order so it can be edited or collected again. The hosted page stays reachable but tells the customer the invoice was voided and there is nothing to pay.

Money that already moved determines the path to void:

  • Partially paid by check or wire: reverse the manual payments back to zero, which returns the invoice to open, then void.
  • Any card money collected: the invoice is settled business. Refund it; it keeps its paid or partially_paid status with refund_status telling the story.

Like manual recording, void is rejected while online collection is in flight (INVOICE_HAS_OPEN_CHECKOUT, INVOICE_HAS_ACTIVE_PAYMENT_INTENT, INVOICE_HAS_OPEN_PAYMENT_ATTEMPT), so a customer mid-payment never has the invoice yanked out from under them.

Refund an Invoiced Order#

Refunds flow through the standard Refunds API against the backing order; there is no invoice-specific refund endpoint. The invoice reflects the outcome on its own axis: refunded_money accumulates, refund_status moves through partially_refunded to refunded, and invoice.partially_refunded or invoice.refunded fires. The invoice's status does not move backward; a paid invoice that was fully refunded reads status: "paid", refund_status: "refunded", which is exactly how your accountant thinks about it.

Download the PDF#

Bash
curl https://api.withflintpay.com/v1/invoices/inv_1kmn0aExample/pdf \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -o invoice-1042.pdf

The PDF renders from the invoice's snapshot: for a draft that is a preview of the current content, and from send onward it is the frozen billing document, immune to anything that later happens to the order. The endpoint returns application/pdf directly, so pipe it to a file or proxy it to your own back office.

Handle Webhook Events#

Delivery outcomes and inbound payments happen long after your API call returns, so webhooks are how your system finds out. Register an endpoint with the invoice events you care about:

Bash
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-invoicing-001" \
  -d '{
    "url": "https://example.com/webhooks/flint",
    "enabled_events": [
      "invoice.sent",
      "invoice.paid",
      "invoice.partially_paid",
      "invoice.manual_payment_recorded",
      "invoice.manual_payment_reversed",
      "invoice.voided",
      "invoice.delivery_failed"
    ],
    "description": "Invoice lifecycle"
  }'
EventFires when
invoice.sentThe invoice was issued and the initial email was attempted
invoice.paidAn online card payment settled and the balance reached zero
invoice.partially_paidAn online card payment settled with a balance remaining
invoice.manual_payment_recordedAn offline payment was recorded
invoice.manual_payment_reversedAn offline payment was reversed
invoice.voidedThe invoice was voided
invoice.delivery_succeededAn email (initial send or reminder) was delivered
invoice.delivery_failedAn email delivery failed; check delivery-attempts and consider a resend
invoice.partially_refundedA refund landed, with some collected money remaining
invoice.refundedCollected money was fully refunded

Every invoice event's payload identifies the invoice and carries its post-event state, so most handlers never need a follow-up fetch:

JSON
{
  "invoice_id": "inv_1kmn0aExample",
  "order_id": "ord_1kmn0aExample",
  "customer_id": "cus_1kmn0aExample",
  "invoice_number": "1042",
  "status": "partially_paid",
  "refund_status": "none",
  "amount_due_money": {"amount": 100000, "currency": "USD"}
}

One subtlety worth coding for: invoice.paid fires for online card payments. An offline payment that clears the balance arrives as invoice.manual_payment_recorded whose payload shows "status": "paid", not as a separate invoice.paid. If your fulfillment or dunning logic keys on "fully paid", trigger it on either invoice.paid or any invoice event whose payload status is paid, and it will be right on both rails.

See the Webhooks guide for endpoint registration, signature verification, and retry behavior.

Inspect Delivery and History#

Two sub-resources give the invoice a paper trail.

Delivery attempts record every email, initial send and reminders alike, with delivery_type (send or reminder), the addresses used, and a status of pending, sent, or failed with an error message when there is one:

Bash
curl "https://api.withflintpay.com/v1/invoices/inv_1kmn0aExample/delivery-attempts" \
  -H "Authorization: Bearer YOUR_API_KEY"

Events are the audit timeline: draft_created, sent, viewed, payment_applied, manual_payment_recorded, manual_payment_reversed, refund_succeeded, reminder_sent, token_regenerated, voided, and friends, each with a timestamp and the actor that caused it. When a customer says "we never got it", this is where the conversation ends:

Bash
curl "https://api.withflintpay.com/v1/invoices/inv_1kmn0aExample/events" \
  -H "Authorization: Bearer YOUR_API_KEY"

Query and Reconcile#

GET /v1/invoices is the receivables report. Filters compose, and everything money-related is queryable:

ParameterWhat it selects
statusOne or more of draft, open, partially_paid, paid, void; repeat or comma-separate
customer_id, order_idInvoices for a customer or the invoice on an order
is_overdue, has_amount_dueThe chase list and the open-balance list
due_after, due_before, created_after, created_beforeRFC3339 time windows
external_reference_idExact match on your correlation ID
querySearch across invoice ID, invoice number, external reference, recipient email, and reference
sort_by, sort_directioncreated_at, updated_at, due_at, invoice_number, or amount_due_money, ascending or descending

So "aging report, biggest exposures first" is one call:

Bash
curl -G https://api.withflintpay.com/v1/invoices \
  -H "Authorization: Bearer YOUR_API_KEY" \
  --data-urlencode "has_amount_due=true" \
  --data-urlencode "sort_by=amount_due_money" \
  --data-urlencode "sort_direction=desc"

Single fetches can pull related records inline with expand, saving the follow-up requests:

Bash
curl "https://api.withflintpay.com/v1/invoices/inv_1kmn0aExample?expand=customer,order" \
  -H "Authorization: Bearer YOUR_API_KEY"

Results paginate with page_token like every list endpoint; see Pagination.

Test the Flow End to End#

Run the whole loop in test mode with your flint_test_... key:

  1. Create a quick pay draft and send it. Both calls are above; the send response carries public_url.
  2. Open public_url in a browser. You are seeing exactly what your customer sees, and the invoice's viewed_at is now set.
  3. Click "Pay invoice" and pay with the standard test card, 4242 4242 4242 4242, any future expiry, any CVC.
  4. Confirm the loop the way production will run: your webhook endpoint received invoice.paid, and GET /v1/invoices/{invoice_id} shows status: "paid" with amount_due_money at zero.
  5. Repeat with the offline rail: send a second invoice, record a partial manual payment, watch partially_paid arrive, then reverse it and watch the balance reopen.

If your sandbox can't take card payments yet, the hosted invoice page has no way to collect and the pay flow shows no payment methods. Check GET /v1/capabilities?capability=accept_card_payments; once it reports ready, reload the page.

Declines, 3D Secure challenges, and other card scenarios are in the Testing guide.

Errors You Will Encounter#

All errors use the standard error envelope. The ones specific to invoicing, grouped by what you were doing:

DoingCodeMeaning and fix
CreatingINVOICE_SOURCE_REQUIRED, INVOICE_SOURCE_CONFLICTProvide exactly one of order_id or quick_pay
CreatingORDER_ALREADY_HAS_ACTIVE_INVOICEThe order is claimed by a non-void invoice; void it first
Creating, sendingORDER_NOT_OPEN, ORDER_ALREADY_HAS_PAYMENTS, ORDER_ALREADY_HAS_REFUNDSThe order is not billable; invoice orders before collecting on them
Editing, sendingINVOICE_NOT_DRAFTOnly drafts can be edited or sent; issued invoices change through their action endpoints
Sending, scheduling, remindingRECIPIENT_EMAIL_REQUIREDSet recipient_email on the draft first
SendingORDER_HAS_OPEN_CHECKOUT, ORDER_HAS_ACTIVE_PAYMENT_INTENT, ORDER_HAS_MANUAL_PAYMENTSThe order has competing collection activity; close, cancel, or resolve it before sending
SchedulingSCHEDULED_SEND_IN_PASTscheduled_send_at must be a future RFC3339 timestamp
Collecting, remindingINVOICE_NOT_COLLECTIBLEThe invoice is not open or partially_paid with a balance remaining
Recording, voidingINVOICE_HAS_OPEN_CHECKOUT, INVOICE_HAS_ACTIVE_PAYMENT_INTENT, INVOICE_HAS_OPEN_PAYMENT_ATTEMPTOnline collection is in flight; close the session or cancel the intent, attempts expire on their own
RecordingINVALID_AMOUNT, CURRENCY_MISMATCH, AMOUNT_EXCEEDS_BALANCEPositive amount, invoice currency, no more than amount_due_money
ReversingINVOICE_NOT_REVERSIBLE, REVERSAL_EXCEEDS_MANUAL_PAYMENTSReversals apply to sent invoices, up to the manual amount currently applied
VoidingINVOICE_NOT_VOIDABLEOnly unpaid drafts and open invoices void; reverse or refund collected money first
Link managementINVOICE_NOT_SENT, INVOICE_LINK_UNAVAILABLELinks exist only after send; a lapsed link needs regenerate-public-link
CollectingINVOICE_COLLECTION_BLOCKEDSubscription-renewal inventory hold; collection resumes when the shortfall is resolved

Requests for an unknown invoice_id return 404; if an invoice you just created seems missing, check that you are calling with the same mode key that created it, since test and live invoices are separate.

Rate this doc