Troubleshooting Web Elements
This guide covers issues specific to v3. For v2, see the v2 Troubleshooting guide.
Need help? Email support@basistheory.com with your package version, browser, framework, and a reproduction.
v3-Specific Gotchas
Initialization is synchronous
v3 init is synchronous — do not await it.
// WRONG — await returns undefined
const bt = await BasisTheory('<PUBLIC_API_KEY>');
// CORRECT
const bt = BasisTheory('<PUBLIC_API_KEY>');
Elements load lazily when first mounted. The bt instance is immediately usable.
CDN global name changed
The CDN global in v3 is BasisTheory (capitalized), not basistheory.
<script>
document.addEventListener('DOMContentLoaded', () => {
const bt = BasisTheory('<PUBLIC_API_KEY>'); // uppercase B and T
});
</script>
Element type names changed
v3 uses different element type strings:
| v2 | v3 |
|---|---|
'card' | Removed — use separate 'cardNumber', 'expiry', 'cvv' elements |
'cardExpirationDate' | 'expiry' |
'cardVerificationCode' | 'cvv' |
'text' | 'text' (unchanged) |
// WRONG (v2 types)
bt.createElement('card', { targetId: 'card' });
bt.createElement('cardExpirationDate', { targetId: 'expiry' });
// CORRECT (v3 types)
bt.createElement('cardNumber');
bt.createElement('expiry');
bt.createElement('cvv');
Change event payload changed
The change event in v3 uses event.detail, not direct properties on the event object.
// WRONG (v2 shape)
element.on('change', (event) => {
if (event.complete) { ... }
if (event.error) { ... }
});
// CORRECT (v3 shape)
element.on('change', (event) => {
if (event.detail.isValid) { ... }
if (event.detail.error) { ... }
if (event.detail.isEmpty) { ... }
});
on() returns an unsubscribe function
In v3, element.on() returns () => void — call it to stop listening. It no longer returns a Subscription object.
const unsubscribe = cardNumberEl.on('change', handler);
// Later, to remove the listener:
unsubscribe();
React: Provider API changed
v3 uses apiKey prop directly on BasisTheoryProvider. The useBasisTheory(key, opts) hook pattern from v2 is gone — useBasisTheory() now takes no arguments and must be called inside the provider tree.
// WRONG (v2 pattern)
function PaymentForm() {
const { bt } = useBasisTheory('<PUBLIC_API_KEY>', { legacy: true });
...
}
// CORRECT (v3 pattern)
function App() {
return (
<BasisTheoryProvider apiKey='<PUBLIC_API_KEY>'>
<PaymentForm />
</BasisTheoryProvider>
);
}
function PaymentForm() {
const { bt } = useBasisTheory(); // no arguments
...
}
Common Issues
Elements not appearing after mount
Check these in order:
-
API key type — must be a Public key with
token:createpermission. Private and Management keys will fail. -
Container exists before mount — the selector must match an element already in the DOM when
mount()is called:// Mount after the DOM is ready
document.addEventListener('DOMContentLoaded', async () => {
const bt = BasisTheory('<PUBLIC_API_KEY>');
const cardNumberEl = bt.createElement('cardNumber');
await cardNumberEl.mount('#card-number-element'); // container must exist
}); -
Ad blocker or CSP — see Network & Security below.
Tokenization fails immediately without a network request
The SDK validates element state before making any API call. If an element's value is incomplete or invalid when you call bt.tokens.create(), the promise rejects synchronously.
// The change event tells you whether the element is ready
let cardNumberValid = false;
cardNumberEl.on('change', (event) => {
cardNumberValid = event.detail.isValid;
});
// Only call tokens.create when all fields are valid
form.addEventListener('submit', async (e) => {
e.preventDefault();
if (!cardNumberValid) return; // guard before calling
const token = await bt.tokens.create({ ... });
});
Element disappears after conditional rendering
If you unmount a container from the DOM and remount it, the element loses its state. Use CSS to show/hide instead:
// WRONG — unmounts the element when showForm is false
{showForm && <div id="card-number-element" />}
// CORRECT — keeps the element mounted, hidden via CSS
<div id="card-number-element" style={{ display: showForm ? 'block' : 'none' }} />
Mount timeout
Elements have a 15-second timeout for mounting. If the iframe doesn't load in time:
- Check network requests to
*.basistheory.comin DevTools — look for blocked or failing requests - Disable ad blockers / privacy extensions and retry
- Verify CSP allows
frame-srcandscript-srcforhttps://*.basistheory.com - Ensure DevTools isn't set to block requests or disable cache
try {
await cardNumberEl.mount('#card-number-element');
} catch (error) {
if (error.message.includes('timeout') || error.message.includes('Failed to load')) {
// likely ad blocker or network block
showNetworkErrorMessage();
}
}
Value resolution timeout during tokenization
If bt.tokens.create() hangs and then times out, the secure iframe communication is being interrupted:
- Most common cause: ad blocker or privacy extension blocking cross-origin
postMessage - Test in incognito mode to rule out extensions
- Corporate proxies with TLS inspection can break iframe communication
Network & Security
Required CSP directives
<meta http-equiv="Content-Security-Policy"
content="frame-src https://*.basistheory.com;
script-src https://*.basistheory.com;
connect-src https://*.basistheory.com" />
Optionally, to allow Datadog telemetry (helps Basis Theory diagnose issues):
connect-src https://*.browser-intake-datadoghq.com
Trusted Types
If your app uses Trusted Types, add this policy before calling BasisTheory():
trustedTypes.createPolicy('default', {
createScriptURL: (input) => {
if (new URL(input).origin === 'https://js.basistheory.com') return input;
return undefined;
},
});
Firewall / corporate network
Whitelist these domains:
*.basistheory.com*.browser-intake-datadoghq.com(telemetry)
TLS inspection (deep packet inspection) can break iframe cross-origin communication. Work with your network team to exempt *.basistheory.com.
Ad blockers
Ad blockers can block the Elements script or iframe from loading. Signs:
- Element containers are empty with no console errors
- Network requests to
basistheory.comare missing in DevTools - No errors thrown, but elements never appear
Ask users to temporarily disable ad blockers, or show a graceful fallback:
try {
const bt = BasisTheory('<PUBLIC_API_KEY>');
const cardNumberEl = bt.createElement('cardNumber');
await cardNumberEl.mount('#card-number-element');
} catch (error) {
if (error.message.includes('Failed to load') || error.message.includes('timeout')) {
showAdBlockerMessage();
}
}
Server-Side Rendering (SSR)
Elements create secure iframes and rely on window, document, and postMessage — APIs that don't exist in Node.js. The SDK cannot render server-side, but every major SSR framework provides a way to defer client-only code to hydration.
Next.js App Router
Mark the component that uses Elements with 'use client'. Next.js will skip it during SSR and only render it in the browser.
'use client';
import { BasisTheoryProvider, CardNumberElement, ExpiryElement, CVVElement } from '@basis-theory/react-elements';
export function PaymentForm() {
return (
<BasisTheoryProvider apiKey='<PUBLIC_API_KEY>'>
<CardNumberElement />
<ExpiryElement />
<CVVElement />
</BasisTheoryProvider>
);
}
Next.js Pages Router
Use dynamic with ssr: false to exclude the component from the server render entirely.
import dynamic from 'next/dynamic';
const PaymentForm = dynamic(() => import('../components/PaymentForm'), { ssr: false });
export default function CheckoutPage() {
return <PaymentForm />;
}
Remix
Render Elements inside a useEffect or behind a clientOnly check so they only mount in the browser.
import { useState, useEffect } from 'react';
import { BasisTheoryProvider, CardNumberElement } from '@basis-theory/react-elements';
export default function PaymentForm() {
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
if (!mounted) return null;
return (
<BasisTheoryProvider apiKey='<PUBLIC_API_KEY>'>
<CardNumberElement />
</BasisTheoryProvider>
);
}
Vue / Nuxt
In Nuxt, wrap Elements in a <ClientOnly> component to prevent SSR. In plain Vue with SSR, use onMounted to initialize after hydration.
<!-- Nuxt: ClientOnly prevents server rendering -->
<template>
<ClientOnly>
<PaymentForm />
</ClientOnly>
</template>
<!-- Vue + Web Elements: initialize in onMounted -->
<script setup>
import { onMounted, ref } from 'vue';
const mounted = ref(false);
onMounted(async () => {
const bt = BasisTheory('<PUBLIC_API_KEY>');
const cardNumberEl = bt.createElement('cardNumber');
await cardNumberEl.mount('#card-number-element');
mounted.value = true;
});
</script>
<template>
<div id="card-number-element" />
</template>
Web Elements (no framework)
The DOMContentLoaded pattern already handles this — the script only runs in the browser.
<script>
document.addEventListener('DOMContentLoaded', () => {
const bt = BasisTheory('<PUBLIC_API_KEY>');
const cardNumberEl = bt.createElement('cardNumber');
cardNumberEl.mount('#card-number-element');
});
</script>
Mobile & WebView
- iOS WKWebView — ensure cross-origin iframe communication is allowed. The WebView must not restrict
postMessage. - Android WebView (Cordova) — avoid
file://protocol; host onhttps://. - Aggressive tab suspension — mobile OSes may suspend background tabs and interrupt iframe communication. Re-mount elements when the page becomes visible again using the
visibilitychangeevent. - Touch input — elements handle touch natively. Avoid custom touch handlers on element containers.
Debug mode
Pass debug: true when initializing the SDK to enable verbose logging from the secure element iframes.
const bt = BasisTheory('<PUBLIC_API_KEY>', { debug: true });
All logs appear in the browser console with a 🔍 prefix and include a timestamp and the source package:
🔍 [14:23:01.452] [Elements] CMD_SET_CONFIG received { debug: true, ... }
🔍 [14:23:01.471] [Element] Config received { elementType: 'cardNumber', ... }
🔍 [14:23:01.490] [Element] Element ready { elementType: 'cardNumber', elementId: '...' }
🔍 [14:23:04.112] [Element] Input event { isValid: false, isEmpty: false, ... }
🔍 [14:23:06.380] [Element] Input event { isValid: true, isEmpty: false, ... }
🔍 [14:23:07.201] [Elements] GET_VALUE broadcast initiated { elementCount: 3 }
Log prefixes:
[Elements]— coordinator iframe (orchestrates tokenization, manages sessions)[Element]— individual input iframe (cardNumber, expiry, cvv, text)
Safe to share with support. Logs are PCI-sanitized — sensitive field values are never logged. Instead, only their length is recorded (e.g. number_length: 16). Structural metadata like element type, event flags, and error codes are safe to include in a support ticket.
Interpreting common log sequences
Normal mount:
[Elements] CMD_SET_CONFIG received ← SDK sent config to coordinator
[Element] Config received ← element iframe received config
[Element] Element ready ← iframe loaded and interactive
Mount timeout or failure:
[Elements] CMD_SET_CONFIG received ← coordinator got config
← no [Element] logs follow = iframe blocked
If you see coordinator logs but no [Element] logs, the input iframe failed to load — check for ad blockers, CSP issues, or network blocks on *.basistheory.com.
Tokenization:
[Elements] GET_VALUE broadcast initiated
[Element] Value resolved ← element sent value to coordinator
[Elements] Tokenization request sent
If GET_VALUE broadcast initiated appears but no Value resolved follows, the BroadcastChannel between iframes is being interrupted (typically a corporate proxy with TLS inspection).
Debugging events
function debugElement(element, name) {
element.on('ready', () => console.log(`${name}: ready`));
element.on('change', (e) => console.log(`${name}: change`, e.detail));
element.on('focus', () => console.log(`${name}: focus`));
element.on('blur', () => console.log(`${name}: blur`));
element.on('error', (e) => console.error(`${name}: error`, e.detail));
}
const cardNumberEl = bt.createElement('cardNumber');
debugElement(cardNumberEl, 'cardNumber');
await cardNumberEl.mount('#card-number-element');
Getting Support
Email support@basistheory.com with:
- Package and version (
@basis-theory/web-elements@x.x.xor@basis-theory/react-elements@x.x.x) - Install method (npm / CDN)
- Framework and version (React 18, Next.js 14, etc.)
- Browser and OS
- Whether ad blockers or corporate proxies are active
- Console output and network request logs from DevTools
- Steps to reproduce