Subscriptions
A renewal is just an order that happened on a schedule.
Most billing systems produce their own universe of invoices, statuses, and webhooks alongside the sales you already had, and you reconcile the two forever. Flint's subscription is a schedule with a card attached: five fields to create, three to update. Every cycle it produces an ordinary order, so a renewal refunds with the same call, reports through the same query, and lands on the same webhook listener as a checkout sale. Nothing downstream has to learn a second shape.
The object
Five fields to create, three to update. The plan holds the price, so the subscription never has to.
The output
One real order per cycle, with origin set to subscription and the subscription id on it.
When it fails
past_due, then four retries across sixteen days, then the end action you chose.
01The claim
Two orders. One shape.
Left is a one-time sale from a hosted checkout. Right is the fourth month of a subscription. Read the keys, not the values.
{
"data": {
"order_id": "ord_1kmn0aExample",
"origin": "checkout",
"status": "closed",
"payment_status": "paid",
"line_items": [
{ "order_line_item_id": "oli_1kmn0aExample",
"name": "Trail Runner 2", "quantity": 1 }
],
"pricing_amounts": {
"total_money": { "amount": 12800, "currency": "USD" }
},
"settlement_amounts": {
"paid_money": { "amount": 12800, "currency": "USD" },
"outstanding_money": { "amount": 0, "currency": "USD" }
}
}
}Nothing here knows what a subscription is.
{
"data": {
"order_id": "ord_2pqr7bExample",
"origin": "subscription",
"subscription_id": "sub_1kmn0aExample",
"status": "closed",
"payment_status": "paid",
"line_items": [
{ "order_line_item_id": "oli_2pqr7bExample",
"name": "Pro plan", "quantity": 1 }
],
"pricing_amounts": {
"total_money": { "amount": 2900, "currency": "USD" }
},
"settlement_amounts": {
"paid_money": { "amount": 2900, "currency": "USD" },
"outstanding_money": { "amount": 0, "currency": "USD" }
}
}
}One field more, one value changed. Everything else is in the same place, with the same names.
The difference is origin and subscription_id. That is the entire delta. There is no separate billing object holding the money, no invoice table with its own lifecycle running beside your orders, and no second refund path to implement.
A report that sums orders already includes recurring revenue. A refund tool that works on orders works on renewals. A fulfillment worker listening for a paid order fires for a subscription the same way it fires for a cart. You did not integrate billing, you kept using the thing you already had.
02The object
Five fields to create. Three to update.
The smallness is deliberate. It is the reason the orders can be ordinary.
A plan holds the shape of the deal: the interval, the line items, the trial, and any setup fee or contract term. It is the thing you create once and reuse.
curl -X POST https://api.withflintpay.com/v1/subscription-plans \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"name": "Pro",
"currency": "USD",
"billing_interval": "monthly",
"billing_interval_count": 1,
"trial_period_days": 14,
"line_items": [
{ "name": "Pro plan", "quantity": 1,
"unit_price_money": { "amount": 2900, "currency": "USD" } }
]
}'Line items on the plan, priced in minor units. This is where money lives.
- POST /v1/subscription-plans
- billing_interval_count
- setup_fee_money
- contract_term_months
- early_termination_fee_money
The subscription itself carries almost nothing.
A plan, a customer, a payment method, an optional day of the month to bill on, and metadata. It has no prices, because the plan has them, and no totals, because the orders will have them.
curl -X POST https://api.withflintpay.com/v1/subscriptions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"plan_id": "plan_1kmn0aExample",
"customer_id": "cus_1kmn0aExample",
"payment_method_id": "pm_1kmn0aExample",
"billing_anchor_day": 11
}'Four of the five create fields. The fifth, metadata, is optional annotation.
{
"data": {
"subscription_id": "sub_1kmn0aExample",
"status": "trialing",
"plan_id": "plan_1kmn0aExample",
"customer_id": "cus_1kmn0aExample",
"payment_method_id": "pm_1kmn0aExample",
"billing_anchor_day": 11,
"trial_end": "2026-08-11T00:00:00Z",
"current_period_start": "2026-07-28T00:00:00Z",
"current_period_end": "2026-08-11T00:00:00Z",
"next_billing_at": "2026-08-11T00:00:00Z",
"cancel_at_period_end": false
}
}Dates and status. The period boundaries are computed from the anchor day, not stored by you.
- POST /v1/subscriptions
- billing_anchor_day
- current_period_end
- cancel_at_period_end
And this is everything you can change afterwards.
Three fields: swap the card, arrange the ending, annotate. There is no plan, no quantity, and no price in this body, which is why there is no proration.
curl -X PATCH \
https://api.withflintpay.com/v1/subscriptions/sub_1kmn0aExample \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"payment_method_id": "pm_2pqr7bExample"
}'Swapping payment_method_id mid-dunning is how a past_due subscription recovers.
- POST /v1/subscriptions/{subscription_id}/pause
- POST /v1/subscriptions/{subscription_id}/resume
- POST /v1/subscriptions/{subscription_id}/cancel
- pause_duration_cycles
- cancel_immediately
03When it breaks
Break the renewal. Watch it recover.
Anything can charge a card on the first of the month. What separates a billing system is the month the card says no. Decline a charge, fix it mid-dunning, pause, cancel: every status, event, and retry day below is the live API's vocabulary, and each settled cycle is a record you can click open.
Click a settled cycle to inspect the record behind it. Every one is a real order or a real subscription state, one GET away.
The engine is simulated; the vocabulary is not. Every status, event, and field above is the live API's, and the default story has already run before you touch it.
The retry schedule is fixed and documented: four attempts across sixteen days, then the end action you configured. It is not a black box that decides when to give up on your revenue, and it is not something you build. What you choose is what happens at the end of it, and whether you go looking for the customer during it.
Every transition above emits an event. There are eight, and the failure ones matter most, because a dunning window you do not know about is a dunning window you cannot act inside.
- subscription.created
- subscription.activated
- subscription.payment_succeeded
- subscription.payment_failed
- subscription.past_due
- subscription.paused
- subscription.resumed
- subscription.canceled
04The consequences
Everything downstream is the thing you already built.
Four jobs that usually need subscription-specific code, and here do not.
Refunding a renewal.
The ordinary refund endpoint, naming a line and a quantity on the renewal order. No subscription-specific refund path exists because none is needed.
curl -X POST https://api.withflintpay.com/v1/refunds \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: refund-nov-renewal" \
-d '{
"order_id": "ord_2pqr7bExample",
"reason": "requested_by_customer",
"line_items": [
{ "order_line_item_id": "oli_2pqr7bExample",
"quantity": 1,
"tax_refund_mode": "automatic" }
]
}'The same call you already make for a one-time sale, with a different order id.
Finding every renewal.
origin is a filter on the ordinary orders list, so recurring revenue is a query rather than an export from somewhere else.
curl -G https://api.withflintpay.com/v1/orders \
-H "Authorization: Bearer YOUR_API_KEY" \
-d origin=subscription \
-d subscription_id=sub_1kmn0aExample \
-d sort_by=created_atDrop the subscription_id and you have every renewal on the account.
- GET /v1/orders
- origin
- subscription_id
The invoice reaches the subscription through the order.
An invoice carries order_id and no subscription field at all. That is deliberate rather than missing: the order is the join, so there is exactly one place where a cycle's money is true.
{
"data": {
"invoice_id": "inv_1kmn0aExample",
"invoice_number": "INV-00042",
"order_id": "ord_2pqr7bExample",
"status": "paid",
"paid_money": { "amount": 2900, "currency": "USD" },
"outstanding_money": { "amount": 0, "currency": "USD" }
}
}One hop to the order, one more to the subscription. Never a second source of truth.
And the numbers a recurring business runs on.
{
"data": {
"snapshot_metrics": {
"mrr_by_currency": [{ "amount": 482900, "currency": "USD" }],
"arr_by_currency": [{ "amount": 5794800, "currency": "USD" }],
"status_counts": {
"active": 214, "trialing": 31, "past_due": 6, "paused": 4
}
},
"window_metrics": {
"new_subscriptions": 18,
"canceled_subscriptions": 5,
"subscription_collected_money_by_currency": [
{ "amount": 470100, "currency": "USD" }
]
}
}
}Computed from the same records, so it cannot disagree with your orders.
- GET /v1/analytics/subscriptions
- mrr_by_currency
- status_counts
- canceled_subscriptions
A renewal is an order, tagged with where it came from.
originRefunds use the ordinary endpoint, per line and per quantity.
POST /v1/refundsThe invoice joins to the subscription through the order, not beside it.
order_idRecovering a failed renewal is a card swap, not a new billing run.
PATCH /v1/subscriptions/{subscription_id}MRR, ARR, and status counts come from the same records.
GET /v1/analytics/subscriptions05Limits
What this does not do.
Fixed plans, fixed intervals, and nothing that mutates a live subscription.
No proration, and no plan changes. Nothing in the API changes a subscription's plan, quantity, or price after it exists. Teams model an upgrade as canceling and recreating, which works and is not the same product as a billing engine that computes a mid-cycle credit. If your pricing page has an upgrade button that has to charge the difference today, that is a real gap and you should weigh it as one.
No usage or metered billing. No usage ingestion, no metered price, no tiered or volume pricing. A plan is fixed line items at a fixed interval.
No automatic card updater. When an issuer reissues a card, Flint does not receive the new number from the network. An expired card declines and dunning runs, and recovering it means the customer enters a new one.
No revenue recognition, and no invoice-level tax engine for subscriptions. If your finance team needs ASC 606 schedules or deferred revenue reporting out of the billing system, that is a different category of product.
Trials need a card. The trial validates a payment method up front rather than letting someone start with nothing on file. That is a deliberate choice about conversion quality, and it is the wrong one for a product whose funnel depends on frictionless signup.
06The other verdict
When to buy a billing platform instead.
These are good products solving a harder version of this problem, and some of you have that version.
If you bill on consumption, if your pricing has tiers and volume breaks, if you need proration on every upgrade, or if finance needs revenue schedules out of the same system, buy a billing platform. Orb, Lago, and Metronome are built for metered and hybrid pricing; Chargebee and Recurly are built for the subscription operations and dunning surface at scale. Flint does not compete with them on that ground.
What Flint is for is the case underneath that one, and it is a very common case: you already sell something, the money already works, and now some of it should recur. In that situation a billing platform is a second system of record you did not need, and the integration you are quoting is mostly the cost of keeping the two agreeing with each other.
FAQ
Questions worth asking first.
Does a subscription renewal create a real order?
Yes, and that is the whole design. Each cycle produces an order with origin set to subscription and the subscription_id on it. It carries line items, computed totals, and a payment status exactly like an order from a checkout, so it appears in the same list endpoint, refunds through the same endpoint, and reaches the same webhook listener. There is no parallel billing record to reconcile against your sales.
How do I refund a subscription charge?
With the ordinary refund endpoint, against the renewal order. There is no subscription-specific refund path because none is needed: name the order line and the quantity, and tax is allocated for you. A refund on a renewal and a refund on a one-time sale are the same call with a different order id.
What happens when a renewal payment fails?
The subscription moves to past_due and the retry schedule arms: four attempts across sixteen days. Each failure emits subscription.payment_failed. If a retry succeeds the subscription returns to active and that cycle produces its order as normal. If the retries run out, the configured end action fires, which is either cancel or pause. Updating payment_method_id at any point during the window puts the new card into the next retry.
Does Flint support proration or plan upgrades?
No. There is no endpoint that changes a subscription's plan, quantity, or price, so there is no mid-cycle change to prorate. The update surface is three fields: the payment method, cancel_at_period_end, and metadata. In practice teams model an upgrade as canceling one subscription and creating another, which is a real answer but not the same thing as a proration engine, and you should judge it on that basis.
Can I pause a subscription without losing prepaid time?
Yes. Pausing suspends billing and preserves the period the customer already paid for, so resuming does not charge them again for time they already own. Pause accepts a duration in cycles, or runs open-ended until you resume. Both transitions emit events, subscription.paused and subscription.resumed.
Does Flint do usage-based or metered billing?
No. There is no usage ingestion, no metered price, and no tiered or volume pricing. A plan carries fixed line items at a fixed interval. If you bill on consumption, a metering platform is the right purchase and this is not it.
How do customers sign up for a subscription?
The simplest path is a payment link carrying a plan_id, which gives you a hosted signup page that collects the card and starts the subscription without you building a frontend. You can also create the subscription directly once you hold a payment method. Signup is generally the only frontend decision recurring billing forces on you.
What happens when a saved card expires?
The renewal declines and dunning runs, the same as any other failure. There is no automatic card updater, so Flint does not silently receive a new card number from the network when one is reissued. Recovering an expired card means getting the customer to enter a new one, which the past_due window and its events exist to give you time to do.
Start
A renewal by tomorrow morning.
Free sandbox keys, no credit card. A daily plan bills in about twenty four hours, which is the fastest way to watch a real renewal land as a real order.
1 · create a daily plan
2 · mint a signup link with its plan id
3 · open the link, pay with 4242 4242 4242 4242
Any future expiry, any three digit CVC. The subscription starts on the card the hosted page collects.
4 · tomorrow, the renewal is an order
Then read these three.
- Subscription billing guideTrials, the retry schedule, pause semantics, and the end actions.
- Subscriptions API referenceEvery field on the plan, the subscription, and the lifecycle endpoints.
- The invoicing APIWhat the invoice on a renewal order can do once it exists.
Sandbox behavior worth knowing.
Test mode is a property of the key rather than a flag on the request, so there is no environment switch to forget. There are no test clocks, which is why the recipe uses a daily interval instead of pretending a month can be fast forwarded.