Invoicing

An invoice records that a specific customer owes you a specific amount, then tracks collection across card, ACH debit, and payments received elsewhere. Flint can email a hosted payment page, charge a saved payment method, or leave collection to your application. It also records every attempt and renders a PDF that accounts-payable teams can file.

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. Issuing it assigns the invoice number, freezes the billing snapshot, and makes the receivable collectible. Delivery is an explicit choice at issue time. From there, payments or closure actions move it forward:

The Invoices reference has the status diagram and every field on the object. paid, void, and uncollectible are final; credited is the one status that can move backward, when a credit note allocation is reversed.

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

  • outstanding_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, ACH debit, 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 issued, 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 closes. This keeps outstanding_money truthful.

is_overdue is computed, never stored: it flips to true when due_at passes while a balance remains. Passing due_at also starts the follow-up Flint runs for you, which is reminder emails on your configured cadence, an invoice.overdue event, and a late-fee notice if the invoice's terms carry one. See Follow up on an unpaid invoice.

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",
    "collection": {"mode": "buyer_initiated"},
    "payment_due": {"type": "absolute", "due_at": "2026-08-01T00:00:00Z"},
    "recipient_email": "ap@example.com",
    "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"}
      }]
    },
    "collection": {
      "mode": "buyer_initiated",
      "payment_policy": {"enabled_payment_options": ["card", "ach_debit"]}
    },
    "payment_due": {"type": "absolute", "due_at": "2026-08-01T00:00:00Z"},
    "recipient_email": "ap@example.com",
    "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",
    "status": "draft",
    "refund_status": "none",
    "collection_block_status": "none",
    "due_at": "2026-08-01T00:00:00Z",
    "recipient_email": "ap@example.com",
    "outstanding_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 issue.

invoice_number is absent on drafts. It is assigned transactionally at issue, 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#

collection and payment_due are required at create time. Use merchant_default in either input to inherit the matching invoice settings. The resolved values are stored on the invoice so later settings changes do not rewrite an existing receivable.

These fields are editable while the invoice is a draft:

FieldWhat it does
collectionbuyer_initiated, automatic, external, or merchant_default; automatic collection requires a saved payment_method_id.
payment_duenone, absolute, payment_terms, customer_default, or merchant_default.
recipient_emailWhere invoice email goes when issue uses delivery_mode: email.
cc_emailsAdditional recipients copied on the initial send and every reminder.
payment_dueResolves the due date. An absolute value carries due_at; terms reference invoice_payment_term_id.
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 issuance and delivery to Flint at a future time. See Schedule the issue.
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 issued, an invoice is immutable except through its action endpoints; edits return INVOICE_NOT_DRAFT. If an issued invoice is wrong, close 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 issue.

Issue the invoice#

Bash
curl -X POST https://api.withflintpay.com/v1/invoices/inv_1kmn0aExample/issue \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: issue-inv-1kmn0a-001" \
  -d '{"delivery_mode": "email"}'
JSON
{
  "data": {
    "invoice": {
      "invoice_id": "inv_1kmn0aExample",
      "status": "open",
      "invoice_number": "1042",
      "issued_at": "2026-07-02T17:20:04Z",
      "outstanding_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"
    }
  }
}

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

  1. Moves draft to open, assigns invoice_number, and stamps issued_at.
  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. Delivers according to delivery_mode: email, caller_managed, or merchant_default.
  5. Takes ownership of collection on the backing order.

Issuance is authoritative even if email fails. The invoice is open and collectible when issue returns; check delivery_attempt.status (sent, pending, or failed) when delivery mode resolves to email. A failed delivery is retried with send-reminder, not by issuing again.

Issue validates more strictly than draft creation because it is the last moment to catch a conflict. Email delivery requires recipient_email (RECIPIENT_EMAIL_REQUIRED). Issue rejects when the backing order is no longer billable or has competing collection activity. Resolve the conflict, then retry issue with the same idempotency key.

