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

# ThreeDS object - Elements SDK

> Authenticate a tokenized card with 3D Secure from the browser and drive the challenge to a terminal result.

`ThreeDS` coordinates 3D Secure from the browser after a card is tokenized. Get one from `radiumone.threeDS()`. One flow runs at a time per instance — a concurrent call throws `three-ds:in_progress`; use a separate instance for parallel flows.

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

For the full integration walkthrough (session amount pinning, challenge presentation, results), see [3D Secure with Elements](/elements/three-d-secure/add-three-d-secure).

## `authenticate(context, options?)`

Run 3D Secure for a bound card, from start to a terminal outcome, in one call.

Collects browser data, calls `POST /gateway/v1/3ds/authenticate`, then performs whatever the gateway asks (device data collection, challenge, decoupled approval), polling `GET /gateway/v1/3ds/status` until a terminal status. Waits are capped at 5 minutes. No amount or currency is sent — the gateway takes them from the session.

Only one flow may run per instance.

Error codes: the `three-ds:*` codes below mean the call was constructed wrong. The `urn:radiumone:three-ds:*` codes are flow failures to handle at runtime; gateway error URNs are passed through verbatim. Full catalogue: `docs/reference/error-codes.json`.

<ResponseField name="ctx" type="ThreeDSAuthenticateContext" required>
  Session and card token. See `ThreeDSAuthenticateContext`.

  <Expandable title="ThreeDSAuthenticateContext properties" defaultOpen>
    <ResponseField name="cardToken" type="string" required>
      `BindResult.token` from a successful `Elements.submit`.
    </ResponseField>

    <ResponseField name="sessionId" type="string" required>
      `session_id` from `POST /gateway/v1/sessions` — the same session used for `submit()`.
    </ResponseField>

    <ResponseField name="sessionSecret" type="string" required>
      `session_secret` for the same session. Held in memory for the call only.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="opts" type="ThreeDSHandleOptions">
  Presentation, cancellation and decoupled-UI options.

  <Expandable title="ThreeDSHandleOptions properties" defaultOpen>
    <ResponseField name="challengeContainer" type="HTMLElement | string" default="a fixed full-viewport overlay created by the SDK">
      Element or CSS selector where the challenge iframe (or the decoupled waiting UI) mounts.
    </ResponseField>

    <ResponseField name="challengePresentation" type="'auto' | 'iframe' | 'redirect'" default="'auto'">
      How to present a challenge.

      * `'auto'` — iframe, unless the gateway's `prefer_redirect` hint asks for redirect. - `'iframe'` — always an embedded iframe. - `'redirect'` — always a full-page redirect; requires `returnUrl` and a challenge that is `redirectable` (or carries `prefer_redirect`), otherwise rejects with `urn:radiumone:three-ds:challenge-display-unsupported`.

      On the redirect path the page navigates away, so the pending promise never settles; the return page completes the flow with `ThreeDS.resume`.
    </ResponseField>

    <ResponseField name="onDecoupled" type="(info: { expires_at: string; poll_interval_ms: number; }) => void">
      Called once when a decoupled (out-of-band) approval starts, so you can show your own countdown or cancel UI. The SDK still runs the status polling.
    </ResponseField>

    <ResponseField name="returnUrl" type="string">
      Your 3DS return page. Setting it enables the redirect path; the page must load the SDK and call `ThreeDS.resume`.

      The SDK does not send this value to the gateway — the gateway redirects the shopper to the return URL configured for your merchant account, which must match.
    </ResponseField>

    <ResponseField name="signal" type="AbortSignal">
      Abort the flow (for example when the shopper closes your payment panel). Aborting tears down all 3DS UI and rejects with `urn:radiumone:three-ds:cancelled`. Do not authorize after a cancellation.
    </ResponseField>
  </Expandable>
</ResponseField>

**Returns** `Promise<ThreeDSResult>` — The terminal status and the 3DS `ref` for authorization.

**Throws:**

