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

# Quickstart - Get started

> Create a hosted-checkout session, redirect a shopper, and confirm the payment server-side, end to end in sandbox.

The fastest way to see a payment go through RadiumOne end to end: create a hosted-checkout session on your server, redirect the shopper to it, and confirm the result server-side once they return.

## How it works

1. Your server creates a checkout session with the amount and your `success_url`/`cancel_url`.
2. Your website redirects the shopper to the returned `checkout_url`.
3. The shopper pays on the RadiumOne-hosted page.
4. RadiumOne redirects the shopper back to your `success_url` (or `cancel_url`).
5. Your server confirms the final status with an authenticated request before fulfilling the order.

## Before you begin

<Info>
  You need a sandbox secret key (`r1sk_test_…`). See [Sandbox and API keys](/get-started/sandbox-and-api-keys) if you don't have one yet.
</Info>

<Info>
  Amounts are always integers in the currency's minor unit. For example, `5000` for `SGD` means SGD 50.00.
</Info>

<Card title="See a working example" icon="https://mintcdn.com/em0fk61bt0xne2vlr6nxf3fcbpolerxo3jp1selutawoc6zh/WXb70u-OVIjYC_Ax/images/icons/github-mark.svg?fit=max&auto=format&n=WXb70u-OVIjYC_Ax&q=85&s=d9ebef687aeb48a9199f87bc4ce3b299" horizontal href="https://github.com/cubepay/radiumone-checkout-demo" width="16" height="16" data-path="images/icons/github-mark.svg">
  Clone an example cart that creates a session and redirects to hosted checkout, if you'd rather start from running code.
</Card>

## Steps

<Steps>
  <Step title="Create a checkout session">
    From your server, create a checkout session ([API reference](/hosted-checkout/reference/checkout-sessions/create-a-checkout-session)) for the order. Use one `order_reference` per order **attempt** — replaying the same reference within the session's TTL safely returns the existing session instead of creating a duplicate.

    <Warning>
      Replaying `order_reference` matches it as-is, with no comparison of the rest of the body. If you send a **different amount** on the retry, RadiumOne silently keeps the original session's amount instead of rejecting the request. See [Prevent duplicate payments](/get-started/api-basics/prevent-duplicate-payments) for the full guide.
    </Warning>

    <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="Redirect the shopper">
    Redirect the shopper's browser to `data.checkout_url` from the response. The shopper enters their card on the RadiumOne-hosted page — your site never sees it.
  </Step>

  <Step title="Handle the return page">
    RadiumOne redirects the shopper back to your `success_url` (or `cancel_url` if they cancel or the session expires). This return page may carry query parameters describing the outcome, but treat them as a hint only — don't fulfill the order from them directly. If the shopper never returns to this page at all, see [Confirm payment when the redirect never arrives](/hosted-checkout/handle-failures/redirect-not-received).
  </Step>

  <Step title="Confirm the result server-side">
    From your server, retrieve the checkout session by ID ([API reference](/hosted-checkout/reference/checkout-sessions/get-a-checkout-session)) and check its `status`, `order_reference`, and `amount` before you fulfill the order.

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

    <Warning>
      Treat any client-side redirect or callback as a hint only. Always confirm the final payment status from your server, using an authenticated `GET` request or a webhook — never from a query parameter or browser postMessage alone.
    </Warning>
  </Step>
</Steps>

## Handle the result

A checkout session's `status` is one of `pending`, `processing`, `completed`, `failed`, `expired`, or `cancelled`. Only fulfill the order once `status` is `completed` and the `order_reference` and `amount` match what you created — see [Verify the payment result](/hosted-checkout/verify-payment-result) for the full signature and confirmation guide.

## Test your integration

Use the sandbox test cards and scenarios in [Test your integration](/resources/test-your-integration) to try approvals, declines, and timeouts before you move on.

## Go-live notes

* Swap your sandbox keys and hosts for production ones — see [Moving to production](/get-started/sandbox-and-api-keys#moving-to-production).
* Always confirm payment status from your server, never from the return-page query string alone.
* Register your production webhook endpoint so you have a second, asynchronous confirmation path.
* Work through the full [go-live checklist](/resources/go-live-checklist) before accepting real payments.

## Next steps

<Columns cols={2}>
  <Card title="Choose your integration" icon="git-compare" href="/get-started/choose-your-integration">
    Compare hosted checkout with Elements + the Payments API.
  </Card>

  <Card title="Hosted checkout" icon="panel-top" href="/hosted-checkout/overview">
    Customize the checkout page, add 3DS, and go beyond the quickstart.
  </Card>

  <Card title="Webhooks" icon="webhook" href="/payments-api/webhooks/overview">
    Get notified about payment events instead of polling.
  </Card>

  <Card title="Manage payments" icon="repeat" href="/payments-api/payment-lifecycle">
    Capture, void, refund, and track transactions.
  </Card>
</Columns>
