Skip to main content

Receive Cards via API / Webhooks

When building APIs for various businesses and services like e-commerce platforms, payment processors and facilitators, financial institutions, subscription services, travel and hospitality companies, fintech startups, it may become necessary to receive cardholder data in the request payloads. However, it can be challenging to navigate PCI DSS and ensure that your application meets all the necessary security controls required to comply.

In this guide, we will demonstrate how to set up a Basis Theory Proxy to intercept API requests and webhooks containing cards, while securely storing the cardholder data as tokens with the Basis Theory Platform. Given this guide is followed step by step, you are substantially de-scoping your servers and database from PCI DSS compliance. If you want to learn more how we can help you meet up to 95% of the PCI requirements, or if you need help filling out your SAQ, reach out to our team!

Receive Cards

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)

Provisioning Resources

In this section, we will explore the bare minimum resources to create a Proxy for your API, that will receive cards and store them securely.

Management Application

To create all subsequent resources, you will need a Management Application.

Click here to create a Management Application or login to your Basis Theory account and create a new application with the following settings:

  • Name - Resource Creator
  • Application Type - Management
  • Permissions: application:create, proxy:create
Save the API Key from the created Management Application as it will be used later in this guide.

Private Application

Next you will need a Private Application to associate later with the Proxy. This will be used to create card tokens in the vault.

Using the Management Application key to authorize the request, call Basis Theory API to create a new Private Application:

curl "https://api.basistheory.com/applications" \
-X "POST" \
-H "BT-API-KEY: <API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"name": "Backend",
"type": "private",
"permissions": [ "token:create", "token:read" ]
}'
Be sure to replace <API_KEY> with the Management API Key you created previously.
Save the id from the created Private Application as it will be used later in this guide.

Pre-Configured Proxy

Now we will create the Proxy that will listen to HTTP requests containing card data that we need to store in our Basis Theory Tenant. To achieve that, we will leverage a Request Transform code that handles the request body to tokenize and redact the cardholder data, and include the new token.

The API contract is customizable and can follow any desired format:

In this example, we are handling application/json content type in the request payload.

payload.json
{
"reference": "REF1234",
"currency": "USD",
"payment_method": {
"number": "4242424242424242",
"expiration_month": 12,
"expiration_year": 2025,
"cvc": "123"
}
}
requestTransform.js
module.exports = async function (req) {
const { bt, args, configuration } = req;
const { body, headers } = args;

const { payment_method, ...rest } = body;

const token = await bt.tokens.create({
type: 'card',
data: payment_method,
})

return {
body: {
payment_method: token,
...rest
},
headers,
}
};

Let's store the contents of the requestTransform.js file into a variable:

request_transform_code=$(cat requestTransform.js)

And call Basis Theory API to create the Proxy:

curl "https://api.basistheory.com/proxies" \
-X "POST" \
-H "BT-API-KEY: <API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"name": "Gateway Proxy",
"destination_url": "https://echo.basistheory.com/anything",
"request_transform": {
"code": '"$(echo $request_transform_code | jq -Rsa .)"'
},
"application": {
"id": "45c124e7-6ab2-4899-b4d9-1388b0ba9d04"
},
"require_auth": false
}'

Important things to notice in the request above:

  1. <API_KEY> is the Management Application Key, used to authenticate the request;
  2. destination_url should be replaced with your API endpoint;
  3. 45c124e7-6ab2-4899-b4d9-1388b0ba9d04 is the id of the Private Application, which is used to initialize the bt instance injected in the req parameter;
  4. request_transform_code is passed in plaintext form;
  5. require_auth: false means that invoking the Proxy won't require a Basis Theory API Key.
Save the key from the created Proxy as it will be used later to invoke it.

Done! These are all the resources necessary. Let's see how to actually use them.

Invoking the Proxy

Let's see how your clients or partners would invoke your API through the Proxy.

curl 'https://api.basistheory.com/proxy' \
-X 'POST' \
-H 'Content-Type: application/json' \
-H 'BT-PROXY-KEY: TDEyQmkhQMpGiZd13FSRQ9' \
-d '{
"reference": "REF1234",
"currency": "USD",
"payment_method": {
"number": "4242424242424242",
"expiration_month": 12,
"expiration_year": 2025,
"cvc": "123"
}
}'
Be sure to replace TDEyQmkhQMpGiZd13FSRQ9 with the Proxy Key you created previously.

Your API at destination_url will be called with the client-informed payload, except the cardholder data would be replaced by the newly created token.

Customizing Tokens

The steps so far cover most cases when you need to receive cards in your API 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:

requestTransform.js
module.exports = async function (req) {
...
const token = await bt.tokens.create({
type: 'card',
data: payment_method,
fingerprintExpression: '{{ data.number }}',
})
...
};

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 }}"
},
"search_indexes": [],
"containers": [
"/pci/high/"
]
}

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

requestTransform.js
module.exports = async function (req) {
...
const token = await bt.tokens.create({
type: 'card',
data: payment_method,
fingerprintExpression: '{{ data.number }}',
deduplicateToken: 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

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 mechanisms, advanced payment processing routing, account type differentiation and other core functionality. 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.

requestTransform.js
module.exports = async function (req) {
...
const token = await bt.tokens.create({
type: 'card',
data: payment_method,
fingerprintExpression: '{{ data.number }}',
deduplicateToken: true,
mask: {
number:'{{ data.number | card_mask: "true", "true" }}',
expiration_month: '{{ data.expiration_month }}',
expiration_year: '{{ data.expiration_year }}',
},
})
...
};

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 }}"
},
"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. It is also common to determine your own custom token format when creating a payments API.

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

module.exports = async function (req) {
...
const token = await bt.tokens.create({
id: generateTokenId(),
type: 'card',
data: payment_method,
fingerprintExpression: '{{ data.number }}',
deduplicateToken: true,
mask: {
number:'{{ data.number | card_mask: "true", "true" }}',
expiration_month: '{{ data.expiration_month }}',
expiration_year: '{{ data.expiration_year }}',
},
})
...
};

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 }}"
},
"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.

Key Considerations

Authentication

The Proxy we configured in this guide doesn't require a Basis Theory API Key to be invoked. Most-likely you will need to assert authentication on requests before the Proxy starts processing the payload for tokenization.

To achieve that, you can make a call to your authentication server as the first step in the Request Transform code. For example:

const fetch = require('node-fetch');
const { AuthenticationError } = require('@basis-theory/basis-theory-reactor-formulas-sdk-js');

module.exports = async function (req) {
const { bt, args, configuration } = req;
const { body, headers } = args;

// forwards Authorization header to auth server
const response = await fetch('https://auth.example.com', {
method: 'post',
headers: { 'Authorization': headers['Authorization'] },
});
const { authenticated } = await response.json();

if (!authenticated) {
// returns a 401 to the requester
throw new AuthenticationError();
}

// do tokenization
...
}

Custom Hostname

Requesting your customers or partners to invoke an API such as https://api.basistheory.com/proxy?bt-proxy-key=TDEyQmkhQMpGiZd13FSRQ9 may not be the most elegant approach in some circumstances.

If you want to have a custom hostname like https://secure.yourdomain.com or https://payments.yourservice.com for your Pre-Configured Proxy, follow these steps.

Conclusion

The best practices prescribed in this guide ensure that your APIs are compliant with the PCI-DSS standards and your clients' sensitive card data is protected. The token.id forwarded to your API by the Proxy 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: