Skip to main content

Services

Services let you tokenize, retrieve, update, encrypt, and manage sensitive data collected by elements. Raw values never pass through your application — the SDK resolves element references directly from the secure iframe when it sends requests to the Basis Theory API.

How element references work

When you pass an element instance anywhere in a service call's data object, the SDK replaces it with the actual sensitive value at request time. Your code only ever holds an element reference — never the raw value.

const token = await bt.tokens.create({
type: 'card',
data: {
number: cardNumberEl, // ← element reference, not a string
expiration_month: expiryEl,
expiration_year: expiryEl,
cvc: cvvEl,
},
});

// token.id is all you get back — the PAN is never in this scope
console.log(token.id);

A card expiry element holds both the month and the year in a single MM/YY value. Reference the same expiry element from both expiration_month and expiration_year — the SDK detects that the two fields resolve to the same element, parses the value, and sends the numeric month and year separately. This applies anywhere element references are used, including proxy requests.

You can mix element references with plain values freely:

await bt.tokens.create({
type: 'token',
data: {
ssn: ssnElement, // sensitive — resolved from iframe
name: 'Jane Smith', // plain — sent as-is
account_id: '12345', // plain — sent as-is
},
metadata: {
source: 'checkout',
},
});

tokens.create

Creates a persistent token. Accepts element references in data at any depth.

const token = await bt.tokens.create({
type: 'card',
data: {
number: cardNumberEl,
expiration_month: expiryEl,
expiration_year: expiryEl,
cvc: cvvEl,
},
});

console.log(token.id); // 'tok_...'
console.log(token.type); // 'card'

Request options:

FieldTypeDescription
typestringToken type (e.g. 'card', 'token')
dataobjectData payload — may contain element references at any depth
metadataRecord<string, string>Non-sensitive key/value pairs stored with the token
containersstring[]Container paths that restrict access to this token
maskobjectMasking expression for the token response
aliasesstring[]Alternate IDs for the token
search_indexesstring[]Fields to index for search
fingerprint_expressionstringExpression used to generate the token fingerprint
expires_atstringISO 8601 expiration timestamp

Response (TokenizeResult):

FieldTypeDescription
idstringToken ID — safe to store and send to your backend
type'card' | 'token'Token type
maskstringMasked representation of the data
brandstringCard brand (card tokens only)
expiry{ month: number; year: number }Expiry (card tokens only)
metadataobjectMetadata stored with the token
try {
const token = await bt.tokens.create({
type: 'card',
data: {
number: cardNumberEl,
expiration_month: expiryEl,
expiration_year: expiryEl,
cvc: cvvEl,
},
metadata: { source: 'checkout' },
});

await sendToBackend(token.id);
} catch (err) {
handleError(err);
}

tokens.retrieve

Retrieves a token. By default, returns masked data. To access the raw value, the request must use a session API key authorized by a Private Application on your backend.

const token = await bt.tokens.retrieve(tokenId, {
apiKey: sessionApiKey, // obtained from an authorized session
});
Never call tokens.retrieve with a Public API key to display raw data. Use a session key authorized on your backend — this ensures Basis Theory audit logs capture the access.

tokens.update

Updates mutable token fields. The token type is immutable and cannot be changed. Element references are accepted in data.

await bt.tokens.update(tokenId, {
data: {
number: cardNumberEl,
expiration_month: expiryEl,
expiration_year: expiryEl,
cvc: cvvEl,
},
metadata: { updated_by: 'user' },
});

tokens.encrypt

Performs client-side JWE encryption using a provided EC public key in PEM format. The SDK uses ECDH-ES key agreement with A256GCM authenticated encryption — you need an EC key (X25519 or P-256), not an RSA key. The encrypted payload never touches the Basis Theory API — it is returned directly to your code for you to handle.

const encrypted = await bt.tokens.encrypt(
{
type: 'card',
data: {
number: cardNumberEl,
expiration_month: expiryEl,
expiration_year: expiryEl,
cvc: cvvEl,
},
},
publicKeyPEM, // EC public key in PEM format (ECDH-ES + A256GCM)
'key-id-123' // key identifier included in the JWE header
);

console.log(encrypted.encrypted); // JWE compact serialization string

You can also encrypt multiple tokens at once by passing an object:

const result = await bt.tokens.encrypt(
{
card: { type: 'card', data: { number: cardNumberEl, ... } },
ssn: { type: 'token', data: ssnEl },
},
publicKeyPEM,
'key-id-123'
);

