> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hypermid.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Session lifecycle

> The states a payment moves through, and which are terminal.

`status` is stored verbatim and returned on every read. `completed` means the
destination-side payment was confirmed; a broadcast or payer-claimed
transaction is not completion.

| Status                 | Meaning                                                                   | Terminal |
| ---------------------- | ------------------------------------------------------------------------- | -------- |
| `awaiting_destination` | a multi-destination **intent**; the payer has not chosen where funds land | no       |
| `created`              | session exists; the payer has not acted                                   | no       |
| `pending`              | funds observed, settlement in flight                                      | no       |
| `completed`            | the recipient has been paid                                               | **yes**  |
| `failed`               | settlement could not complete                                             | **yes**  |
| `expired`              | the session's TTL elapsed unpaid                                          | **yes**  |
| `refunded`             | funds were returned to the payer                                          | **yes**  |

## Intents come before the session

A [multi-destination deposit](/guides/deposit) — created with `destinations` or a
multi-pair `destinationRecipients` — does not start as a session. It starts as an
**intent**: `status: "awaiting_destination"`, no quote, no deposit address, no
fiat order. An intent never reaches a paid state on its own.

```
create (multi-destination)
  → awaiting_destination            an intent, not yet a session
  → GET  /v1/payments/:id/public    → destinationCandidates[]
  → PUT  /v1/payments/:id/destination   provisional · reversible · repeatable
  → POST /v1/payments/:id/destination   freeze · one-way → materialises a session
  → created → pending → completed…  the normal lifecycle above
```

`PUT` records a tentative choice and leaves the intent open; `POST` freezes it
into an ordinary single-destination session and the rest of this table applies.
A single-destination create (the ordinary shape, or a one-item multi-destination
list) skips the intent entirely and starts at `created`.

## Reading the timeline

A session that never left `created` was **never paid** — look at the payer's
wallet, not at our settlement. One that reached `pending` and stopped has funds
in flight and is worth investigating on-chain.

`expiresIn` sets the TTL at creation; the default comes from server config.
Expiry is not a failure — it usually means the payer walked away.

Never fulfil from `onSuccess`, an iframe callback, a requested amount, or a
payer-claimed transaction hash. A signed webhook is a prompt to perform a
server-side session read; the read must show exactly `completed`. A merchant
without a backend uses the Dashboard as the authority, with email serving only
as a prompt to check it.

`duplicate`, `late`, and `out-of-bounds` are exception outcomes, not successful
payments. A duplicate is an additional confirmed payment on a consumed
single-use link; late means payment reached a cancelled or expired session;
out-of-bounds means the verified amount missed the session's configured bounds.
Keep each for reconciliation and do not fulfil automatically.

Keys select an environment. Keep live and test keys, links, webhooks and
session reads separate; never use a test result to fulfil a live order.

## Idempotency

`orderId` is the idempotency key. Creating twice with the same one returns the
existing **non-terminal** session and a `200` rather than a `201`, so a retried
request cannot charge a payer twice. Send one, and send the same one on retry.
