Agentic Payments Implementation
This guide builds the whole Agentic Payments flow: register a source as a payment method, create an allowance for one merchant, verify the customer where a rail requires it, and mint a credential in the format your checkout needs. It closes with the failure modes worth handling before launch.
The worked example uses a Basis Theory vaulted card as the source and walks the rails, ceremonies, and credential formats that source produces. Those are data reported by the resources, not fixed steps: where your responses return different rails, providers, or formats, follow what they return rather than the example values.
The examples use a test tenant and https://api.test.basistheory.com; they can be run before production onboarding and never reach a live payment provider. At go-live, use https://api.basistheory.com with production-tenant keys. Do not send test-tenant traffic to the production hostname.
Read the Agentic Payments overview first if you have not yet; this guide assumes you know what a rail is and why minting spends the allowance.
Here is the whole flow, with the rails this guide's card source produces, before you start building it:
Getting Started
To get started, you will need to create a Basis Theory Account and a TEST Tenant.
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:create,agentic:payment-method:create,agentic:allowance:verify
key from the created Public Application as it will be used later in this guide.Private Application
You will need a Private Application to allow your backend to call Basis Theory APIs. Click here to create one using the Basis Theory Customer Portal.
This will create an application with the following Access Controls:
- Permissions:
agentic:payment-method:get,agentic:payment-method:delete,agentic:allowance:create,agentic:allowance:get,agentic:allowance:update,agentic:allowance:delete,agentic:credential:create,agentic:credential:get
key from the created Private Application as it will be used later in this guide.Both public permissions are also valid on private applications. Add them to your private application if your backend creates or retries payment methods, drives verification in automated tests, or otherwise performs those operations server-side.
The Agentic permissions do not grant token access. Agentic Payments resolves the referenced source within the tenant. If the same public application also creates that token, give it the token-creation permission required by your chosen collection flow.
Create a Card Token Source
Every payment method starts from a source, in this example the source type is a Basis Theory card token. Agentic Payments never accepts a raw card number — only the token ID — which keeps the card in your vault and out of every agentic request.
If you already store cards with Basis Theory, you have this token and can skip ahead. If you are starting from a checkout form, collect the card with Elements so the raw card never reaches your servers, or send it directly to the Create Token API if your systems are already in PCI scope.
curl 'https://api.test.basistheory.com/tokens' \
-X 'POST' \
-H 'BT-API-KEY: <PRIVATE_API_KEY>' \
-H 'Content-Type: application/json' \
--data '{
"type": "card",
"data": {
"number": "4242424242424242",
"expiration_month": 12,
"expiration_year": 2030,
"cvc": "123"
}
}'
{
"id": "7d9f4a48-4f14-4b29-9f0a-3b4dd75f6c21",
"tenant_id": "77cb0024-123e-41a8-8ff8-a3d5a0fa8a08",
"type": "card",
"mask": {
"number": "XXXXXXXXXXXX4242",
"expiration_month": 12,
"expiration_year": 2030
},
"fingerprint": "AKCUXS83DokKo4pDRKSAy4d42t9i8dcP1X2jijwEBCQH",
"created_at": "2030-07-06T17:29:00.000Z"
}
Save the token id. It is the source for every payment method you create from this card.
The token must be a card token that Basis Theory can read within the same tenant. Anything else, including a card_number token, is rejected with 400 INVALID_TOKEN.
Create a Payment Method
A payment method registers a source for agentic use; in this guide, that source is the card token from the previous step. This is the call that makes it spendable later: Basis Theory registers it with every provider that supports it, in parallel, and reports the outcome of each registration separately.
Create one with the Create Payment Method API. The source object identifies what is being registered, so switch on its type rather than assuming a card. The consumer.email is required because rails that verify the customer use it to identify them.
This operation accepts either a public or private application with agentic:payment-method:create. The example uses the public application so a frontend that just tokenized the card can register it without forwarding the token ID through your backend.
curl 'https://api.test.basistheory.com/agentic/payment-methods' \
-X 'POST' \
-H 'BT-API-KEY: <PUBLIC_API_KEY>' \
-H 'BT-IDEMPOTENCY-KEY: pm-shopper-primary-card-v1' \
-H 'Content-Type: application/json' \
--data '{
"source": {
"type": "basis_theory_card_token",
"token_id": "7d9f4a48-4f14-4b29-9f0a-3b4dd75f6c21"
},
"consumer": {
"email": "shopper@example.com"
}
}'
{
"id": "pm_h2Kd91mAqLwX",
"source": {
"type": "basis_theory_card_token",
"token_id": "7d9f4a48-4f14-4b29-9f0a-3b4dd75f6c21"
},
"status": "active",
"consumer": {
"email": "shopper@example.com",
"id": "8e7d3f98-3e7b-4ab2-bd48-44e70cc8ec3f"
},
"card": {
"brand": "visa",
"bin": "424242",
"last4": "4242",
"expiration_month": 12,
"expiration_year": 2030,
"funding": "credit",
"display": {
"art_url": "https://assets.vims.visa.com/vims/cardart/6f7c1e9d",
"background_color": "#1A1F71",
"foreground_color": "#FFFFFF",
"description": "Visa Signature",
"issuer_name": "Example Bank"
}
},
"rails": [
{
"rail": "agentic-token",
"provider": "vic",
"status": "enabled",
"provider_ids": {
"vpan_enrollment_id": "6f7c1e9d",
"provisioned_token_id": "8a2b4c1f"
}
},
{
"rail": "spt",
"provider": "stripe",
"status": "enabled",
"provider_ids": {
"payment_method_id": "pm_1RgaXk",
"customer_id": "cus_Sc0FpX"
}
}
],
"created_at": "2030-07-06T17:30:00.000Z",
"updated_at": "2030-07-06T17:30:00.000Z"
}
Save the payment method id. Save consumer.id too, whether you supplied it or let Basis Theory generate it: it is how you list one customer's payment methods later with GET /agentic/payment-methods?consumer_id={consumer_id}.
Read the Rails Before You Use the Payment Method
A 201 means the payment method exists.
It does not mean the card is spendable. Each rail was provisioned at a third party and reports its own status.
| Status | Meaning | What to Do |
|---|---|---|
enabled | The rail provisioned successfully | Create allowances against it |
pending | Provisioning will finish asynchronously | Retry the rail to pick up the result |
error | The provider rejected or failed the registration | Read error.code, then retry the rail if the cause is transient |
A rail with status: "error" always carries an error object with a stable code. A rail in any other status never carries one.
Creation deliberately succeeds even when every rail fails: the payment method is persisted with its failures readable at GET /agentic/payment-methods/{payment_method_id}/errors, and each rail can be retried in place.
Retry one rail at a time with the Retry Payment Method Rail API. Only pending and error rails can be retried.
curl 'https://api.test.basistheory.com/agentic/payment-methods/pm_h2Kd91mAqLwX/rails/retry' \
-X 'POST' \
-H 'BT-API-KEY: <PUBLIC_API_KEY>' \
-H 'Content-Type: application/json' \
--data '{
"rail": "spt",
"provider": "stripe"
}'
The response is the complete updated payment method, so you can read the new rail status from it directly. A provider that rejects the retry keeps the rail in error and answers with the code naming the refusal, which is not always a 422: 422 PROVIDER_ENROLLMENT_FAILED, 422 RAIL_ENROLLMENT_INVALID, and 422 CARD_REJECTED are conclusive provider rejections, while 409 RAIL_ALREADY_ENROLLED means the provider already holds this enrollment. Branch on type, not on the status.
Selecting a Rail
The rail and provider pair together identify a rail. Read both from the resource and echo both back on any request that selects one. The provider is decided by the source rather than by you, and the API reference lists every valid rail and provider combination.
The provider_ids on each rail are that provider's own reference identifiers. They are informational: quote them in a support conversation, but do not build logic on their shape, which varies by provider and can change.
Rendering the Card to Your Customer
card.display is the issuing network's own presentation data for the card, populated from whichever network registered it. Use it instead of guessing at issuer branding: art_url for the issuer's card art, background_color and foreground_color for a card tile when there is no art, description for the product name such as Visa Signature, and issuer_name for the bank.
Every field is optional, so fall back to {brand} •••• {last4} when one is absent. A rail retry that eventually succeeds backfills display if the first attempt did not return it.
Creating the Same Source Twice
When supplied, BT-IDEMPOTENCY-KEY defines whether two create calls are the same operation. Reusing the same key and body returns the first payment method. A different key, or omitting the key, creates a distinct payment method even for the same source and consumer. Persist the payment method ID or list by consumer_id if your application wants to reuse one registration.
DELETE /agentic/payment-methods/{payment_method_id} revokes everything downstream. Every allowance backed by the payment method is cancelled, including any provider-side mandates, and no further verification or minting is possible on any of them. Treat it as the customer revoking the source, not as cleanup.Create an Allowance
An allowance is the mandate. It records the amount, the merchant, a customer-facing description of what the spending is for, and when it expires. It is the resource you show a customer for approval, so it deliberately contains nothing provider-specific: no source details, no network or processor artifacts.
Create one with the Create Allowance API. Amounts are major-unit decimal strings paired with an ISO 4217 currency, and expires_at must be in the future.
curl 'https://api.test.basistheory.com/agentic/allowances' \
-X 'POST' \
-H 'BT-API-KEY: <PRIVATE_API_KEY>' \
-H 'BT-IDEMPOTENCY-KEY: allowance-office-supplies-v1' \
-H 'Content-Type: application/json' \
--data '{
"payment_method_id": "pm_h2Kd91mAqLwX",
"amount": { "value": "100.00", "currency": "USD" },
"merchant": {
"name": "Acme Store",
"url": "https://acme.example.com",
"country_code": "US",
"category_code": "5732"
},
"description": "Buy approved office supplies from Acme Store",
"expires_at": "2030-07-08T00:00:00.000Z",
"metadata": { "order_ref": "po-1138" }
}'
{
"id": "alw_Xw92mKvB3dQz",
"payment_method_id": "pm_h2Kd91mAqLwX",
"status": "active",
"amount": { "value": "100.00", "currency": "USD" },
"amount_spent": { "value": "0.00", "currency": "USD" },
"amount_reserved": { "value": "0.00", "currency": "USD" },
"amount_available": { "value": "100.00", "currency": "USD" },
"credentials_count": 0,
"merchant": {
"name": "Acme Store",
"url": "https://acme.example.com",
"country_code": "US",
"category_code": "5732"
},
"description": "Buy approved office supplies from Acme Store",
"metadata": { "order_ref": "po-1138" },
"expires_at": "2030-07-08T00:00:00.000Z",
"rails": [
{
"rail": "agentic-token",
"provider": "vic",
"status": "pending_verification",
"credential_formats": ["card", "network-token", "mpp"],
"provider_ids": { "instruction_id": "vic_ins_4f8a" }
},
{
"rail": "spt",
"provider": "stripe",
"status": "active",
"credential_formats": ["identifier", "mpp"]
}
],
"created_at": "2030-07-06T17:31:00.000Z",
"updated_at": "2030-07-06T17:31:00.000Z"
}
Save the allowance id. Everything that follows, verification and minting alike, is scoped to it.
The Rails Tell You What Happens Next
The two statuses in that response are the whole story of the rest of this guide.
A rail that arrives active needs nothing further. Its provider requires no verification ceremony, so you can mint from it right now. In this response, that is spt.
A rail that arrives pending_verification cannot mint yet. Its provider will not release a credential until the customer approves this specific mandate, and until that happens, minting from it returns 400 RAIL_NOT_ACTIVE. In this response, that is agentic-token, which the next section verifies.
A rail that failed at its provider comes back error, and as with payment methods, the allowance is still created. Retry that rail with the Retry Allowance Rail API rather than rebuilding the mandate. Only error rails can be retried, and the matching payment method rail must still be enabled.
Each allowance rail also advertises the credential_formats it can produce. Read that list rather than assuming: it is how an agent picks a representation without knowing anything about the source underneath.
Allowance Status Is Not Rail Status
| Field | Values | Describes |
|---|---|---|
Allowance status | active, cancelled, expired | The mandate. active means it has not been cancelled and has not expired |
Rail status | active, pending_verification, error | One way of spending that mandate |
An allowance whose status is active can have no usable rail at all. Before verifying or minting, read the rail you intend to use, not the allowance.
Using Metadata for Your Own References
metadata is optional JSON that you own: Basis Theory stores it, returns it unchanged, and never acts on it or sends it to a provider. Use it to correlate the allowance with your own order, cart, or workflow, and do not put anything sensitive in it.
Verify the Customer with Web Agentic
An allowance rail in pending_verification requires the customer to approve the mandate before it can mint a credential. The Web Agentic SDK runs the complete browser flow: it advances the verification API, collects device context, renders the customer prompts, opens the provider-owned ceremonies, and resolves only after the API reports the rail active.
Skip this section when every rail you intend to mint from is already active.
Install the SDK
- npm
- Yarn
- pnpm
npm install @basis-theory/web-agentic
yarn add @basis-theory/web-agentic
pnpm add @basis-theory/web-agentic
Initialize one SDK instance with the public application key you created at the start of this guide. The key needs only agentic:allowance:verify.
import { AgenticVerification } from "@basis-theory/web-agentic";
export const agenticVerification = AgenticVerification({
apiKey: "<PUBLIC_API_KEY>",
displayName: "Example Agent",
});
For a test tenant, also set apiBaseUrl to https://api.test.basistheory.com/agentic. Production uses https://api.basistheory.com/agentic by default.
Start Verification
Pass the allowance ID and the exact rail and provider pair from the allowance to verifyAllowance. The allowance created earlier in this guide returned rail agentic-token with provider vic, so that is what the example passes.
import { agenticVerification } from "./agenticVerification.js";
const result = await agenticVerification.verifyAllowance(
"alw_Xw92mKvB3dQz",
{
rail: "agentic-token",
provider: "vic",
},
);
console.log(result.status); // "active"
Do not infer the provider from source details such as a card brand or number. Read the rail and provider pair from the allowance response and carry it into the frontend state that starts verification.
The SDK handles every supported ceremony branch. For the card-network providers, for instance, that means:
- Visa session initialization, OTP method selection, code entry, passkey registration, and passkey authentication
- Mastercard's hosted managed-authentication window and authoritative
completepolling - Recoverable restarts, invalid OTP retries, popup closure, cancellation, timeouts, and teardown
The built-in UI opens customer-facing popups from its own confirmation buttons, preserving the browser user activation that WebAuthn requires.
This is what the customer sees for this guide's worked example — the agentic-token rail with provider vic — rendered by the built-in UI with the default appearance:
1Select a verification method
2Enter the code
3Confirm the Visa ceremony
4Verification in progress (popup)
5Verification completeWith the Mastercard provider, the SDK renders a single confirmation that opens Mastercard's hosted verification window instead of the OTP and passkey prompts; the progress and outcome screens are the same. The full set of built-in states, including retries and errors, is shown in the Web Agentic SDK reference.
Customize the Verification Experience
The default UI renders in a Shadow DOM, so application styles do not leak into the verification modal. Set the four appearance colors to match your interface and override any user-facing copy with strings.
const agenticVerification = AgenticVerification({
apiKey: "<PUBLIC_API_KEY>",
appearance: {
primaryColor: "#1d4ed8",
secondaryColor: "#e5e7eb",
backgroundColor: "#ffffff",
fontColor: "#111827",
},
strings: {
methodSelectTitle: "Confirm your identity",
continueButton: "Continue securely",
successTitle: "You're verified",
},
});
primaryColor controls primary buttons, selected methods, focus states, and spinners. secondaryColor controls cards, code inputs, and secondary buttons. backgroundColor sets the modal surface, and fontColor sets headings and labels.
When one prompt needs application-owned UI, pass only that handler and leave ui: true. This example replaces the OTP screen while the SDK keeps rendering method selection, ceremony confirmations, progress, retries, and success:
const agenticVerification = AgenticVerification({
apiKey: "<PUBLIC_API_KEY>",
handlers: {
collectOtp: ({ method, maxAttempts, attempt, error }) =>
openYourOtpDialog({ method, maxAttempts, attempt, error }),
},
});
Set ui: false and provide a handler for every prompt when your application owns them all. The SDK still owns the provider iframes and popups, message validation, state machine, polling, and teardown. See Customize the Web Agentic SDK for the full handler contract.
If you need to own the browser protocols and API state machine too, follow Own the Browser Verification Flow. Most integrations should use the SDK because the direct protocol has more security and lifecycle responsibilities than a custom visual layer.
Handle the Result
The promise resolves only when the API returns an active rail:
import {
ApiError,
PopupBlockedError,
VerificationCancelledError,
VerificationNotSupportedError,
} from "@basis-theory/web-agentic";
try {
const result = await agenticVerification.verifyAllowance(
"alw_Xw92mKvB3dQz",
{ rail: "agentic-token", provider: "vic" },
);
if (result.status === "active") {
enableCredentialMinting();
}
} catch (error) {
if (error instanceof VerificationCancelledError) {
showTryAgainLater();
} else if (error instanceof PopupBlockedError) {
askCustomerToAllowPopups();
} else if (error instanceof VerificationNotSupportedError) {
offerAnotherActiveRailOrPaymentMethod();
} else if (error instanceof ApiError) {
reportVerificationError(error.type, error.traceId);
}
}
Treat API error type values as an open set. Use the typed SDK errors for the recovery choices your UI needs, and include traceId when contacting Basis Theory Support.
Call agenticVerification.dispose() when the page or component permanently unmounts. Disposal aborts an active run and removes the SDK's iframe, popup, listeners, timers, and UI host.
Mint a Credential
A credential is what the agent presents at checkout. Mint one with the Create Payment Credential API, choosing a rail that is active on the allowance and a format that rail advertises.
Two rules govern every mint. Send a BT-IDEMPOTENCY-KEY, one key per business operation such as per checkout attempt, so a retry cannot mint and spend twice. And mint for the amount of the checkout in front of you: the allowance is drawn down when the credential is issued, whether or not the purchase completes.
curl 'https://api.test.basistheory.com/agentic/allowances/alw_Xw92mKvB3dQz/credentials' \
-X 'POST' \
-H 'BT-API-KEY: <PRIVATE_API_KEY>' \
-H 'BT-IDEMPOTENCY-KEY: checkout-1138-attempt-1' \
-H 'Content-Type: application/json' \
--data '{
"rail": "agentic-token",
"provider": "vic",
"amount": { "value": "25.00", "currency": "USD" },
"credential": { "format": "card" }
}'
The amount currency must match the allowance currency, and the value cannot exceed amount_available. A mint that would take the allowance past its total returns 400 ALLOWANCE_AMOUNT_EXCEEDED. Only one mint runs on an allowance at a time; a different mint arriving while one is in flight returns 409 ALLOWANCE_CREDENTIAL_IN_PROGRESS.
Choose a Format
The format is the credential's representation, not another rail. Read credential_formats on the allowance rail and pick from it. Requesting a format the rail does not support returns 400.
| Format | The Agent Receives | Use it When | Payload Required |
|---|---|---|---|
card | Single-use card fields with a dynamic security code | Your checkout takes ordinary card fields | None |
network-token | A network token plus a single-use cryptogram | Your processor accepts tokenized transactions with network dynamic data | None |
identifier | A processor's own agentic token | The recipient business accepts that processor's tokens | network_business_profile |
mpp | A complete MPP credential | The merchant challenges for payment over HTTP | The merchant's challenge |
- card
- network-token
- identifier
- mpp
A single-use card credential scoped to the mandate, issued by the rail's provider. Use it wherever your checkout takes ordinary card fields. The security code is dynamic and issued for this credential.
{
"rail": "agentic-token",
"provider": "vic",
"amount": { "value": "25.00", "currency": "USD" },
"credential": { "format": "card" }
}
{
"id": "cred_pQ8vNx2LmRs4",
"rail": "agentic-token",
"provider": "vic",
"amount": { "value": "25.00", "currency": "USD" },
"credential": {
"format": "card",
"value": {
"number": "4000001000004242",
"expiration_month": 12,
"expiration_year": 2030,
"cvc": "123"
}
},
"expires_at": "2030-07-08T00:00:00.000Z"
}
That number is issued by the card network for this mandate. It is not the customer's card number, and it cannot be spent outside the allowance.
A network payment token plus a single-use cryptogram. Use it when your processor accepts tokenized transactions with network dynamic data.
{
"rail": "agentic-token",
"provider": "vic",
"amount": { "value": "25.00", "currency": "USD" },
"credential": { "format": "network-token" }
}
{
"id": "cred_dK7mXr4TnBw9",
"rail": "agentic-token",
"provider": "vic",
"amount": { "value": "25.00", "currency": "USD" },
"credential": {
"format": "network-token",
"value": {
"payment_token": "4000001000004242",
"expiration_month": 6,
"expiration_year": 2034,
"eci": "07",
"cryptogram": {
"type": "CARD_APPLICATION_CRYPTOGRAM_SHORT_FORM",
"value": "AmDDBjkH/4A=",
"expires_at": "2030-07-06T18:00:00.000Z"
}
}
},
"expires_at": "2030-07-06T18:00:00.000Z"
}
You do not choose the cryptogram type: Basis Theory requests each provider's native type behind the scenes and always publishes CARD_APPLICATION_CRYPTOGRAM_SHORT_FORM, so the response shape does not vary by provider. Read provider to tell who issued the credential. The API reference documents every field and the normalization applied to it.
The cryptogram is transaction-scoped and short-lived. The credential's expires_at is the earliest limit Basis Theory knows about, usually the cryptogram's expiry; a provider that supplies no cryptogram expiration falls back to the allowance's. Use the credential immediately either way.
A processor's own agentic token, returned as its identifier string. This example mints from the spt rail, where the token is a Stripe Shared Payment Token: network_business_profile is required and is Stripe's Business Network Profile ID for the recipient business, which scopes the token to that recipient.
{
"rail": "spt",
"provider": "stripe",
"amount": { "value": "10.00", "currency": "USD" },
"credential": {
"format": "identifier",
"payload": { "network_business_profile": "np_acme_store" }
}
}
{
"id": "cred_wY3tKm9ZpXv7",
"rail": "spt",
"provider": "stripe",
"amount": { "value": "10.00", "currency": "USD" },
"credential": {
"format": "identifier",
"value": "spt_1RgaZcFPC5QUO6ZCDVZuVA8q"
},
"expires_at": "2030-07-08T00:00:00.000Z"
}
The Shared Payment Token is the bearer credential. It is returned here and is never persisted, logged, copied into provider_ids, or available to support.
A complete Machine Payments Protocol credential. Use it when the agent hit a merchant that answered with an HTTP payment challenge, and you want a credential the agent can present directly.
The merchant advertises one or more Payment challenges in a WWW-Authenticate header. Select the one matching the rail you are minting from, parse its parameters into an object, and pass it through as payload.challenge.
{
"rail": "spt",
"provider": "stripe",
"credential": {
"format": "mpp",
"payload": {
"challenge": {
"id": "ch_9xK2",
"realm": "api.acme.example",
"method": "stripe",
"intent": "charge",
"request": "eyJhbW91bnQiOiI0OTk5IiwiY3VycmVuY3kiOiJ1c2QiLCJtZXRob2REZXRhaWxzIjp7Im5ldHdvcmtJZCI6Im5wX2FjbWVfc3RvcmUifX0",
"description": "Acme Store checkout",
"expires": "2030-07-07T17:00:00.000Z"
}
}
}
}
{
"id": "cred_zR5wPn8KqYt2",
"rail": "spt",
"provider": "stripe",
"amount": { "value": "49.99", "currency": "USD" },
"credential": {
"format": "mpp",
"value": "eyJjaGFsbGVuZ2UiOnsiaWQiOiJjaF85eEsyIiwicmVhbG0iOiJhcGkuYWNtZS5leGFtcGxlIiwibWV0aG9kIjoic3RyaXBlIiwiaW50ZW50IjoiY2hhcmdlIiwicmVxdWVzdCI6ImV5SmhiVzkxYm5RaU9pSTBPVGs1SWl3aVkzVnljbVZ1WTNraU9pSjFjMlFpTENKdFpYUm9iMlJFWlhSaGFXeHpJanA3SW01bGRIZHZjbXRKWkNJNkltNXdYMkZqYldWZmMzUnZjbVVpZlgwIiwiZGVzY3JpcHRpb24iOiJBY21lIFN0b3JlIGNoZWNrb3V0IiwiZXhwaXJlcyI6IjIwMzAtMDctMDdUMTc6MDA6MDAuMDAwWiJ9LCJwYXlsb2FkIjp7InNwdCI6InNwdF9tb2NrX3pSNXdQbjhLIn19"
},
"expires_at": "2030-07-07T17:00:00.000Z"
}
The agent retries the merchant request with that value as an Authorization: Payment <credential.value> header.
There is no amount in the request. Basis Theory decodes it from the challenge, which is what makes this format a natural fit for one-shot checkouts: the amount the merchant asked for is exactly the amount that leaves the allowance. If you do send amount, it must match the challenge.
Which challenge you select has to match the rail. The spt rail requires method: "stripe" or the method: "spt" Basis Theory extension, and the agentic-token rail requires method: "card". Only intent: "charge" is supported. For an agentic-token MPP Card credential, supply billing_address when the challenge sets billingRequired, and cardholder_full_name whenever you know it.
For MPP Card, the merchant owns an RSA key pair and publishes the public key as an RSA JWK in the challenge or a same-origin JWKS. Basis Theory uses it to produce an RSA-OAEP-256 and A256GCM compact JWE. Basis Theory Client Encryption Keys from POST /keys are X25519 keys and are not compatible with this MPP flow.
The API reference documents the full challenge contract, the encryption requirements for MPP Card, and every validation applied to a decoded request.
credential.value is returned exactly once, in this response, and is never persisted. A later GET returns metadata only, and replaying the same supplied BT-IDEMPOTENCY-KEY returns 409 CREDENTIAL_PAYLOAD_UNAVAILABLE rather than the value again. Capture it when it arrives. If you lose it, mint a new credential with a new key or no key, which spends the allowance again.What the Mint Did to the Allowance
Read the allowance afterwards and the accounting is visible:
{
"id": "alw_Xw92mKvB3dQz",
"status": "active",
"amount": { "value": "100.00", "currency": "USD" },
"amount_spent": { "value": "25.00", "currency": "USD" },
"amount_reserved": { "value": "0.00", "currency": "USD" },
"amount_available": { "value": "75.00", "currency": "USD" },
"credentials_count": 1,
"...": "..."
}
amount_available is always amount minus amount_spent minus amount_reserved. A reservation exists while one mint is in flight. If that process disappears, the exact stale reservation is conservatively moved to amount_spent before the next mint, allowance update, cancellation, or verification attempt; it is never released on an indeterminate outcome. credentials_count increases only when a credential record and payload were successfully committed.
You can mint repeatedly from one allowance until the budget is exhausted. Each mint is independent, and different mints from the same allowance can use different rails and formats.
Read State and Manage Allowances
Everything you need after the happy path is a read or a lifecycle change on one of the three resources.
| Call | Returns |
|---|---|
GET /agentic/payment-methods | Payment methods for the tenant, filterable by consumer_id and status |
GET /agentic/payment-methods/{payment_method_id} | One payment method with its current rail states |
GET /agentic/payment-methods/{payment_method_id}/errors | Sanitized provider failures for the payment method and its downstream operations |
GET /agentic/allowances | Allowances with live spend totals, filterable by payment_method_id and status |
GET /agentic/allowances/{allowance_id} | One allowance with its rails and balances |
GET /agentic/allowances/{allowance_id}/errors | Sanitized allowance setup, verification, and mint failures |
GET /agentic/allowances/{allowance_id}/credentials | Credential metadata for one allowance |
GET /agentic/payment-credentials | Credential metadata across the tenant |
Credential reads return metadata only: the ID, rail, provider, format, amount, status, and timestamps. Credential values — card numbers, network tokens, cryptograms, processor tokens, MPP payloads — are never readable after the mint that produced them.
Every list endpoint uses the platform's cursor pagination: size defaults to 20 and accepts 1 through 100, and start takes the previous response's pagination.next unmodified. An out-of-range size or a malformed start returns 400 rather than being quietly corrected, so a paging bug surfaces where you made it.
List endpoints default to active resources. Pass status=all when you need history, such as for a customer-facing activity view. Cancelled and expired allowances remain visible because their spend history matters.
These endpoints are the source of truth for resource state. When your application needs to know whether a rail is usable, read it rather than inferring it from browser events.
Change an Allowance
PATCH /agentic/allowances/{allowance_id} can change amount, description, and expires_at, and requires at least one of them. Everything else about a mandate is immutable, including the merchant. An empty body is rejected before any provider is contacted.
Basis Theory propagates the change to any rail that maintains provider-side allowance state before committing it locally, and blocks minting for that window so an agent cannot spend against a mandate that is mid-change. If a provider rejects the change, the allowance is left exactly as it was.
Two limits are worth designing around:
- The amount is locked once any capacity has been spent. This includes an indeterminate mint whose capacity was consumed even though no credential payload was returned. Create a second allowance instead of raising the first.
- Expiration is terminal. After
expires_atpasses, aPATCHreturnsALLOWANCE_EXPIRED. Extending the timestamp cannot revive the allowance or provider-side state, so create a new allowance.
A PATCH whose values already match the allowance returns the existing resource without contacting providers and without changing updated_at. Money and timestamps are compared by value rather than by formatting, so "10" and "10.00" are the same amount.
Cancel an Allowance
DELETE /agentic/allowances/{allowance_id} sets the allowance to cancelled and cancels any provider-side mandate associated with its rails. No further credentials can be minted from it. The allowance stays readable, and repeating a successful delete succeeds without a second provider call.
Remove a Payment Method
DELETE /agentic/payment-methods/{payment_method_id} cancels every allowance backed by the payment method, including any provider-side mandates, and blocks all further verification and minting. This is the operation to call when a customer revokes agentic access to a source.
Concurrency
Basis Theory serializes operations that touch provider state on the same resource. Verification, an allowance update, a cancellation, and a mint reservation are mutually exclusive, and a request that arrives during one of them returns a 409 whose code names the operation in flight, such as ALLOWANCE_VERIFICATION_IN_PROGRESS. Repeat the exact request after a short delay, preserving its idempotency key when it has one.
Only one credential mint can be in flight on an allowance. A second operation with a different key or no key returns 409 ALLOWANCE_CREDENTIAL_IN_PROGRESS; the same supplied key returns 409 IDEMPOTENCY_IN_PROGRESS. Reservations and balance changes are atomic conditional writes, so concurrent requests cannot overspend the mandate.
Handle Failures and Retries
Agentic Payments depends on third-party providers, so failures are normal operating conditions rather than exceptions. Branch on the response's stable type, not only its HTTP status.
Every failure falls into one of five classes, and the class determines what is safe to do next.
| Class | HTTP | Allowance Effect | Safe Next Action |
|---|---|---|---|
| Your request was malformed | 400 INVALID_BODY, 413, 415, 404 NOT_FOUND | Unchanged | Fix the request framing. These are raised before anything is read and never reach a provider |
| Your request was invalid | 400 | Unchanged | Fix the request and repeat it |
| The provider said no conclusively | 422 or 409 RAIL_ALREADY_ENROLLED | Unchanged, except that a rejected mint releases its reservation | Address the code-specific cause rather than retrying unchanged. A deliberate credential retry is a new operation: use a new key or omit it |
| Something else is in flight | 409 …_IN_PROGRESS | A mint may be reserved | Repeat the exact request after a delay, preserving its key when it has one |
| The outcome is unknown | 409 CREATE_OUTCOME_UNKNOWN or 409 CREDENTIAL_OUTCOME_UNKNOWN | A credential amount is consumed as spent | Treat the operation as terminal and follow the code-specific action below |
Your Request Was Invalid
400 VALIDATION_ERROR means nothing left the process. The response body carries an errors object mapping every offending field path to its messages, not just the first one, so an agent correcting its own request can fix everything in one pass.
400 RAIL_NOT_ACTIVE and 400 NO_ACTIVE_RAILS are the two you will see most, and neither is a validation typo. RAIL_NOT_ACTIVE on a mint means that allowance rail still needs verification. NO_ACTIVE_RAILS on an allowance create means the payment method has no enabled rail, so retry a payment method rail before trying again.
The Provider Said No
A credential-mint 422 is conclusive: either nothing was dispatched or the provider explicitly reported that it issued nothing. Its reservation is released. Other provider failures may leave an error rail that you can retry in place.
The code names the recovery: CARD_REJECTED means try a different card, PROVIDER_ENROLLMENT_FAILED and PROVIDER_ALLOWANCE_FAILED mean retry the rail, RAIL_ENROLLMENT_INVALID means the provider no longer recognizes what this rail was provisioned with, so retry the rail and register the source again as a new payment method if that fails, PROVIDER_VERIFICATION_FAILED means restart verification from start, PROVIDER_CREDENTIALS_FAILED means the mint was refused and a deliberate new attempt needs a new key, and PROVIDER_NOT_CONFIGURED means contact us, because no retry will succeed. Errors and Recovery lists every code.
One conclusive rejection is not a 422: 409 RAIL_ALREADY_ENROLLED means the provider already holds this enrollment. It ends in neither _IN_PROGRESS nor _OUTCOME_UNKNOWN, so repeating it unchanged will not help.
Something Else Is in Flight
Operations that mutate provider state on the same resource are mutually exclusive, and a 409 ending in _IN_PROGRESS names the operation holding it. All of these mean the same thing operationally: wait briefly and repeat the exact request, preserving its idempotency key when it has one.
Only one mint can reserve a given allowance. A mint with a different key or no key gets ALLOWANCE_CREDENTIAL_IN_PROGRESS; a replay of the running mint's supplied key gets IDEMPOTENCY_IN_PROGRESS. Both prevent duplicate provider calls and overspending.
PAYMENT_METHOD_DELETE_INCOMPLETE is different: a previous delete stopped partway through its child cascade. Retry DELETE on that payment method. Other operations deliberately do not revive it because some child allowances may already be cancelled.
The Outcome Is Unknown
Basis Theory cannot reliably query a provider later to recover a bearer credential or prove that it issued nothing. Unknown outcomes therefore become conservative terminal results rather than reconciliation work.
| Code | Durable Result | What to Do |
|---|---|---|
CREATE_OUTCOME_UNKNOWN | The payment method or allowance may exist, and the key cannot create again | List by consumer_id or payment_method_id. Only use a new key after checking for the resource |
CREDENTIAL_OUTCOME_UNKNOWN | The amount is consumed as amount_spent; no credential record or bearer value is available | Do not retry this operation. Continue with the remaining allowance capacity or create a new allowance |
A crashed request can temporarily leave its amount in amount_reserved. Once its five-minute operation lease is stale, the next mint, allowance update, cancellation, or verification attempt moves that exact reservation to amount_spent before proceeding. There is no release or reconciliation endpoint because stored state cannot prove the credential was never issued.
Idempotency
Payment method creates, allowance creates, and credential mints all accept the optional BT-IDEMPOTENCY-KEY. Supplying it opts into retry handling. Agentic retains failures and does not apply the platform default's 24-hour expiration; its records are durable and never become reusable. Without a key, every request is a new operation.
The successful credential response is deliberately different from other creates: its bearer value is never persisted, so a same-key replay returns 409 CREDENTIAL_PAYLOAD_UNAVAILABLE rather than the value. The full replay matrix for creates and mints is in Errors and Recovery.
Reading Error History
Both resources keep their own history of provider failures, retained for 90 days:
GET /agentic/payment-methods/{payment_method_id}/errorsGET /agentic/allowances/{allowance_id}/errors
Each entry carries a stable code, a display-safe title and detail you can show a customer, the provider, operation, and rail, the IDs of the resources involved, an optional provider correlation ID, and occurred_at. Provider response bodies, provider messages, sensitive source data, and credential values never appear in them, which is what makes these endpoints safe to surface in your own customer-facing UI.
A failed mint can carry a payment_credential_id even though no credential resource exists. That ID identifies the attempt in Basis Theory's logs, which makes it the single most useful thing to quote in a support conversation.
If You Do Need to Escalate
Send the identifiers, never the secrets.
Include: the payment method ID, the allowance ID, the credential ID, the mint or attempt timestamp, the amount and currency, the merchant name and URL, the rail and provider, the BT-TRACE-ID from the response, and any provider correlation ID from the error history. For a checkout decline, add the merchant or processor decline code and reference.
credential.value to support, by any channel. These are bearer credentials. Basis Theory does not need them to diagnose anything, and the identifiers above are sufficient.For a declined checkout, start with the merchant, its acquirer, or your payment processor rather than with Basis Theory. They hold the authorization response, and Basis Theory does not: minting the credential is the last event it observes.
Test Before Production
Everything in this guide runs against https://api.test.basistheory.com in a test tenant with deterministic mock providers. No request reaches a provider, and your frontend code is identical: only the API hostname, tenant keys, and provider-hosted URLs inside responses differ. The mock ceremonies speak the same browser protocols as the live ones, and allowance accounting behaves exactly as it does in production. The testing reference lists each mock ceremony and the card that triggers each scenario.
Rehearse these before you go live:
- The happy path of each verification ceremony your rails use — for the card-network rails, the Visa mock, including the one-time-code branch and passkey registration, and the Mastercard mock, including the callback bridge and the authoritative
completecall. - A payment method where one rail succeeds and the other fails, so your UI handles partial success.
- A provisioning failure that succeeds on retry, so you exercise the rail retry endpoint.
- A mint that the provider refuses, so you confirm the allowance is not spent.
- A mint whose provider outcome is unknown, so you confirm the allowance capacity is consumed and your agent does not retry it as the same operation.
- An over-allowance mint and an idempotency-key replay.
At go-live, change the API host to https://api.basistheory.com, replace both public and private test keys with production-tenant keys, and keep the permissions equally narrow. For Web Agentic, remove the test apiBaseUrl override so the SDK uses its production default. Test and production resources are isolated, so create new payment methods and allowances in production.
Limitations to Design Around
These are product boundaries worth knowing before you build.
Minting spends the allowance permanently. There is no void, release, or refund-to-allowance operation. Size allowances to expected spend and mint per checkout.
Basis Theory has no settlement visibility. Credential status stays created and never reflects the merchant's authorization, capture, or settlement. Reconcile outcomes with your processor.
Verification is per allowance rail. On rails that require it, a customer who approves ten allowances completes ten ceremonies. Prefer fewer, well-scoped allowances over many tiny ones when the same merchant is involved.
An allowance is bound to one merchant. The merchant is required at creation and immutable afterward: there is no multi-merchant, category-scoped, or merchant-agnostic mandate. An agent shopping across three merchants needs three allowances.
The amount is locked after any capacity is spent, and expiration is terminal. An indeterminate mint counts as spent even when no credential payload was returned. Plan for creating a new allowance rather than growing an existing one.
Visa's non-code step-up methods are not supported. If a card offers no one-time-code method, its agentic-token rail cannot be verified, though the spt rail remains available.
FAQ
Can I call the Agentic Payments API from my frontend?
Yes, for two permission scopes. A public application may use agentic:payment-method:create to create payment methods or retry their rails, and agentic:allowance:verify to drive customer verification directly from the browser. All other Agentic Payments permissions require a private application and must stay on your backend.
Which rail should I use?
Choose by the properties each rail reports rather than by name. For instance, use spt when you want the shortest path to a working checkout and your recipient is set up with Stripe, since it needs no verification ceremony. Use agentic-token when you want the mandate enforced inside the card network, or when your checkout needs card fields or a network token. Many payment methods support both, and one allowance can mint from either.
Decide per request, not once at build time. Read the allowance rail's status and its credential_formats, and pick from what is actually there. Basis Theory may route a rail through a different provider over time, so do not hard-code business logic to a provider name — the exception is a credential format that genuinely needs provider-specific input, such as the recipient business profile an identifier credential requires.
What happens if the customer abandons verification?
Nothing is lost. The allowance stays active, and the unverified rail stays pending_verification. Call verifyAllowance again when the customer comes back. The Web Agentic SDK restarts from start and removes its previous browser resources.
Can the same customer verify on a different device?
Yes, but do not assume verification state transfers between devices. Start verification normally and let the provider decide whether the new device needs one-time-code step-up, passkey registration, or authentication. Your integration follows the returned actions rather than selecting a branch itself.
Do I need to mint the whole allowance at once?
No, and usually you should not. Mint per checkout with the checkout's amount. An allowance can produce many credentials until its budget is exhausted, and each one can use a different rail and format.
Can two agents share one allowance?
Technically yes, and spend accounting is safe under concurrency. Only one mint runs on the allowance at a time; another receives 409 ALLOWANCE_CREDENTIAL_IN_PROGRESS and can repeat its exact request later. Whether sharing is a good idea is a product decision: an allowance is the record of what one customer approved for one merchant, so a shared allowance makes attribution harder. The optional agent_id field records which agent an allowance is for, though it is attribution only and grants no authority. Authorization is always your API key's permissions.
How long is a credential valid?
Until the earliest known limit among the allowance's expiration, the MPP challenge's expiry when one applies, and a provider-supplied credential limit. A network-token cryptogram is transaction-scoped and should be used immediately. A provider that supplies no cryptogram expiration falls back to the allowance expiration; that fallback is not a promise that the cryptogram remains usable until then.
Can I reuse a credential for a second purchase?
No. A credential is minted for one amount, and the card and network-token formats carry dynamic data valid for a single transaction. Mint a new credential for the next purchase.
What if I lose the credential value?
It cannot be recovered. The spendable value is returned once and never persisted, and a same-key replay returns 409 CREDENTIAL_PAYLOAD_UNAVAILABLE rather than the value. Minting with a new idempotency key or no key creates a new credential and spends the allowance again. Capture the value in the same request handler that receives it. If the original call returned CREDENTIAL_OUTCOME_UNKNOWN, its capacity is already consumed even though no credential metadata exists.
Why did my transaction decline when the credential minted successfully?
Because those are two different things. Minting means the provider issued a credential. Declining means the merchant's authorization failed, which happens downstream and for reasons Basis Theory does not see: velocity rules, fraud scoring, an address mismatch, insufficient funds. Start with the merchant, its acquirer, or your processor, and see Handle Failures and Retries for the identifiers to bring if you need us.
How do I show the customer what their agent spent?
Read the allowance for live totals, and GET /agentic/allowances/{allowance_id}/credentials for successfully returned credential records with amounts, formats, and timestamps. amount_spent can be greater than the sum of those records when an indeterminate mint consumed capacity. For a full activity view including cancelled and expired mandates, list allowances with status=all. Bear in mind this is a record of credential issuance, not of purchases completed.