* [`three-ds:invalid_context`](/elements/errors/three-d-secure-errors#3d-secure-errors) (`validation_error`) — `ctx` is not an object.
* [`three-ds:missing_session_id`](/elements/errors/three-d-secure-errors#3d-secure-errors) (`validation_error`) — `sessionId` missing or empty.
* [`three-ds:missing_session_secret`](/elements/errors/three-d-secure-errors#3d-secure-errors) (`validation_error`) — `sessionSecret` missing or empty.
* [`three-ds:missing_card_token`](/elements/errors/three-d-secure-errors#3d-secure-errors) (`validation_error`) — `cardToken` missing or empty.
* [`three-ds:in_progress`](/elements/errors/three-d-secure-errors#3d-secure-errors) (`rate_limited`) — a flow is already running on this instance.
* [`urn:radiumone:three-ds:cancelled`](/elements/errors/three-d-secure-errors#3d-secure-errors) (`validation_error`) — retryable — `opts.signal` was aborted.
* [`urn:radiumone:three-ds:challenge-timeout`](/elements/errors/three-d-secure-errors#3d-secure-errors) (`network_error`) — retryable — no terminal status before the deadline.
* [`urn:radiumone:three-ds:provider-unavailable`](/elements/errors/three-d-secure-errors#3d-secure-errors) (`network_error`) — the gateway was unreachable or returned a malformed response.
* [`urn:radiumone:three-ds:action-not-found`](/elements/errors/three-d-secure-errors#3d-secure-errors) (`api_error`) — 404 for the action reference.
* [`urn:radiumone:three-ds:unsupported-action`](/elements/errors/three-d-secure-errors#3d-secure-errors) (`validation_error`) — unknown action type; update the SDK.
* [`urn:radiumone:three-ds:action-malformed`](/elements/errors/three-d-secure-errors#3d-secure-errors) (`validation_error`) — the gateway action is missing required fields.
* [`urn:radiumone:three-ds:render-mode-unsupported`](/elements/errors/three-d-secure-errors#3d-secure-errors) (`validation_error`) — the gateway sent a gateway-hosted challenge.
* [`urn:radiumone:three-ds:challenge-display-unsupported`](/elements/errors/three-d-secure-errors#3d-secure-errors) (`load_error`) — redirect requested but not possible.
* `urn:radiumone:three-ds:http-<status>` (`api_error`) — gateway HTTP error without a URN; retryable when status is 5xx.
* `urn:radiumone:*` (`api_error`) — gateway error, passed through verbatim with `retryAllowed`.

## `handle(action, options?)`

Drive a gateway 3DS action (obtained by your server) to a terminal outcome.

Same flow and errors as `ThreeDS.authenticate`, minus the initial authenticate call and its argument checks. Pass the gateway action object unchanged.

<ResponseField name="action" type="ThreeDSAction" required>
  The gateway action. See `ThreeDSAction`.
</ResponseField>

<ResponseField name="opts" type="ThreeDSHandleOptions">
  Presentation, cancellation and decoupled-UI options.

  <Expandable title="ThreeDSHandleOptions properties" defaultOpen>
    <ResponseField name="challengeContainer" type="HTMLElement | string" default="a fixed full-viewport overlay created by the SDK">
      Element or CSS selector where the challenge iframe (or the decoupled waiting UI) mounts.
    </ResponseField>

    <ResponseField name="challengePresentation" type="'auto' | 'iframe' | 'redirect'" default="'auto'">
      How to present a challenge.

      * `'auto'` — iframe, unless the gateway's `prefer_redirect` hint asks for redirect. - `'iframe'` — always an embedded iframe. - `'redirect'` — always a full-page redirect; requires `returnUrl` and a challenge that is `redirectable` (or carries `prefer_redirect`), otherwise rejects with `urn:radiumone:three-ds:challenge-display-unsupported`.

      On the redirect path the page navigates away, so the pending promise never settles; the return page completes the flow with `ThreeDS.resume`.
    </ResponseField>

    <ResponseField name="onDecoupled" type="(info: { expires_at: string; poll_interval_ms: number; }) => void">
      Called once when a decoupled (out-of-band) approval starts, so you can show your own countdown or cancel UI. The SDK still runs the status polling.
    </ResponseField>

    <ResponseField name="returnUrl" type="string">
      Your 3DS return page. Setting it enables the redirect path; the page must load the SDK and call `ThreeDS.resume`.

      The SDK does not send this value to the gateway — the gateway redirects the shopper to the return URL configured for your merchant account, which must match.
    </ResponseField>

    <ResponseField name="signal" type="AbortSignal">
      Abort the flow (for example when the shopper closes your payment panel). Aborting tears down all 3DS UI and rejects with `urn:radiumone:three-ds:cancelled`. Do not authorize after a cancellation.
    </ResponseField>
  </Expandable>
</ResponseField>

**Returns** `Promise<ThreeDSResult>` — The terminal status and the 3DS `ref` for authorization.

**Throws:**

* [`three-ds:invalid_action`](/elements/errors/three-d-secure-errors#3d-secure-errors) (`validation_error`) — `action` is not an object with a string `type`.
* [`three-ds:in_progress`](/elements/errors/three-d-secure-errors#3d-secure-errors) (`rate_limited`) — a flow is already running on this instance.
* `ElementsError` `urn:radiumone:three-ds:*` and gateway URNs — as for `ThreeDS.authenticate`.

## `getPendingRedirect()`

Detect whether this page load is the return from a redirect challenge.

Reads the pending reference from `sessionStorage`, then from the `action_ref` URL query or fragment parameter. Call at the top of your return page.

**Returns** `PendingRedirect | null` — The pending redirect, or `null` if this is not a 3DS return.

**`PendingRedirect` (the returned value):**

<ResponseField name="action_ref" type="string" required>
  The in-flight 3DS action reference being resumed.
</ResponseField>

## `resume()`

Finish a redirect challenge on the return page.

Needs only the publishable key: reads the pending reference (see `ThreeDS.getPendingRedirect`) and fetches the final status once. Safe to call again. No card Elements or session secret are required.

**Returns** `Promise<ThreeDSResult>` — The terminal status and the 3DS `ref`.

**Throws:**

* [`urn:radiumone:three-ds:action-malformed`](/elements/errors/three-d-secure-errors#3d-secure-errors) (`validation_error`) — no pending redirect, or the status is not terminal yet.
* `ElementsError` `urn:radiumone:three-ds:provider-unavailable`, `urn:radiumone:three-ds:action-not-found`, gateway URNs — as for `ThreeDS.authenticate`.

## `ThreeDSResult`

<ResponseField name="ref" type="string" required>
  3DS reference to pass to your server's authorization call.
</ResponseField>

<ResponseField name="status" type="ThreeDSStatus" required>
  Terminal status — never `DECOUPLED` or `PENDING`. See `ThreeDSStatus`.
</ResponseField>

**`ThreeDSStatus` values:** `AUTHENTICATED`, `ATTEMPTED`, `NOT_AUTHENTICATED`, `FAILED`, `REJECTED`, `DECOUPLED`, `EXPIRED`, `PENDING`

## `ThreeDSAction`

For integrations that obtain the first 3DS action server-side and pass it to `handle()` unchanged — most integrations never touch this shape directly.

<ResponseField name="action_ref" type="string" required>
  Gateway reference for this action; used for continue and status calls.
</ResponseField>

<ResponseField name="challenge" type="{ acs_url: string; fields: Record<string, string>; window_size?: string; notification_ref: string; redirectable?: boolean; }">
  Issuer challenge step. Present on `challenge` actions.

  <Expandable title="challenge shape">
    <ResponseField name="acs_url" type="string" required />

    <ResponseField name="fields" type="Record<string, string>" required />

    <ResponseField name="window_size" type="string" />

    <ResponseField name="notification_ref" type="string" required />

    <ResponseField name="redirectable" type="boolean" />
  </Expandable>
</ResponseField>

<ResponseField name="decoupled" type="{ expires_at: string; poll_interval_ms?: number; }">
  Out-of-band approval step. Present on `decoupled` actions.

  <Expandable title="decoupled shape">
    <ResponseField name="expires_at" type="string" required />

    <ResponseField name="poll_interval_ms" type="number" />
  </Expandable>
</ResponseField>

<ResponseField name="hosted" type="{ url: string; }">
  Hosted challenge URL for `gateway_hosted` render mode. Not supported yet.

  <Expandable title="hosted shape">
    <ResponseField name="url" type="string" required />
  </Expandable>
</ResponseField>

<ResponseField name="method" type="{ url: string; fields: Record<string, string>; timeout_ms?: number; }">
  Device data collection step. Present on `method` actions.

  <Expandable title="method shape">
    <ResponseField name="url" type="string" required />

    <ResponseField name="fields" type="Record<string, string>" required />

    <ResponseField name="timeout_ms" type="number" />
  </Expandable>
</ResponseField>

<ResponseField name="poll_interval_ms" type="number" default="5000 for challenge, 3000 for decoupled">
  Status poll interval in milliseconds for `challenge` and `decoupled` actions. Values below 1000 are raised to 1000.
</ResponseField>

<ResponseField name="prefer_redirect" type="boolean">
  Gateway hint that the browser handles a full-page redirect better than an iframe (for example an in-app webview). Honoured when `challengePresentation` is `'auto'`.
</ResponseField>

<ResponseField name="render_mode" type="ThreeDSRenderMode">
  Challenge rendering mode. Present on `challenge` actions.
</ResponseField>

<ResponseField name="status" type="ThreeDSStatus">
  Outcome. Required on terminal action types; a terminal action without a valid status rejects with `urn:radiumone:three-ds:action-malformed`.
</ResponseField>

<ResponseField name="type" type="ThreeDSActionType" required>
  What the SDK must do next. See `ThreeDSActionType`.
</ResponseField>

## `ThreeDSBrowserData`

Collected automatically by `authenticate()` — exported for reference, not something you construct.

<ResponseField name="color_depth" type="number" required>
  `screen.colorDepth`.
</ResponseField>

<ResponseField name="java_enabled" type="boolean" required>
  `navigator.javaEnabled()`; `false` when unavailable.
</ResponseField>

<ResponseField name="javascript_enabled" type="boolean" required>
  Always `true`.
</ResponseField>

<ResponseField name="language" type="string" required>
  `navigator.language` (BCP 47 tag).
</ResponseField>

<ResponseField name="screen_height" type="number" required>
  `screen.height` in CSS pixels.
</ResponseField>

<ResponseField name="screen_width" type="number" required>
  `screen.width` in CSS pixels.
</ResponseField>

<ResponseField name="tz_offset" type="number" required>
  UTC offset in minutes, from `Date#getTimezoneOffset()` — positive when behind UTC, so UTC+8 is `-480`.
</ResponseField>

## Example

```js theme={null}
const threeDS = radiumone.threeDS();
const controller = new AbortController();

const { status, ref } = await threeDS.authenticate(
  { sessionId, sessionSecret, cardToken: token },
  {
    signal: controller.signal,
    challengeContainer: "#three-ds",
    returnUrl: "https://shop.example.com/checkout/3ds-return",
  },
);

if (status === "AUTHENTICATED" || status === "ATTEMPTED") {
  await chargeWithThreeDsRef(token, ref);
} else {
  showDeclined(status); // resolved decline — do not charge
}
```

```js theme={null}
// Return page (redirect fallback)
const pending = RadiumOne.init("r1pk_test_YOUR_KEY").threeDS().getPendingRedirect();
if (pending) {
  const { status, ref } = await radiumone.threeDS().resume();
  // same branch as above
}
```

## Gateway calls

The SDK makes these gateway HTTP calls on your behalf — `bind` runs from `elements.submit()`, the rest from `authenticate()`/`handle()`/`resume()`. You don't call these directly; this is for troubleshooting with network traces or support.

| Call | Method + path | Called from | SDK API(s) | Notes |
| - | - | - | - | - |
| `bind` | `POST /gateway/v1/sessions/{session_id}/bind` | `card iframe (origin {cdnOrigin})` | `Elements.submit` | 15s timeout. Up to 2 retries (1 s, 2 s backoff) when retry\_allowed is true or no HTTP response was received. |
| `3ds-authenticate` | `POST /gateway/v1/3ds/authenticate` | `merchant page` | `ThreeDS.authenticate` | Body: `{ session_id, card, browser } — no amount or currency`. |
| `3ds-continue` | `POST /gateway/v1/3ds/continue` | `merchant page` | `ThreeDS.authenticate`, `ThreeDS.handle` | Body: `{ action_ref }`. |
| `3ds-status` | `GET /gateway/v1/3ds/status/{action_ref}` | `merchant page` | `ThreeDS.authenticate`, `ThreeDS.handle`, `ThreeDS.resume` | Every poll\_interval\_ms (default 5000 challenge, 3000 decoupled, minimum 1000), up to 5 minutes. |

Base URL is the API origin for the key's environment (production `api.radiumone.io`, sandbox `api-sandbox.radiumone.io`) — see [Content Security Policy](/elements/content-security-policy#origins-per-environment).

## Errors

3DS-specific error codes (`three-ds:*`) and gateway URNs are documented on [3D Secure errors](/elements/errors/three-d-secure-errors). Status-outcome tables (what each `status` means and whether to authorize) live on [Authentication results](/elements/three-d-secure/authentication-results). For recovery steps, see [Handle 3D Secure failures in Elements](/elements/handle-failures/three-ds-failures).
