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).

Note:

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:

cURL
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
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"},
        "fulfillment": {"requirement": "none"}
      }]
    },
    "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"}
  }'
Response
{
  "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"}
  }
}

A line item you describe yourself, without a catalog variant_id or bundle_id, must say whether it needs fulfilling: "fulfillment": {"requirement": "none"} for services and anything else with nothing to ship. Leaving it out returns FULFILLMENT_REQUIREMENT_REQUIRED.

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. While the invoice is a draft, snapshot.merchant_display_name shows your current business name; issuing the invoice fixes the name on it.

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 charges payment_method_id, or the customer's default card when it is omitted.
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.

cURL
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 409 Conflict with 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
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"}'
Response
{
  "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.
Warning:

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.

Automatic tax adds one rule: the invoice takes a single payment for the full balance, so issue rejects a partial schedule with AUTOMATIC_TAX_INVOICE_INSTALLMENTS_UNSUPPORTED and a later partial payment fails with AUTOMATIC_TAX_PARTIAL_PAYMENT_UNSUPPORTED. Editing the order after issue leaves the frozen calculation behind, and collection fails with INVOICE_TAX_SNAPSHOT_STALE until you void the invoice and issue it again. See Tax on an invoice.

Schedule the issue#

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

cURL
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
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 card when the invoice is issued. The buyer can pay an outstanding amount by card from the hosted page, and POST /collect can retry with the saved card or another saved card.
  • 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.

Pay from the hosted invoice#

Customers normally self-serve from the email or hosted page. The hosted page also accepts a card payment for an outstanding automatic invoice. Call this endpoint yourself when your own surface needs to start the payment, for example a "Pay now" button inside your customer portal:

cURL
curl -X POST https://api.withflintpay.com/v1/invoices/inv_1kmn0aExample/checkout-session \
  -H "Authorization: Bearer YOUR_API_KEY"
Response
{
  "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",
      "url": "https://checkout.withflintpay.com/checkout/cs_1kmn0aExample#checkout_token=..."
    },
    "checkout_access": {
      "checkout_auth_token": "ckat_v1..."
    },
    "reused_existing": false
  }
}

Send the buyer to checkout_session.url. Keep checkout_access.checkout_auth_token on your backend.

The endpoint atomically resolves one compatible checkout session and invoice attempt. A current open session of the requested surface, before its fixed deadline, is returned with reused_existing: true and a 200; a newly created pair returns with a 201. It works for buyer_initiated and automatic invoices while they are open or partially_paid with a balance remaining. Automatic invoices accept card payments only.

To bring the buyer back to your customer account after they pay, pass return_url:

cURL
curl -X POST https://api.withflintpay.com/v1/invoices/inv_1kmn0aExample/checkout-session \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{"return_url": "https://account.cedarandstone.com/invoices/inv_1kmn0aExample"}'

return_url must be an HTTPS address of your customer account: a path under /{merchant_id} on account.withflintpay.com, your active custom account domain, or the host of customer_account.merchant_account_url when you host the account yourself. In test mode, http://localhost and http://127.0.0.1 with any port also work. Anything else fails with INVALID_RETURN_URL. A reused session takes a new return_url until a payment starts on it. After that it keeps the one it has, so a payment in progress returns the buyer where it started.

When the invoiced order has items to ship or pick up, the hosted checkout asks the buyer how to receive them, from the delivery methods in settings.checkout.default_delivery_method_ids. If those methods cannot deliver every item, the endpoint returns FULFILLMENT_METHOD_ASSIGNMENT_UNSATISFIABLE. Set the default, as Delivery options shows, before you send the invoice.

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.

Pay in your own checkout#

To collect the invoice in a checkout you build, send "surface": "embedded" to the same endpoint, or to POST /v1/me/invoices/{invoice_id}/checkout-session with a customer session. Offering Affirm also needs redirects.success_redirect_url.

cURL
curl -X POST https://api.withflintpay.com/v1/invoices/inv_1kmn0aExample/checkout-session \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"surface": "embedded"}'

The response has the same fields without a hosted page: checkout_session.url is omitted, and checkout_session.payment_collection carries the Stripe account, publishable key, and Elements options for your page. Pay the session's order with the checkout credential, as Build your own checkout describes.

An invoice has one open checkout session at a time. A request for the session's current surface returns it with a new credential and reused_existing: true. A request for the other surface replaces the session when no payment is in progress on it, and fails with 409 CHECKOUT_SURFACE_CHANGE_NOT_ALLOWED when one is.

Note:

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#

An automatic invoice selects its card when you issue it: collection.payment_method_id, or the customer's default card when that is omitted. Issuing fails with INVOICE_AUTOPAY_PAYMENT_METHOD_REQUIRED if there is neither. Flint charges that card right after issue, or on each installment's due date for an invoice with a payment schedule, as a charge the customer is not present for. The buyer can pay an outstanding amount with a different card from the hosted invoice. The hosted page accepts cards only for automatic invoices. Save a card and charge it later covers the full flow.