Schedule the issue#

Set scheduled_send_at on a draft and Flint issues and delivers it for you at that time:

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). Delivery preconditions are checked when you schedule and again when the moment arrives.

To cancel, set "scheduled_send_at": null. Issuing 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 shows the frozen invoice document. Buyer-initiated invoices offer the card and ACH debit options allowed by their payment policy. A partially paid invoice shows how much remains. Closed invoices explain that there is nothing left to collect. The first customer view stamps viewed_at and records a viewed audit event.

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, along with any checkout credentials derived from it. The underlying session and payment lineage are preserved. The response carries the new public_url. Regeneration only exists after issue (INVOICE_NOT_ISSUED before that), and reminder emails always carry the active link.

Collect payment#

Collection follows the invoice's frozen collection_mode:

  • buyer_initiated uses the hosted page and offers the payment options frozen in the invoice's payment policy.
  • automatic charges a saved payment method through POST /collect.
  • external accepts payments recorded through POST /manual-payments.

Only one active payment attempt can exist across all rails. Every attempt is durable and queryable, including failures returned by the collection call.

Buyer-initiated 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-08-15T00:00:00Z"
    },
    "checkout_session": {
      "checkout_session_id": "cs_1kmn0aExample",
      "status": "open",
      "invoice_id": "inv_1kmn0aExample"
    },
    "hosted_checkout": {
      "url": "https://checkout.withflintpay.com/checkout/cs_1kmn0aExample#checkout_token=...",
      "checkout_auth_token": "..."
    },
    "reused_existing": false
  }
}

The endpoint atomically resolves one compatible checkout session and invoice attempt. Any current open session before its fixed deadline is returned with reused_existing: true and a 200; a newly created pair returns with a 201. It only works for buyer_initiated invoices while they are open or partially_paid with a balance remaining.

If an expired or terminal session still has payment work resolving, Flint returns retryable INVOICE_PAYMENT_RESOLVING instead of opening competing collection. Hosted invoice checkout waits and re-evaluates automatically. Regenerating the invoice public link revokes the prior link and any checkout credentials derived from it while preserving the session and payment lineage.

The invoice_payment_attempt records the collection try. expected_amount_money captures the target balance. ACH debit attempts can remain processing and include expected_settlement_at; terminal statuses are settled, failed, canceled, and expired.

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.

Automatic collection#

Automatic invoices freeze a saved payment method when issued. Start an off-session attempt with:

Bash
curl -X POST https://api.withflintpay.com/v1/invoices/inv_1kmn0aExample/collect \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: collect-inv-1kmn0a-001" \
  -d '{}'

The response includes the updated invoice and invoice_payment_attempt. A declined attempt remains queryable even when the call returns a payment error. Flint retries the saved card for you on the schedule in invoices.autopay_retry_policy; call collect again with a new idempotency key when you want to try sooner. Autopay works with reusable saved cards only, so ACH debit and Affirm invoices collect through the hosted page instead.

List attempts with GET /v1/invoices/{invoice_id}/payment-attempts, fetch one by ID, or request cancellation with POST /v1/invoices/{invoice_id}/payment-attempts/{invoice_payment_attempt_id}/cancel. Cancellation can be asynchronous while an ACH debit is processing; inspect cancellation_requested_at, canceled_at, and the current status.

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 only while an online payment is resolving, because the customer might be mid-payment for a balance your check just changed. That case returns retryable INVOICE_PAYMENT_RESOLVING; wait for the payment to converge and retry. An open checkout session where the customer has not started paying does not block. A partial payment leaves the session open and the next launch realigns collection to the new balance; a payment that clears the balance invalidates the open checkout with reason invoice_paid_elsewhere, so the hosted page can never collect twice.

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.

Follow up on an unpaid invoice#

Issuing an invoice schedules its follow-up. Flint watches the due date and, while a balance remains, sends reminders on your cadence, marks the invoice overdue, raises a late fee notice, and retries a saved card. You can still drive any of it yourself.

