Getting Started
You need to collect a card from your users without raw card numbers ever touching your servers. This guide walks you through the complete setup: installing the SDK, creating card elements, validating input, and tokenizing the result.
By the end you will have a working payment form that collects card data securely and returns a token your backend can use to charge the card.
Prerequisites
You need a Public Application API key with the token:create permission. Public keys are safe to use in frontend code because they can only create tokens, not read sensitive data.
To create one, log in to the Basis Theory Portal, create a new application of type "Public", and grant the token:create permission.
1. Install
- Web Elements
- React Elements
- npm
- yarn
- CDN
npm install --save @basis-theory/web-elements@beta
yarn add @basis-theory/web-elements@beta
1
- npm
- yarn
npm install --save @basis-theory/react-elements@beta
yarn add @basis-theory/react-elements@beta
React Elements includes @basis-theory/web-elements as a dependency. You do not need to install both.
2. Initialize
The SDK is initialized synchronously — you get a ready-to-use instance immediately. Elements are loaded lazily when they are first mounted.
- Web Elements
- React Elements
- ES Module
- CDN
import BasisTheory from '@basis-theory/web-elements';
const bt = BasisTheory('<PUBLIC_API_KEY>');
After loading the CDN script, BasisTheory is available as a global. Call it inside DOMContentLoaded so the script has had a chance to load:
<script>
document.addEventListener('DOMContentLoaded', () => {
try {
const bt = BasisTheory('<PUBLIC_API_KEY>');
// Create and mount elements here
} catch (error) {
console.error('Failed to initialize Elements:', error);
}
});
</script>
Wrap your application (or the relevant subtree) with BasisTheoryProvider. All element components and the useBasisTheory hook must be descendants of this provider.
import { BasisTheoryProvider } from '@basis-theory/react-elements';
function App() {
return (
<BasisTheoryProvider apiKey='<PUBLIC_API_KEY>'>
<PaymentForm />
</BasisTheoryProvider>
);
}
3. Add Card Elements
v3 uses three separate elements — one for the card number, one for the expiration date, and one for the CVV. Each renders as a secure iframe inside the container you specify.
- Web Elements
- React Elements
Add containers to your HTML:
<form id="payment-form">
<div id="card-number-element"></div>
<div id="expiry-element"></div>
<div id="cvv-element"></div>
<button type="submit" id="submit-btn" disabled>Pay</button>
</form>
Create and mount the elements:
const cardNumberEl = bt.createElement('cardNumber');
const expiryEl = bt.createElement('expiry');
const cvvEl = bt.createElement('cvv');
await Promise.all([
cardNumberEl.mount('#card-number-element'),
expiryEl.mount('#expiry-element'),
cvvEl.mount('#cvv-element'),
]);
import { useRef } from 'react';
import {
CardNumberElement,
ExpiryElement,
CVVElement,
useBasisTheory,
} from '@basis-theory/react-elements';
function PaymentForm() {
const cardNumberRef = useRef(null);
const expiryRef = useRef(null);
const cvvRef = useRef(null);
return (
<form>
<CardNumberElement ref={cardNumberRef} />
<ExpiryElement ref={expiryRef} />
<CVVElement ref={cvvRef} />
<button type="submit">Pay</button>
</form>
);
}
4. Wait for the Element to Be Ready
Each element emits a ready event once its iframe has fully loaded and the element is interactive. If you need to drive the element programmatically (call focus(), clear(), etc.) after mounting, wait for ready first.
- Web Elements
- React Elements
cardNumberEl.on('ready', () => {
// Safe to call cardNumberEl.focus(), cardNumberEl.clear(), etc.
});
<CardNumberElement
ref={cardNumberRef}
onReady={() => {
// The element is mounted and ready for interaction
}}
/>
For straightforward collect-and-tokenize forms you do not need to wait for ready before the user can type. The element becomes interactive as soon as it mounts. ready is primarily useful when you need to drive the element programmatically.
5. Handle Validation State
Each element emits a change event whenever the user types. Use it to track whether all fields are complete and valid before enabling the submit button.
The change event payload is in event.detail:
isValid— input passes all validation rulesisEmpty— input is emptyerror— validation error object, ornullif valid
- Web Elements
- React Elements
const submitBtn = document.getElementById('submit-btn');
let cardNumberValid = false;
let expiryValid = false;
let cvvValid = false;
function updateSubmitButton() {
submitBtn.disabled = !(cardNumberValid && expiryValid && cvvValid);
}
cardNumberEl.on('change', (event) => {
cardNumberValid = event.detail.isValid;
updateSubmitButton();
});
expiryEl.on('change', (event) => {
expiryValid = event.detail.isValid;
updateSubmitButton();
});
cvvEl.on('change', (event) => {
cvvValid = event.detail.isValid;
updateSubmitButton();
});
import { useState, useRef } from 'react';
import {
CardNumberElement,
ExpiryElement,
CVVElement,
} from '@basis-theory/react-elements';
function PaymentForm() {
const cardNumberRef = useRef(null);
const expiryRef = useRef(null);
const cvvRef = useRef(null);
const [cardNumberValid, setCardNumberValid] = useState(false);
const [expiryValid, setExpiryValid] = useState(false);
const [cvvValid, setCvvValid] = useState(false);
const allValid = cardNumberValid && expiryValid && cvvValid;
return (
<form>
<CardNumberElement
ref={cardNumberRef}
onChange={(event) => setCardNumberValid(event.detail.isValid)}
/>
<ExpiryElement
ref={expiryRef}
onChange={(event) => setExpiryValid(event.detail.isValid)}
/>
<CVVElement
ref={cvvRef}
onChange={(event) => setCvvValid(event.detail.isValid)}
/>
<button type="submit" disabled={!allValid}>Pay</button>
</form>
);
}
The change event payload also includes cardBrand, last4, and bin on cardNumber elements. See Events for the full reference.
6. Tokenize on Submit
When the user submits the form, pass the element references directly to bt.tokens.create. The SDK sends the raw card data directly from the iframes to Basis Theory — your application code never sees the card number.
- Web Elements
- React Elements
document.getElementById('payment-form').addEventListener('submit', async (e) => {
e.preventDefault();
try {
const token = await bt.tokens.create({
type: 'card',
data: {
number: cardNumberEl,
expiration_month: expiryEl,
expiration_year: expiryEl,
cvc: cvvEl,
},
});
// Send token.id to your backend to charge the card
console.log('Token created:', token.id);
} catch (error) {
console.error('Tokenization failed:', error);
}
});
import { useState, useRef } from 'react';
import {
CardNumberElement,
ExpiryElement,
CVVElement,
useBasisTheory,
} from '@basis-theory/react-elements';
function PaymentForm() {
const { bt } = useBasisTheory();
const cardNumberRef = useRef(null);
const expiryRef = useRef(null);
const cvvRef = useRef(null);
const handleSubmit = async (e) => {
e.preventDefault();
try {
const token = await bt.tokens.create({
type: 'card',
data: {
number: cardNumberRef.current,
expiration_month: expiryRef.current,
expiration_year: expiryRef.current,
cvc: cvvRef.current,
},
});
// Send token.id to your backend to charge the card
console.log('Token created:', token.id);
} catch (error) {
console.error('Tokenization failed:', error);
}
};
return (
<form onSubmit={handleSubmit}>
<CardNumberElement ref={cardNumberRef} />
<ExpiryElement ref={expiryRef} />
<CVVElement ref={cvvRef} />
<button type="submit">Pay</button>
</form>
);
}
token.id is a safe, non-sensitive identifier you send to your backend. Use it with the Basis Theory API or a proxy to charge the card through your payment processor.
Complete Example
- Web Elements
- React Elements
<!DOCTYPE html>
<html>
<head>
<title>Payment Form</title>
<!-- Copy the CDN script tag from the Install section above -->
</head>
<body>
<form id="payment-form">
<div id="card-number-element"></div>
<div id="expiry-element"></div>
<div id="cvv-element"></div>
<button type="submit" id="submit-btn" disabled>Pay</button>
</form>
<script>
document.addEventListener('DOMContentLoaded', async () => {
try {
// BasisTheory is available as a global after the CDN script loads
const bt = BasisTheory('<PUBLIC_API_KEY>');
const cardNumberEl = bt.createElement('cardNumber');
const expiryEl = bt.createElement('expiry');
const cvvEl = bt.createElement('cvv');
await Promise.all([
cardNumberEl.mount('#card-number-element'),
expiryEl.mount('#expiry-element'),
cvvEl.mount('#cvv-element'),
]);
const submitBtn = document.getElementById('submit-btn');
let cardNumberValid = false;
let expiryValid = false;
let cvvValid = false;
function updateSubmitButton() {
submitBtn.disabled = !(cardNumberValid && expiryValid && cvvValid);
}
cardNumberEl.on('change', (event) => {
cardNumberValid = event.detail.isValid;
updateSubmitButton();
});
expiryEl.on('change', (event) => {
expiryValid = event.detail.isValid;
updateSubmitButton();
});
cvvEl.on('change', (event) => {
cvvValid = event.detail.isValid;
updateSubmitButton();
});
document.getElementById('payment-form').addEventListener('submit', async (e) => {
e.preventDefault();
submitBtn.disabled = true;
try {
const token = await bt.tokens.create({
type: 'card',
data: {
number: cardNumberEl,
expiration_month: expiryEl,
expiration_year: expiryEl,
cvc: cvvEl,
},
});
console.log('Token created:', token.id);
} catch (error) {
console.error('Tokenization failed:', error);
updateSubmitButton();
}
});
} catch (error) {
console.error('Failed to initialize Elements:', error);
}
});
</script>
</body>
</html>
import { useState, useRef } from 'react';
import {
BasisTheoryProvider,
CardNumberElement,
ExpiryElement,
CVVElement,
useBasisTheory,
} from '@basis-theory/react-elements';
function PaymentForm() {
const { bt, error } = useBasisTheory();
const cardNumberRef = useRef(null);
const expiryRef = useRef(null);
const cvvRef = useRef(null);
const [cardNumberValid, setCardNumberValid] = useState(false);
const [expiryValid, setExpiryValid] = useState(false);
const [cvvValid, setCvvValid] = useState(false);
const [submitting, setSubmitting] = useState(false);
const allValid = cardNumberValid && expiryValid && cvvValid;
if (error) return <div>Failed to load payment form.</div>;
const handleSubmit = async (e) => {
e.preventDefault();
setSubmitting(true);
try {
const token = await bt.tokens.create({
type: 'card',
data: {
number: cardNumberRef.current,
expiration_month: expiryRef.current,
expiration_year: expiryRef.current,
cvc: cvvRef.current,
},
});
console.log('Token created:', token.id);
} catch (err) {
console.error('Tokenization failed:', err);
} finally {
setSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit}>
<CardNumberElement
ref={cardNumberRef}
onChange={(event) => setCardNumberValid(event.detail.isValid)}
/>
<ExpiryElement
ref={expiryRef}
onChange={(event) => setExpiryValid(event.detail.isValid)}
/>
<CVVElement
ref={cvvRef}
onChange={(event) => setCvvValid(event.detail.isValid)}
/>
<button type="submit" disabled={!allValid || submitting}>
{submitting ? 'Processing...' : 'Pay'}
</button>
</form>
);
}
export default function App() {
return (
<BasisTheoryProvider apiKey='<PUBLIC_API_KEY>'>
<PaymentForm />
</BasisTheoryProvider>
);
}
Next Steps
- Element Types — available element types and their options
- Events — full event reference for
change,focus,blur,error, and more - Services — tokenize, sessions, proxy, and HTTP client
- Initialization Options — full init options and framework-specific setup
- Element Lifecycle — understand how elements move from creation to cleanup
- Migration Guide — upgrading from v2
- Troubleshooting — common issues and solutions