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.
Statuses and what to do
Section titled “Statuses and what to do”| 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 |
Rules the state machine guarantees
Section titled “Rules the state machine guarantees”- 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 toreorgedand apayment.reorgedwebhook 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_requiredevent. - Quotes are immutable. Refreshing a quote supersedes the old one; it never edits it.
Customer-facing labels
Section titled “Customer-facing labels”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.