Every scheduled action is gated on the invoice still being collectible. A payment that lands first cancels the rest, and nothing fires against a paid, void, uncollectible, or credited invoice.

Reminder cadence#

Reminders come from invoices.reminder_policy in your settings. Each rule is an offset in days from the due date, negative for a heads-up before it:

JSON
{
  "invoices": {
    "reminder_policy": {
      "rules": [
        {"days_from_due": -3},
        {"days_from_due": 7},
        {"days_from_due": 21}
      ]
    }
  }
}

Days are counted in the invoice's timezone, so "seven days after due" lands at the same local time of day rather than drifting across a daylight-saving change. With no rules configured, no automatic reminders go out.

The policy is frozen onto the invoice at issue. Editing your settings changes the cadence for invoices you issue from then on and leaves the ones already in flight alone.

Each firing emits invoice.reminder_due. What happens next depends on the invoice's delivery mode: email sends the reminder for you, and caller_managed sends nothing, so the webhook is your cue to send it from your own system. The payload carries delivery_mode so one handler covers both.

Pause and resume#

Once a customer replies, promises a check, or disputes the bill, stop the cadence without unpicking the schedule:

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

Both endpoints return the full invoice, and reminders_paused_at tells you which state it is in. POST /v1/invoices/{invoice_id}/resume-reminders clears it. Both require a collectible invoice (INVOICE_NOT_COLLECTIBLE).

Pausing suppresses automatic reminders only. send-reminder still works while paused, and invoice.overdue and invoice.late_fee_due still fire.

Late fees#

If the invoice's payment terms carry a late fee policy, Flint emits invoice.late_fee_due once the grace period elapses, with the fee computed from the current balance:

JSON
{
  "invoice_id": "inv_1kmn0aExample",
  "due_at": "2026-08-01T00:00:00Z",
  "base_outstanding_money": {"amount": 250000, "currency": "USD"},
  "late_fee_money": {"amount": 3750, "currency": "USD"},
  "late_fee_policy": {"type": "percentage", "percent": 1.5, "grace_period_days": 10}
}

The fee is a notification, not a charge. Flint does not add it to the invoice or move the balance. Bill it by issuing a second invoice, or waive it by ignoring the event.

Saved-card retries#

An automatic invoice whose first charge declines retries on invoices.autopay_retry_policy.retry_day_offsets, measured in days from the failure. Retries only run against a reusable saved card; ACH debit and Affirm are not available for invoice autopay. Like the reminder policy, the retry schedule is frozen at issue.

Send one yourself#

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. Use it for the follow-up that does not fit a cadence: a resend after a bounce, or the call you make after speaking to the customer.

To run your own chase list instead of the built-in cadence, leave reminder_policy empty and query the receivables:

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, 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 outstanding_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.
  • Any issued credit note against it: void returns INVOICE_HAS_ISSUED_CREDIT_NOTE. Reverse the allocations, void the credit note, then void the invoice.

Like manual recording, void is rejected with retryable INVOICE_PAYMENT_RESOLVING while an online payment is resolving, so a customer mid-payment never has the invoice yanked out from under them. An idle open checkout does not block void; it is invalidated with reason invoice_voided and the hosted page tells the customer there is nothing to pay.

Mark an invoice uncollectible#

Use POST /v1/invoices/{invoice_id}/mark-uncollectible when an issued balance will not be paid. Flint atomically writes off the remaining collectible balance, stamps uncollectible_at and closed_at, cancels idle collection work, and emits invoice.marked_uncollectible. An active payment attempt must settle or be canceled first.

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 issue 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.issued",
      "invoice.paid",
      "invoice.partially_paid",
      "invoice.manual_payment_recorded",
      "invoice.manual_payment_reversed",
      "invoice.voided",
      "invoice.delivery_failed"
    ],
    "description": "Invoice lifecycle"
  }'
A draft was created
A draft was edited, or an issued invoice's status changed outside the events below
The invoice number and immutable billing snapshot were assigned
A scheduled issue could not complete
An initial invoice email was attempted
due_at passed with a balance remaining
A reminder rule fired; delivery_mode says whether Flint sent the email
The late fee grace period elapsed; carries the computed fee, which is not charged
Credit note allocations closed the invoice with no remaining balance
An online payment settled and the balance reached zero
An online payment settled with a balance remaining
An ACH debit entered processing
The remaining balance was written off
An offline payment was recorded
An offline payment was reversed
The invoice was voided
An online payment attempt failed; the invoice stays collectible
An attempt expired before payment completed
An attempt was canceled
An email (initial send or reminder) was delivered
An email delivery failed; check delivery-attempts and consider a resend
A collection block engaged; see Collection Blocks
The block cleared and the invoice is collectible again
A refund landed, with some collected money remaining
Collected 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",
  "outstanding_money": {"amount": 100000, "currency": "USD"}
}

One subtlety to code for: invoice.paid fires for online 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.

Credit notes have their own six events under credit_note.*, listed in the Credit notes guide. 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, reminder_due, overdue, late_fee_due, credited, 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, uncollectible, or credited
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 outstanding_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=outstanding_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"

All list endpoints paginate with page_token; 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 issue it. Both calls are above; the issue 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 outstanding_money at zero.
  5. Repeat with the offline rail: issue a second invoice, record a partial manual payment, watch partially_paid arrive, then reverse it and watch the balance reopen.

If a sandbox payment option is unavailable, verify the matching merchant capability and the invoice's frozen payment_policy, then reload the hosted 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:

When creating: Provide exactly one of order_id or quick_pay
When creating: Provide exactly one of order_id or quick_pay
When creating: The order is claimed by a non-void invoice; void it first
ORDER_NOT_OPEN
When creating, issuing: The order is not billable; invoice orders before collecting on them
ORDER_ALREADY_HAS_PAYMENTS
When creating, issuing: The order is not billable; invoice orders before collecting on them
ORDER_ALREADY_HAS_REFUNDS
When creating, issuing: The order is not billable; invoice orders before collecting on them
When editing, issuing: Only drafts can be edited or issued
When issuing with email, scheduling, reminding: Set recipient_email on the draft first
ORDER_HAS_OPEN_CHECKOUT
When issuing: Resolve competing collection activity before issue
ORDER_HAS_ACTIVE_PAYMENT_INTENT
When issuing: Resolve competing collection activity before issue
ORDER_HAS_MANUAL_PAYMENTS
When issuing: Resolve competing collection activity before issue
SCHEDULED_SEND_IN_PAST
When scheduling: scheduled_send_at must be a future RFC3339 timestamp
When collecting, reminding: The invoice is not open or partially_paid with a balance remaining
When collecting, recording, voiding: An online payment is resolving; wait for it to converge, then retry the same request
When collecting: The invoice's collection state changed mid-request; refresh the invoice and retry
When recording: Positive amount, invoice currency, no more than outstanding_money
When recording: Positive amount, invoice currency, no more than outstanding_money
AMOUNT_EXCEEDS_BALANCE
When recording: Positive amount, invoice currency, no more than outstanding_money
When reversing: Reversals apply to issued invoices, up to the manual amount currently applied
REVERSAL_EXCEEDS_MANUAL_PAYMENTS
When reversing: Reversals apply to issued invoices, up to the manual amount currently applied
When voiding: Only unpaid drafts and open invoices void; reverse or refund collected money first
When voiding: Void the issued credit note before the invoice
When crediting: Credit notes attach to issued invoices that were not voided
When editing: The draft changed mid-request; fetch the invoice and retry
When pausing, resuming reminders: Reminder controls apply while a balance remains
When collecting, reminding, link management: Links exist only after issue; regenerate a lapsed link before hosted collection or reminders
When collecting: Subscription-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