> ## 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.

# Abandoned checkouts - Hosted checkout

> How to detect and reconcile a checkout session the shopper never completed.

<Info>
  **TL;DR:** No signal ever arrives when a shopper simply leaves — the session stays `pending` until it expires. Cancel it proactively, or reconcile by polling after the TTL.
</Info>

A shopper closing the tab, navigating away, or clicking their browser's back button doesn't send any signal to your server — the session simply stays `pending` until it expires.

## When this happens

* The shopper closes the tab or browser before entering payment details.
* The shopper clicks back or navigates to another site mid-checkout.
* In embedded mode, the shopper navigates your own page away from the iframe.

## What you see

| Signal | Value |
| - | - |
| Redirect | None — the shopper's browser never returns to `success_url` or `cancel_url` |
| Embedded event | None — closing or navigating away doesn't post a `CHECKOUT_*` message |
| Session status | Stays `pending` until the TTL elapses, then moves to `expired` |
| Webhook | None |

## What to do

<Steps>
  <Step title="Don't wait on a redirect or event that will never arrive">
    There's no signal from the shopper's browser for this case — build your own UX around a timeout on your side (for example, "we didn't hear back — did you complete your purchase?") rather than waiting indefinitely.
  </Step>

  <Step title="Optionally cancel the session proactively">
    If your own UI detects the shopper has left (for example, they started a new checkout for the same cart), cancel the still-`pending` session explicitly instead of waiting for it to expire ([API reference](/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>
  </Step>

  <Step title="Reconcile by polling after the TTL, if you don't cancel proactively">
    Otherwise, check the session's status after its TTL has elapsed:

    <CodeGroup>
      ```bash cURL theme={null}
      #!/usr/bin/env bash
      # Authenticated merchant view of a checkout session. Branch on data.status;
      # never on gateway_response_code. Note: GET timestamps are epoch
      # milliseconds, unlike the ISO string returned at create time.
      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 checkout_id to verify}"

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

      ```javascript Node.js theme={null}
      #!/usr/bin/env node
      // Authenticated merchant view of a checkout session. Branch on data.status;
      // never on gateway_response_code. 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 retrieveCheckoutSession() {
        const res = await fetch(`${CHECKOUT_BASE}/api/v1/checkout/sessions/${checkoutId}`, {
          headers: { "X-Api-Key": secretKey },
        });
        const payload = await res.json();
        if (!res.ok) {
          throw new Error(`checkout session fetch failed: ${payload.code ?? payload.type} (${res.status})`);
        }
        // Confirm order_reference and amount match your order before fulfilling.
        return payload;
      }

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

      ```python Python theme={null}
      #!/usr/bin/env python3
      """Authenticated merchant view of a checkout session. Branch on ``status``;
      never on ``gateway_response_code``.
      """
      import json
      import os

      import requests

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


      def retrieve_checkout_session() -> dict:
          checkout_id = os.environ["RADIUMONE_CHECKOUT_ID"]
          resp = requests.get(
              f"{CHECKOUT_BASE}/api/v1/checkout/sessions/{checkout_id}",
              headers={"X-Api-Key": os.environ.get("RADIUMONE_SECRET_KEY", "")},
              timeout=30,
          )
          payload = resp.json()
          if not resp.ok:
              code = payload.get("code") or payload.get("type")
              raise RuntimeError(f"checkout session fetch failed: {code} ({resp.status_code})")
          # Confirm order_reference and amount match your order before fulfilling.
          return payload


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

    A `status` of `expired` confirms nothing was charged. If you need to prompt the shopper to try again, see [Handle expired checkout sessions](/hosted-checkout/handle-failures/session-expired).
  </Step>
</Steps>

## Related

<Columns cols={2}>
  <Card title="Redirect integration" icon="arrow-right" href="/hosted-checkout/redirect-integration">
    The full redirect-mode flow.
  </Card>

  <Card title="Embed hosted checkout" icon="panel-top" href="/hosted-checkout/embedded-integration">
    The full embedded-mode flow.
  </Card>

  <Card title="Handle expired checkout sessions" icon="alarm-clock" href="/hosted-checkout/handle-failures/session-expired">
    What happens once the TTL elapses.
  </Card>

  <Card title="Handle failures" icon="triangle-alert" href="/hosted-checkout/handle-failures/overview">
    All ten failure scenarios, symptom → page.
  </Card>
</Columns>
