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

# Tokenization failures - Elements SDK

> How to branch on an Elements tokenization error, show the shopper the right message, and decide whether it's safe to retry.

`elements.submit()` calls RadiumOne directly from inside the card iframe. When the tokenization ("bind") call fails, `submit()` rejects with an `ElementsError` that carries the gateway's error verbatim — your job is to branch on `retryAllowed`, not on the HTTP status.

<Info>
  TL;DR: `submit()` rejects with a gateway `ElementsError` → branch on `retryAllowed`, not the HTTP status; most codes mean start a new session.
</Info>

## When this happens

* The session or its encryption key expired or was never valid.
* The card data itself fails validation inside the iframe.
* The publishable key is missing, malformed, or belongs to a different merchant than the session.
* The session is already bound to a different card, or has hit its per-session bind attempt lockout.
* The gateway's tokenization service is temporarily unavailable (a genuine outage, distinct from a lockout).

## What you see

`submit()` rejects with an `ElementsError` whose `code` is the gateway's problem+json `type` URN, verbatim — including a URN this SDK build doesn't recognize yet, so match on the string directly.

| Signal | Value |
| - | - |
| `err.code` | A `urn:radiumone:token:*`, `urn:radiumone:checkout:*`, `urn:radiumone:auth:*`, or `urn:radiumone:gateway:*` URN |
| `err.retryAllowed` | The **only** retry signal — never infer retryability from `err.code`'s HTTP status alone |
| `err.customerMessage` | Display-safe text |
| `err.retryAfter` | Seconds, informational only |

Most tokenization errors are **not** retryable — they mean the session, key, or card data itself is unusable and need a fresh session. The one exception today is a genuine service outage, which the SDK already retries twice internally before it gives up.

## What to do

<Steps>
  <Step title="Always branch on retryAllowed">
    ```js theme={null}
    try {
      const { token } = await elements.submit({ sessionId, sessionSecret, pubkeyJws });
    } catch (err) {
      if (err.name !== "ElementsError") throw err;
      showError(err.customerMessage ?? "Payment could not be processed.");
      if (err.retryAllowed) {
        offerRetryButton();
      } else {
        // Most codes: start over with a new session rather than resubmitting.
        createNewSessionAndRestart();
      }
    }
    ```
  </Step>

  <Step title="Start a new session for non-retryable codes">
    Create a payment session ([API reference](/payments-api/reference/sessions/create-tokenization-session)) and re-render the card fields — don't call `submit()` again with the same session context.

    <CodeGroup>
      ```bash cURL theme={null}
      #!/usr/bin/env bash
      # Create a tokenization session for Elements. Pass session_id/session_secret/
      # pubkey_jws to the browser unchanged — never re-serialize pubkey_jws.
      set -euo pipefail

      API_BASE="${RADIUMONE_API_BASE:-https://api-sandbox.radiumone.io/gateway}"
      : "${RADIUMONE_ACCESS_TOKEN:?set RADIUMONE_ACCESS_TOKEN to a Bearer access token}"

      curl -sS -X POST "$API_BASE/v1/sessions" \
        -H "Content-Type: application/json" \
        -H "Authorization: Bearer $RADIUMONE_ACCESS_TOKEN" \
        -d @request.json
      ```

      ```javascript Node.js theme={null}
      #!/usr/bin/env node
      // Create a tokenization/checkout session for Elements. Node 18+ ESM fetch.
      // Env: RADIUMONE_ACCESS_TOKEN, RADIUMONE_API_BASE (optional override).
      import { readFileSync } from "node:fs";

      const API_BASE = process.env.RADIUMONE_API_BASE || "https://api-sandbox.radiumone.io/gateway";
      const accessToken = process.env.RADIUMONE_ACCESS_TOKEN;
      const body = JSON.parse(readFileSync(new URL("./request.json", import.meta.url)));

      async function createPaymentSession() {
        const res = await fetch(`${API_BASE}/v1/sessions`, {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            Authorization: `Bearer ${accessToken}`,
          },
          body: JSON.stringify(body),
        });
        const payload = await res.json();
        if (!res.ok) {
          throw new Error(`sessions create failed: ${payload.type ?? payload.code} (${res.status})`);
        }
        // Pass session_id, session_secret and pubkey_jws to the browser byte-for-byte.
        return payload;
      }

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

      ```python Python theme={null}
      #!/usr/bin/env python3
      """Create a tokenization/checkout session for Elements. Python 3.10+, requests."""
      import json
      import os
      from pathlib import Path

      import requests

      API_BASE = os.environ.get("RADIUMONE_API_BASE", "https://api-sandbox.radiumone.io/gateway")


      def create_payment_session() -> dict:
          body = json.loads((Path(__file__).parent / "request.json").read_text())
          resp = requests.post(
              f"{API_BASE}/v1/sessions",
              json=body,
              headers={"Authorization": f"Bearer {os.environ.get('RADIUMONE_ACCESS_TOKEN', '')}"},
              timeout=30,
          )
          payload = resp.json()
          if not resp.ok:
              raise RuntimeError(f"sessions create failed: {payload.get('type') or payload.get('code')} ({resp.status_code})")
          # Pass session_id, session_secret and pubkey_jws to the browser byte-for-byte.
          return payload


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

  <Step title="Offer a retry button for the outage code">
    For `urn:radiumone:checkout:tokenization-unavailable` (`retryAllowed: true`), the SDK has already retried twice internally — a further retry is a merchant-UI decision, not an automatic one.
  </Step>
</Steps>

## Prevent it

* Charge immediately after a successful `submit()` — see [Handle expired sessions and card tokens](/elements/handle-failures/expired-sessions-and-tokens) for why a stale token still fails even after a successful bind.
* Never reuse a session across shoppers or across an abandoned-then-resumed checkout — each checkout attempt gets its own session.

## Test it

See [Test your integration](/resources/test-your-integration#elements) for sandbox test cards and scenarios.

## Related

<Columns cols={2}>
  <Card title="Accept a card payment" icon="credit-card" href="/elements/accept-a-card-payment">
    Where `submit()` fits in the full charge flow.
  </Card>

  <Card title="Tokenization errors" icon="triangle-alert" href="/elements/errors/tokenization-errors">
    The full tokenization error code table.
  </Card>
</Columns>
