Statuses & Lifecycles
Most Flint resources carry more than one status, and each answers a different question. An Order has five. A Return has four. Reading the wrong one, or inferring one from another, is the most common source of integration bugs on this API.
Two habits avoid nearly all of them:
- Read the field that answers your question.
payment_statussays whether money arrived.fulfillment_statussays whether goods left. Neither implies the other, and an order can bepaidandnot_fulfilledfor weeks. - Treat status values as an open set. Flint adds values as products grow. Switch on the ones you handle and let anything else fall through to a safe default rather than an exception.
The same word means different things in different fields. paid on an Order means the balance is settled; paid on a Payout means the money reached the bank. pending appears on refunds, payouts, fulfillments, and returns with four different meanings. Always read the value alongside its field name.
Orders#
An Order is the durable record, so it carries the most projections.
| Field | Question | Values |
|---|---|---|
status | Is the order still open for changes? | open, closed |
payment_status | Has the money arrived? | unpaid, partially_paid, paid |
fulfillment_status | Have the goods left? | not_fulfilled, partially_fulfilled, fulfilled, canceled |
refund_status | Has money gone back? | none, partially_refunded, refunded |
inventory_exception_status | Did stock fail after payment? | paid_inventory_failed, resolved |
status is narrower than people expect. It tracks whether the order is mutable, not whether it is finished. An order that was paid and fully refunded stays closed with payment_status: paid and refund_status: refunded, because money did settle before it came back. Reconstructing "was this refunded" from status alone will be wrong.
See Orders first for the model and Order activities for the history log.
Payments#
Two objects, and the difference matters. A PaymentIntent is one payment leg. A payment attempt is one execution across the legs you selected.
| Resource | Values |
|---|---|
PaymentIntent.status | requires_payment_method, requires_confirmation, requires_action, processing, requires_capture, succeeded, canceled, expired |
payment attempt status | processing, requires_action, requires_retry, finalizing, requires_capture, partially_succeeded, succeeded, failed, canceled, expired |
Three of these are worth knowing before you write the branch:
requires_capturemeans an authorization succeeded and is holding funds. Nothing settles until you capture it, and the authorization expires if you do not.requires_retryis an unknown outcome, not a decline. The provider timed out and the charge may have gone through, so resume the same attempt rather than starting a new one.finalizingis neither running nor finished. The money outcome is settled and Flint is still applying it to the order. Starting a new payment here charges the buyer twice.
On an attempt, prefer is_resumable over pattern-matching the status. Handling declines covers the full recovery model.
Refunds#
pending, in_transit, requires_action, succeeded, partially_succeeded, failed, canceled.
A refund is asynchronous. A 201 means Flint accepted it, not that the buyer has their money. Wait for refund.updated reaching a terminal value, and expect partially_succeeded on multi-payment orders where some legs settle and others do not. See Refunds.
Returns#
A Return keeps four statuses precisely because the decision, the goods, and the money move at their own pace.
| Field | Question |
|---|---|
decision_status | Did we say yes? |
merchandise_status | Did the goods come back? |
resolution_status | Did the buyer get their value? |
status | Is the whole thing finished? |
completion_blockers is the authoritative list of what is still owed, and an empty array is the real "done" signal. Act on return.completed, never on a refund succeeding: a resolution can settle while merchandise work is still open. Full value sets are in the Returns reference, and the model is in Returns.
Invoices and subscriptions#
| Resource | Values |
|---|---|
Invoice.status | draft, open, partially_paid, paid, void |
Invoice.refund_status | none, partially_refunded, refunded |
Subscription.status | trialing, active, paused, past_due, canceled, incomplete |
past_due is a billing state, not an entitlement decision. Whether a past-due subscriber keeps access is your policy, so read the status and decide rather than assuming Flint has cut them off. incomplete means the first payment never completed, so the subscription never really started.
See Invoicing and Subscription billing.
Checkout sessions#
open, paid, partially_paid, expired, closed, invalidated.
invalidated is distinct from expired: the session was made unusable by a change to the underlying order, not by the clock running out. Both are terminal, and neither means the buyer paid.
Fulfillment, shipments, and packages#
Fulfillment statuses vary by type, which is why the set is wide: pending, scheduled, accepted, on_hold, in_progress, preparing, picked, packed, ready, dispatched, completed, no_show, canceled, failed.
Packages run created, packed, shipped, in_transit, out_for_delivery, delivery_attempted, delivered, exception, returned, and voided. Shipments expose the same states as a read-only aggregate of their packages: exceptions and voids dominate, then the least advanced active package determines the shipment state.
A shipment also carries a direction. A return leg uses direction: return and is how merchandise comes back, which is why return tracking lives here rather than on the Return.
Disputes and payouts#
Dispute.status runs warning_needs_response, warning_under_review, warning_closed, needs_response, under_review, won, lost, prevented. The warning_* values are early fraud warnings, which are not yet disputes and may never become one.
Payout.status is pending, in_transit, paid, failed, canceled, with a separate reversal_status of none, reversing, reversed. A paid payout can still be reversed, so read both.
Which values are final#
Anything not listed here can still change, so keep polling or keep listening.
| Resource | Final values |
|---|---|
Order status | closed |
| PaymentIntent | succeeded, canceled, expired |
| Payment attempt | succeeded, partially_succeeded, failed, canceled, expired, requires_capture |
| Refund | succeeded, partially_succeeded, failed, canceled |
| Invoice | paid, void |
| Subscription | canceled |
| Checkout session | paid, expired, closed, invalidated |
| Fulfillment | completed, canceled, failed, no_show |
| Shipment | delivered, returned, voided |
| Dispute | won, lost, prevented, warning_closed |
| Payout | paid, failed, canceled |
| Return | completed, declined, canceled |
Two of these are final but not finished, and both cost real money if you treat them as done:
requires_captureis terminal for the attempt and pending for the money. Funds are held and expire uncaptured.- Order
closedstops mutation, not activity. Refunds, disputes, and returns all continue against a closed order. - Return
completedcan be reopened to record late compensating facts. Nothing else in the table reverses.
Reading statuses safely#
- The resource is the truth, events are the notification. Webhooks are at-least-once and can arrive out of order. When the next action depends on current state, read the resource.
- Compare revisions, not arrival order. Resources that support optimistic concurrency carry a revision. A payload with an older revision than you already applied is stale.
- Do not switch exhaustively. New status values are additive and non-breaking under
/v1. Adefaultbranch is required, not defensive. - Money statuses are not permissions.
past_due,requires_capture, andpartially_paiddescribe what happened to the money. What access that buys is your product decision.
Next steps#
- Webhooks: delivery, retries, and deduplication.
- Handling declines: the payment recovery model in full.
- Errors: every error code and whether it is retryable.
