Skip to main content

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

  1. Collect: Elements securely collect card data in an isolated iframe
  2. Encrypt: Card data is encrypted in the browser using a public key
  3. Configure: Create a Pre-Configured Proxy with {{ encrypted }} expressions in the request transform
  4. Invoke: Your backend sends the encrypted payload to the Proxy via the BT-ENCRYPTED header
  5. Tokenize: The Proxy decrypts and tokenizes the card on Basis Theory infrastructure
  6. Process: The Proxy forwards the payment request to your processor
  7. 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.

Make sure to use your work email (e.g., john.doe@yourcompany.com)

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
Save the API Key from this Management Application.

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
Save the API Key from this Private Application.

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)
Save the API Key from this Public Application.

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 -X POST "https://api.basistheory.com/keys" \
-H "BT-API-KEY: <MANAGEMENT_API_KEY>" \
-H "Content-Type: application/json"
Save both the 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.

index.html
<!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>
index.js
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();
Replace <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 "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
}'
Make sure to replace <MANAGEMENT_API_KEY> and <STRIPE_SECRET_KEY>.

What's happening in this configuration?

Request Transform (tokenize):

  • Defines the {{ encrypted }} expression that will read the BT-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
The {{ 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.

server.js
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'));
Replace <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:

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
});

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"
}
Make sure to replace <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.
You cannot access sensitive data in the 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.

Learn more