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

# 3DS challenges and redirects - Elements SDK

> Customize where a 3D Secure challenge renders, handle the redirect return page, and support decoupled authentication and cancellation.

Most 3DS authentications resolve frictionlessly with no shopper interaction.
When the issuer requires a challenge, `threeDS.authenticate()` presents it in
an iframe or modal by default. Some issuers instead require a top-level
redirect to their own page — this guide covers both.

## Container vs. modal

By default, a challenge renders in a centered modal overlay
(`role="dialog"`, non-dismissable). To render it inline instead, pass
`challengeContainer`:

```js theme={null}
await threeDS.authenticate(
  { sessionId, sessionSecret, cardToken: token },
  { challengeContainer: "#three-ds-challenge" }
);
```

`challengeContainer` accepts an element or a CSS selector. Use it when you want
the challenge to appear inside your own checkout layout instead of a floating
overlay.

## Choosing how the challenge presents

`challengePresentation` controls whether a challenge uses an iframe or a
full-page redirect:

| Value | Behavior |
| - | - |
| `auto` (default) | Redirects only if the issuer's response sets `prefer_redirect` **and** you passed `returnUrl`. Otherwise renders in an iframe/modal. |
| `iframe` | Always renders in an iframe/modal. |
| `redirect` | Always redirects. Throws `three-ds:challenge-display-unsupported` if a redirect isn't possible for this authentication — see [Handle 3D Secure failures in Elements](/elements/handle-failures/three-ds-failures). |

Use `redirect` (or rely on `auto` with `returnUrl` set) inside in-app webviews,
where a same-origin iframe challenge may not render reliably.

<Note>
  `returnUrl` only switches which local path the browser's top-level redirect
  comes back to — it isn't sent to or validated by the gateway. The gateway
  always resolves the challenge against the return URL registered on your
  account (see the enablement note below), so pointing `returnUrl` at a
  different path doesn't change where the issuer is told to redirect.
</Note>

## How it works: the redirect path

1. The browser leaves your page in a top-level form submission to the
   issuer's authentication page.
2. The shopper completes the challenge there.
3. The issuer redirects back to your `returnUrl`, with `#action_ref=` in the
   URL fragment (some flows use `?action_ref=`; your return page should
   handle both).
4. Your return page resumes the pending 3DS flow and reads the final status.
5. Your server charges using the resulting reference, same as the non-redirect
   path.

### The return page

```js theme={null}
// On your returnUrl page, on load:
const threeDS = radiumone.threeDS();
const pending = threeDS.getPendingRedirect();

if (pending) {
  const { status, ref } = await threeDS.resume();
  // same status handling as threeDS.authenticate() — see
  // /three-d-secure/elements-sdk and /three-d-secure/authentication-results
}
```

<Danger>
  Serve the redirect return page with `Referrer-Policy: no-referrer`, and
  don't load third-party scripts on it. The `action_ref` in the URL is a
  capability — anything that can read the URL or a `Referer` header sent from
  this page can look up the 3DS status.
</Danger>

<Info>
  Registering `returnUrl` with your account requires enablement — [contact
  support](/resources/support#request-enablement) to allow-list it.
</Info>

Redirect and return routes need a slightly loosened Content Security Policy
(`form-action` and `frame-src` allowing `https:`) scoped to just those routes
— see [Content Security Policy](/elements/content-security-policy).

<Warning>
  3DS calls run on your own checkout page, not a RadiumOne-hosted one — your
  CSP's `connect-src` must include your API origin. Without it, the browser
  silently blocks the status calls and a challenge times out after 5 minutes
  instead of failing fast. See [Content Security Policy:
  troubleshooting](/elements/content-security-policy#troubleshooting).
</Warning>

## Decoupled authentication

Some issuers authenticate out-of-band — for example, the shopper approves the
payment in their banking app instead of typing a code. Pass `onDecoupled` to
show your own waiting UI:

```js theme={null}
await threeDS.authenticate(
  { sessionId, sessionSecret, cardToken: token },
  {
    onDecoupled: ({ expiresAt, pollIntervalMs }) => {
      showWaitingPanel("Approve this payment in your banking app");
    },
  }
);
```

The SDK polls the authentication status on your behalf until it reaches a
final state or expires — you don't need to poll yourself.

## Cancellation

Pass an `AbortSignal` to let the shopper cancel a challenge in progress (for
example with a "Cancel" button):

```js theme={null}
const controller = new AbortController();
cancelButton.onclick = () => controller.abort();

await threeDS.authenticate(
  { sessionId, sessionSecret, cardToken: token },
  { signal: controller.signal }
);
```

A cancelled authentication rejects with `urn:radiumone:three-ds:cancelled`.
Return the shopper to the payment form — don't retry automatically. See
[Handle 3D Secure failures in Elements](/elements/handle-failures/three-ds-failures)
for this and other rejected-flow cases.

## In-app webviews

In-app browsers (for example inside a mobile app's webview) don't always
support cross-origin iframes reliably. Set `challengePresentation: "redirect"`
and pass a `returnUrl` that opens back inside your webview.

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

## Server policy

<Warning>
  Whichever presentation you use, your server must still independently
  validate the `three_ds.ref` at charge time — never trust a browser-reported
  status alone. See [Server policy](/get-started/three-d-secure#server-policy).
</Warning>

## Next steps

<Columns cols={2}>
  <Card title="Authentication results" icon="triangle-alert" href="/elements/three-d-secure/authentication-results">
    Status meanings and 3DS error remedies.
  </Card>

  <Card title="Content Security Policy" icon="lock-keyhole" href="/elements/content-security-policy">
    CSP directives for card fields, 3DS iframes and redirects.
  </Card>
</Columns>
