Collect and Tokenize Cards in One Call
Process card payments faster with a single API call. Collect cards securely with Elements, encrypt them in the browser, and tokenize them in your backend—all without sensitive data touching your systems.
Why this approach?
Traditional card tokenization requires multiple steps: collect the card and create the token with Elements, then use that token in your payment processing. This creates latency and complexity.
With Proxy Transforms, you can:
- Reduce latency – One Proxy call instead of separate tokenize + process steps
- Eliminate custom code – No reactors or backend tokenization logic required
- Tokenize in your backend – Tokenization happens automatically during the Proxy call from your backend
- Strengthen security – Sensitive data never touches your systems; Elements isolates card data, encryption happens in the browser, and tokenization occurs on Basis Theory infrastructure
- Simplify integration – Reference created tokens in both the destination request and response
Here are the steps we'll walk through in this guide
- Collect: Elements securely collect card data in an isolated iframe
- Encrypt: Card data is encrypted in the browser using a public key
- Configure: Create a Pre-Configured Proxy with
{{ encrypted }}
expressions in the request transform - Invoke: Your backend sends the encrypted payload to the Proxy via the
BT-ENCRYPTED
header - Tokenize: The Proxy decrypts and tokenizes the card on Basis Theory infrastructure
- Process: The Proxy forwards the payment request to your processor
- Return: The payment response is returned with the token ID appended
Getting Started
To get started, you will need to create a Basis Theory Account and a TEST Tenant.
Create a Management Application
You'll need a Management Application to create a Client Encryption Key and configure your Proxy.
Click here to create one or create it manually with:
- Name: Tokenize Encrypted Guide
- Application Type: Management
- Permissions:
keys:create
,proxy:create
Create a Private Application for Proxy Invocation
Create a Private Application to invoke the Proxy and tokenize card data.
Click here to create one or create it manually with:
- Name: Proxy Invoke App
- Application Type: Private
- Permissions:
proxy:invoke
Create a Public Application
Create a Public Application to initialize Elements and encrypt card data in the browser.
Click here to create one or create it manually with:
- Name: Encrypt Card App
- Application Type: Public
- Permissions: (none required for encryption)
Create a Client Encryption Key
Generate an encryption key pair for browser-side encryption. Elements uses the public key to encrypt card data in the browser; Basis Theory holds the private key to decrypt during Proxy processing.
- cURL
- Node
- Python
curl -X POST "https://api.basistheory.com/keys" \
-H "BT-API-KEY: <MANAGEMENT_API_KEY>" \
-H "Content-Type: application/json"
import { BasisTheoryClient } from "@basis-theory/node-sdk";
const client = new BasisTheoryClient({
apiKey: "<MANAGEMENT_API_KEY>"
});
const key = await client.keys.create();
console.log(key.id); // Save this Key ID
console.log(key.publicKeyPEM); // Save this public key for browser encryption
from basistheory import BasisTheoryClient
client = BasisTheoryClient(api_key="<MANAGEMENT_API_KEY>")
key = client.keys.create()
print(key.id) # Save this Key ID
print(key.public_key_pem) # Save this public key for browser encryption
id
and publicKeyPEM
from the response. You'll need them in the next steps. Replace <MANAGEMENT_API_KEY>
with your Management Application key.Collect and encrypt card data
Use Basis Theory Elements to securely collect card data in isolated iframes and encrypt it in the browser.
- JavaScript
- React
<!DOCTYPE html>
<html>
<body>
<div id="cardNumber"></div>
<div style="display: flex;">
<div id="cardExpirationDate" style="width: 100%;"></div>
<div id="cardVerificationCode" style="width: 100%;"></div>
</div>
<button id="submit">Submit Payment</button>
<pre id="result"></pre>
<script type="module" src="index.js"></script>
</body>
</html>
import { basistheory } from '@basis-theory/web-elements';
let bt;
let cardNumberElement, cardExpirationDateElement, cardVerificationCodeElement;
async function init() {
bt = await basistheory('<PUBLIC_API_KEY>');
// Create card elements
cardNumberElement = bt.createElement('cardNumber', {
targetId: 'cardNumber'
});
cardExpirationDateElement = bt.createElement('cardExpirationDate', {
targetId: 'cardExpirationDate'
});
cardVerificationCodeElement = bt.createElement('cardVerificationCode', {
targetId: 'cardVerificationCode'
});
// Mount elements
await Promise.all([
cardNumberElement.mount('#cardNumber'),
cardExpirationDateElement.mount('#cardExpirationDate'),
cardVerificationCodeElement.mount('#cardVerificationCode'),
]);
// Bind card brand to CVC
cardNumberElement.on('change', ({ cardBrand }) => {
cardVerificationCodeElement.update({ cardBrand });
});
document.getElementById('submit').addEventListener('click', submit);
}
async function submit() {
try {
// Encrypt the card data
const encryptedPayload = await bt.tokens.encrypt({
tokenRequests: {
type: 'card',
data: {
number: cardNumberElement,
expiration_month: cardExpirationDateElement.month(),
expiration_year: cardExpirationDateElement.year(),
cvc: cardVerificationCodeElement
}
},
public_key: '<PUBLIC_KEY_PEM>',
key_id: '<KEY_ID>'
});
// Base64 encode the encrypted payload
const base64Encrypted = btoa(encryptedPayload.encrypted);
// Send encrypted payload to your backend
const response = await fetch('/api/process-payment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
encrypted: base64Encrypted,
amount: 5000, // $50.00 in cents
currency: 'usd'
})
});
const result = await response.json();
document.getElementById('result').innerHTML = JSON.stringify(result, null, 2);
} catch (error) {
console.error('Payment failed:', error);
}
}
init();
import React, { useRef, useState } from 'react';
import {
BasisTheoryProvider,
CardNumberElement,
CardExpirationDateElement,
CardVerificationCodeElement,
useBasisTheory,
} from '@basis-theory/react-elements';
function PaymentForm() {
const { bt } = useBasisTheory('<PUBLIC_API_KEY>');
const cardNumberRef = useRef(null);
const cardExpirationRef = useRef(null);
const cardVerificationRef = useRef(null);
const [cardBrand, setCardBrand] = useState();
const [result, setResult] = useState();
const submit = async () => {
try {
// Encrypt the card data
const encryptedPayload = await bt?.tokens.encrypt({
tokenRequests: {
type: 'card',
data: {
number: cardNumberRef.current,
expiration_month: cardExpirationRef.current.month(),
expiration_year: cardExpirationRef.current.year(),
cvc: cardVerificationRef.current
}
},
public_key: '<PUBLIC_KEY_PEM>',
key_id: '<KEY_ID>'
});
// Base64 encode the encrypted payload
const base64Encrypted = btoa(encryptedPayload.encrypted);
// Send encrypted payload to your backend
const response = await fetch('/api/process-payment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
encrypted: base64Encrypted,
amount: 5000, // $50.00 in cents
currency: 'usd'
})
});
const data = await response.json();
setResult(JSON.stringify(data, null, 2));
} catch (error) {
console.error('Payment failed:', error);
}
};
return (
<BasisTheoryProvider bt={bt}>
<CardNumberElement
id="cardNumber"
ref={cardNumberRef}
onChange={({ cardBrand }) => setCardBrand(cardBrand)}
/>
<div style={{ display: 'flex' }}>
<CardExpirationDateElement
id="cardExpiration"
ref={cardExpirationRef}
style={{ width: "100%" }}
/>
<CardVerificationCodeElement
id="cardCvc"
ref={cardVerificationRef}
cardBrand={cardBrand}
style={{ width: "100%" }}
/>
</div>
<button onClick={submit}>Submit Payment</button>
{result && <pre>{result}</pre>}
</BasisTheoryProvider>
);
}
export default function App() {
return <PaymentForm />;
}
<PUBLIC_API_KEY>
with your Public Application key, <PUBLIC_KEY_PEM>
and <KEY_ID>
with values from the Client Encryption Key you created.Configure your Pre-Configured Proxy
Create a Proxy with request and response transforms to tokenize the encrypted card and append the token ID to your payment response.
- cURL
- Node
- Python
curl "https://api.basistheory.com/proxies" \
-H "BT-API-KEY: <MANAGEMENT_API_KEY>" \
-H "Content-Type: application/json" \
-X "POST" \
-d '{
"name": "Tokenize and Process Payment",
"destination_url": "https://api.stripe.com/v1/charges",
"request_transforms": [
{
"type": "tokenize",
"options": {
"token": {
"type": "card",
"data": "{{ encrypted | json: '$.data' }}",
"metadata": {
"source": "checkout_flow"
}
},
"identifier": "card_token"
}
}
],
"response_transforms": [
{
"type": "append_json",
"options": {
"value": "{{ transform_identifier: 'card_token' | json: '$.id' }}",
"location": "$.basis_theory_token_id"
}
}
],
"configuration": {
"STRIPE_SECRET_KEY": "sk_test_..."
},
"require_auth": true
}'
import { BasisTheoryClient } from "@basis-theory/node-sdk";
const client = new BasisTheoryClient({
apiKey: "<MANAGEMENT_API_KEY>"
});
const proxy = await client.proxies.create({
name: 'Tokenize and Process Payment',
destinationUrl: 'https://api.stripe.com/v1/charges',
requestTransforms: [
{
type: 'tokenize',
options: {
token: {
type: 'card',
data: "{{ encrypted | json: '$.data' }}",
metadata: {
source: 'checkout_flow'
}
},
identifier: 'card_token'
}
}
],
responseTransforms: [
{
type: 'append_json',
options: {
value: "{{ transform_identifier: 'card_token' | json: '$.id' }}",
location: '$.basis_theory_token_id'
}
}
],
configuration: {
STRIPE_SECRET_KEY: 'sk_test_...'
},
requireAuth: true
});
console.log('Proxy created:', proxy.key); // Save this Proxy key
from basistheory import BasisTheoryClient
client = BasisTheoryClient(api_key="<MANAGEMENT_API_KEY>")
proxy = client.proxies.create(
name="Tokenize and Process Payment",
destination_url="https://api.stripe.com/v1/charges",
request_transforms=[
{
"type": "tokenize",
"options": {
"token": {
"type": "card",
"data": "{{ encrypted | json: '$.data' }}",
"metadata": {
"source": "checkout_flow"
}
},
"identifier": "card_token"
}
}
],
response_transforms=[
{
"type": "append_json",
"options": {
"value": "{{ transform_identifier: 'card_token' | json: '$.id' }}",
"location": "$.basis_theory_token_id"
}
}
],
configuration={
"STRIPE_SECRET_KEY": "sk_test_..."
},
require_auth=True
)
print(f"Proxy created: {proxy.key}") # Save this Proxy key
<MANAGEMENT_API_KEY>
and <STRIPE_SECRET_KEY>
.What's happening in this configuration?
Request Transform (tokenize
):
- Defines the
{{ encrypted }}
expression that will read theBT-ENCRYPTED
header when you invoke the Proxy - Extracts card data from the encrypted payload using
{{ encrypted | json: '$.data' }}
- Creates a card token during the Proxy invocation
- Assigns identifier
card_token
for referencing in subsequent transforms
Response Transform (append_json
):
- References the created token using
{{ transform_identifier: 'card_token' | json: '$.id' }}
- Appends the token ID to the payment response at
$.basis_theory_token_id
{{ encrypted }}
expression in the Proxy configuration tells the transform where to find the encrypted data. When you invoke the Proxy, you pass the base64-encoded encrypted payload via the BT-ENCRYPTED
header.Process a payment
Send the encrypted card payload from your backend to the Proxy. The Proxy tokenizes the card, charges it via Stripe, and returns the response with the token ID appended.
- Node
- Python
- cURL
import express from 'express';
import fetch from 'node-fetch';
const app = express();
app.use(express.json());
app.post('/api/process-payment', async (req, res) => {
const { encrypted, amount, currency } = req.body;
try {
// Invoke the Proxy with encrypted card data
const proxyResponse = await fetch('https://api.basistheory.com/proxy', {
method: 'POST',
headers: {
'BT-API-KEY': '<PRIVATE_API_KEY>',
'BT-PROXY-KEY': '<PROXY_KEY>',
'BT-ENCRYPTED': encrypted,
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Bearer {{ configuration.STRIPE_SECRET_KEY }}'
},
body: new URLSearchParams({
amount: amount,
currency: currency,
description: 'Charge via Proxy transform'
})
});
const result = await proxyResponse.json();
// Result includes Stripe charge response + basis_theory_token_id
console.log('Payment processed:', result.id);
console.log('Card token stored:', result.basis_theory_token_id);
// Store result.basis_theory_token_id in your database
res.json(result);
} catch (error) {
console.error('Payment error:', error);
res.status(500).json({ error: error.message });
}
});
app.listen(3000, () => console.log('Server running on port 3000'));
from flask import Flask, request, jsonify
import requests
from urllib.parse import urlencode
app = Flask(__name__)
@app.route('/api/process-payment', methods=['POST'])
def process_payment():
data = request.json
encrypted = data.get('encrypted')
amount = data.get('amount')
currency = data.get('currency')
try:
# Invoke the Proxy with encrypted card data
proxy_response = requests.post(
'https://api.basistheory.com/proxy',
headers={
'BT-API-KEY': '<PRIVATE_API_KEY>',
'BT-PROXY-KEY': '<PROXY_KEY>',
'BT-ENCRYPTED': encrypted,
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Bearer {{ configuration.STRIPE_SECRET_KEY }}'
},
data=urlencode({
'amount': amount,
'currency': currency,
'description': 'Charge via Proxy transform'
})
)
result = proxy_response.json()
# Result includes Stripe charge response + basis_theory_token_id
print(f"Payment processed: {result.get('id')}")
print(f"Card token stored: {result.get('basis_theory_token_id')}")
# Store result['basis_theory_token_id'] in your database
return jsonify(result)
except Exception as e:
print(f"Payment error: {e}")
return jsonify({'error': str(e)}), 500
if __name__ == '__main__':
app.run(port=3000)
curl "https://api.basistheory.com/proxy" \
-H "BT-API-KEY: <PRIVATE_API_KEY>" \
-H "BT-PROXY-KEY: <PROXY_KEY>" \
-H "BT-ENCRYPTED: <ENCRYPTED_PAYLOAD_FROM_FRONTEND>" \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "Authorization: Bearer {{ configuration.STRIPE_SECRET_KEY }}" \
-X POST \
-d "amount=5000¤cy=usd&description=Charge via Proxy transform"
<PRIVATE_API_KEY>
with your Private Application key, <PROXY_KEY>
with the Proxy key from the previous step, and <ENCRYPTED_PAYLOAD_FROM_FRONTEND>
with the base64-encoded encrypted payload from the browser.Example Response
The Stripe charge response is returned with the Basis Theory token ID appended:
{
"id": "ch_3QHlP92eZvKYlo2C0SjZbUTp",
"object": "charge",
"amount": 5000,
"currency": "usd",
"status": "succeeded",
"description": "Charge via Proxy transform",
"payment_method": "pm_1QHlP82eZvKYlo2C...",
"basis_theory_token_id": "d2cbc1b4-5c3a-45a3-9ee2-392a1c475ab4"
}
Store basis_theory_token_id
in your database alongside the Stripe charge ID. You can now use this token for future operations.
Advanced: Include card metadata in your payment response
After tokenizing the card, you may want to return additional card details (like brand or last 4 digits) in your payment response. This is useful for displaying card information to users or storing it in your database.
Add multiple append_json
response transforms to include card properties from the tokenized card:
- Node
- Python
const proxy = await client.proxies.create({
name: 'Stripe Proxy with Card Metadata',
destinationUrl: 'https://api.stripe.com/v1/charges',
requestTransforms: [
{
type: 'tokenize',
options: {
token: {
type: 'card',
data: "{{ encrypted | json: '$.data' }}"
},
identifier: 'card_token'
}
}
],
responseTransforms: [
// Append the token ID
{
type: 'append_json',
options: {
value: "{{ transform_identifier: 'card_token' | json: '$.id' }}",
location: '$.basis_theory_token_id'
}
},
// Append card brand (visa, mastercard, etc.)
{
type: 'append_json',
options: {
value: "{{ transform_identifier: 'card_token' | json: '$.card.brand' }}",
location: '$.card_brand'
}
},
// Append last 4 digits
{
type: 'append_json',
options: {
value: "{{ transform_identifier: 'card_token' | json: '$.card.last4' }}",
location: '$.card_last4'
}
}
],
configuration: { STRIPE_SECRET_KEY: 'sk_test_...' },
requireAuth: true
});
proxy = client.proxies.create(
name="Stripe Proxy with Card Metadata",
destination_url="https://api.stripe.com/v1/charges",
request_transforms=[
{
"type": "tokenize",
"options": {
"token": {
"type": "card",
"data": "{{ encrypted | json: '$.data' }}"
},
"identifier": "card_token"
}
}
],
response_transforms=[
# Append the token ID
{
"type": "append_json",
"options": {
"value": "{{ transform_identifier: 'card_token' | json: '$.id' }}",
"location": "$.basis_theory_token_id"
}
},
# Append card brand (visa, mastercard, etc.)
{
"type": "append_json",
"options": {
"value": "{{ transform_identifier: 'card_token' | json: '$.card.brand' }}",
"location": "$.card_brand"
}
},
# Append last 4 digits
{
"type": "append_json",
"options": {
"value": "{{ transform_identifier: 'card_token' | json: '$.card.last4' }}",
"location": "$.card_last4"
}
}
],
configuration={"STRIPE_SECRET_KEY": "sk_test_..."},
require_auth=True
)
Example Response with Card Metadata
{
"id": "ch_3PqR...",
"amount": 5000,
"currency": "usd",
"status": "succeeded",
// Appended by response transforms
"basis_theory_token_id": "d2cbc1b4-5c3a-45a6-9c1f-7e1e5e1e5e1e",
"card_brand": "visa",
"card_last4": "4242"
}
<STRIPE_SECRET_KEY>
. Use transform_identifier
to reference any property from tokens created in request transforms. Access token properties using JSONPath like $.card.brand
or $.card.last4
. See the full Token Object schema for all available properties.data
property via transform_identifier
.Testing
Use test cards to verify your implementation before going to production. Test cards help you simulate different payment scenarios and error conditions.
// Use Basis Theory test cards
const testCards = {
visa: '4242424242424242',
mastercard: '5555555555554444',
amex: '378282246310005'
};
Going to production
Security considerations
- Store Proxy keys securely – Treat Proxy keys like API keys; only use them in your backend, never in the browser
- Configure
require_auth: true
– Prevent anonymous Proxy access in production - Audit token lifecycle – Set up production webhook endpoints to monitor token lifecycle events like creation, updates, and expirations
Conclusion
You've built a streamlined payment flow that:
- Collects cards securely – Elements isolate sensitive data in iframes, keeping it out of your application code
- Encrypts end-to-end – Data is encrypted in the browser and protected in transit to Basis Theory
- Tokenizes automatically – No separate tokenization step or custom reactor code
- Processes payments faster – One Proxy call replaces multi-step workflows
- Returns token IDs – Store tokens for recurring charges, refunds, or card updates
This pattern works for any payment processor. Swap Stripe for Adyen, Braintree, Checkout.com, or your custom payment API—just update the destination_url
and request body format.