Skip to content

The payment state machine

Status transitions are decided only by the API, centrally, using a compare-and-set on the intent version, and each one is written to the append-only timeline together with its evidence. Background workers never mutate a status directly; they submit observations and the state machine evaluates them.

Payment intent state machineHappy path: created, awaiting_payment, detected, confirming, confirmed. created can become canceled. awaiting_payment can become expired; expired becomes late_payment when a transfer arrives after expiry. confirming can become underpaid, overpaid or reorged. confirmed becomes reorged after a deep reorg. reorged returns to confirming when the transfer is re-included, or goes to manual_review when it is dropped. underpaid and late_payment go to manual_review. manual_review and overpaid end in resolved with a recorded decision. failed is only used with a validated reason.quote lockedseenin a block≥ requiredmerchant cancelsquote expiredexcessshortreorg ⇄ re-includeddeep reorgtransfer after expiryneeds decisiondecision + evidencemerchant decisiontransfer droppedcreatedawaiting_paymentdetectedconfirmingconfirmedcanceledexpiredunderpaidoverpaidreorgedfailedlate_paymentmanual_reviewresolved
in progress / finalneeds a decisionreorganised or failedEvery transition is decided centrally by the API with a compare-and-set on the intent version and recorded in the append-only timeline with its evidence. On small screens the diagram scrolls sideways.
Status Means Fulfillable Your action
created Intent created; the customer has not chosen a network yet no Show “awaiting payment”
awaiting_payment Quote locked, unique address assigned no Wait
detected A transfer was seen but is not in a block yet no Wait — it can still disappear
confirming The transfer is in a block and collecting confirmations no Wait
confirmed Expected amount reached the required confirmations yes Fulfil (after verifying the webhook and re-reading the intent)
underpaid Less than quoted was confirmed no Decide: request the remainder, accept partially or refund off-chain; then resolve
overpaid More than quoted was confirmed yes Fulfil; contact the customer about the excess; then resolve
expired The quote expired before any payment was seen no Offer a new checkout if the customer still wants to pay
late_payment A transfer arrived after expiry no Review — the price may have moved; accept, refund or reject; then resolve
reorged A block containing the payment was replaced no Pause or reverse fulfilment; wait for re-confirmation or review
manual_review Held for a documented decision no Review the evidence; resolve
resolved A recorded decision closed the intent per decision Follow your decision (resolution.kind)
failed Failed for a validated reason only no Read status_reason
canceled You cancelled before any transfer no Nothing
  • Exactly-once crediting. Each chain transfer has a deterministic identity (network + transaction + output or log index). Delivering the same observation twice never credits twice.
  • Asset identity, not symbols. A transfer counts only if its network, contract address and decimals match the registry entry of the quoted asset. A token that merely shares a symbol is recorded and flagged, never credited.
  • No silent reversal. A final status never quietly goes back. If the chain reorganises after confirmed, the intent moves to reorged and a payment.reorged webhook is sent; it confirms again only with new evidence.
  • On-time is decided at first sight. A transfer first observed before the quote expired counts towards the payment even if it confirms later. A transfer first seen after expiry is classified late.
  • Extra payments after confirmation do not change a confirmed intent; they create a reconciliation discrepancy and a payment.review_required event.
  • Quotes are immutable. Refreshing a quote supersedes the old one; it never edits it.

The hosted checkout, dashboard and @gryphem-pay/react render each status with a text label and a symbol, in English, Portuguese (Brazil) and Finnish — never colour alone. If you build your own UI, do the same.