Skip to main content

Receive Cards via API / Webhooks

Payments aren't just a payments problem; they're a gateway to innovation across industries. Whether you're building an e-commerce platform, a vertical SaaS solution for logistics or hospitality, or enabling AI agents to handle programmatic purchases, the need to accept sensitive cardholder data through APIs is inevitable. Yet, as you've likely discovered, integrating this data often triggers a compliance nightmare: PCI DSS requirements that balloon your security scope, slow down development, and force tough trade-offs between innovation and risk.

In this guide, we'll show you how to build PCI-free payment APIs using Basis Theory's Proxy to intercept incoming requests and webhooks, seamlessly tokenizing card data before it ever touches your systems. By following these steps, you'll keep your servers and databases out of the sensitive data flow while maintaining full control over routing, customization, and optimization.

Receive Cards Diagram

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

You will need a Management Application to provision resources. Click here to create one using the Basis Theory Customer Portal.

This will create an application with the following Access Controls:

  • Permissions: application:create, proxy:create
Save the key from the created Management Application as it will be used later in this guide.

Public Application

You will need a Public Application to authenticate requests. Click here to create one using the Basis Theory Customer Portal.

This will create an application with the following Access Controls:

  • Permissions: token-intent:create
Save the id from the created Public 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 vault in our Basis Theory Tenant. To achieve that, we will leverage a Request Transform code that handles the request body to create a Token Intent, which temporarily stores the card information and can be later converted to a long-term 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
const { BasisTheoryClient } = require('@basis-theory/node-sdk-2');

module.exports = async function (req) {
const {
applicationOptions: { apiKey },
args: { body, headers }
} = req;

const client = new BasisTheoryClient({ apiKey });

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

const tokenIntent = await client.tokenIntents.create({
type: 'card',
data: payment_method,
})

return {
body: {
payment_method: tokenIntent,
...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: <MANAGEMENT_API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"name": "Gateway Proxy",
"destination_url": "https://echo.basistheory.com/anything",
"request_transforms": [
{
"code": '"$(echo $request_transform_code | jq -Rsa .)"'
}
],
"application": {
"id": "<PUBLIC_APPLICATION_ID>"
},
"require_auth": false
}'

Important things to notice in the request above:

  1. <MANAGEMENT_API_KEY> is the Management Application Key, used to authenticate the request above;
  2. destination_url should be replaced with your API endpoint;
  3. request_transform_code is passed in plaintext form;
  4. <PUBLIC_APPLICATION_ID> is the Public Application id which will be used to create Token Intents;
  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 partners/customers would invoke your API through the Proxy.

curl 'https://api.basistheory.com/proxy' \
-X 'POST' \
-H 'Content-Type: application/json' \
-H 'BT-PROXY-KEY: <PROXY_KEY>' \
-d '{
"reference": "REF1234",
"currency": "USD",
"payment_method": {
"number": "4242424242424242",
"expiration_month": 12,
"expiration_year": 2025,
"cvc": "123"
}
}'
Be sure to replace PROXY_KEY 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.

Don't want customers calling a "basistheory.com" domain? Check the Custom Hostname section.

Key Considerations

Token Intents

The Token Intents that we are creating in the Proxy's Request Transform are ephemeral by nature, and were designed to streamline collection and validation from public-facing applications (e.g., checkout pages, payment APIs, etc.). This approach mitigates risks from public API key abuse, reduces unexpected costs from fraudulent Tokens, and optimizes performance for high-volume or untrusted data flows, keeping your systems PCI-free.

Once you receive the request containing the token intent, you can use it to verify or charge the card (e.g., via a payment service provider) before converting the Token Intent to a long-term Token.

Authentication

The Proxy we configured in this guide doesn't require a Basis Theory API Key to be invoked. We strongly recommend that you assert authentication on the incoming requests forward by the Proxy to your endpoint.

If you need to assert authentication before tokenization, you can make a call to your authentication server as the first step in the Request Transform code. For example:

requestTransform.js
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
...
}

Alternatively, if you use JSON Web Tokens (JWT), you can verify the JWT's signature using a public key before the Proxy processes the payload for tokenization. This approach eliminates the need for an additional network request to your APIs, streamlining client authentication and improving performance.

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

const verifySignature = async (token, publicKeyPem) => {
const key = await jose.JWK.asKey(publicKeyPem, "pem");
const verifier = await jose.JWS.createVerify(key);
const result = await verifier.verify(token);
return JSON.parse(result.payload.toString());
};

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

try {
const jwtPayload = await verifySignature(
headers['Authorization'], // getting the JWT from the Authorization header
configuration.PUBLIC_KEY_PEM // getting PEM-encoded RSA public key from the Proxy configuration
);
} catch (error) {
// returns a 401 to the requester
throw new AuthenticationError();
}

// do tokenization
...
}

Monitoring

With requests flowing through Basis Theory API first before hitting your endpoints, it may be tricky to monitor and identify issues that happen top-funnel.

Subscribing to Webhooks is the best way to gain observability in such requests, specifically by ingesting proxy.invoked events.

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 tokenIntent.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.

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