@cubepay/radiumone-js
v1.0.0-beta.8
Published
RadiumOne Elements — PCI-compliant card input fields (core SDK)
Readme
@cubepay/radiumone-js
PCI-compliant card input fields for your checkout page. RadiumOne Elements renders secure, iframe-isolated card inputs that encrypt card data client-side — your server never sees raw card numbers. Merchants qualify for PCI DSS SAQ A (the simplest compliance level).
How It Works
Your Page RadiumOne (js.radiumone.io)
+------------------------+ +---------------------------+
| Checkout form | | Sandboxed iframe |
| +------------------+ | | +---------------------+ |
| | Card Element |--+- mount -+->| Card input fields | |
| +------------------+ | | +---------------------+ |
| | | |
| elements.submit() ----+---------+-> Encrypt -> Bind API |
| | | (card data stays here) |
| <-- { token, brand } -+---------+-- Return token |
+------------------------+ +---------------------------+
|
v
Send token to your
server for paymentCard data is encrypted as JWE (RSA-OAEP-256 + A256GCM) inside the iframe and sent directly to the tokenization API. Your page only receives a one-time token.
Installation
CDN (recommended for vanilla JS)
<script src="https://js.radiumone.io/elements/v1/radiumone.min.js"></script>npm (recommended for bundlers/frameworks)
npm install @cubepay/radiumone-js
# or
pnpm add @cubepay/radiumone-js
# or
yarn add @cubepay/radiumone-jsFor React, see @cubepay/react-radiumone-js instead.
Quick Start
1. Get your publishable key
Sign up at radiumone.io to get your API keys:
- Publishable key (
r1pk_prod_*orr1pk_test_*) — used client-side in the browser - Secret key (
r1sk_prod_*orr1sk_test_*) — used server-side only, never expose in frontend code
2. Create a session (server-side)
Before collecting card details, create a session from your server:
curl -X POST https://api.radiumone.io/v1/sessions \
-H "Authorization: Bearer r1sk_prod_your_secret_key" \
-H "Content-Type: application/json" \
-d '{"amount": 2500, "currency": "usd"}'Response:
{
"id": "sess_abc123",
"secret": "sess_secret_xyz789"
}Pass id and secret to your frontend.
3. Collect card details (client-side)
CDN usage:
<div id="card-element"></div>
<button id="pay-btn" disabled>Pay $25.00</button>
<script src="https://js.radiumone.io/elements/v1/radiumone.min.js"></script>
<script>
const radiumone = RadiumOneSDK.RadiumOne.init('r1pk_prod_your_key');
const elements = radiumone.elements();
const card = elements.create('card');
card.mount('#card-element');
card.on('change', (event) => {
document.getElementById('pay-btn').disabled = !event.complete;
});
document.getElementById('pay-btn').addEventListener('click', async () => {
const result = await elements.submit(sessionId, sessionSecret);
// Send result.token to your server to complete the payment
await fetch('/api/charge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: result.token }),
});
});
</script>npm usage:
import { loadRadiumOne } from '@cubepay/radiumone-js';
const radiumone = await loadRadiumOne('r1pk_prod_your_key');
if (!radiumone) throw new Error('Failed to load SDK');
const elements = radiumone.elements();
const card = elements.create('card');
card.mount('#card-element');
card.on('change', (event) => {
submitButton.disabled = !event.complete;
if (event.error) showError(event.error.message);
});
// When the user clicks "Pay"
const result = await elements.submit(sessionId, sessionSecret);
console.log(result.token); // "tok_abc123"
console.log(result.lastFour); // "1111"
console.log(result.cardBrand); // "visa"API Reference
RadiumOne.init(publishableKey, options?)
Initialize the SDK. Used with CDN script tag.
| Param | Type | Description |
|-------|------|-------------|
| publishableKey | string | Your publishable key (r1pk_prod_* or r1pk_test_*) |
| options.locale | string | Locale for labels/errors (default: 'en') |
Returns a RadiumOne instance.
loadRadiumOne(publishableKey, options?)
Async loader for npm users. Same params as init(). Returns Promise<RadiumOne | null> (null during SSR).
import { loadRadiumOne } from '@cubepay/radiumone-js';
const radiumone = await loadRadiumOne('r1pk_prod_your_key');radiumone.elements(options?)
Create an Elements instance for managing card inputs.
| Param | Type | Description |
|-------|------|-------------|
| options.appearance | AppearanceOptions | Theme and styling (see Appearance) |
| options.locale | string | Override locale for this Elements instance |
elements.create(type, options?)
Create a card input element.
| Param | Type | Description |
|-------|------|-------------|
| type | 'card' \| 'cardNumber' \| 'cardExpiry' \| 'cardCvv' | Element type |
| options.showIcon | boolean | Show card brand icon (default: true) |
| options.autoAdvance | boolean | Auto-advance to next field on completion (default: true) |
element.mount(target)
Mount the element to a DOM container.
card.mount('#card-element'); // CSS selector
card.mount(document.getElementById('card-element')); // HTMLElementelement.on(event, handler)
Subscribe to element events.
card.on('change', (event) => { /* ... */ });
card.on('ready', (event) => { /* ... */ });
card.on('focus', (event) => { /* ... */ });
card.on('blur', (event) => { /* ... */ });element.unmount() / element.destroy()
Remove the element from DOM. unmount() allows re-mounting; destroy() is permanent.
elements.submit(sessionId, sessionSecret)
Submit all card fields — encrypts card data and tokenizes it.
const result = await elements.submit(sessionId, sessionSecret);Returns a BindResult:
| Field | Type | Description |
|-------|------|-------------|
| token | string | One-time payment token — send this to your server |
| bin | string | First 6 digits of card number |
| lastFour | string | Last 4 digits of card number |
| cardBrand | string | Detected brand (visa, mastercard, amex, etc.) |
| sessionId | string | Session ID used |
| status | string | Bind status |
| cvvProvided | boolean | Whether CVV was provided |
elements.getState()
Get the current state of all fields (async — queries iframes for live state).
const state = await elements.getState();
console.log(state.complete); // true if all fields are filled and valid
console.log(state.errors); // array of validation errorselements.destroy()
Destroy all elements and clean up iframes.
Events
change
Fired when the field value, validity, or completion state changes.
card.on('change', (event) => {
event.elementType; // 'card' | 'cardNumber' | 'cardExpiry' | 'cardCvv'
event.empty; // true if field is empty
event.complete; // true if field value is complete and valid
event.valid; // true if current value passes validation
event.brand; // 'visa' | 'mastercard' | 'amex' | ... (cardNumber/card only)
event.error; // { code, message, field } or null
});ready
Fired when the iframe is loaded and the field is interactive.
focus / blur
Fired when the field receives/loses focus.
cardTypeChange (cardNumber only)
Fired when the detected card brand changes as the user types.
cardNumber.on('cardTypeChange', (event) => {
event.brand; // 'visa' | 'mastercard' | 'amex' | ...
event.binLength; // number of digits used for detection
});Error Handling
All SDK errors are instances of ElementsError:
import { ElementsError } from '@cubepay/radiumone-js';
try {
const result = await elements.submit(sessionId, sessionSecret);
} catch (err) {
if (err instanceof ElementsError) {
console.error(err.type); // 'validation_error' | 'api_error' | 'network_error' | ...
console.error(err.code); // e.g. 'required', 'bind:failed'
console.error(err.message); // Human-readable message
console.error(err.field); // Which field caused the error (if applicable)
}
}| Error Type | When |
|------------|------|
| validation_error | Required fields empty, invalid card number, expired card |
| api_error | Bind API returned an error (invalid session, declined) |
| network_error | Network failure or request timeout |
| encryption_error | Card data encryption failed |
| load_error | SDK failed to load or iframe timed out |
Split Fields
Use separate inputs for card number, expiry, and CVV when you need individual control:
const cardNumber = elements.create('cardNumber');
const cardExpiry = elements.create('cardExpiry');
const cardCvv = elements.create('cardCvv');
cardNumber.mount('#card-number');
cardExpiry.mount('#card-expiry');
cardCvv.mount('#card-cvv');
// Detect card brand as user types
cardNumber.on('cardTypeChange', (event) => {
brandIcon.src = `/icons/${event.brand}.svg`;
});
// Track per-field errors
cardNumber.on('change', (e) => { numberError.textContent = e.error?.message ?? ''; });
cardExpiry.on('change', (e) => { expiryError.textContent = e.error?.message ?? ''; });
cardCvv.on('change', (e) => { cvvError.textContent = e.error?.message ?? ''; });Tab navigation between split fields is handled automatically.
Appearance
Customize card fields to match your checkout design.
Themes
Three built-in themes:
const elements = radiumone.elements({
appearance: { theme: 'default' }, // Bordered inputs (default)
});
const elements = radiumone.elements({
appearance: { theme: 'flat' }, // Underline-only inputs
});
const elements = radiumone.elements({
appearance: { theme: 'minimal' }, // Subtle background, no border
});Variables
Override individual design tokens:
const elements = radiumone.elements({
appearance: {
theme: 'default',
variables: {
colorPrimary: '#0070f0', // Focus border, icons
colorBackground: '#ffffff', // Input background
colorText: '#1a1a1a', // Input text
colorTextPlaceholder: '#9ca3af', // Placeholder text
colorDanger: '#dc2626', // Error states
colorSuccess: '#16a34a', // Valid states
fontFamily: 'system-ui, sans-serif',
fontSize: '16px',
fontWeight: '400',
borderRadius: '6px',
borderWidth: '1px',
borderColor: '#d1d5db',
focusBorderColor: '#0070f0',
focusBoxShadow: '0 0 0 3px rgba(0, 112, 240, 0.15)',
colorScheme: 'light', // 'light' | 'dark' | 'auto'
},
},
});CSS Rules
For fine-grained control, target specific element states:
const elements = radiumone.elements({
appearance: {
rules: {
'.Input': { padding: '14px 16px', lineHeight: '1.5' },
'.Input--focus': { borderColor: '#6366f1' },
'.Input--invalid': { borderColor: '#ef4444' },
'.Input--complete': { borderColor: '#22c55e' },
'.Input--disabled': { opacity: '0.5' },
'.Label': { fontSize: '14px', fontWeight: '500' },
'.Error': { color: '#ef4444', fontSize: '13px' },
'.Icon': { width: '24px' },
},
},
});Dynamic Updates
Update appearance after initialization without destroying iframes:
elements.update({
appearance: {
variables: { colorPrimary: '#6366f1' },
},
});CSP Configuration
Add to your Content Security Policy headers:
script-src https://js.radiumone.io;
frame-src https://js.radiumone.io;You do not need connect-src for our API — the tokenization call happens from inside the iframe (our origin), not from your page.
Browser Support
| Browser | Minimum Version | |---------|----------------| | Chrome / Edge | 90+ | | Firefox | 90+ | | Safari | 14+ | | Samsung Internet | 15+ |
Requires HTTPS (secure context) for Web Crypto API.
Test Cards
Use these with publishable keys starting with r1pk_test_:
| Number | Brand | Result |
|--------|-------|--------|
| 4111 1111 1111 1111 | Visa | Success |
| 5500 0000 0000 0004 | Mastercard | Success |
| 3782 822463 10005 | Amex | Success |
| 6011 0000 0000 0004 | Discover | Success |
| 4000 0000 0000 0002 | Visa | Declined |
Security
- Card data never leaves the iframe as plaintext
- JWE encryption (RSA-OAEP-256 + A256GCM) via Web Crypto API
- Iframe isolated from merchant page by cross-origin boundary
- CSS property allowlist prevents data exfiltration attacks
credentials: 'omit'on all API calls from iframe- Runtime script attestation for tamper detection
- Nonce-based postMessage protocol prevents replay attacks
TypeScript
Full type definitions are included. Import types as needed:
import type {
ChangeEvent,
BindResult,
CardBrand,
ElementsError,
AppearanceOptions,
} from '@cubepay/radiumone-js';License
Proprietary. See LICENSE.md in the published package for details.
