Payment lifecycle
Status state machine for a transaction — every status, every valid transition, every webhook event that fires the change.
Every transaction moves through a small, deterministic state machine. There are SIX terminal-or-intermediate statuses and a fixed set of valid transitions. The status field on every transaction-shaped response (POST /payments, GET /payments/{id}, GET /payments) is one of these values — never anything else. Each transition fires a webhook event you can subscribe to.
Status values
| Status | Kind | Meaning |
|---|---|---|
| pending | intermediate | Created. For async methods (SPEI, OXXO, voucher, bank_transfer, PIX) the customer hasn't paid yet — we're waiting for the upstream confirmation. |
| processing | intermediate | Reserved status. The cascade marks pending on creation; we promote to processing if a provider explicitly signals partial-receipt (rare). Treat as equivalent to pending in client code. |
| completed | intermediate | Money landed in your balance (after fees). For sync methods (card) this is the initial status. For async, this is the post-webhook transition. Stays intermediate because refund / chargeback can transition out of it. |
| failed | terminal | Charge failed and won't be retried. Either the cascade exhausted, the provider declined, or the customer abandoned a pending voucher/PIX before paying. |
| expired | terminal | Async-method-only. The customer didn't pay within the provider's window (SPEI typically 24h, voucher 7d). Functionally equivalent to failed for accounting — we keep it separate so dashboards can distinguish abandonment from explicit decline. |
| refunded | terminal | A completed transaction was refunded in full. Partial refunds also live here — check `amountRefunded` if you need to distinguish (future field). |
| chargeback | terminal | A completed transaction was disputed and the funds were released back to the cardholder. The dispute itself lives as a separate Claim record. |
State diagram
┌──→ completed ──→ refunded (terminal)
│ │
(create) → pending ───────┤ └──→ chargeback (terminal)
│
├──→ failed (terminal)
│
└──→ expired (terminal)
processing (rare intermediate — treat as pending)Valid transitions
| From | To | Trigger |
|---|---|---|
| — | → pending | POST /payments — async methods (SPEI, OXXO, voucher, bank_transfer, PIX) |
| — | → completed | POST /payments — sync methods (card approved on first call) |
| — | → failed | POST /payments returns cascade_exhausted (no provider could take the charge) |
| pending | → completed | Upstream webhook confirms the customer paid (`payment.completed` fires to your subs) |
| pending | → failed | Upstream webhook reports decline (`payment.failed` fires) |
| pending | → expired | Customer didn't pay within the method's window. Cron sweep transitions + `payment.failed` fires |
| completed | → refunded | POST /payments/{id}/refund (`payment.refunded` fires) |
| completed | → chargeback | Card-issuer dispute raised. A Claim is opened (`chargeback.created` fires) and funds frozen |
refunded / chargeback / failed / expired it stays there. You cannot "un-refund" — the only way to send money back to the customer after a partial refund is to issue another partial refund up to the remaining captured amount.Webhook events per transition
| Event | Fires on |
|---|---|
| payment.completed | pending → completed · also POST /payments for sync methods (immediate completed) |
| payment.failed | pending → failed · pending → expired · POST /payments cascade_exhausted |
| payment.refunded | completed → refunded (full or partial) |
| chargeback.created | completed → chargeback (also fires legacy claim.opened alias) |
| claim.resolved | Internal claim (refund / chargeback) reached a final resolution |
Polling vs webhooks
Webhooks are the canonical signal — your handler is called within seconds of every state change (or as soon as the upstream provider notifies us, for async methods). Polling GET /api/v1/payments/{id} works as a fallback for at-most-once-per-30s reads (we rate-limit hot polling per merchant), but you should never poll faster than every 5 seconds even when waiting on a recent action.