Payment states
Payment states mirror the underlying provider (card processor, Net30 for invoice). Throttle normalises them so your handler is provider- agnostic.
State machine
stateDiagram-v2 [*] --> pending: POST /payments or buyer click pending --> authorized: Provider/Net30 authorizes pending --> failed: provider declines authorized --> captured: capture call authorized --> voided: void before capture captured --> refunded: full refund captured --> partially_refunded: partial refund partially_refunded --> refunded: subsequent refunds total to full refunded --> [*] voided --> [*] failed --> [*]
States
-
pending— payment row created, no provider response yet. UI shows a spinner; back-end has the row id but no auth/capture proof. -
authorized— provider authorised the amount (funds reserved). Most card flows auto-capture so this is brief; manual-capture flows stay here until the merchant explicitly captures. -
captured— funds moved. Triggerspayment.capturedoutbound event. Order can now transition topaid. Capture may be partial: pass anamountbelow the authorized total toPOST /v1/payments/{id}/capture. The captured value is tracked oncapturedAmount(the event carries bothcapturedAmountandauthorizedAmount); the uncaptured remainder is released by the processor, and refunds are capped at the captured amount. -
failed— provider declined OR a network/timeout error during processing. Carrieserror.code+error.message. Terminal. -
voided— authorisation released before capture. Funds never moved. Terminal. -
partially_refunded— at least one refund processed but total refunded is less than captured amount. Still accepts further refunds. -
refunded— total refunded equals captured amount. Terminal.
Net30 special cases
-
Net30 payments go through
pending→authorizedon invoice issue, then →capturedwhen the buyer pays the invoice (manual mark-paid OR ACH webhook). -
invoice.overduefires when an authorized Net30 payment passes itsdueDate; the payment row staysauthorizeduntil paid or voided.
Disputes & chargebacks
Disputes are tracked out-of-band from the status machine, on the disputed boolean plus disputeReason / disputeOpenedAt (all exposed on
the payment object). A payment keeps its underlying status (usually captured) while disputed.
- Card chargebacks are ingested automatically from the
payment provider: an inbound
dispute.*/chargeback.*webhook setsdisputed = trueand firespayment.disputed. A merchant-favourable resolution (won / reversed) clears the flag and firespayment.dispute_cleared. - Net30 disputes are opened/cleared manually from the dashboard and fire the same two events.