Workflows
End-to-end recipes: collecting and reconciling invoices, watching usage, tracking subscriptions.
End-to-end recipes for the things integrators actually build. Each one names the endpoints, the order, and the failure modes worth handling.
Collect payment on an invoice
Invoices are issued by the billing engine (subscription renewals, usage period close) or by your team in the dashboard. This API turns an issued invoice into something a customer can pay.
curl -s -X POST $FYNEX_API_BASE/invoices/4180/send \
-H "Authorization: Bearer $FYNEX_API_KEY"
{
"invoice": { "id": 4180, "status": "sent", "paymentLinkId": 991, "...": "..." },
"paymentLinkUrl": "https://pay.example-host/p/7bc1f2a9e4d5"
}
The URL points at the Fynex hosted payment page for your deployment (the host is configured per environment, so do not hard-code it — always use the URL this response returns). Send it to your customer, embed it, or redirect to it. When the document is configured for email delivery, Fynex also emails the customer — calling this endpoint is what triggers that.
Retrying is safe, and needs no idempotency header. The invoice's unique payment-link association is the durable safeguard: an invoice that already has a link returns that same link rather than minting a second, even for simultaneous calls, and does not re-send the email.
The payment carries the document. Whichever rail collects an invoice — the hosted link or off-session auto-charge — the resulting payment is stamped with what it settled, so you never have to reconstruct it from an order reference:
- The payment link's
metadataholdsbillingInvoiceId,invoiceNumber,contractId,agreementNumberandsellerCustomerId. The contract and its agreement number reach the payments side nowhere else. - The payment row carries
billing_invoice_id, and so does its checkout session — a direct answer to "which invoice did this payment collect", with no join through the link. - The acquirer's order description reads
Invoice <number>, which is what makes the document visible in acquirer dashboards and dispute correspondence. The merchant reference is deliberately unchanged: it is the duplicate-detection and settlement-mapping key and stays machine-shaped. PaymentCompletedcarriesinvoiceNumberandcontractIdwhen the payment collected an invoice, so it can be joined toInvoicePaidand to the document itself.
Three cases answer 422, and they are not retryable — the state has to
change first:
| Message | Meaning |
|---|---|
invoice is collected by bank transfer and cannot create a payment link |
The document instructs the buyer to wire money to a named account. A simultaneously payable card link would let a transfer and a card charge both succeed. |
invoice has nothing left to collect |
Zero total, or stored credit already covers it. There is nothing to ask the customer for. |
invoice is not in a sendable state |
Not issued yet (draft), or already paid, voided or written_off. A settled invoice never hands out a payment link. It is also the catch-all: if status reads issued and send still answers this, the document itself is faulty (a collection snapshot that is missing or unreadable) — nothing you send will fix it, so raise it with Fynex rather than retrying. |
Know when an invoice is paid
InvoicePaid on the webhook pipe is the push signal (see Webhooks);
polling remains right for reconciliation.
Do not assume the settlement was something you or the buyer did. Where the
deployment has off-session collection switched on (billing.invoice.auto_charge,
a platform flag), a sent or overdue invoice is charged against the customer's
saved card by a background loop — paidVia still reads payment_link, because it settles on
the same rail. Sellers can likewise have documents raised for them by the
recurring and usage invoice passes. Both are platform switches, off
until turned on, and both mean GET /invoices can change with no call of
yours: reconcile against the list for a period rather than against the
documents you happen to have touched.
curl -s $FYNEX_API_BASE/invoices/4180 \
-H "Authorization: Bearer $FYNEX_API_KEY"
Read invoice.status:
| Status | Meaning |
|---|---|
draft |
Not yet issued. Not a legal document; not collectible. |
issued |
Numbered and legally real; no collection instrument yet. |
sent |
A payment link exists; awaiting payment. |
paid |
Settled. paidVia says how: payment_link (hosted checkout), bank_transfer (a matched deposit) or credit (stored credit covered it in full). |
overdue |
Past its dueDate and still unpaid. |
voided |
Cancelled before money moved; voidedAt and voidReason explain. |
written_off |
Collection abandoned; writtenOffAt and writeOffReason explain. |
Treat this list as open: a status you do not recognize should not crash your integration. Branch on the ones you handle and fall through for the rest.
Poll open invoices every few minutes at most; a fixed 15-minute sweep over
GET /invoices is usually better than polling each document. Bank transfers
settle when the deposit is matched, which is hours-to-days — do not build a
tight loop waiting for one.
A credit note (invoiceType: "credit_note") is a separate document that
corrects an earlier one, named in originalInvoiceNumber. It does not change
the original: to compute what a customer owes, sum the invoice and its credit
notes rather than expecting the original to shrink. Fetch them with
?corrects=<invoiceNumber>.
Reconcile an invoice against its payout
"The customer paid — where is the money?" is one call per invoice:
curl -s "$FYNEX_API_BASE/invoices/4180/settlement" \
-H "Authorization: Bearer $FYNEX_API_KEY"
Read it in this order: each entry in payments is one attempt on the
invoice's collection link, failed ones included — the newest settled one is
your money. Inside it, each entry in payouts is a payout's claim on that
payment: state: "finalized" with a completedAt means it left; reserved
means it is scheduled; an empty payouts array means the payment settled but
has not been picked into a payout yet. attributedMinor is that payout's
slice — a payment can be split across payouts, and the slices sum to what
left.
An invoice settled by bank transfer or stored credit has no link trail, and
the response's note says which case you are looking at — do not treat an
empty payments list as an error. Payout detail beyond the id lives on the
Payments API; this call gives you the id to look it up with.
Reconcile a period
For month-end, take the file. One call returns every invoice of the window as CSV (or NDJSON — the list API's objects, one per line), with money in integer minor units and column names that say so. Both dates are required; there is deliberately no default window:
curl -s "$FYNEX_API_BASE/invoices/export?issuedFrom=2026-08-01&issuedTo=2026-09-01" \
-H "Authorization: Bearer $FYNEX_API_KEY" -o invoices_2026-08.csv
The paged list below remains right when you are feeding an application rather
than closing books. Bound it by issue date rather than walking history until
you cross the boundary — the range is half-open (issuedFrom inclusive,
issuedTo exclusive), so adjacent months never double-count a document:
# First page of August
curl -s "$FYNEX_API_BASE/invoices?issuedFrom=2026-08-01&issuedTo=2026-09-01&limit=100" \
-H "Authorization: Bearer $FYNEX_API_KEY"
# Next page: pass the previous response's nextCursor
curl -s "$FYNEX_API_BASE/invoices?issuedFrom=2026-08-01&issuedTo=2026-09-01&limit=100&cursor=4081" \
-H "Authorization: Bearer $FYNEX_API_KEY"
Filters worth combining:
contractId=42— one customer relationship.sellerCustomerId=7— every document addressed to one of your customers, across contracts. This is how you pull a customer statement without walking the whole period.status=sent— one lifecycle state (draft,issued,sent,paid,overdue,voided,written_off).invoiceType=credit_note— corrections only. The full set isstandard,credit_note,simplified(retail below the threshold) andmodified(UK retail above it, VAT-inclusive line prices).corrects=UK2607AA-2608AAB— the credit notes issued against one invoice number, without scanning history for them.origin=usage— the subsystem that produced the document:recurring,usage,milestone,project,one_time,adhoc,marketplace. (subscriptionis a deprecated alias ofrecurring: still accepted, no longer produced, and it resolves to the same set asrecurring— see below.)
origin, status and invoiceType reject an unrecognized value with 400,
so a typo cannot masquerade as "no such invoices". contractId and corrects
are matched as given: a well-formed but wrong value legitimately returns an
empty page, so check those two against GET /contracts and the invoice number
you meant.
origin=recurring and origin=subscription return the same rows. The second
is the pre-schema name, and documents issued before the rename still carry it
— filtering either way includes both, so a monthly report cannot silently lose
the older half.
customerName and customerEmail are returned on the single-invoice read and
on send, but not on list rows: they live in the document's frozen payload,
and reading them for a whole page would mean parsing every document. To group
a report by customer, filter with sellerCustomerId rather than fetching each
invoice — that keeps a monthly reconciliation to a handful of requests instead
of one per document.
For accounting, the figures per document are grandTotalMinor (what was
invoiced), creditAppliedMinor (funded by stored credit rather than cash),
and collectibleMinor (the grand total less that credit — what collection
asks the customer for).
Two traps worth stating plainly:
creditSettledMinoris not a payment total. It counts only the stored credit drawn down; a card- or transfer-paid invoice reports0there.collectibleMinordoes not drop to zero when an invoice is paid. It describes the ask, not the outcome.
So "has this been paid?" is answered by status (and paidVia for how), never
by arithmetic on the amounts.
Because paging is keyset, not offset, documents issued while you are walking the pages never shift rows into or out of a page you already fetched. Start a fresh walk to pick them up.
Watch usage against plan limits
For metered contracts, the snapshot answers "where is this customer in the current period":
curl -s $FYNEX_API_BASE/contracts/42/usage \
-H "Authorization: Bearer $FYNEX_API_KEY"
{
"contractId": 42,
"metrics": [
{
"metricName": "api_calls",
"used": "10250",
"includedUnits": "10000",
"capQuantity": "50000",
"capMode": "hard",
"percentOfCap": "20.5",
"percentOfPlan": "102.5",
"periodStart": "2026-08-01",
"periodEnd": "2026-08-31"
}
]
}
Useful signals:
percentOfPlanabove 100 means the customer is into overage — that is what will be rated onto the next usage invoice.capQuantitywithcapMode: "hard"is the ceiling enforcement uses. Warn customers well beforepercentOfCapreaches 100 rather than at it.- Quantities are decimal strings; compare with a decimal type.
An unknown contractId answers 404. A real contract with no metered usage
answers 200 with an empty metrics array — that is a valid state, not an
error.
Usage aggregates continuously and closes at period end, so a snapshot is a live read, not a final invoice figure. Reconcile money against invoices.
Reconcile stored credit
Balances across all contracts:
curl -s $FYNEX_API_BASE/credits \
-H "Authorization: Bearer $FYNEX_API_KEY"
{
"balances": [
{"currency": "EUR", "creditType": "purchased", "balanceMinor": 250000, "isLiability": true}
]
}
isLiability: true marks credit that was paid for — purchased,
enterprise and the engine-minted proration (unserved time returned on a
mid-term downgrade) — which is unearned revenue you still owe service against.
Everything you granted (promotional, manual, gift, ai_token,
marketplace) is not a liability. Finance wants these separated, so do not
sum them blindly. Paid-for credit also never expires; granted credit may.
One contract's balances plus its ledger:
curl -s "$FYNEX_API_BASE/contracts/42/credits?limit=50" \
-H "Authorization: Bearer $FYNEX_API_KEY"
The ledger is append-only and newest-first. kind is topup (credit granted
or bought), deduction (consumed into an invoice), expiry (a lapsed lot
removed) or reversal (a deduction handed back when its invoice was voided);
signedDeltaMinor is positive for topup and reversal and negative for
deduction and expiry, and invoiceId links a deduction to the invoice it funded. Corrections are new rows, never edits, so
replaying the ledger from the beginning always reproduces the current
balance.
Page it the same way as invoices: limit sets the page, and while hasMore
is true you pass nextCursor back as cursor. A long-lived contract's
history exceeds one page, and a ledger you cannot read to the end cannot be
reconciled.
See what the next invoice will carry
GET /contracts/{contractId}/invoices/upcoming projects the contract's next
document — before it exists, and without writing anything.
curl -s "$FYNEX_API_BASE/contracts/42/invoices/upcoming" \
-H "Authorization: Bearer $FYNEX_API_KEY"
The lines array is the document as it would be composed, each line tagged
with the engine that produced it: subscription (a term falling due, priced
at a scheduled plan change when one takes effect by the term's end),
proration (an unbilled mid-term amendment the engine would bind to that
term — a charge as an extra line, a credit as a discount on the recurring
one), usage (an open metered period rated by the same engine
POST /prices/evaluate answers with) and seat (a per-seat period rated from
the contract's seat schedule, to date). nextIssueDate is the day the
document would appear, and collectibleEstimateMinor is what your buyer would
be asked for after stored credit is drawn down.
Every figure is an estimate, and estimate: true says so on every
response. Metered usage keeps accruing after asOf; the period close
resolves a rollover carry this projection assumes is zero; an amendment can
still move before the recurring pass binds it. Amounts are also pre-tax:
the engine copies a document's tax treatment from a prior invoice on the same
contract when it issues, so no tax is projected here and grandTotalMinor
equals subtotalMinor. Read the notes array — it names every reason the
figures are estimates and every exclusion made (an unpriced metric, a
foreign-currency line, a capability switched off), which is what stops a
number in this response from being read as a commitment.
Use it to show a customer what is coming, to check a plan change landed
before the term bills, or to catch an unpriced metric while there is still
time to price it. Reconcile against the issued document
(GET /invoices/{invoiceId}), never the other way round.
Track subscription state
curl -s "$FYNEX_API_BASE/subscriptions?contractId=42" \
-H "Authorization: Bearer $FYNEX_API_KEY"
To answer "is this customer currently being billed, and for how much", read
status, priceMinor + currency, and billingFrequency. There is no
separate plan object — those three fields are the plan.
Watch for:
pendingPriceMinor/pendingPriceChangeAt— a scheduled change (usually a downgrade) that takes effect at term end. Showing onlypriceMinorwill misinform a customer who already requested a change.cancelRequestedAt/cancelEffectiveAt— cancellation with notice. The subscription keeps serving until the effective date, sostatusalone is not "are they leaving".trialEndwithstatus: "trial"— not yet paying.status: "past_due"— collection failed. What happens next depends on a switch the seller controls, so do not hard-code either answer:- With off-session collection enabled (
billing.invoice.auto_charge), a failed charge is retried on a bounded schedule — a few attempts, spaced apart — and the subscription is moved topast_dueonly once that budget is spent. It can come back on its own when a later payment succeeds, which is whatBillingSubscriptionRecoveredannounces. Do not treatpast_dueas terminal and do not start your own retry loop against it; you would be charging the same card twice. - Without it, nothing retries:
past_dueis a queue for you to work, and the way out is the customer paying the outstanding invoice or a new card being added.
- With off-session collection enabled (
In both cases the events to watch are BillingSubscriptionPastDue and
BillingSubscriptionRecovered, and the invoice's outstandingMinor is what
says how much is actually still owed.
- pauseEndsAt — an auto-resume date for a paused subscription.
Lifecycle changes are public API operations, yours to make: POST
/subscriptions/{subscriptionId}/cancel, /pause, /resume, /end-trial and
/change-plan. Each route documents its allowed starting states — see
Subscriptions. The dashboard is an alternative operator surface, not the
only way to manage a subscription.
Retries and rate limits
Reads are safe to retry. So is POST .../send: the invoice can hold only one
payment link, so a retry returns the existing one.
429— you exceeded the per-seller budget. Wait theRetry-Afterseconds; do not retry sooner.X-RateLimit-Remaining, when present, lets you slow down before you hit it. When the rate-limit headers are absent the surface-wide budget is not being enforced at that moment; the three write quotas (POST /contracts,/contracts/{contractId}/amendments,/invoices) instead answer503withRetry-Afterwhile their limiter is unreachable — see Errors.5xx— retry with exponential backoff and jitter.4xxother than429— do not retry; the request has to change.
A retry loop that ignores Retry-After turns one throttled request into a
sustained overage. If you are paging a large history, a small delay between
pages is cheaper than being throttled mid-walk.