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_moneyis what is still owed. It starts at the billed total and runs down as payments apply.paid_moneyis what has been collected across card, ACH debit, and recorded offline payments.refunded_moneytracks money returned after collection. Refunds live on a separate axis,refund_status(none,partially_refunded,refunded), so a paid invoice stayspaideven 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_INVOICEuntil 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_moneytruthful.
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:
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:
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"}
}'
{
"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:
| Field | What it does |
|---|---|
collection | buyer_initiated, automatic, external, or merchant_default; automatic collection requires a saved payment_method_id. |
payment_due | none, absolute, payment_terms, customer_default, or merchant_default. |
recipient_email | Where invoice email goes when issue uses delivery_mode: email. |
cc_emails | Additional recipients copied on the initial send and every reminder. |
payment_due | Resolves the due date. An absolute value carries due_at; terms reference invoice_payment_term_id. |
reference | A customer-facing reference such as a PO number, shown on the invoice and PDF. |
service_at | The service date shown on the document, for billing that trails delivery. |
memo | A message to the customer, shown on the invoice. |
footer | Fine print at the bottom of the invoice and PDF. |
scheduled_send_at | Hands issuance and delivery to Flint at a future time. See Schedule the issue. |
metadata, external_reference_id | Your 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.
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#
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"}'
{
"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:
- Moves
drafttoopen, assignsinvoice_number, and stampsissued_at. - 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.
- Mints the hosted payment link and returns it as
public_url. - Delivers according to
delivery_mode:email,caller_managed, ormerchant_default. - 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:
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:
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_initiateduses the hosted page and offers the payment options frozen in the invoice's payment policy.automaticcharges a saved payment method throughPOST /collect.externalaccepts payments recorded throughPOST /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:
curl -X POST https://api.withflintpay.com/v1/invoices/inv_1kmn0aExample/checkout-session \
-H "Authorization: Bearer YOUR_API_KEY"
{
"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:
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:
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:
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:
{
"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:
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:
{
"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#
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:
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.
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
paidorpartially_paidstatus withrefund_statustelling 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#
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:
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"
}'
due_at passed with a balance remainingdelivery_mode says whether Flint sent the emaildelivery-attempts and consider a resendEvery invoice event's payload identifies the invoice and carries its post-event state, so most handlers never need a follow-up fetch:
{
"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:
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:
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:
| Parameter | What it selects |
|---|---|
status | One or more of draft, open, partially_paid, paid, void, uncollectible, or credited |
customer_id, order_id | Invoices for a customer or the invoice on an order |
is_overdue, has_amount_due | The chase list and the open-balance list |
due_after, due_before, created_after, created_before | RFC3339 time windows |
external_reference_id | Exact match on your correlation ID |
query | Search across invoice ID, invoice number, external reference, recipient email, and reference |
sort_by, sort_direction | created_at, updated_at, due_at, invoice_number, or outstanding_money, ascending or descending |
So "aging report, biggest exposures first" is one call:
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:
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:
- Create a quick pay draft and issue it. Both calls are above; the issue response carries
public_url. - Open
public_urlin a browser. You are seeing exactly what your customer sees, and the invoice'sviewed_atis now set. - Click "Pay invoice" and pay with the standard test card,
4242 4242 4242 4242, any future expiry, any CVC. - Confirm the loop the way production will run: your webhook endpoint received
invoice.paid, andGET /v1/invoices/{invoice_id}showsstatus: "paid"withoutstanding_moneyat zero. - Repeat with the offline rail: issue a second invoice, record a partial manual payment, watch
partially_paidarrive, 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:
order_id or quick_payorder_id or quick_payORDER_NOT_OPENORDER_ALREADY_HAS_PAYMENTSORDER_ALREADY_HAS_REFUNDSrecipient_email on the draft firstORDER_HAS_OPEN_CHECKOUTORDER_HAS_ACTIVE_PAYMENT_INTENTORDER_HAS_MANUAL_PAYMENTSSCHEDULED_SEND_IN_PASTscheduled_send_at must be a future RFC3339 timestampopen or partially_paid with a balance remainingINVOICE_PAYMENT_RESOLVINGRetryableoutstanding_moneyoutstanding_moneyAMOUNT_EXCEEDS_BALANCEoutstanding_moneyREVERSAL_EXCEEDS_MANUAL_PAYMENTSINVOICE_COLLECTION_BLOCKEDRetryableRequests 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.
Related docs#
- Invoices API Reference: every field on every endpoint.
- Credit notes: correcting an issued invoice without moving money.
- Payment links vs checkout sessions vs invoices: pick the right collection surface.
- Why Flint is orders-first: the order underneath every invoice.
- Refunds: returning collected money.
- Webhooks: signature verification and delivery semantics.
- Idempotency: safe retries for every invoice write.
- Money & currency: minor units and currency handling.
