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

# Authentication - Get started

> How the Payments API (token exchange) and Checkout API (X-Api-Key) authenticate, key types, scopes, and least-privilege key practices.

<Danger>
  Never use a secret key (`r1sk_…`) in browser code, mobile apps, or anywhere a shopper can inspect it. Secret keys belong on your server only.
</Danger>

RadiumOne's two APIs authenticate differently — the Payments API exchanges
your key for a short-lived access token, while the Checkout API takes your
secret key directly on every request:

| | Payments API | Checkout API (Hosted checkout) |
| - | - | - |
| Header | `Authorization: Bearer <access_token>` | `X-Api-Key: <secret key>` |
| Sent directly? | No — exchange your key for a token first | Yes — every request |
| Key type accepted | Secret or publishable (differently-scoped tokens) | Secret key only — a publishable key is rejected |
| Details below | [Payments API authentication](#payments-api-authentication) | [Checkout API authentication](#checkout-api-authentication) |

## Key types

| Key | Prefix | Where it lives | Can do |
| - | - | - | - |
| Publishable key | `r1pk_prod_…` / `r1pk_test_…` | Browser-safe | Create a tokenization session, discover payment methods |
| Secret key | `r1sk_prod_…` / `r1sk_test_…` | Server only | Everything a publishable key can, plus create/manage transactions and settlement, and authenticate Checkout API requests |

| Environment | Purpose | Keys |
| - | - | - |
| **Sandbox** | Build and test your integration. No real money moves. | `r1pk_test_…` / `r1sk_test_…` |
| **Production** | Accept real payments from shoppers. | `r1pk_prod_…` / `r1sk_prod_…` |

Sandbox and production use separate credentials, hosts and webhook endpoints — see [Sandbox and API keys](/get-started/sandbox-and-api-keys).

See [Sandbox and API keys](/get-started/sandbox-and-api-keys) for how to get
keys for your account.

## Payments API authentication

Every request to the Payments API (except the token exchange itself) uses a
`Bearer` access token, not the API key directly:

<Steps>
  <Step title="Exchange your API key">
    [`POST /v1/auth/token`](/payments-api/reference/authentication/exchange-api-key-for-jwt)
    with your API key. A secret-key exchange also
    returns a refresh token; a publishable-key exchange does not — publishable
    keys never receive a refresh token, so a stolen browser-side key can't be
    used to mint long-lived credentials. If the exchange fails, see [Fix
    rejected or expired access tokens](/payments-api/handle-failures/authentication-failures).
  </Step>

  <Step title="Use the access token">
    Send it as `Authorization: Bearer <access_token>` on every subsequent
    request. It's an opaque JWE string — treat it as an opaque bearer
    credential, never decode or inspect it.
  </Step>

  <Step title="Refresh before it expires">
    Access tokens are short-lived. Before expiry, call
    [`POST /v1/auth/token/refresh`](/payments-api/reference/authentication/rotate-refresh-token)
    with your refresh token to get a new pair.
    Refresh tokens are single-use — each refresh rotates to a new one.
  </Step>

  <Step title="Revoke when you're done">
    [`POST /v1/auth/token/revoke`](/payments-api/reference/authentication/revoke-refresh-token)
    invalidates a refresh token immediately.
    Idempotent — safe to call even if it's already revoked or unknown.
  </Step>
</Steps>

## Scopes and least-privilege keys

Scopes apply to Payments API access tokens only — the Checkout API checks
only whether the key is a secret key, not a per-operation scope.

Access tokens carry scopes inherited from the API key that created them. A
**secret key's default scopes include every operation** — transaction
create/capture/void/refund and **standalone (unreferenced) refund** —
unless you request a narrower key. Because standalone refunds move money
with very little to check them against, request a key scoped to only
the operations your integration actually needs, and keep a separate,
more narrowly-scoped key for anything high-risk. See
[Security and PCI scope](/resources/security-and-pci#key-safety) for the
full key-safety checklist (storage, rotation, emergency revocation,
monitoring).

A publishable key's default scopes are limited to creating and binding a
tokenization session — it cannot create a transaction.

## Outlet-bound keys

A key can be bound to a specific outlet. Omitting `outlet_id` on a request
uses the key's bound outlet (or your default outlet); requesting a
different outlet than the key is bound to is rejected with
`403 urn:radiumone:auth:outlet-binding-violation` on the Payments API, or
`422` on the Checkout API — see [Checkout API
errors](/hosted-checkout/errors/api-errors#auth-outlet-binding-violation).
This applies to both APIs.

## Checkout API authentication

Checkout API requests (create, retrieve, and cancel a hosted-checkout
session) don't use the token exchange above. Instead, send your **secret**
key directly on every request as an `X-Api-Key` header — the same secret key
you use for the Payments API, from [Sandbox and API
keys](/get-started/sandbox-and-api-keys).

<Steps>
  <Step title="Send your secret key on every request">
    No exchange step — the secret key itself authenticates each call. A
    **publishable** key is rejected: the Checkout API's create/cancel
    operations require a secret key.

    <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="Keep it server-side">
    Same rule as above — the Checkout API's `X-Api-Key` is your secret key,
    so create, retrieve, and cancel calls must all originate from your
    server, never the shopper's browser. See [Verify the payment
    result](/hosted-checkout/verify-payment-result) for how your server
    confirms an outcome with an authenticated `GET`.
  </Step>
</Steps>

### Auth failures

A missing, malformed, unrecognized, or wrong-type `X-Api-Key` is rejected
before your request body is even validated. See [Checkout API errors —
Authentication and
authorization](/hosted-checkout/errors/api-errors#authentication-and-authorization)
for the full catalog — missing/malformed/unrecognized key, a publishable key
used where a secret key is required, the domain allow-list, and the
outlet-binding case — each with its exact code, HTTP status, and what to do.
