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

# Customize checkout - Hosted checkout

> Line items, shopper details, branding, locale, currency, and metadata for a hosted-checkout session.

Beyond the required `amount`, `currency`, `order_reference`, `success_url`, and `cancel_url` on [create a checkout session](/hosted-checkout/reference/checkout-sessions/create-a-checkout-session), a checkout session accepts several optional fields to shape what the shopper sees and what you get back.

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

## Line items and adjustments

Add an itemized breakdown with `line_items[]` (`name`, `quantity`, `unit_amount`) and optional `adjustments[]` (`kind`: `tax`, `shipping`, `discount`, or `fee`; `label`; signed `amount`). `adjustments` requires `line_items` — an adjustments-only payload is rejected, since there's no base total for the deltas to apply to.

The math is enforced: the sum of `line_items` amounts plus the sum of `adjustments` amounts must equal the session's top-level `amount`, or the request is rejected with [`400 validation:line_items_mismatch`](/hosted-checkout/errors/api-errors#checkout-validation-line-items-mismatch). Discount adjustments must be zero or negative; tax, shipping, and fee adjustments must be zero or positive.

Each `line_items` entry must be a JSON object with a string `name` — a `null` or non-object entry, or a non-string `name`, returns [`400 validation:invalid_input`](/hosted-checkout/errors/api-errors#checkout-validation-invalid-input) instead of a generic server error.

```json theme={null}
{
  "amount": 10000,
  "currency": "SGD",
  "order_reference": "ORD-1001",
  "success_url": "https://shop.example.com/pay/success?cid={CHECKOUT_ID}",
  "cancel_url": "https://shop.example.com/pay/cancel?order=ORD-1001",
  "line_items": [{ "name": "T-shirt", "quantity": 2, "unit_amount": 4500 }],
  "adjustments": [{ "kind": "shipping", "label": "Standard shipping", "amount": 1000 }]
}
```

## Shopper and billing details

Send what you know about the shopper's billing details with `billing_details` — cardholder name, email, phone, and billing address. It's shown to the shopper on the pay page and used for 3-D Secure once the hosted-checkout challenge ships (see [3DS with hosted checkout](/hosted-checkout/three-d-secure)):

<CodeGroup>
  ```bash cURL theme={null}
  #!/usr/bin/env bash
  # `billing_details`, with a billing address — `name` is the CARDHOLDER name
  # (must match the card); send it plus email or phone to support 3DS payer
  # authentication once hosted-checkout 3DS ships.
  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
  // `billing_details`, with a billing address. Node 18+ ESM fetch.
  // Env: RADIUMONE_SECRET_KEY, RADIUMONE_CHECKOUT_BASE (optional).
  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)));

  async function createCheckoutSessionWithBillingDetails() {
    const res = await fetch(`${CHECKOUT_BASE}/api/v1/checkout/sessions`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-Api-Key": secretKey,
      },
      body: JSON.stringify(body),
    });
    const payload = await res.json();
    if (!res.ok) {
      throw new Error(`checkout session create failed: ${payload.code ?? payload.type} (${res.status})`);
    }
    return payload;
  }

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

  ```python Python theme={null}
  #!/usr/bin/env python3
  """``billing_details``, with a billing address."""
  import json
  import os
  from pathlib import Path

  import requests

  CHECKOUT_BASE = os.environ.get("RADIUMONE_CHECKOUT_BASE", "https://checkout-sandbox.radiumone.io")


  def create_checkout_session_with_billing_details() -> dict:
      body = json.loads((Path(__file__).parent / "request.json").read_text())
      resp = requests.post(
          f"{CHECKOUT_BASE}/api/v1/checkout/sessions",
          json=body,
          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 create failed: {code} ({resp.status_code})")
      return payload


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

| Field | Notes |
| - | - |
| `name` | The **cardholder** name (must match the card) — not a shipping or account name. Missing it doesn't fail the request, but can reduce 3DS frictionless approval once the challenge ships. |
| `email` / `phone` | Send at least one; both is better, for the same reason as `name`. |
| `address.line1` / `line2` / `city` / `postal_code` / `country` | Optional; `country` is ISO 3166-1 alpha-2. |
| `address.state` | US/CA only — omit it everywhere else. Sending it for other markets can cause a 3DS downgrade. |

Every field is optional and none of them ever fail the request on their own — an invalid or over-length sub-field is dropped rather than rejected, so omit a field entirely rather than sending an empty string.

<Warning>
  **Breaking change: `customer` is no longer accepted.** Sending it — any value, including `null` — returns [`400 validation:invalid_input`](/hosted-checkout/errors/api-errors#checkout-validation-invalid-input). Send `billing_details` instead.
</Warning>

If you're embedding checkout in an iframe and it isn't rendering after you've set these fields, the cause is almost always domain registration or CSP, not these fields — see [Fix embedded checkout that won't load](/hosted-checkout/handle-failures/embedded-checkout-not-loading).

## Branding profile

Reference a branding profile configured for your account with `branding_profile_id` (optional, at most 64 characters). If you omit it, or the ID doesn't match a configured profile, your account's default branding applies — the request never fails because of a bad `branding_profile_id`. You can also override individual settings for just this checkout with a `branding` object. See [Brand the payment page](/hosted-checkout/branding) for how profiles are set up, how a checkout resolves which branding to use, and the full settings and override reference.

## Locale

Set `locale` to one of the accepted values. Only some of the accepted locales currently render translated copy — everything else falls back to English:

| Accepted | Rendered |
| - | - |
| `en`, `zh`, `ja`, `ko`, `th`, `id`, `ms`, `vi`, `fil` | `en`, `zh` |

An unrecognized `locale` value is rejected outright — pick one from the accepted list even if it isn't yet rendered.

## Currency and amount

`currency` is a 3-letter ISO 4217 code, one of: `SGD`, `USD`, `EUR`, `GBP`, `JPY`, `AUD`, `HKD`, `CNY`, `MYR`, `THB`, `IDR`, `PHP`, `VND`, `KRW`, `INR`, `TWD`, `CAD`, `NZD` — and must also be enabled for your merchant account. It's case-insensitive on input (upper-cased before storage).

`amount` must be an integer in minor units and at least `50` (for example, the minimum for `SGD` is `50` = SGD 0.50) — smaller amounts are rejected. There's no enforced maximum on the Checkout API itself; an excessively large amount is instead rejected by the payment gateway with `422 gateway:request_rejected`.

## Metadata

Attach your own key-value data with `metadata`. Metadata must be a JSON object. Serialized as compact JSON, it can be at most 4096 UTF-16 code units, limit included. Most characters count as 1 unit, including Chinese and Thai; emoji count as 2. A larger object is rejected with [`400 validation:invalid_input`](/hosted-checkout/errors/api-errors#checkout-validation-invalid-input) — the error's `detail` text may still say "under 4096 bytes", but the limit is measured in UTF-16 code units as described here. Use it for your own order/customer references; it's echoed back on the session but never interpreted by RadiumOne.

## `state` and `outlet_id`

* `state`: an opaque string (1–512 characters) you choose. It's echoed back unchanged on your `success_url`/`cancel_url` redirects (when present) so you can carry your own request-scoped nonce through the flow. It is **not** part of the redirect signature — verify it independently if you rely on it.
* `outlet_id`: only needed for multi-outlet merchants. Omit it to use your key's bound outlet (or your account default). It must be a canonical UUID — a malformed value returns `400 validation:invalid_input`; a well-formed value that isn't your outlet, or isn't bound to your key, returns `422`.

## Next steps

<Columns cols={2}>
  <Card title="Redirect to hosted checkout" icon="arrow-right" href="/hosted-checkout/redirect-integration">
    Put these fields to use in a full integration.
  </Card>

  <Card title="3DS with hosted checkout" icon="shield-check" href="/hosted-checkout/three-d-secure">
    How `billing_details` feeds 3D Secure.
  </Card>

  <Card title="Fix embedded checkout that won't load" icon="triangle-alert" href="/hosted-checkout/handle-failures/embedded-checkout-not-loading">
    Domain registration and CSP for embedded mode.
  </Card>
</Columns>