To try the fixed card again before the next scheduled retry, start an attempt with:

cURL
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 card selected at issue on the schedule in invoices.autopay_retry_policy; call collect again with a new idempotency key when you want to try sooner. Set payment_method_id on collect to charge another active saved card belonging to the same customer for that attempt. This does not change the card for future automatic charges. 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
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
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:

Response
{
  "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
curl -X PATCH https://api.withflintpay.com/v1/invoices/inv_1kmn0aExample \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: pause-inv-1kmn0a-001" \
  -d '{ "reminders_paused": true }'

The response is the full invoice. reminders_paused tells you which state it is in, and reminders_paused_at records when reminders were paused. Send "reminders_paused": false to resume. Sending the current value succeeds without changing the invoice. The invoice must be collectible (INVOICE_NOT_COLLECTIBLE). Every other invoice field is draft-only, so send reminders_paused alone, optionally with expected_version. Reminder times that passed while paused do not fire on resume.

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:

Response
{
  "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}
}

To assess the fee, call POST /v1/invoices/{invoice_id}/late-fees with {} for an unscheduled invoice. For a scheduled invoice, send the event's invoice_schedule_entry_id. Flint recalculates the fee from the current unpaid principal and frozen policy, so a payment received after the notification can reduce the assessment.

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

Set the payment term’s late_fee_policy.application_mode to automatic to have Flint assess the fee after its grace period. Omission defaults to manual; a webhook handler can call this endpoint when it receives invoice.late_fee_due. Automatic assessment waits for active payments to resolve and sends a fee notice when invoice email delivery is enabled. Changing a payment term affects future invoices only. Assessment is once per invoice or overdue schedule entry, including when requests use different idempotency keys. A fee raises the outstanding balance and appears as a dated addendum on the hosted invoice and PDF. The issued line items, tax, and original total stay fixed. Manual assessment does not initiate a payment or send an email.

The response includes late_fees, with each fee's calculation inputs, assessed amount, paid amount, waived amount, and outstanding amount. To waive the unpaid remainder, call POST /v1/invoices/{invoice_id}/late-fees/{invoice_late_fee_id}/waive with {"reason_message":"Courtesy waiver"}. The reason is visible only to the merchant. A waiver leaves collected money in place; use the Refunds API for refunds.

Listen for invoice.late_fee_assessed and invoice.late_fee_waived to track balance changes. V1 supports fixed and percentage policies without recurring fees, compounding, or tax calculation on the fee.

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
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
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
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. Read the credit note IDs from details[].blocking_resources. 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. When the refund corrects the bill itself, such as a line billed at the wrong rate on a paid invoice, refund a credit note instead: the customer gets a numbered document, and the refund carries its credit_note_id and invoice_id. Either way, 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
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
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"
  }'

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

Response
{
  "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
curl "https://api.withflintpay.com/v1/invoices/inv_1kmn0aExample/delivery-attempts" \
  -H "Authorization: Bearer YOUR_API_KEY"

Activities are the history timeline: draft_created, sent, viewed, payment_attempt_started, payment_applied, manual_payment_recorded, manual_payment_reversed, refund_succeeded, reminder_sent, reminder_due, overdue, late_fee_due, late_fee_assessed, late_fee_waived, credited, token_regenerated, voided, and friends. Each has an activity_type, a created_at timestamp, the actor_type (merchant, buyer, or system) that caused it, and typed details for its type, such as the invoice_delivery_attempt_id and to_email of a sent email or the payment_intent_id and amount_money of an applied payment. When a customer says "we never got it", this is where the conversation ends:

cURL
curl "https://api.withflintpay.com/v1/invoices/inv_1kmn0aExample/activities?type=delivery_sent" \
  -H "Authorization: Bearer YOUR_API_KEY"

Activities list newest first. Pass sort_direction=asc to read them in order, and type to keep only some activity types. Repeat type or pass comma-separated values, such as type=delivery_sent,reminder_sent, to match any of them.

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:

cURL
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
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.
Warning:

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#

Invoice state conflicts return 409 Conflict with type: "conflict_error". Invalid request inputs return 400 Bad Request with type: "validation_error".

All errors use the standard error envelope. 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
  • When creating, issuing: The order is not billable; invoice orders before collecting on them
  • When creating, issuing: The order is not billable; invoice orders before collecting on them
  • 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
  • When issuing: Resolve competing collection activity before issue
  • When issuing: Resolve competing collection activity before issue
  • When issuing: Resolve competing collection activity before issue
  • 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
  • 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
  • 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
  • HTTP 409
    When launching checkout: A payment is in progress on the open session, so its surface can't change; finish or cancel it, or request its current surface
  • When launching checkout: An embedded session that offers Affirm needs redirects.success_redirect_url

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.

Was this helpful?