console.log(result.card.encrypted);
console.log(result.ssn.encrypted);

tokenize

Creates multiple tokens in a single request with an arbitrary data shape. The response mirrors the structure you send, with each token replaced by its result.

const result = await bt.tokenize({
card: {
type: 'card',
data: {
number: cardNumberEl,
expiration_month: expiryEl,
expiration_year: expiryEl,
cvc: cvvEl,
},
},
ssn: {
type: 'token',
data: ssnEl,
},
});

console.log(result.card.id); // card token ID
console.log(result.ssn.id); // SSN token ID

Use tokenize when you need to create several tokens in one round-trip, or when you want to preserve a custom key structure in the response.


tokenIntents.create

Creates a short-lived token intent. Token intents are not persisted as tokens — they are validated server-side and converted to a token by your backend using the Basis Theory API. Use them when you want to validate card data before committing to a full token.

const intent = await bt.tokenIntents.create({
type: 'card',
data: {
number: cardNumberEl,
expiration_month: expiryEl,
expiration_year: expiryEl,
cvc: cvvEl,
},
});

console.log(intent.id); // send this to your backend

Token intents expire quickly. Pass intent.id to your backend, which authorizes it and converts it to a token using the Basis Theory API.

tokenIntents.get

Retrieves an existing token intent by ID. Requires a session API key (see Sessions).

const intent = await bt.tokenIntents.get(intentId, {
apiKey: sessionApiKey,
});

sessions.create

Creates a session that grants temporary elevated access to tokenized data. Sessions must be authorized by a Private Application on your backend before they can be used.

// 1. Create a session on the frontend
const session = await bt.sessions.create();

// 2. Send session.nonce to your backend to authorize it
const { apiKey } = await authorizeSessionOnBackend(session.nonce);

// 3. Use the authorized session API key to retrieve sensitive data
const token = await bt.tokens.retrieve(tokenId, { apiKey });

Session response:

FieldTypeDescription
noncestringOne-time nonce — send to your backend to authorize the session
expires_atstringISO 8601 expiration timestamp
Sessions are the correct mechanism for revealing tokenized data to a user (e.g., showing a saved card). The session nonce must be authorized server-side — it cannot be used directly from the frontend.

proxy

Forwards an HTTP request through Basis Theory to a third-party endpoint. Element references placed anywhere in the body are resolved to their raw values before the request leaves Basis Theory — so you can send a card number (or any value collected by a secure element) to an external API without it ever entering your application code.

The HTTP verb is chosen by the method name:

bt.proxy.get(options);
bt.proxy.post(options);
bt.proxy.put(options);
bt.proxy.patch(options);
bt.proxy.delete(options);

Request options (ProxyRequestOptions):

FieldTypeDescription
urlstringDestination URL (ephemeral proxy). Mutually exclusive with proxyKey
proxyKeystringKey of a pre-configured proxy. Mutually exclusive with url
headersRecord<string, string>Headers forwarded to the destination request
bodyobjectRequest body — may contain element references at any depth
includeResponseHeadersbooleanWhen true, the result is a ProxyResponse envelope instead of the raw response body
apiKeystringSession key that authorizes the call, overriding the key passed to BasisTheory() for this request only

You must specify exactly one of url or proxyKey — passing both, or neither, rejects with a validation error before any request is made.

Invoking the proxy requires a key with the proxy:invoke permission. The Public API key passed to BasisTheory() at initialization does not have it, so authorize each call with a backend-issued session key granted proxy:invoke, passed as apiKey — the v3 equivalent of v2's per-call session key. The examples below pass this key.

Response:

By default, proxy.* resolves to the destination's response body. When includeResponseHeaders: true, it resolves to a ProxyResponse:

FieldTypeDescription
dataunknownThe destination's response body
headersRecord<string, string>Response headers returned by the destination
The destination's response is returned directly to your application code. In v2, the proxy response was a synthetic representation by default — sensitive values stayed inside the element and were surfaced with setValue, rather than entering your code. In v3, proxy.* resolves to the destination's raw response body, so any sensitive data the destination returns (for example, a card number in a charge response) lands in your application and enters your compliance scope. If you need to keep sensitive values out of your application scope, use a response transform to tokenize or redact them before the response is returned — see the Issue Cards guide for an example that tokenizes cardholder data returned by the issuer. Treat the entire proxy response as potentially sensitive.

For authenticated destinations, use a pre-configured proxy (proxyKey). The destination's credentials are stored in the proxy configuration and injected server-side, so they never appear in browser code:

// Send collected card data to a third-party endpoint without it touching your server
const response = await bt.proxy.post({
proxyKey: '<YOUR_PROXY_KEY>', // destination auth is injected server-side by the pre-configured proxy
apiKey: sessionApiKey, // session authorized with proxy:invoke on your backend
body: {
amount: 4999,
card: {
number: cardNumberEl, // ← element reference, resolved inside the secure iframe
expiration_month: expiryEl,
expiration_year: expiryEl,
cvc: cvvEl,
},
},
});

// `response` is the destination's raw response body — treat it as sensitive.
// Do not log it directly; forward only a non-sensitive identifier to your backend or logs.

To call an arbitrary destination without a pre-configured proxy, pass url instead of proxyKey:

const response = await bt.proxy.post({
url: 'https://api.processor.com/charges',
apiKey: sessionApiKey,
body: { card: { number: cardNumberEl, expiration_month: expiryEl, expiration_year: expiryEl, cvc: cvvEl } },
});
A url request lets the browser choose the destination. If your page is compromised — cross-site scripting or a malicious dependency — attacker code can call proxy.* with the same element references and a destination it controls, forwarding sensitive values (card number, CVC) to an arbitrary HTTPS endpoint. SSRF protection blocks internal targets, not public attacker domains. Whenever the body carries sensitive element data, use a pre-configured proxy (proxyKey) so the destination is fixed in the proxy configuration server-side. Reserve url for non-sensitive payloads or destinations your backend independently allowlists.

Any headers you supply on a url request originate in the browser and are trivially extractable by anyone inspecting the page — never put processor or destination secrets (API keys, bearer tokens) in browser-supplied headers. For authenticated destinations, use a pre-configured proxy (proxyKey) so the credential is injected server-side and never reaches the client.

To inspect response headers, set includeResponseHeaders: true:

const { data, headers } = await bt.proxy.post({
url: 'https://api.processor.com/charges',
apiKey: sessionApiKey,
body: { /* ... */ },
includeResponseHeaders: true,
});

Proxy requests are not retried — they may be non-idempotent operations.

URL requirements

When you use url (ephemeral proxy), the destination is validated before any request is made. The request is rejected with a validation error if the URL:

  • Is not a valid, parseable URL.
  • Does not use the https: scheme — http: and all other schemes are rejected.
  • Contains userinfo (https://user:pass@host — credentials in the URL are not allowed).
  • Targets localhost, a loopback or link-local IPv6 address, or a private/internal IPv4 address (including encoded IP formats). This is SSRF protection.

Headers that cannot be forwarded

The SDK rejects the request (before sending) if headers contains any of the following. They are blocked to prevent request smuggling, connection manipulation, proxy bypass, and credential leakage:

GroupHeaders
Request smugglingHost, Content-Length, Transfer-Encoding, TE, Trailer
Connection controlConnection, Upgrade, Keep-Alive, Proxy-Connection
Proxy bypassVia, Proxy-Authorization
Basis Theory credentialsBT-API-KEY, BT-PROXY-KEY, BT-PROXY-URL, BT-IDEMPOTENCY-KEY
Forwarded / client IPX-Forwarded-For, X-Forwarded-Host, X-Real-IP
SerializationContent-Type

Header names are matched case-insensitively. Content-Type is rejected because the SDK owns serialization — the body is always sent as JSON.


Error handling

All service methods return promises and reject on failure. Errors follow a consistent shape:

try {
const token = await bt.tokens.create({ ... });
} catch (err) {
if (err.status === 400) {
// Validation error — field-level detail is in err.body.errors
console.error(err.body?.errors);
} else if (err.status === 401) {
// Invalid or expired API key
} else {
// Network error or unexpected failure
console.error(err.message);
}
}

Element validation errors surface before the API call. If an element's value is invalid or incomplete when you call a service, the SDK rejects the promise immediately with a validation error — no network request is made.

// If cardNumberEl has an incomplete or invalid value when this runs,
// the promise rejects synchronously before hitting the API
const token = await bt.tokens.create({
type: 'card',
data: { number: cardNumberEl, ... },
});

API naming conventions

SDK method and property names use JavaScript camelCase and are automatically converted to snake_case when sent to the Basis Theory API. The one exception is tokenize, which accepts snake_case keys directly in the request body (e.g. expiration_month).