Payment Reliability Lab

Payment simulator

R$

The same seed and the same faults produce the same run, delays included.

Scenarios

Baseline
Provider faults
Webhook faults
Unresolved

Chaos controls

none active

Authorization never answers. The request may still have been processed.

Authorization returns 502. Nothing happened on the acquirer side.

The acquirer is down. Retrying it achieves nothing.

The capture call never answers. Nobody knows whether the money moved.

The settlement webhook is delivered twice, sharing one event id.

A late authorization webhook lands after settlement has completed.

Faults are read only by the acquirer adapters. The orchestration code cannot see this panel — it reacts to what the provider interface returns.

History

Nothing run yet in this session.

CREATED→PROCESSING→AUTHORIZED→CAPTURED→COMPLETED
exitsCAPTURE_PENDINGDECLINEDFAILED

No transaction yet. Pick a scenario and run a payment.

0 events

Run a payment to see the calls it makes.

—
Success rate
0Transactions
0Completed
0Failed
0Declined
0Pending
0Retries
0Failovers
0Dup. webhooks
0In flight

What this demonstrates

An explicit state machine

Every state change goes through one transition table. A payment cannot reach COMPLETED without having been CAPTURED, and no branch of the code can invent a shortcut, because there is no code path that writes a state directly.

Declines are not failures

DECLINED and FAILED are separate terminal states. One is a decision the acquirer made about the money; the other is infrastructure falling over. Retrying the second is correct. Retrying the first is, at best, rude.

Idempotency, in both directions

Outbound, every authorization attempt carries the same key, so an acquirer that already decided replays that decision instead of charging twice. Inbound, every webhook carries an event id, so a redelivery is recognised and dropped.

Retries with backoff and jitter

Three attempts per provider, doubling the wait each time, with half the window randomised. Jitter is not decoration: without it, everything that failed together retries together, and the recovering provider is knocked over by its own clients.

A timeout is not a failure

When a call times out, the request may well have been processed — the answer is what got lost. That is why a timed-out capture moves to CAPTURE_PENDING rather than FAILED, and why retrying one is only safe behind an idempotency key.

Failover, and its cost

An unavailable provider is not retried, it is replaced. But the second acquirer has never seen the idempotency key the first one holds, so failover trades one risk for another — which is exactly why it is reserved for providers that are genuinely down.

Eventual consistency

Settlement arrives by webhook, whenever it arrives. The payment sits at CAPTURED in the meantime, which is correct rather than stale. A late webhook aiming at a stage already passed is refused by the same table that governs everything else.

Reconciliation as a first-class action

When nobody knows what happened, the answer is to go and ask, not to guess. Reconciliation queries the provider and accepts three answers: it was captured, it was not, or it is still unknown — and the third leaves the payment exactly where it was.

The log is the state

Nothing stores a status field. The payment is whatever reducing its events produces, which is why a page reload reconstructs it exactly, and why the timeline can never disagree with the badge next to it.