Collect Cards
When building an e-commerce application, subscription service, or enabling one-time purchases, one of the critical requirements is collecting and storing cardholder data securely. However, it can be challenging to navigate the complex regulatory landscape, particularly PCI DSS, and ensure that your application meets all the necessary security standards.
In this guide, we will set up SDKs to capture cards in the frontend, Web or Mobile, and securely store the cardholder data as tokens with the Basis Theory Platform. Given it is followed as it is, your user-facing applications and database are completely removed from the compliance scope.
Getting Started
To get started, you will need a Basis Theory account and a Tenant.
Creating a Public Application
Next you will need you'll 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
Configuring Basis Theory Elements
Basis Theory Elements is available for the following technologies. Click below for detailed instructions on how to install and configure it.
Adding Card Elements
Once properly installed and configured, add the Card Elements to your application. This will enable users to type in their card data in your form, while refraining from your systems to come in contact with it.
- JavaScript
- React
- iOS
- Android
<div id="cardNumber"></div>
<div style="display: flex;">
<div id="cardExpirationDate" style="width: 100%;"></div>
<div id="cardVerificationCode" style="width: 100%;"></div>
</div>
import { BasisTheory } from '@basis-theory/basis-theory-js';
let bt;
let cardNumberElement;
let cardExpirationDateElement;
let cardVerificationCodeElement;
async function init() {
bt = await new BasisTheory().init('test_1234567890', { elements: true });
// Creates Elements instances
cardNumberElement = bt.createElement('cardNumber', {
targetId: 'myCardNumber' // (custom) used for tracking validation errors
});
cardExpirationDateElement = bt.createElement('cardExpirationDate', {
targetId: 'myCardExpiration'
});
cardVerificationCodeElement = bt.createElement('cardVerificationCode', {
targetId: 'myCardVerification'
});
// Mounts Elements in the DOM in parallel
await Promise.all([
cardNumberElement.mount('#cardNumber'),
cardExpirationDateElement.mount('#cardExpirationDate'),
cardVerificationCodeElement.mount('#cardVerificationCode'),
]);
// Binds card brand to verification code element
cardNumberElement.on('change', ({ cardBrand }) => {
cardVerificationCodeElement.update({ cardBrand });
});
}
init();
import React, { useRef, useState } from 'react';
import {
BasisTheoryProvider,
CardNumberElement,
CardExpirationDateElement,
CardVerificationCodeElement,
useBasisTheory,
} from '@basis-theory/basis-theory-react';
export default function App() {
const { bt } = useBasisTheory('test_1234567890', { elements: true });
// Refs to get access to the Elements instance
const cardNumberRef = useRef(null);
const cardExpirationRef = useRef(null);
const cardVerificationRef = useRef(null);
// stores the current card brand in state, to pass to CardVerificationCodeElement
const [cardBrand, setCardBrand] = useState();
return (
<BasisTheoryProvider bt={bt}>
<CardNumberElement
id="myCardNumber"
ref={cardNumberRef}
onChange={({ cardBrand }) => setCardBrand(cardBrand)}
/>
<div style={{ display: 'flex' }}>
<div style={{ width: "100%" }}>
<CardExpirationDateElement
id="myCardExpiration"
ref={cardExpirationRef}
/>
</div>
<div style={{ width: "100%" }}>
<CardVerificationCodeElement
id="myCardVerification"
ref={cardVerificationRef}
cardBrand={cardBrand}
/>
</div>
</div>
</BasisTheoryProvider>
);
}
import Foundation
import UIKit
import BasisTheoryElements
import Combine
class ViewController: UIViewController {
@IBOutlet weak var cardNumberTextField: CardNumberUITextField!
@IBOutlet weak var expirationDateTextField: CardExpirationDateUITextField!
@IBOutlet weak var cvcTextField: CardVerificationCodeUITextField!
override func viewDidLoad() {
super.viewDidLoad()
// Binds card brand to verification code element
let cvcOptions = CardVerificationCodeOptions(cardNumberUITextField: cardNumberTextField)
cvcTextField.setConfig(options: cvcOptions)
}
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.basistheory.android.view.CardNumberElement
android:id="@+id/card_number"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<com.basistheory.android.view.CardExpirationDateElement
android:id="@+id/expiration_date"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<com.basistheory.android.view.CardVerificationCodeElement
android:id="@+id/cvc"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
class MainActivity : AppCompatActivity() {
private lateinit var cardNumberElement: CardNumberElement
private lateinit var cardExpirationDateElement: CardExpirationDateElement
private lateinit var cardVerificationCodeElement: CardVerificationCodeElement
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
cardNumberElement = findViewById(R.id.card_number)
cardExpirationDateElement = findViewById(R.id.expiration_date)
cardVerificationCodeElement = findViewById(R.id.cvc)
// Binds card brand to verification code element
cardVerificationCodeElement.cardNumberElement = cardNumberElement
}
}
Using a single Card Element
Alternatively, you can declare a single Card Element that features all three basic cardholder data inputs in a single element.
- JavaScript
- React
- iOS
- Android
<div id="card"></div>
import { BasisTheory } from '@basis-theory/basis-theory-js';
let bt;
let cardElement;
async function init () {
bt = await new BasisTheory().init('test_1234567890', { elements: true });
cardElement = bt.createElement('card');
await cardElement.mount('#card');
};
init();
import React, { useRef } from 'react';
import {
BasisTheoryProvider,
CardElement,
useBasisTheory,
} from '@basis-theory/basis-theory-react';
export default function App() {
const { bt } = useBasisTheory('test_1234567890', { elements: true });
// Ref to get access to the Element instance
const cardRef = useRef(null);
return (
<BasisTheoryProvider bt={bt}>
<CardElement
id="myCard"
ref={cardRef}
/>
</BasisTheoryProvider>
);
}
CardElement is not yet available for iOS. If you need this feature, please let us know.
https://basistheory.com/contact
CardElement is not yet available for Android. If you need this feature, please let us know.
https://basistheory.com/contact
Storing Cards
Now that you are securely capturing the cardholder data in your user-facing application(s), it is time to store it in your Basis Theory Tenant.
To do this, we will call the Create Token endpoint from the SDK, passing the Card Elements as data points in the payload. This way, the card information is securely transferred from the frontend Elements to the Basis Theory vault, where they will reside in the encrypted form.
Add a submit function along with a button to trigger it:
- JavaScript
- React
- iOS
- Android
<div id="cardNumber"></div>
<div style="display: flex;">
<div id="cardExpirationDate" style="width: 100%;"></div>
<div id="cardVerificationCode" style="width: 100%;"></div>
</div>
<button onclick="submit();">Submit</button>
import { BasisTheory } from '@basis-theory/basis-theory-js';
let bt;
let cardNumberElement;
let cardExpirationDateElement;
let cardVerificationCodeElement;
async function init () { ... }
async function submit () {
try {
const token = await bt.tokens.create({
type: 'card',
data: {
number: cardNumberElement,
expiration_month: cardExpirationDateElement.month(),
expiration_year: cardExpirationDateElement.year(),
cvc: cardVerificationCodeElement,
}
});
// store token.id in your database
} catch (error) {
console.error(error);
}
}
init();
import React, { useRef, useState } from 'react';
import {
BasisTheoryProvider,
CardNumberElement,
CardExpirationDateElement,
CardVerificationCodeElement,
useBasisTheory,
} from '@basis-theory/basis-theory-react';
export default function App() {
const { bt } = useBasisTheory('test_1234567890', { elements: true });
// Refs to get access to the Elements instance
const cardNumberRef = useRef(null);
const cardExpirationRef = useRef(null);
const cardVerificationRef = useRef(null);
// stores the current card brand in state, to pass to CardVerificationCodeElement
const [cardBrand, setCardBrand] = useState();
const submit = async () => {
try {
const token = await bt.tokens.create({
type: 'card',
data: {
number: cardNumberRef.current,
expiration_month: cardExpirationRef.month(),
expiration_year: cardExpirationRef.year(),
cvc: cardVerificationRef.current,
}
});
// store token.id in your database
} catch (error) {
console.error(error);
}
}
return (
<BasisTheoryProvider bt={bt}>
...
<button onClick={submit}>Submit</button>
</BasisTheoryProvider>
);
}
import Foundation
import UIKit
import BasisTheoryElements
import Combine
class ViewController: UIViewController {
@IBOutlet weak var cardNumberTextField: CardNumberUITextField!
@IBOutlet weak var expirationDateTextField: CardExpirationDateUITextField!
@IBOutlet weak var cvcTextField: CardVerificationCodeUITextField!
@IBAction func tokenize(_ sender: Any) {
let body: [String: Any] = [
"type": "card",
"data": [
"number": self.cardNumberTextField,
"expiration_month": self.expirationDateTextField.month(),
"expiration_year": self.expirationDateTextField.year(),
"cvc": self.cvcTextField
]
]
BasisTheoryElements.tokenize(body: body, apiKey: config.btApiKey!) { token, error in
guard error == nil else {
print(error)
return
}
// store token.id in your database
}
}
override func viewDidLoad() { ... }
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
...
<Button
android:id="@+id/submit_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:backgroundTint="#00A4BA"
android:text="Submit" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
class MainActivity : AppCompatActivity() {
private lateinit var cardNumberElement: CardNumberElement
private lateinit var cardExpirationDateElement: CardExpirationDateElement
private lateinit var cardVerificationCodeElement: CardVerificationCodeElement
private lateinit var button: Button;
private val bt = BasisTheoryElements.builder()
.apiKey("test_1234567890")
.build()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
cardNumberElement = findViewById(R.id.card_number)
cardExpirationDateElement = findViewById(R.id.expiration_date)
cardVerificationCodeElement = findViewById(R.id.cvc)
button = findViewById(R.id.submit_button)
button.setOnClickListener {
submit()
}
// Binds card brand to verification code element
cardVerificationCodeElement.cardNumberElement = cardNumberElement
}
private fun submit() {
val token = runBlocking {
bt.tokens.create(object {
val type = "card"
val data = object {
val number = cardNumberElement
val expiration_month = cardExpirationDateElement.month()
val expiration_year = cardExpirationDateElement.year()
val cvc = cardVerificationCodeElement
}
})
}
// store token.id in your database
}
}
CardElement
, simply pass its reference as the data
attribute of the token payload.The created card token
object contains the non-sensitive information about the tokenized card, which follows the Card Token specification:
{
"id": "3d0b4395-5581-4197-a1b8-2513b2db926d",
"type": "card",
"tenant_id": "30032404-e199-4e86-b61d-d1a0ac3927d7",
"data": {
"number": "XXXXXXXXXXXX4242",
"expiration_month": 12,
"expiration_year": 2025
},
"created_by": "cfcdc1e2-ea53-4c5b-8962-7b6c62d93dc8",
"created_at": "2023-04-14T13:56:45.9869344+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": [],
"expires_at": "2023-04-14T19:56:44.116+00:00",
"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.
Notice how the card number
, also known as Primary Account Number (PAN), has been masked before it is returned to your application. This default behavior prevents your application being pulled in PCI DSS scope, for it only comes in contact with a truncated portion of the cardholder data. Later in this guide we will learn how to customize masking.
Customizing the Card 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 abuse of credit cards, 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 come in contact with the plaintext data. For cards, it is common to index in the Primary Account Number (PAN), but 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:
- JavaScript
- React
- iOS
- Android
async function submit () {
try {
const token = await bt.tokens.create({
type: 'card',
data: {
number: cardNumberElement,
expiration_month: cardExpirationDateElement.month(),
expiration_year: cardExpirationDateElement.year(),
cvc: cardVerificationCodeElement,
},
fingerprintExpression: '{{ data.number }}',
});
} catch (error) {
console.error(error);
}
}
export default function App() {
const submit = async () => {
try {
const token = await bt.tokens.create({
type: 'card',
data: {
number: cardNumberRef.current,
expiration_month: cardExpirationRef.month(),
expiration_year: cardExpirationRef.year(),
cvc: cardVerificationRef.current,
},
fingerprintExpression: '{{ data.number }}',
});
} catch (error) {
console.error(error);
}
}
return (...);
}
class ViewController: UIViewController {
@IBAction func tokenize(_ sender: Any) {
let body: [String: Any] = [
"type": "card",
"data": [
"number": self.cardNumberTextField,
"expiration_month": self.expirationDateTextField.month(),
"expiration_year": self.expirationDateTextField.year(),
"cvc": self.cvcTextField
],
"fingerprint_expression": "{{ data.number }}"
]
BasisTheoryElements.tokenize(body: body, apiKey: config.btApiKey!) { token, error in
guard error == nil else {
print(error)
return
}
}
}
}
class MainActivity : AppCompatActivity() {
private fun submit() {
val token = runBlocking {
bt.tokens.create(object {
val type = "card"
val data = object {
val number = cardNumberElement
val expiration_month = cardExpirationDateElement.month()
val expiration_year = cardExpirationDateElement.year()
val cvc = cardVerificationCodeElement
}
val fingerprint_expression = "{{ data.number }}"
})
}
}
}
This will return a token.fingerprint
that can be used to distinguish the card, and optionally prevent creation of a duplicate token. See the example below:
- JavaScript
- React
- iOS
- Android
async function submit () {
try {
const token = await bt.tokens.create({
type: 'card',
data: {
number: cardNumberElement,
expiration_month: cardExpirationDateElement.month(),
expiration_year: cardExpirationDateElement.year(),
cvc: cardVerificationCodeElement,
},
fingerprintExpression: '{{ data.number }}',
deduplicateToken: true,
});
} catch (error) {
console.error(error);
}
}
export default function App() {
const submit = async () => {
try {
const token = await bt.tokens.create({
type: 'card',
data: {
number: cardNumberRef.current,
expiration_month: cardExpirationRef.month(),
expiration_year: cardExpirationRef.year(),
cvc: cardVerificationRef.current,
},
fingerprintExpression: '{{ data.number }}',
deduplicateToken: true,
});
} catch (error) {
console.error(error);
}
}
return (...);
}
class ViewController: UIViewController {
@IBAction func tokenize(_ sender: Any) {
let body: [String: Any] = [
"type": "card",
"data": [
"number": self.cardNumberTextField,
"expiration_month": self.expirationDateTextField.month(),
"expiration_year": self.expirationDateTextField.year(),
"cvc": self.cvcTextField
],
"fingerprint_expression": "{{ data.number }}",
"deduplicate_token": true
]
BasisTheoryElements.tokenize(body: body, apiKey: config.btApiKey!) { token, error in
guard error == nil else {
print(error)
return
}
}
}
}
class MainActivity : AppCompatActivity() {
private fun submit() {
val token = runBlocking {
bt.tokens.create(object {
val type = "card"
val data = object {
val number = cardNumberElement
val expiration_month = cardExpirationDateElement.month()
val expiration_year = cardExpirationDateElement.year()
val cvc = cardVerificationCodeElement
}
val fingerprint_expression = "{{ data.number }}"
val deduplicate_token = true
})
}
}
}
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
When a card token is created, the returned data follows a default masking pattern that reveals the last 4 digits of card. This is useful for displaying the card to the end-user without revealing the full number, generating receipts and payment history, etc.
However, being able to preserve the Bank Identification Number (BIN) from the card number can also be required for several reasons, including fraud detection, payment processing, customer service or account management. PCI DSS allows applications to reveal up to 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 what segments of the PAN is useful to you with a single expressions filter: card_mask
.
- JavaScript
- React
- iOS
- Android
async function submit () {
try {
const token = await bt.tokens.create({
type: 'card',
data: {
number: cardNumberElement,
expiration_month: cardExpirationDateElement.month(),
expiration_year: cardExpirationDateElement.year(),
cvc: cardVerificationCodeElement,
},
mask: {
number:'{{ data.number | card_mask: "true", "true" }}',
expiration_month: '{{ data.expiration_month }}',
expiration_year: '{{ data.expiration_year }}',
},
});
} catch (error) {
console.error(error);
}
}
export default function App() {
const submit = async () => {
try {
const token = await bt.tokens.create({
type: 'card',
data: {
number: cardNumberRef.current,
expiration_month: cardExpirationRef.month(),
expiration_year: cardExpirationRef.year(),
cvc: cardVerificationRef.current,
},
mask: {
number:'{{ data.number | card_mask: "true", "true" }}',
expiration_month: '{{ data.expiration_month }}',
expiration_year: '{{ data.expiration_year }}',
},
});
} catch (error) {
console.error(error);
}
}
return (...);
}
class ViewController: UIViewController {
@IBAction func tokenize(_ sender: Any) {
let body: [String: Any] = [
"type": "card",
"data": [
"number": self.cardNumberTextField,
"expiration_month": self.expirationDateTextField.month(),
"expiration_year": self.expirationDateTextField.year(),
"cvc": self.cvcTextField
],
"mask": [
"number": "{{ data.number | card_mask: 'true', 'true' }}",
"expiration_month": "{{ data.expiration_month }}",
"expiration_year": "{{ data.expiration_year }}"
],
]
BasisTheoryElements.tokenize(body: body, apiKey: config.btApiKey!) { token, error in
guard error == nil else {
print(error)
return
}
}
}
}
class MainActivity : AppCompatActivity() {
private fun submit() {
val token = runBlocking {
bt.tokens.create(object {
val type = "card"
val data = object {
val number = cardNumberElement
val expiration_month = cardExpirationDateElement.month()
val expiration_year = cardExpirationDateElement.year()
val cvc = cardVerificationCodeElement
}
val mask = object {
val number = "{{ data.number | card_mask: 'true', 'true' }}"
val expiration_month = "{{ data.expiration_month }}"
val expiration_year = "{{ data.expiration_year }}"
}
})
}
}
}
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 this filter.
Aliasing
By default, the generated token id
will be a Universally Unique Identifier (UUID).
- Backwards compatibility, customization, alternative for masking
Conclusion
By following the best practices prescribed in this guide, you can ensure that your user-facing applications are compliant with the latest security standards while protecting your users' sensitive data. 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, meeting compliance requirements and reducing the risk of exposure in case of data breaches.
TODO: add next steps