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

# Service outages - Hosted checkout

> What the shopper sees during a payment service outage, and how to confirm no charge was made.

<Info>
  **TL;DR:** A gateway outage never charges anything. Retry the create call if no session exists yet, or retry on the **same** session if the shopper was already paying — either way, nothing was charged.
</Info>

RadiumOne Checkout can fail to reach the payment gateway at two different points — creating a session, and completing a charge on the hosted page — and each one leaves things in a different, but always safe, state: **nothing is ever charged when the gateway can't be reached.**

## When this happens

* **At session create**: a protective breaker is open for gateway reads, or the gateway itself returns a server error while creating the session.
* **At the hosted page, while the shopper is paying**: the gateway is unreachable partway through the charge attempt.

## What you see

| Point of failure | What you see | Session left in |
| - | - | - |
| Session create | Your create call itself fails — `422 gateway:unavailable` (with a `retry_after_ms` hint) or `502 gateway:request_failed` | No session was created at all |
| Charge attempt, on the hosted page | The shopper sees a retry message ("no charge was made"); in embedded mode, your page receives `CHECKOUT_ERROR` | Released back to `pending` — the shopper can retry immediately, on the **same** session |

<Info>
  These are deliberately different outcomes. Charge attempts are never gated behind the availability breaker — refusing a charge because of an unrelated read-path hiccup would cost you a sale for negligible protection, since the gateway itself already dedupes by `request_id`. A charge-time outage instead comes from the gateway call genuinely failing to connect, and the session is released, not failed, so the shopper doesn't lose their place.
</Info>

## What to do

<Steps>
  <Step title="Branch on where the failure happened">
    If your **create call** returned an error, no session exists — see the next step. If the shopper is on the hosted page and sees a retry message (or you receive `CHECKOUT_ERROR`), skip to the step after.
  </Step>

  <Step title="Retry the create call">
    Nothing was created, so it's safe to call create again with the same body ([API reference](/hosted-checkout/reference/checkout-sessions/create-a-checkout-session)). If the error included `retry_after_ms`, wait at least that long first:

    <CodeGroup>
      ```bash cURL theme={null}
      #!/usr/bin/env bash
      # Create a hosted-checkout session (Live: `billing_details` field). Redirect
      # the shopper to checkout_url. Same order_reference within the TTL replays
      # the existing session (201) instead of creating a duplicate — safe to retry.
      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}"

      curl -sS -X POST "$CHECKOUT_BASE/api/v1/checkout/sessions" \
        -H "Content-Type: application/json" \
        -H "X-Api-Key: $RADIUMONE_SECRET_KEY" \
        -d @request.json
      ```

      ```javascript Node.js theme={null}
      #!/usr/bin/env node
      // Create a hosted-checkout session (Live: `billing_details` field). Redirect
      // the shopper to checkout_url. Node 18+ ESM fetch.
      // Env: RADIUMONE_SECRET_KEY, RADIUMONE_CHECKOUT_BASE (optional override).
      //
      // Same order_reference within the TTL replays the existing session (201)
      // instead of creating a duplicate — safe to retry with the same body.
      import { readFileSync } from "node:fs";

      const CHECKOUT_BASE = process.env.RADIUMONE_CHECKOUT_BASE || "https://checkout-sandbox.radiumone.io";
      const secretKey = process.env.RADIUMONE_SECRET_KEY;
      const body = JSON.parse(readFileSync(new URL("./request.json", import.meta.url)));

      // Exponential backoff with jitter: attempt 1 waits ~250-500ms, doubling each
      // attempt, capped at 4s -- avoids hammering the gateway in a tight retry loop.
      function backoffMs(attempt) {
        const base = Math.min(250 * 2 ** (attempt - 1), 4000);
        return base + Math.random() * base;
      }

      async function createCheckoutSession(maxAttempts = 3) {
        for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
          let res;
          try {
            res = await fetch(`${CHECKOUT_BASE}/api/v1/checkout/sessions`, {
              method: "POST",
              headers: {
                "Content-Type": "application/json",
                "X-Api-Key": secretKey,
              },
              body: JSON.stringify(body), // same order_reference every attempt
            });
          } catch (networkErr) {
            if (attempt === maxAttempts) throw networkErr;
            await new Promise((r) => setTimeout(r, backoffMs(attempt)));
            continue;
          }

          if (res.status >= 500) {
            if (attempt === maxAttempts) throw new Error(`server error ${res.status} after ${attempt} attempts`);
            await new Promise((r) => setTimeout(r, backoffMs(attempt)));
            continue;
          }

          const payload = await res.json();
          if (!res.ok) {
            throw new Error(`checkout session create failed: ${payload.code ?? payload.type} (${res.status})`);
          }
          return payload; // redirect the shopper to payload.data.checkout_url
        }
        throw new Error("unreachable");
      }

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

      ```python Python theme={null}
      #!/usr/bin/env python3
      """Create a hosted-checkout session (Live: ``billing_details`` field).
      Redirect the shopper to checkout_url.

      Same order_reference within the TTL replays the existing session (201)
      instead of creating a duplicate — safe to retry with the same body.
      """
      import json
      import os
      import random
      import time
      from pathlib import Path

      import requests

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


      def backoff_seconds(attempt: int) -> float:
          """Exponential backoff with jitter: attempt 1 waits ~0.25-0.5s, doubling
          each attempt, capped at 4s -- avoids hammering the gateway in a loop."""
          base = min(0.25 * 2 ** (attempt - 1), 4.0)
          return base + random.random() * base


      def create_checkout_session(max_attempts: int = 3) -> dict:
          body = json.loads((Path(__file__).parent / "request.json").read_text())
          headers = {"X-Api-Key": os.environ.get("RADIUMONE_SECRET_KEY", "")}

          for attempt in range(1, max_attempts + 1):
              try:
                  resp = requests.post(f"{CHECKOUT_BASE}/api/v1/checkout/sessions", json=body, headers=headers, timeout=30)
              except requests.exceptions.Timeout:
                  if attempt == max_attempts:
                      raise
                  time.sleep(backoff_seconds(attempt))
                  continue

              if resp.status_code >= 500:
                  if attempt == max_attempts:
                      raise RuntimeError(f"server error {resp.status_code} after {attempt} attempts")
                  time.sleep(backoff_seconds(attempt))
                  continue

              payload = resp.json()
              if not resp.ok:
                  code = payload.get("code") or payload.get("type")
                  raise RuntimeError(f"checkout session create failed: {code} ({resp.status_code})")
              return payload  # redirect the shopper to payload["data"]["checkout_url"]

          raise RuntimeError("unreachable")


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

  <Step title="Confirm the session is back to pending, then let the shopper retry">
    For a charge-time outage, confirm the session's state before assuming it's safe to retry ([API reference](/hosted-checkout/reference/checkout-sessions/get-a-checkout-session)):

    <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 `pending` confirms no charge happened and the shopper can try paying again on the same session. Retrying on the same session reuses that session's payment key, so even an accidental double-retry can't create two charges.
  </Step>

  <Step title="Never fulfil on the outage signal itself">
    Neither the retry message nor `CHECKOUT_ERROR` is proof of anything beyond "the attempt didn't go through" — they're not decline or success signals. Confirm the eventual outcome the same way as any other attempt: webhook or authenticated `GET`.
  </Step>
</Steps>

## Related

<Columns cols={2}>
  <Card title="API errors" icon="triangle-alert" href="/hosted-checkout/errors/api-errors#configuration-and-availability">
    `gateway:unavailable` and `gateway:request_failed`, with retry rules.
  </Card>

  <Card title="Embedded checkout events" icon="webhook" href="/hosted-checkout/reference/embedded-events">
    The full `CHECKOUT_ERROR` payload shape.
  </Card>

  <Card title="Confirm payment when the redirect never arrives" icon="wifi-off" href="/hosted-checkout/handle-failures/redirect-not-received">
    A shopper-side connectivity gap, not a gateway outage.
  </Card>

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