# Payment lifecycle (/concepts/payment-lifecycle)

A one-time checkout involves three records. The **checkout session** is the payment link your
server creates. The **order** is what your server asked the payer to pay, keyed by your `order_id`.
A **payment** is one transaction the payer signed, which Fianto broadcasts to Solana and follows
until it is final. Each has its own statuses, and they move together only at a few points.

**Order and payment statuses**

1. Order: `PENDING` → `PAID` at finalized, or `PENDING` → `EXPIRED` when its session expires.
2. `EXPIRED` → `PAID` when a late payment still settles.
3. `EXPIRED` → `PENDING` when you create a new session with the same `order_id`.
4. A cancelled session leaves the order `PENDING`. An order is never `CANCELED`.
5. Payment: `PROCESSING` → `CONFIRMED` → `SUCCEEDED` at finalized.
6. A payment can fail from `PROCESSING` or `CONFIRMED`, and `FAILED` → `PROCESSING` if the transfer lands after all.
7. A payment whose session was superseded (a newer session for the order, or changed order terms) is recorded `FAILED` at finalized, even though its funds landed. It never shows as `SUCCEEDED` first.
8. Payments found by the reference sweep start at `CONFIRMED`.
9. `SUCCEEDED` is final. `EXPIRED` and `FAILED` are not.

## Checkout session [#checkout-session]

Sessions of both modes, payment and subscription, use these statuses. A session starts `OPEN` and
leaves it once.

| Status | Meaning | What you can do |
|---|---|---|
| `OPEN` | The link works until expires_at: between 5 minutes and 24 hours after create, 30 minutes by default. | Reissue the link, or cancel it. |
| `COMPLETED` | Its payment finalized while the session was still OPEN. Fianto sends checkout.session.completed. | Nothing: the session is done. |
| `EXPIRED` | Nobody paid before expires_at. Fianto sends checkout.session.expired. | Create a new session with the same order_id. |
| `CANCELED` | You cancelled it through the API, or the payer cancelled it on the checkout page. Fianto sends checkout.session.canceled and no order event. | Create a new session with the same order_id. |

A session can be cancelled only while it is `OPEN` with no live payment. Fianto also cancels an
`OPEN` subscription session itself when its link is outdated, because your receiving wallet or the
treasury changed since the session took its snapshot of the plan: the payer's attempt is refused with
`checkout_link_outdated`, and `checkout.session.canceled` is sent.

## Order [#order]

A subscription session creates no order; only payment-mode sessions have one.

| Status | Meaning | What you can do |
|---|---|---|
| `PENDING` | Waiting for a payment. A cancelled session leaves the order here, with no end date. | Wait, or create a new session for the same order_id. |
| `PAID` | A payment for it finalized. Fianto sends order.paid. | Fulfil the order. |
| `EXPIRED` | Its session expired before a payment finalized. Fianto sends order.expired. | Create a new session for the same order_id: the order returns to PENDING. |

The moves are:

* `PENDING` → `PAID` when a payment finalizes.
* `PENDING` → `EXPIRED` when its session expires.
* `EXPIRED` → `PAID` when a late payment still settles.
* `EXPIRED` → `PENDING` when your server creates a new session with the same `order_id`. The order
  takes the new request's terms. A cancelled session's order is reused the same way.

Only a `PAID` order refuses a new session, with 409 `order_already_paid`. No status is ever written
to an order for a cancel: the order simply stays `PENDING`.

## Payment [#payment]

| Status | Meaning | What you can do |
|---|---|---|
| `PROCESSING` | The payer signed and Fianto sent the transaction to Solana. | Wait. |
| `CONFIRMED` | Solana confirmed the transaction; it is not finalized yet. Payments that Fianto finds on its own start here. | Do not fulfil yet. |
| `SUCCEEDED` | Finalized on Solana. | The order is PAID. |
| `FAILED` | Fianto did not accept this payment. It can fail from PROCESSING or CONFIRMED. | Check whether the USDC arrived anyway before you tell the payer anything. |

The moves are:

* `PROCESSING` → `CONFIRMED` → `SUCCEEDED`.
* `PROCESSING` or `CONFIRMED` → `FAILED`.
* `FAILED` → `PROCESSING` when the transfer lands after all.

A payment whose session was superseded, by a newer session for the same order or by changed order
terms, is recorded `FAILED` at finalized, even though its funds landed. It is never shown as
`SUCCEEDED` first: Fianto decides this in the same step that settles the payment. `SUCCEEDED` is
final.

> **FAILED does not mean no money moved:**
>
> A `FAILED` payment can have its USDC in your wallet: a transfer that finalized with the wrong amount
> or fee, a transfer that lands after Fianto wrote it off, or a payment whose session was superseded.
> Check the payment before you tell a payer anything. See
> [Orders and payments](/merchants/orders-and-payments).

Neither `EXPIRED` (order) nor `FAILED` (payment) is final, so keep your webhook route listening
after each of them.

## Where the three meet [#where-the-three-meet]

* **At finalized.** The payment becomes `SUCCEEDED` and the order `PAID`, and Fianto sends
  `order.paid`. When the session was still `OPEN`, it becomes `COMPLETED` and Fianto also sends
  `checkout.session.completed`. Nothing of this happens at confirmed; see
  [Finality](/concepts/finality).
* **A payment for a closed session.** A payment that finalizes after its session `EXPIRED` or was
  `CANCELED` still pays the order: `PAID` and `order.paid`, but no `checkout.session.completed`. If a
  newer session was opened for the order in the meantime, the payment is `FAILED` instead, with its
  funds landed.

## The expiry sweep [#the-expiry-sweep]

Every 30 seconds Fianto expires `OPEN` sessions whose time has run out. A payment in flight keeps a
session from expiring. An expired session sends `checkout.session.expired`, and its order becomes
`EXPIRED` and sends `order.expired`.

A session can also lapse without the sweep having reached it. When your server then creates a new
session for the same `order_id`, the lapsed one is expired and replaced: it still sends
`checkout.session.expired`, but there is no `order.expired`, because the order is reopened for the
new session.

## The reference sweep [#the-reference-sweep]

Every session has a reference, and the transfer of the price carries it. Every 60
seconds Fianto looks for transfers with the reference of each open session, and keeps looking for 24
hours after a session closes. This catches transfers that Fianto did not broadcast itself. A
payment found this way starts at `CONFIRMED`.

A transfer that carries a session's reference but is not that session's payment is recorded on the
order. Every such transfer is recorded as an unmatched transfer; Fianto also sends
`order.duplicate_payment` when the order is already `PAID` or has another payment that has not
failed. Its `duplicate` names that transfer's `signature` and `amount`. Otherwise no webhook is
sent. Fianto records up to 20 unmatched transfers per session and ignores any beyond that.

> **Duplicate and unmatched transfers reached your wallet:**
>
> In both cases the USDC is in your wallet. Fianto never holds your funds and has no refund API: refund
> the payer yourself, from your own wallet.

## See also [#see-also]

- [Checkout sessions](/developers/checkout-sessions): Create, reissue and cancel sessions from your server.

- [Orders and payments](/merchants/orders-and-payments): The same statuses as the dashboard shows them.

- [Finality](/concepts/finality): Why Fianto waits for finalized.

- [Webhook events](/developers/webhooks/events): What each checkout and order event carries.