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

> ## Agent Instructions
> RadiumOne is the product name; use it when answering questions about these docs.
> Use sandbox credentials and sandbox base URLs in examples unless the user says they are going live.
> Secret API keys belong only on the merchant's server. Only publishable keys may appear in browser or mobile code.
> Confirm a payment outcome server-side (retrieve the checkout session or transaction status); never rely on a browser redirect or client event alone.
> Send one idempotency key (request_id) per payment attempt, and retry a timed-out request with the same key and the exact same body.
> Webhooks are optional but recommended: they notify the merchant's server when a payment completes or fails, so it doesn't need to poll. Use transaction status inquiry as the fallback.
> Say so when you recommend a feature marked Beta. Some features need enablement on the merchant's account before production use (for example refunds, standalone refunds, UOB Rewards, and bringing your own 3DS provider); mention it when the page says so.
> Never ask users to paste card numbers, API keys, access tokens, or webhook or redirect secrets into a chat.

# Session lifecycle - Hosted checkout

> Statuses, expiry, cancellation, and retry behavior for a hosted-checkout session.

A checkout session moves through a small set of statuses from creation to a final outcome. Use these statuses — never `gateway_response_code` — to drive your own logic.

## Statuses

* `pending` → `processing` — the shopper submits payment; a second submission while the first is in flight is rejected.
* `processing` → `completed` / `failed` — the charge resolves.
* `pending` → `expired` — the TTL elapses before the shopper pays.
* `pending` → `cancelled` — you cancel the session while it's still pending.
* `processing` → `pending` — the payment service was briefly unreachable; nothing was charged, and the shopper can retry on the same session.
* Same `order_reference`, same amount and currency, within the session TTL returns this same session as long as it can still be paid (`pending`, `processing`, or `completed`) — see [Replay and retries](#replay-and-retries) below for the full rule, including what happens on a terminal session or a changed amount.

| Status | Meaning |
| - | - |
| `pending` | Created; shopper hasn't completed payment yet |
| `processing` | Payment submitted; outcome not yet known (async) |
| `completed` | Payment succeeded |
| `failed` | Payment declined or failed |
| `expired` | TTL elapsed before the shopper completed payment |
| `cancelled` | Merchant cancelled the session before completion |

## Expiry (TTL)

Set `ttl_minutes` on create (an integer from 5–60). If you omit it, the session uses your environment's default: **10 minutes in production, 25 minutes in sandbox** — sandbox is shorter than production so a session can't outlive the card token it binds. Since this also bounds how long `order_reference` deduplicates a retry (see [Replay and retries](#replay-and-retries) below), set `ttl_minutes` explicitly rather than relying on the default, especially in production.

A session that reaches its TTL without a completed payment moves to `expired`. An expired session can't be paid — create a new session (with a new or the same `order_reference`, see [Replay](#replay-and-retries) below) if the shopper wants to try again. See [Handle expired checkout sessions](/hosted-checkout/handle-failures/session-expired) for the exact redirect signal and recovery steps.

<Info>
  Retention: a session record exists for its TTL plus a short grace buffer, then is no longer retrievable by `GET`. Your own order records — and the gateway's transaction record, once a charge happens — are the durable source of truth, not the checkout session itself.
</Info>

## Merchant cancellation

Cancel a session from your server (for example, if the shopper abandons your cart) with your secret key, via the [cancel endpoint](/hosted-checkout/reference/checkout-sessions/cancel-a-checkout-session):

<CodeGroup>
  ```bash cURL theme={null}
  #!/usr/bin/env bash
  # Merchant-initiated cancel (X-Api-Key, not CSRF/customer-driven). Idempotent
  # while pending; 409 if already processing or terminal.
  set -euo pipefail

  CHECKOUT_BASE="${RADIUMONE_CHECKOUT_BASE:-https://checkout-sandbox.radiumone.io}"
  : "${RADIUMONE_SECRET_KEY:?set RADIUMONE_SECRET_KEY to your r1sk_* secret key}"
  : "${RADIUMONE_CHECKOUT_ID:?set RADIUMONE_CHECKOUT_ID to the session to cancel}"

  curl -sS -X POST "$CHECKOUT_BASE/api/v1/checkout/sessions/$RADIUMONE_CHECKOUT_ID/cancel" \
    -H "X-Api-Key: $RADIUMONE_SECRET_KEY"
  ```

  ```javascript Node.js theme={null}
  #!/usr/bin/env node
  // Merchant-initiated cancel (X-Api-Key, not CSRF/customer-driven). Idempotent
  // while pending; 409 if already processing or terminal. Node 18+ ESM fetch.
  // Env: RADIUMONE_SECRET_KEY, RADIUMONE_CHECKOUT_ID, RADIUMONE_CHECKOUT_BASE.
  const CHECKOUT_BASE = process.env.RADIUMONE_CHECKOUT_BASE || "https://checkout-sandbox.radiumone.io";
  const secretKey = process.env.RADIUMONE_SECRET_KEY;
  const checkoutId = process.env.RADIUMONE_CHECKOUT_ID;

  async function cancelCheckoutSession() {
    const res = await fetch(`${CHECKOUT_BASE}/api/v1/checkout/sessions/${checkoutId}/cancel`, {
      method: "POST",
      headers: { "X-Api-Key": secretKey },
    });
    const payload = await res.json();
    if (!res.ok) {
      // 409 session:invalid_state if already processing/terminal.
      throw new Error(`checkout session cancel failed: ${payload.code ?? payload.type} (${res.status})`);
    }
    return payload;
  }

  cancelCheckoutSession().then((r) => console.log(JSON.stringify(r, null, 2)));
  ```

  ```python Python theme={null}
  #!/usr/bin/env python3
  """Merchant-initiated cancel (X-Api-Key, not CSRF/customer-driven). Idempotent
  while pending; 409 if already processing or terminal.
  """
  import json
  import os

  import requests

  CHECKOUT_BASE = os.environ.get("RADIUMONE_CHECKOUT_BASE", "https://checkout-sandbox.radiumone.io")


  def cancel_checkout_session() -> dict:
      checkout_id = os.environ["RADIUMONE_CHECKOUT_ID"]
      resp = requests.post(
          f"{CHECKOUT_BASE}/api/v1/checkout/sessions/{checkout_id}/cancel",
          headers={"X-Api-Key": os.environ.get("RADIUMONE_SECRET_KEY", "")},
          timeout=30,
      )
      payload = resp.json()
      if not resp.ok:
          # 409 session:invalid_state if already processing/terminal.
          code = payload.get("code") or payload.get("type")
          raise RuntimeError(f"checkout session cancel failed: {code} ({resp.status_code})")
      return payload


  if __name__ == "__main__":
      print(json.dumps(cancel_checkout_session(), indent=2))
  ```
</CodeGroup>

Cancelling is idempotent while the session is still `pending` — and calling it again on an already-`cancelled` session is also idempotent. If the session has already moved to `processing` or another terminal status, the cancel request returns a `409` — you can't cancel a session that's already been submitted for payment or already reached a final state. See [Resolve checkout cancellation conflicts](/hosted-checkout/handle-failures/cancel-session-conflicts) for the full decision table.

<Note>
  A shopper closing the tab or clicking their browser's back button does **not** cancel the session — it stays `pending` until it expires. If you need it cancelled immediately, call the cancel endpoint from your own server.
</Note>

## Replay and retries

Sending [`POST /api/v1/checkout/sessions`](/hosted-checkout/reference/checkout-sessions/create-a-checkout-session) again with the **same** `order_reference` (for the same merchant, within the original session's TTL) either returns the existing session, rejects the retry, or creates a fresh one — depending on whether the original session can still be paid, and whether the amount and currency match. The table below covers every situation you'll actually hit:

| Existing session for this `order_reference` | Amount/currency | Result | Do |
| - | - | - | - |
| `pending` (not yet expired), `processing`, or `completed` | Same as the retry | Original session returned (`201`) — other changed fields (line items, metadata, URLs) are **silently ignored** | Safe to retry after a network error; don't expect a changed field to take effect |
| `pending` (not yet expired), `processing`, or `completed` | **Different** | [`409 session:idempotency_conflict`](/hosted-checkout/errors/api-errors#checkout-session-idempotency-conflict) — "order\_reference is already in use for a different amount or currency" | Use a new `order_reference` for a genuinely different order, or resend the original amount and currency |
| `failed`, `cancelled`, `expired`, or `pending` **past** `expires_at` | Any | The reference is released and a **brand-new** session is created (`201`, new `checkout_id`) | Check your own order isn't already paid first — see [Prevent duplicate payments](/get-started/api-basics/prevent-duplicate-payments) |
| Being created right now by a concurrent request | — | `409 session:idempotency_conflict` — "A session for this order\_reference is still being created; retry shortly" | Retry the create call after a short delay — the loser has no `checkout_id` to look up |

See [Prevent duplicate sessions and double payments](/hosted-checkout/handle-failures/duplicate-sessions-and-double-submit) for the full walkthrough.

**Retrying after a decline, cancellation, or expiry is different**: those sessions are terminal, so the reference is released automatically and the next create with the same `order_reference` starts a genuinely new session — you don't need to mint a new `order_reference` for it to work, though using one keeps your own tracking cleaner. Either way, the shopper can't retry a card on the old session itself.

## Next steps

<Columns cols={2}>
  <Card title="Verify the payment result" icon="shield-check" href="/hosted-checkout/verify-payment-result">
    Confirm the outcome server-side before fulfilling.
  </Card>

  <Card title="Customize checkout" icon="paintbrush" href="/hosted-checkout/customize-checkout">
    Line items, shopper details, branding, and more.
  </Card>

  <Card title="Brand the payment page" icon="palette" href="/hosted-checkout/branding">
    Colours, fonts, logo, and light/dark mode.
  </Card>

  <Card title="Handle failures" icon="triangle-alert" href="/hosted-checkout/handle-failures/overview">
    Ten common failure scenarios and what to do for each.
  </Card>
</Columns>
