Skip to main content

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:

Implementation flow from a card token to a payment method to an allowance, with the spt and agentic-token rails provisioned in parallel. The spt rail is active immediately; the agentic-token rail starts pending verification and is verified until active. Either active rail can then mint a credential.

Getting Started

To get started, you will need to create a Basis Theory Account and a TEST Tenant.

Be sure to use your work email (e.g., john.doe@yourcompany.com)
Complete the Agentic Payments Setup guide first. It covers tenant onboarding and the browser requirements for customer verification. This is only required once per 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
Save the 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
Save the 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.

Create a Card Token
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"
}
}'
Response
{
"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.

Create a Payment Method
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"
}
}'
Response
{
"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.

StatusMeaningWhat to Do
enabledThe rail provisioned successfullyCreate allowances against it
pendingProvisioning will finish asynchronouslyRetry the rail to pick up the result
errorThe provider rejected or failed the registrationRead 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.

Retry a Failed Rail
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.

Create an Allowance
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" }
}'
Response
{
"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

FieldValuesDescribes
Allowance statusactive, cancelled, expiredThe mandate. active means it has not been cancelled and has not expired
Rail statusactive, pending_verification, errorOne 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 install @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.

agenticVerification.js
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.

Verify the Allowance
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 complete polling
  • 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:

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

Customize the Built-in UI
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:

Replace One Prompt
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:

Handle Verification
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.

Mint a Credential
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.

FormatThe Agent ReceivesUse it WhenPayload Required
cardSingle-use card fields with a dynamic security codeYour checkout takes ordinary card fieldsNone
network-tokenA network token plus a single-use cryptogramYour processor accepts tokenized transactions with network dynamic dataNone
identifierA processor's own agentic tokenThe recipient business accepts that processor's tokensnetwork_business_profile
mppA complete MPP credentialThe merchant challenges for payment over HTTPThe merchant's challenge

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.

Request Body
{
"rail": "agentic-token",
"provider": "vic",
"amount": { "value": "25.00", "currency": "USD" },
"credential": { "format": "card" }
}
Response
{
"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.

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:

GET /agentic/allowances/alw_Xw92mKvB3dQz
{
"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.

CallReturns
GET /agentic/payment-methodsPayment 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}/errorsSanitized provider failures for the payment method and its downstream operations
GET /agentic/allowancesAllowances 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}/errorsSanitized allowance setup, verification, and mint failures
GET /agentic/allowances/{allowance_id}/credentialsCredential metadata for one allowance
GET /agentic/payment-credentialsCredential 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_at passes, a PATCH returns ALLOWANCE_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.

ClassHTTPAllowance EffectSafe Next Action
Your request was malformed400 INVALID_BODY, 413, 415, 404 NOT_FOUNDUnchangedFix the request framing. These are raised before anything is read and never reach a provider
Your request was invalid400UnchangedFix the request and repeat it
The provider said no conclusively422 or 409 RAIL_ALREADY_ENROLLEDUnchanged, except that a rejected mint releases its reservationAddress 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 flight409 …_IN_PROGRESSA mint may be reservedRepeat the exact request after a delay, preserving its key when it has one
The outcome is unknown409 CREATE_OUTCOME_UNKNOWN or 409 CREDENTIAL_OUTCOME_UNKNOWNA credential amount is consumed as spentTreat 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.

CodeDurable ResultWhat to Do
CREATE_OUTCOME_UNKNOWNThe payment method or allowance may exist, and the key cannot create againList by consumer_id or payment_method_id. Only use a new key after checking for the resource
CREDENTIAL_OUTCOME_UNKNOWNThe amount is consumed as amount_spent; no credential record or bearer value is availableDo not retry this operation. Continue with the remaining allowance capacity or create a new allowance
A new idempotency key or a keyless retry always starts a new operation. It cannot recover an unknown credential and may consume allowance capacity again.

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}/errors
  • GET /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.

Never send card numbers, security codes, cryptograms, network tokens, processor tokens, MPP credentials, or any other 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 complete call.
  • 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.