Skip to main content

Collect Cards with Your Inputs

Despite Basis Theory Elements being highly customizable and providing the most secure and efficient solution for collecting cards in user-facing applications, we understand that some companies are experienced with PCI DSS and feel comfortable having their frontend, or parts of it, handle cardholder data and be in scope for compliance.

In this guide, we will set up Basis Theory SDKs to receive cards from your frontend application and securely store the cardholder data as tokens with the Basis Theory Platform. This practice is well-positioned to substantially de-scope your servers, networking resources and database from PCI DSS compliance, while retaining full control of the sensitive data. If you want to learn more how Basis Theory can help you achieve that, reach out to our team!

Collect Cards with your Forms Flowchart

Getting Started

To get started, you will need a Basis Theory account and a Tenant.

Creating a Public Application

Next you will need a Public Application using our PCI-compliant template Collect PCI Data. Click here to create one.

This will create an application with the following Access Controls:

  • Permissions: token:create, token:update
  • Containers: /pci/
  • Transform: mask
Save the API Key from the created Public Application as it will be used later in this guide.

Install the SDK

npm install --save @basis-theory/basis-theory-js
yarn add @basis-theory/basis-theory-js
<!-- Including this tag will export a global/window "BasisTheory" variable -->
<script src="https://js.basistheory.com"></script>

Storing Cards

We won't get into details about particular frontend libraries or custom input implementations, but show how to use the SDKs to store cards in your Basis Theory Tenant.

To do this, we will invoke the Create Token endpoint from the SDK, passing the cardholder data in the payload. This will securely create a token by transferring the card information from your frontend to Basis Theory, where the card will be strongly encrypted and stored in a compliant environment.

Let's use a submit function to make the request, triggered from a button:

<button onclick="submit();">Submit</button>
import { BasisTheory } from '@basis-theory/basis-theory-js';

let bt;

async function init () {
bt = await new BasisTheory().init('<API_KEY>');
};

async function submit () {
try {
const token = await bt.tokens.create({
type: 'card',
data: {
number: '4242424242424242',
expiration_month: 12,
expiration_year: 2025,
cvc: '123',
}
});
// store token.id in your database
} catch (error) {
console.error(error);
}
}

init();
Be sure to replace <API_KEY> with the Public API Key you created in the Creating a Public Application step.

The created card token object contains the non-sensitive information about the tokenized card, which follows the Card Token specification:

{
"id": "d2cbc1b4-5c3a-45a3-9ee2-392a1c475ab4",
"type": "card",
"tenant_id": "15f48eb5-8b52-4cdd-a396-608f7cf001d0",
"data": {
"number": "XXXXXXXXXXXX4242",
"expiration_month": 12,
"expiration_year": 2025
},
"created_by": "4a6ae2a6-79f8-4640-968f-88db913743df",
"created_at": "2023-04-17T12:54:44.8320458+00:00",
"mask": {
"number": "{{ data.number | reveal_last: 4 }}",
"expiration_month": "{{ data.expiration_month }}",
"expiration_year": "{{ data.expiration_year }}"
},
"privacy": {
"classification": "pci",
"impact_level": "high",
"restriction_policy": "mask"
},
"search_indexes": [],
"containers": [
"/pci/high/"
]
}

You can safely store the token's primary key id in your database to link it with the appropriate checkout, user profile, subscription, or any other record that requires association with the card.

Customizing Tokens

The steps so far cover most cases when you need to collect cards in your frontend and store them in a secure location. However, in some scenarios you may need to customize your card tokens for specific business needs or technical requirements. In the following sections, you will find optional steps to follow for common problems solved by Basis Theory Token capabilities.

Deduplication

Companies often find it necessary to uniquely identify cards flowing through their systems for various reasons, which may include: preventing fraudulent transactions, detecting credit cards abuse, building consumer profiles or streamlining payment processing for a better user experience.

By leveraging token fingerprinting, developers can recognize the tokenized data in a customizable fashion, without having to touch with the plaintext data. For cards, it is common to index in the Primary Account Number (PAN). In some cases the expiration date may also be considered.

When making the tokenization request to store the card, pass a fingerprint expression to instruct Basis Theory to calculate the fingerprint for the sensitive data field:

async function submit () {
try {
const token = await bt.tokens.create({
type: 'card',
data: {
number: '4242424242424242',
expiration_month: 12,
expiration_year: 2025,
cvc: '123',
},
fingerprintExpression: '{{ data.number }}',
});
} catch (error) {
console.error(error);
}
}

The new tokens should now have a fingerprint:

{
"id": "d2cbc1b4-5c3a-45a3-9ee2-392a1c475ab4",
"type": "card",
"tenant_id": "15f48eb5-8b52-4cdd-a396-608f7cf001d0",
"data": {
"number": "XXXXXXXXXXXX4242",
"expiration_month": 12,
"expiration_year": 2025
},
"created_by": "4a6ae2a6-79f8-4640-968f-88db913743df",
"created_at": "2023-04-17T12:54:44.8320458+00:00",
"fingerprint": "CC2XvyoohnqecEq4r3FtXv6MdCx4TbaW1UUTdCCN5MNL",
"fingerprint_expression": "{{ data.number }}",
"mask": {
"number": "{{ data.number | reveal_last: 4 }}",
"expiration_month": "{{ data.expiration_month }}",
"expiration_year": "{{ data.expiration_year }}"
},
"privacy": {
"classification": "pci",
"impact_level": "high",
"restriction_policy": "mask"
},
"search_indexes": [],
"containers": [
"/pci/high/"
]
}

If you want to prevent creation of a duplicate token based on the distinguishable fingerprint, add the flag below:

