What are Reactors?
A Reactor is a serverless compute service allowing Node.js code hosted in Basis Theory to be executed against your tokens completely isolated away from your application and systems.
Reactors are invokable from any system that has the ability to make HTTPS requests and access the internet.
How It Works
Reactors are serverless function runtimes, similar to AWS Lambda, Azure Functions, or Cloudflare Workers - except your applications, systems, and infrastructure never touches the sensitive plaintext data.
Reactor Formulas
Using a Reactor starts with the code you’d like to execute - this code block is referred to as a Reactor Formula.
When creating a Reactor Formula you will define the code
, expected request parameters, and any up-front configuration - like secrets - required to execute your function.
Find an exhaustive list of all configuration options in our Reactor Formula API Reference.
Formulas support two different response types, giving you the flexibility to either securely tokenize sensitive outputs or to return raw outputs in the response:
tokenize
- Any object passed will be tokenized.
raw
- Any object passed will be returned from the HTTP request that invoked the Reactor.
module.exports = async function (req) {
// code to execute
return {
tokenize: {
bar: "secret", // will be tokenized
},
raw: {
foo: "plaintext", // will be returned in plaintext
},
};
};
Creating a Reactor
Reactors are created with our Create Reactor endpoint. Once configured each Reactor can be invoked - executing the formula it has been configured with. Creating a new Reactor is as simple as passing in the configuration and Formula to be invoked.
curl "https://api.basistheory.com/reactors" \
-H "BT-API-KEY: <API_KEY_HERE>" \
-H "Content-Type: application/json" \
-X "POST" \
-d '{
"name": "My First Reactor",
"configuration": {
"SERVICE_API_KEY": "key_abcd1234"
},
"formula": {
"id": "17069df1-80f4-439e-86a7-4121863e4678"
}
}'
Invoking a Reactor
Any reactor can be invoked either synchronously or asynchronously depending on the parameters provided within your request.
Reactors may be invoked by any private Application with token:use
permission, which enables the Reactor to detokenize tokens
provided in the request args
. It is recommended that you restrict which tokens a Reactor can detokenize
by only granting token:use
permission on the most-specific container
of tokens that is required.
Synchronous Reactors
Reactors are invoked synchronously by default, unless a callback_url
is specified in your request.
Synchronous reactors are limited to 10 seconds of execution time before they are terminated. If you anticipate your reactor workload
will take longer than this limit, please consider using Asynchronous Reactors instead.
curl "https://api.basistheory.com/reactors/5b493235-6917-4307-906a-2cd6f1a90b13/react" \
-H "BT-API-KEY: key_N88mVGsp3sCXkykyN2EFED" \
-X "POST" \
-d '{
"args": {
"card": "{{fe7c0a36-eb45-4f68-b0a0-791de28b29e4}}",
"customer_id": "myCustomerId1234"
}
}'
Asynchronous Reactors
Reactors are invoked asynchronously by providing a callback_url
parameter within the request.
The reactor will perform some synchronous validation to ensure the request is valid,
then immediately respond with an empty HTTP response with 202 Accepted
status code.
Your reactor code will then be asynchronously executed in our serverless compute environment for up to the configured timeout specified in the timeout_ms
parameter. If not specified, this value defaults to 10000
(i.e. 10 seconds). If the timeout period is exceeded, the reactor will be terminated and you will receive
a callback containing a Reactor Runtime Error indicating that a timeout occurred.
Once the reactor is completed, the response from the reactor code
will be delivered in the body of an HTTP POST
request to your callback_url
.
Your callback endpoint must be hosted using HTTPS, and should respond with a 2xx
status code after receiving the message. If your servers fail to
respond with a 2xx
status code, delivery will retried up to 10 times with exponential backoff over the next ~2.5 hours.
Any errors that occur while executing the reactor will be delivered to the callback_url
. See Reactor Errors for details.
curl "https://api.basistheory.com/reactors/5b493235-6917-4307-906a-2cd6f1a90b13/react" \
-H "BT-API-KEY: key_N88mVGsp3sCXkykyN2EFED" \
-X "POST" \
-d '{
"args": {
"card": "{{fe7c0a36-eb45-4f68-b0a0-791de28b29e4}}",
"customer_id": "myCustomerId1234"
},
"callback_url": "https://my-service.com/webhooks/transactions/b1d16efc-f613-45af-a1d5-57b07ddd741b",
"timeout_ms": 60000
}'
Common Use Cases
Call a 3rd party
Depending on how complex your use case is a Reactor may provide you with an excellent opportunity to mutate data before forwarding it onto a 3rd Party. In the below example, we call httpbin.org (an echo service) with the last 4 characters of our token:
const fetch = require("node-fetch");
module.exports = async function (req) {
const { customer_id } = req.args;
const last4 = customer_id.substring(-4);
const response = await fetch("https://httpbin.org/post", {
method: "POST",
body: last4,
});
const raw = await response.json();
return {
raw,
};
};
Create a pdf document
Creating documents out of sensitive data is a primary need for businesses today, especially in fintech where you need to create and submit 1099s for many businesses:
const fetch = require("node-fetch");
const PDFDocument = require("pdfkit");
module.exports = async function (req) {
const {
token: { data },
} = req.args;
let doc = new PDFDocument();
doc.fontSize(8).text(`Some token data on a pdf: ${data}`, 1, 1);
doc.end();
//Send or upload file to your partner
const response = await fetch("https://httpbin.org/post", {
method: "POST",
body: doc,
});
const raw = await response.json();
return {
raw,
};
};
Generate a text file and send to an SFTP server
Many legacy business process still rely heavily on comma delimited files (CSV), tab delimited files or space-delimited files to transport data between companies, typically using SFTP servers as the endpoint of this data. For example, engaging with partner banks with ACH files requires you to format your file correctly and drop it on to an SFTP server.
const { Client } = require('ssh2');
module.exports = async function (req) {
const { HOST, USERNAME, PASSWORD } = req.configuration;
const data = req.args;
const conn = new Client();
await new Promise((resolve, reject) => {
conn
.on('error', (error) => {
reject(error);
})
.on('ready', () => {
conn.sftp((err, sftp) => {
const writeStream = sftp.createWriteStream('export.csv');
writeStream.on('close', function () {
resolve();
});
data.forEach((row) => {
writeStream.write(row.join(','));
writeStream.write('\n');
});
writeStream.end();
});
})
.connect({
host: HOST,
port: 22,
username: USERNAME,
password: PASSWORD,
debug: console.log,
});
}).finally(() => {
conn.end();
});
return {
raw: {
status: 'ok',
},
};
};
Import file from a partner
When you need to process files of sensitive data without it touching your systems, use a Reactor to desensitize a file before forwarding it on to your systems for your own logic:
module.exports = async function (req) {
const { bt, args } = req;
const { fileString } = args; // "name,ssn\nTheory,555445555"
const rows = fileString.split("\n").map((r) => r.split(","));
await Promise.all(
rows.slice(1).map((row) => {
return bt.tokens
.create({
type: "social_security_number",
data: row[1],
})
.then((token) => (row[1] = token.id));
})
);
const desensitizedFile = rows.map((row) => row.join(",")).join("\n");
return {
raw: desensitizedFile
}
};
Anything you can imagine
When our templates and examples aren’t enough, we enable you to build anything you want to with our Reactors. Start with a blank function like the one below and solve any business problem with the data you need:
module.exports = async function (req) {
const { tokens } = req.args;
// Anything you can dream up 💭
return {
tokenize: { foo: "bar" }, // tokenize data
raw: { foo: "bar" }, // return any data
};
};
FAQ
When do I use a Reactor?
When you’re required to write your own code to solve more complex problems - for example when needing to manipulate the data, create a document, or import a file from a partner.
When would I use the Proxy instead of a Reactor?
When making a simple HTTP request, a simpler implementation can be created using the Proxy.
What does the development lifecycle look like for building Reactors?
Each Reactor runs a single function which can be scoped, coded, and tested all within your normal development tooling and lifecycles. Code written and pushed to your own Github repositories can be used to create new Reactor Formulas using our Terraform Provider or API integrations.
Can we run the reactor code locally to test?
Absolutely, each function you write for your Reactors can be run and tested locally. This code can be treated exactly the same as the existing application code you’re deploying to other infrastructure.
Is there a cold start spin-up time for Reactors?
Our Reactors are designed to be always hot - which allows for fast execution with little spin-up time.
Is there a concept of “sandbox” Reactors?
Our Reactors follow the same development lifecycle as the rest of our platform, allowing you to create a new Tenant to handle any testing of your platform from your staging or development environments.
What are the IP addresses for BT?
We have the list of our public IP addresses here.
How can Reactors reduce the PCI compliance scope of my application?
Using our Reactors allows you to execute code against any PCI classified data, enabling your infrastructure to stay out of PCI compliance.