Core

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

StatusKindMeaning
pendingintermediateCreated. For async methods (SPEI, OXXO, voucher, bank_transfer, PIX) the customer hasn't paid yet — we're waiting for the upstream confirmation.
processingintermediateReserved 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.
completedintermediateMoney 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.
failedterminalCharge failed and won't be retried. Either the cascade exhausted, the provider declined, or the customer abandoned a pending voucher/PIX before paying.
expiredterminalAsync-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.
refundedterminalA completed transaction was refunded in full. Partial refunds also live here — check `amountRefunded` if you need to distinguish (future field).
chargebackterminalA completed transaction was disputed and the funds were released back to the cardholder. The dispute itself lives as a separate Claim record.

State diagram

text
                                ┌──→  completed  ──→  refunded            (terminal)
                                │           │
   (create)  →  pending  ───────┤           └──→  chargeback           (terminal)
                                │
                                ├──→  failed                            (terminal)
                                │
                                └──→  expired                           (terminal)

                                processing  (rare intermediate — treat as pending)

Valid transitions

FromToTrigger
pendingPOST /payments — async methods (SPEI, OXXO, voucher, bank_transfer, PIX)
completedPOST /payments — sync methods (card approved on first call)
failedPOST /payments returns cascade_exhausted (no provider could take the charge)
pendingcompletedUpstream webhook confirms the customer paid (`payment.completed` fires to your subs)
pendingfailedUpstream webhook reports decline (`payment.failed` fires)
pendingexpiredCustomer didn't pay within the method's window. Cron sweep transitions + `payment.failed` fires
completedrefundedPOST /payments/{id}/refund (`payment.refunded` fires)
completedchargebackCard-issuer dispute raised. A Claim is opened (`chargeback.created` fires) and funds frozen
Reverse transitions don't exist. Once a tx is 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

EventFires on
payment.completedpending → completed · also POST /payments for sync methods (immediate completed)
payment.failedpending → failed · pending → expired · POST /payments cascade_exhausted
payment.refundedcompleted → refunded (full or partial)
chargeback.createdcompleted → chargeback (also fires legacy claim.opened alias)
claim.resolvedInternal 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.