The six statuses
A payment intent has exactly six statuses. There is nocreated, no confirming and no overpaid.
Transitions
Status never moves backwards.
completed never returns to processing.
Expiry defaults to 60 minutes
Every payment intent expires 60 minutes after it is created unless you sendexpires_in on create. The exact moment is on the intent as expires_at; read that field rather than computing it yourself.
expires_in is in seconds, from 300 (5 minutes) to 86400 (24 hours). It is a create-time input only: there is no endpoint to extend a live intent, so if a customer needs longer, create a second intent with a new idempotency key.
The crypto amounts are quoted when the intent is created and stay fixed for the whole window, so a longer window leaves you exposed to more exchange-rate movement. A customer who funds the payment by card can push expires_at out to 6 hours regardless of what you set, because the card leg needs that time to settle.
Expiry is applied lazily. A pending intent past its expires_at flips to expired the next time it is loaded, including when the customer opens the hosted checkout page, so a stale pending in your database may already be dead. Trust expires_at over status.
State reasons
state_reason is the machine-readable “why” behind the current status. It has eleven values.
Overpayment is not a status. An accepted overpayment is
status: "completed" with state_reason: "completed_overpaid", and the :detail view of the intent carries an overpayment object with the excess. If you branch on status === "completed" and ignore state_reason, you will fulfil overpaid and underpaid orders identically, which may or may not be what you want.
Cancels land in expired, not in a cancel status. There is no canceled status. A cancel is modelled as an early expiry, and the reason tells you who did it: canceled_by_agent for an agent, canceled_by_merchant for you.
Related status fields
A payment intent carries four other status-shaped fields. None of them are the payment status.
Transactions have their own statuses (
pending, confirming, confirmed, submitted, failed) exposed under transaction on the intent detail view. confirming is a transaction status. It is not a payment intent status.
GET /api/v2/payments?status=confirming is accepted by the list filter but always returns zero rows, because no payment intent ever holds that value. It is a dead filter value.Matching statuses to webhook events
A merchant cancel emits
payment_intent.expired, exactly like a natural expiry — if you already handle expiry, you already handle cancels. Check state_reason to tell them apart.
The agentic cancel path is the exception: it emits payment_intent.canceled, which cannot be subscribed to. See Webhooks. Watch for payment_intent.expired with state_reason: "canceled_by_agent" instead.
