#!/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
#!/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))
Create a session - Hosted checkout
Create a hosted checkout session and get the URL to redirect the shopper to or embed. Safe to retry with the same order reference.
#!/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
#!/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))
Authorizations
Your secret key (r1sk_...). A publishable key is rejected with 400 urn:radiumone:checkout:wrong-key-type.
Body
Amount in the currency's minor units. Minimum 50. No enforced maximum — the payment gateway rejects an excessively large amount with 422 gateway:request_rejected.
x >= 50ISO 4217 currency code, one of the 18 listed here. Case-insensitive on input (upper-cased before storage/comparison); also must be enabled for your merchant account.
SGD, USD, EUR, GBP, JPY, AUD, HKD, CNY, MYR, THB, IDR, PHP, VND, KRW, INR, TWD, CAD, NZD 3Your idempotency key for this session, trimmed of leading/trailing whitespace before the 1–128 length check applies.
1 - 128Redirect target on success. Must be an absolute https:// URL, or plain http://localhost for local development — any other scheme, or a URL that doesn't parse, is rejected with 400 validation:invalid_input. The host is also checked against allowed_domains (exact match or a subdomain of a configured domain); any host is accepted when you haven't configured an allow-list. Use {CHECKOUT_ID} as a literal placeholder if you want the session ID back in the URL — only the first occurrence is substituted. Up to 2048 characters.
2048Redirect target on cancel/decline. Same URL and host-check rules as success_url. {CHECKOUT_ID} is not substituted in this URL. Up to 2048 characters.
2048256How the shopper pays. redirect (default): send the shopper to checkout_url. embed: load checkout_url in an iframe — see Embed hosted checkout.
redirect, embed Session TTL in minutes (5–60). Omit to use your environment's default — 10 minutes in production, 25 minutes in sandbox; always set this explicitly rather than relying on the default, since it also bounds the order_reference idempotency window.
5 <= x <= 60Preferred shopper locale, one of the 9 accepted codes (case-sensitive, lowercase). Only en and zh currently render a fully localized page; other accepted codes fall back to English unless the shopper's browser language is en/zh. An unrecognized value is rejected; a non-string value defaults to en.
en, zh, ja, ko, th, id, ms, vi, fil Your own key/value data, as a JSON object. Serialized as compact JSON, it can be at most 4096 UTF-16 code units, limit included — most characters (including Chinese and Thai) count as 1 unit, emoji as 2. A larger object is rejected with 400 urn:radiumone:checkout:validation-invalid-input.
Id of a branding profile set up for your merchant, to style this payment page. Letters, digits, _, and -, up to 64 characters (profile IDs are UUIDs, and differ between sandbox and production). An ID that doesn't exist, was deleted, or belongs to another merchant falls back to your default profile; with no default profile, the standard RadiumOne look applies. See Branding.
64^[A-Za-z0-9_-]+$Per-session branding override. Each field you send replaces that field of the resolved branding profile (branding_profile_id, or your default profile) for this checkout only. Fields you omit keep the profile value, and an empty object is the same as omitting branding. Same validation rules as a saved profile. An unknown field or an invalid value returns 400 validation:invalid_input naming the field. See Branding.
Show child attributes
Show child attributes
Opaque value round-tripped on the redirect back to you. Not included in the redirect signature — verify it separately from sig.
512Cardholder name, email, phone, and billing address — shown to the shopper on the pay page and used for 3-D Secure. Every field is optional; an invalid sub-field is dropped rather than rejected, so omit a field entirely rather than sending an empty string. See Customize checkout.
Show child attributes
Show child attributes
Removed. Sending this field, with any value, including null, returns 400 validation:invalid_input. Use billing_details instead.
Outlet to charge under — only for multi-outlet merchants. Omit to use your key's bound outlet, or your account default. 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.
Itemized breakdown shown to the shopper. When sent, Σ(quantity × unit_amount) + Σ(adjustments[].amount) must equal amount, or the request is rejected.
Show child attributes
Show child attributes
Tax, shipping, discount, and fee rows applied on top of the line-item subtotal. Requires a non-empty line_items — an adjustments-only payload is rejected.
Show child attributes
Show child attributes