async function submit () {
try {
const token = await bt.tokens.create({
type: 'card',
data: {
number: '4242424242424242',
expiration_month: 12,
expiration_year: 2025,
cvc: '123',
},
fingerprintExpression: '{{ data.number }}',
deduplicateToken: true,
});
} catch (error) {
console.error(error);
}
}

By doing the above, you are instructing Basis Theory to return the existing token if it is found to have the same fingerprint. Click here to learn more about token deduplication.

Masking

By default, card tokens are created with a mask revealing only the last 4 digits of the card number. This is useful for generating receipts and payment history, displaying the card to the end-user without revealing the full number, etc.

In other scenarios, being able to preserve the Bank Identification Number (BIN) from the card number can enable fraud detection, payment processing, customer service or account management. PCI DSS allows applications to reveal up to the first 8 and last 4 digits of a card number, depending on its length and Payment Brand. Luckily, when creating a token, you can express which segments of the PAN are useful to you with a single expressions filter: card_mask. Click here to learn more about this filter.

async function submit () {
try {
const token = await bt.tokens.create({
type: 'card',
data: {
number: '4242424242424242',
expiration_month: 12,
expiration_year: 2025,
cvc: '123',
},
fingerprintExpression: '{{ data.number }}',
deduplicateToken: true,
mask: {
number:'{{ data.number | card_mask: "true", "true" }}',
expiration_month: '{{ data.expiration_month }}',
expiration_year: '{{ data.expiration_year }}',
},
});
} catch (error) {
console.error(error);
}
}

Now, the created token should also reveal the BIN:

{
"id": "d2cbc1b4-5c3a-45a3-9ee2-392a1c475ab4",
"type": "card",
"tenant_id": "15f48eb5-8b52-4cdd-a396-608f7cf001d0",
"data": {
"number": "42424242XXXX4242",
"expiration_month": 12,
"expiration_year": 2025
},
"created_by": "4a6ae2a6-79f8-4640-968f-88db913743df",
"created_at": "2023-04-17T12:54:44.8320458+00:00",
"fingerprint": "CC2XvyoohnqecEq4r3FtXv6MdCx4TbaW1UUTdCCN5MNL",
"fingerprint_expression": "{{ data.number }}",
"mask": {
"number": "{{ data.number | card_mask: 'true', 'true' }}",
"expiration_month": "{{ data.expiration_month }}",
"expiration_year": "{{ data.expiration_year }}"
},
"privacy": {
"classification": "pci",
"impact_level": "high",
"restriction_policy": "mask"
},
"search_indexes": [],
"containers": [
"/pci/high/"
]
}

In the example above, we instruct Basis Theory to reveal both segments, without having to worry about the card brand or number length. Click here to learn more about Masking.

Aliasing

While storing or transmitting tokens between systems, you may encounter restrictive technical constraints that can draw the default token Universally Unique Identifier (UUID) incompatible.

In the example below, we will pass a predefined token id that follows a custom logic, which resembles an alternative format used in the payments industry. This capability enables Token Portability, and it can be specially useful in migration scenarios.

function generateTokenId() {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let tokenId = 'card_';
for (let i = 0; i < 24; i++) {
tokenId += chars.charAt(Math.floor(Math.random() * chars.length));
}
return tokenId;
}

async function submit () {
try {
const token = await bt.tokens.create({
id: generateTokenId(),
type: 'card',
data: {
number: '4242424242424242',
expiration_month: 12,
expiration_year: 2025,
cvc: '123',
},
fingerprintExpression: '{{ data.number }}',
deduplicateToken: true,
mask: {
number:'{{ data.number | card_mask: "true", "true" }}',
expiration_month: '{{ data.expiration_month }}',
expiration_year: '{{ data.expiration_year }}',
},
});
} catch (error) {
console.error(error);
}
}

The returned token object should now have a custom identifier:

{
"id": "card_1Mxqr82eZvKYlo2CSaatci3m",
"type": "card",
"tenant_id": "15f48eb5-8b52-4cdd-a396-608f7cf001d0",
"data": {
"number": "42424242XXXX4242",
"expiration_month": 12,
"expiration_year": 2025
},
"created_by": "4a6ae2a6-79f8-4640-968f-88db913743df",
"created_at": "2023-04-17T12:54:44.8320458+00:00",
"fingerprint": "CC2XvyoohnqecEq4r3FtXv6MdCx4TbaW1UUTdCCN5MNL",
"fingerprint_expression": "{{ data.number }}",
"mask": {
"number": "{{ data.number | card_mask: 'true', 'true' }}",
"expiration_month": "{{ data.expiration_month }}",
"expiration_year": "{{ data.expiration_year }}"
},
"privacy": {
"classification": "pci",
"impact_level": "high",
"restriction_policy": "mask"
},
"search_indexes": [],
"containers": [
"/pci/high/"
]
}

Similarly to masking, aliasing also supports passing custom data-bound expressions, that can generate length and format-preserving token identifiers. Doing such increases compatibility to store or pass tokens between systems, white preserving information about the tokenized data.

For example, use the alias_card filter to generate a synthetic card number as a token identifier, which shares the same BIN and last four digits of the real card number. Click here to learn more about Aliasing.

Conclusion

By following the steps outlined in this guide, you can ensure that your users' sensitive card data is protected by storing it with Basis Theory. The token.id obtained at the end of the Storing Cards section is a synthetic replacement for the sensitive data and can be safely stored in your database, or transmitted through your systems, meeting compliance requirements and reducing the risk of exposure in case of data breaches.

The optional customization steps are meant to showcase platform capabilities that go beyond the examples given. Make sure to explore the provided links within each subsection to learn more about the possibilities for customization.

For next steps, take a look at the following guides to proceed taking the most value of your secured card tokens: