ap@northstreet.example
Invoices
Paid is paid, however it arrived.
Your customer pays the hosted invoice by card. The check that turns up in the mail, the wire, the cash: you record those against the same invoice, and the same balance moves. One record answers what is still owed, what bounced, and whether they ever opened the bill.
one record · outstanding_money runs to zero · Stripe processes the cards
01The problem
A receivable is a join nobody owns.
The invoice lives in one tool, the card lands in another, and the check arrives by mail on a Tuesday.
Ask a team using an invoicing product plus a separate processor what is still owed on an invoice and watch what happens. The total is in the invoicing tool. The card payment, if it came, is in the processor. The check is in a spreadsheet column somebody named paid? and the follow-up is in an email thread. The balance is not stored anywhere; it is a subtraction, done by hand, by whoever asked.
Flint puts the whole receivable on one record, because the invoice is backed by an order and owns collection on it once issued. Here is every question that stack cannot answer without a join, and the endpoint that answers it here.
What is still owed on this invoice, right now.
outstanding_moneyThe check for part of it, recorded where the balance can see it.
POST /v1/invoices/{invoice_id}/manual-paymentsThat same check, returned unpaid two weeks later.
POST /v1/invoices/{invoice_id}/manual-payments/reverseWhether the customer ever opened the bill.
viewed_atWhether the reminder reached their inbox at all.
GET /v1/invoices/{invoice_id}/delivery-attemptsEveryone who is late, as a query rather than a report.
is_overdueReviewed 2026-07-25 against the published API.
02The balance
Two rails, one number.
Card payments are collected. Everything else is recorded. The invoice does not treat those differently once the money is on it.
curl -X POST https://api.withflintpay.com/v1/invoices \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: invoice-north-street-07" \
-d '{
"quick_pay": {
"customer_id": "cus_1kmn0aExample",
"line_items": [
{ "name": "Site audit", "quantity": 1,
"unit_price_money": { "amount": 120000, "currency": "USD" } },
{ "name": "Retainer hours, July", "quantity": 5,
"unit_price_money": { "amount": 10000, "currency": "USD" } }
],
"internal_note": "PO 4417, net 15"
},
"collection": {
"mode": "buyer_initiated",
"payment_policy": { "enabled_payment_options": ["card", "ach_debit"] }
},
"payment_due": {
"type": "absolute",
"due_at": "2026-07-15T00:00:00Z"
},
"recipient_email": "ap@northstreet.example",
"memo": "Thanks for the work this month."
}'quick_pay builds the backing order for you. Pass order_id instead when the order already exists, and exactly one of the two.
{
"data": {
"invoice_id": "inv_1kmn0aExample",
"invoice_number": "INV-0231",
"order_id": "ord_1kmn0aExample",
"status": "partially_paid",
"paid_money": { "amount": 130000, "currency": "USD" },
"outstanding_money": { "amount": 53600, "currency": "USD" },
"refunded_money": { "amount": 0, "currency": "USD" },
"refund_status": "none",
"is_overdue": true,
"issued_at": "2026-07-02T15:31:00Z",
"viewed_at": "2026-07-02T16:04:11Z",
"due_at": "2026-07-15T00:00:00Z"
}
}No join. The balance, the delivery, and the refund state are fields on one read.
{
"event_type": "invoice.manual_payment_recorded",
"data": {
"invoice_id": "inv_1kmn0aExample",
"status": "paid",
"paid_money": { "amount": 183600, "currency": "USD" },
"outstanding_money": { "amount": 0, "currency": "USD" }
}
}An offline payment that clears the balance arrives as invoice.manual_payment_recorded carrying the new status. It does not also fire invoice.paid, so a fulfillment handler listening only for that event would miss the check that paid the bill.
The rule, stated once
No amount field on the hosted page. It collects the whole balance.
POST /v1/invoices/{invoice_id}/checkout-sessionRecording accepts any positive amount up to the balance, which is how an invoice becomes partly paid.
amount_moneyA reversal cannot exceed what was recorded offline, so card money is never quietly undone.
POST /v1/invoices/{invoice_id}/manual-payments/reverseThe hosted page offers the payment options enabled by the invoice policy and merchant capabilities. It reports unavailable methods before the buyer starts payment.
03The receivable
What the person chasing it sees.
Same record, read from the other side. These are the dashboard's own components and its own field set.
And the list it came from
The daily worklist is one query. Filter on is_overdue together with a status, not on its own: it is computed on read and excludes only paid and void invoices, so a draft you wrote last month with a date in the past reports overdue too.
curl -G https://api.withflintpay.com/v1/invoices \
-H "Authorization: Bearer YOUR_API_KEY" \
-d is_overdue=true \
-d status=open \
-d page_size=50The status filter is not decoration. Without it the worklist includes past-due drafts that were never sent to anyone.
Invoice status, the complete set
- draft
- open
- partially_paid
- paid
- void
- uncollectible
- credited
Overdue is not among them, because it is derived rather than stored. Neither is refunded: that lives on its own axis so a settled invoice stays settled.
04Delivery
Unpaid and never arrived are different problems.
Most invoicing APIs tell you an invoice was sent. That is the least useful thing they could tell you.
Reminders, on your cadence
- Cadence
- Day offsets from due. Empty list sends nothing.
- Frozen when
- At send, so a settings edit never rewrites a live receivable.
- Stop one
- pause-reminders. send-reminder still works.
{
"data": [
{
"invoice_delivery_attempt_id": "idel_1kmn0aExample",
"delivery_type": "send",
"channel": "email",
"to_email": "ap@northstreet.example",
"status": "sent",
"sent_at": "2026-07-02T15:31:00Z"
},
{
"invoice_delivery_attempt_id": "idel_2bqr7dExample",
"delivery_type": "reminder",
"channel": "email",
"to_email": "ap@northstreet.example",
"status": "failed",
"error_message": "mailbox full"
}
]
}The second attempt failed and says why. invoice.delivery_failed carries the same news to your webhook endpoint, so a receivables screen can flag it without polling.
# the link leaked, so retire it
curl -X POST \
https://api.withflintpay.com/v1/invoices/inv_1kmn0aExample/regenerate-public-link \
-H "Authorization: Bearer YOUR_API_KEY"The old link stops working immediately, along with any checkout credentials taken from it. The session and a payment already in flight survive, so rotating a leaked link does not cost you money mid-collection.
The invoice event family, all twenty-five
- invoice.created
- invoice.issued
- invoice.issue_failed
- invoice.sent
- invoice.updated
- invoice.marked_uncollectible
- invoice.paid
- invoice.partially_paid
- invoice.payment_processing
- invoice.payment_failed
- invoice.payment_attempt_canceled
- invoice.payment_attempt_expired
- invoice.manual_payment_recorded
- invoice.manual_payment_reversed
- invoice.overdue
- invoice.reminder_due
- invoice.late_fee_due
- invoice.refunded
- invoice.partially_refunded
- invoice.credited
- invoice.delivery_succeeded
- invoice.delivery_failed
- invoice.voided
- invoice.collection_blocked
- invoice.collection_block_resolved
05Underneath
It was an order the whole time.
Which is why refunding an invoice needs no invoice-specific machinery. Correcting one before it is paid is the other question, and that is what a credit note answers.
Refund the order the invoice describes, item by item, with the same call any other order uses.
POST /v1/refundsThe invoice tracks the money returned on its own axis, so paid never moves backward.
refund_statusCredit a sent invoice line by line without moving money. The credited lines carry their discount and tax with them.
POST /v1/credit-notesAllocating the credit draws down the balance. Reversing it puts the balance back, on an append-only record.
unallocated_moneyVoid is for a bill that should not have existed. It is not a refund and it is not reversible.
POST /v1/invoices/{invoice_id}/voidA sent invoice is immutable, and one order carries one live invoice at a time.
snapshotThe audit trail is readable rather than reconstructed: the events route returns the invoice's own history, from draft through every reminder, payment, and reversal. It is a narrower vocabulary than the webhook family, so treat it as the story of the record and the webhooks as the integration surface.
# refund the order the invoice describes, item by item
curl -X POST https://api.withflintpay.com/v1/refunds \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: refund-inv-0231-01" \
-d '{
"order_id": "ord_1kmn0aExample",
"reason": "requested_by_customer",
"line_items": [
{ "order_line_item_id": "li_1kmn0aExample", "quantity": 1 }
]
}'No invoice id in the body. The refund targets the order, and the invoice's refunded_money and refund_status follow from it.
The invoice
Create from an order or a quick_pay block, send, collect, void. Status and balance are computed, never set.
- POST /v1/invoices
- quick_pay.line_items
- outstanding_money
Offline money
Record a check, a wire, or cash with the reference that identifies it, and reverse it when it fails.
- POST /v1/invoices/{invoice_id}/manual-payments
- external_reference_id
- received_at
The document
A frozen snapshot after send, a PDF rendered from it, and a public link you can retire on demand.
- GET /v1/invoices/{invoice_id}/pdf
- POST /v1/invoices/{invoice_id}/regenerate-public-link
- snapshot
Delivery
Every send and reminder logged with its outcome, plus the first time the customer opened the page.
- GET /v1/invoices/{invoice_id}/delivery-attempts
- viewed_at
- invoice.delivery_failed
Follow-up
Reminders on the day offsets you configure, pausable per invoice, plus a one-off send and a computed overdue flag.
- POST /v1/invoices/{invoice_id}/send-reminder
- POST /v1/invoices/{invoice_id}/pause-reminders
- reminders_paused_at
- is_overdue
Credit notes
Credit invoice lines before the money arrives, allocate against the balance, and reverse the allocation when you get it wrong.
- POST /v1/credit-notes
- PATCH /v1/credit-notes/{credit_note_id}
- POST /v1/credit-notes/{credit_note_id}/allocations
- unallocated_money
Refunds and history
Order-backed refunds on their own axis, and the invoice's own event log for support to read.
- POST /v1/refunds
- refunded_money
- refund_status
06Limits
What this does not do.
The hosted invoice page collects the whole balance or nothing. It offers the payment options frozen in the invoice's payment policy and always asks for the full remaining amount. ACH debit can remain in processing until settlement. A wire, check, or other payment taken outside the page is recorded here so the invoice balance stays current.
A late fee is a notice, not a charge. When the payment term carries a late fee policy, the grace period elapsing raises an event with the amount computed from the current balance. Flint does not add it to the invoice or collect it. Bill it on a second invoice, or ignore the event and waive it.
No e-invoicing compliance. Government mandates, clearance models, and networks like Peppol are a specialist vendor's territory. What is here is a US-focused itemized document, a hosted payment page, a PDF, and receivables state you can query.
No proration. A credit note credits whole units or an amount you name against an invoice line. Time-based apportionment across a billing period is a subscriptions question, and it is not something this computes.
One currency per invoice. It comes from the order, and a recorded payment has to match it. There is no conversion and no per-line currency.
The CLI covers four commands. Create, issue, get, and list. Voiding, reminders, recording a payment, and the PDF are REST calls.
07Start
Send one to yourself in about five minutes.
CLI
flint invoices create \
--order ord_1kmn0aExample \
--recipient-email ap@northstreet.example
flint invoices issue @last.inv
flint invoices get @last.inv
flint invoices list --status openCreate it against an order, send it, then read it back. The recipient address is yours in a sandbox, so the email arrives where you can see it.
A sandbox invoice is a real invoice with fake money: the hosted page renders, the email sends, and 4242 4242 4242 4242 pays it. Record a check against it with the manual-payments call and watch the balance move without touching a card at all.
FAQ
The questions that decide it.
What can a customer actually pay an invoice with?
The hosted invoice page offers the payment options frozen in the invoice's payment policy, and it always collects the full remaining balance. Anything you collect outside that page, such as a check, wire, cash, or bank payment taken elsewhere, is recorded against the invoice with the manual-payments endpoint. Both update outstanding_money and status, which is why a partly-paid invoice is a normal state rather than a workaround.
How do partial payments work?
Offline. The hosted page has no amount field and always asks for the whole remaining balance, so a partial payment happens when you record one: manual payments accept any positive amount up to the balance. Record $800 against an $1,836 invoice and it becomes partially_paid with $1,036 still collectible online.
What happens when a check bounces?
Reverse it. The reverse endpoint takes an amount and a reference and puts the balance back, and it works on a paid invoice as well as an open one, so a check that clears an invoice and then returns unpaid reopens it correctly. It can only undo offline amounts, never card money, and it cannot exceed what was recorded manually.
Do reminders send on a schedule?
On the schedule you write. reminder_policy is a list of day offsets from the due date, negative ones fire before it, and the invoice freezes that list when you send it so a settings change never rewrites a receivable already in flight. Leave the list empty and nothing sends automatically. pause-reminders stops the cadence on one invoice the moment a customer replies, and send-reminder still works while it is paused.
Can I tell an unpaid invoice from one that never arrived?
Yes, and they are different rows. Every send and reminder is logged as a delivery attempt with a status and, when it fails, an error message, and invoice.delivery_failed fires on a bounce. The first time the customer opens the hosted page stamps viewed_at. So "sent Tuesday, opened Wednesday, still unpaid" and "never reached their inbox" are two different reads rather than the same silence.
How do refunds work when the money came in on two rails?
Refund the order the invoice describes, item by item, with the standard refunds endpoint. There is no invoice-specific refund call. The invoice tracks refunded_money and refund_status as their own axis, so paid never moves backward and reporting can tell a settled invoice from a settled-then-refunded one.
What is the difference between a refund and a credit note?
A refund returns money that already settled. A credit note reduces a balance that has not been paid: it credits specific invoice lines, carries their discount and tax with them, and produces a numbered PDF for the customer's accounts-payable file. Allocating the credit draws down outstanding_money, and reversing that allocation puts it back, which is why credited is the one invoice status that can move backward.
Does Flint handle e-invoicing mandates?
No. This is US-focused collection: an itemized document, a hosted payment page, a PDF snapshot, and receivables state you can query. Government e-invoicing mandates, clearance models, and networks like Peppol are a dedicated compliance vendor's job, and pretending otherwise would be the expensive kind of wrong.