Skip to main content

Agentic Payments Testing

In a test tenant, every Agentic Payments rail routes to a deterministic mock provider. No request reaches any external provider. The API endpoints, resource shapes, status values, and error contracts match production; the provider outcomes are deterministic test scenarios.

Use test-tenant keys only against https://api.test.basistheory.com. Payment method creation and allowance verification use the public application from the implementation guide; server-side operations use the private application.

The browser protocols are the same shape. Basis Theory-hosted mocks implement each provider's production ceremony protocol — for example, Visa's hidden session iframe and visible passkey popup, and Mastercard's hosted page and callback bridge. Configure the test API hostname and the API responses supply mock provider URLs; integration code is otherwise unchanged.

The allowance accounting is real. Atomic spend, mint reservation and its cleanup after a conclusive failure, idempotency, and over-allowance rejection all behave exactly as they do in production. As in production, a credential that mints successfully consumes its amount permanently; there is no operation that returns it.

The mocks enforce ordering. An out-of-order ceremony step returns 400 VERIFICATION_STATE_INVALID: submit_session requires a prior start, select_otp_method must echo a method_id the previous step offered, and Mastercard's complete requires the authentication session that start created.

Test Sources

Every scenario is keyed on the source backing the payment method. The supported source type is a vaulted card (basis_theory_card_token), and its scenarios are selected by card number.

The matrix is deliberately small. Each card exercises one materially different integration or recovery behavior, and any card number not listed follows its brand's happy path. Tokenize the card in your test tenant first, then use the resulting token as source.token_id.

Because both rails see the same card number, one card can produce different outcomes per rail. That is what makes partial rail success testable with a single payment method.

Visa

Card NumberPayment method railsVerificationScenario
4242424242424242agentic-token enabled, spt enabledFull ceremony to activeVisa happy path
4929980395567582agentic-token enabled, spt enabledsubmit_otp returns 400 INVALID_OTPVerification failure
4000000000000002agentic-token enabled, spt error (CARD_REJECTED)Full ceremony to activePartial rail success. Retrying the spt rail is rejected again
4000000000000119agentic-token enabled, spt errorFull ceremony to activeRecoverable provisioning failure. Retrying the spt rail succeeds
4000000000000341agentic-token enabled, spt enabledFull ceremony to activeCredential issuance failure. A spt mint returns 422 PROVIDER_CREDENTIALS_FAILED and releases the reservation
4000000000009995agentic-token enabled, spt enabledFull ceremony to activeIndeterminate credential outcome. An spt mint returns 409 CREDENTIAL_OUTCOME_UNKNOWN and consumes the requested allowance capacity
Any other Visa numberagentic-token enabled, spt enabledFull ceremony to activeDefault

Mastercard

Card NumberPayment method railsVerificationScenario
5555555555554444agentic-token enabled, spt enabledHosted redirect to activeMastercard happy path
5186160000000001agentic-token error (CARD_REJECTED), spt enabledNot applicablePermanent network rejection. The spt rail still works
5186160000000003agentic-token enabled, spt enabledcomplete returns 422 PROVIDER_VERIFICATION_FAILEDVerification failure, with brand behavior differing from Visa's
Any other Mastercard numberagentic-token enabled, spt enabledHosted redirect to activeDefault

The agentic-token rail follows the card's network: Visa numbers provision vic and Mastercard numbers provision agentpay. Every card also provisions the spt rail, and the Stripe outcomes are keyed on the card number rather than the brand: 4000000000000002 declines permanently, 4000000000000119 fails once and succeeds on retry, 4000000000000341 provisions but conclusively fails to mint, 4000000000009995 simulates a lost mint response, and every other number succeeds.

Mock Credentials

Mock credentials are deterministic so repeated runs see stable values.

FormatValue
cardA Luhn-valid virtual card number, 4000001000004242 for the 4242 Visa card and 5100001000004446 for the 4444 Mastercard, falling back to 4000001000000000 and 5100001000000006 for other cards. Expiration comes from the tokenized card; the security code is derived deterministically
network-tokenThe same virtual number as the payment_token, with a deterministic cryptogram. Visa returns ECI 07 and Mastercard returns ECI 06; cryptogram.type reads CARD_APPLICATION_CRYPTOGRAM_SHORT_FORM on both
identifierA Stripe token identifier prefixed spt_mock_
mppThe same network or Stripe data, packaged for the challenge you supplied

Credential expiry follows each network's real rule rather than a uniform test value. Mastercard supplies no cryptogram expiry — its short-form cryptogram is single-use and bound to the transaction — so cryptogram.expires_at falls back to the allowance's expires_at, in the mock exactly as in production. Visa's cryptogram expiry is network-supplied, and the mock returns a short synthetic window in its place, so do not calibrate "how long is a credential usable" against the Visa mock's value. Use any cryptogram immediately on both networks.

Mock credentials are suitable for integration tests. They are not provider certification evidence.

Mock Provider Ceremonies

The hosted mocks implement the production ceremony protocols, so popup handling, message listeners, and origin checks are all exercised.

Visa

Served at /mock/visa/auth, and used for the same two browser surfaces as the production Visa integration: the hidden session iframe and the visible popup.

  1. Loading. A Visa-branded loading screen while the session initializes.
  2. REGISTER. For a device with no passkey: a "Verify faster" enrollment screen showing the card's last four digits, the consumer email and country from the payment method, and a terms checkbox that gates the continue button.
  3. AUTHENTICATE. For a bound device: a screen showing the card's last four digits, the allowance amount, and your display_name.

The mock implements the full AUTH_READY, CREATE_AUTH_SESSION, AUTH_SESSION_CREATED, AUTHENTICATE, AUTH_COMPLETE message protocol. It also remembers device binding, so after a REGISTER ceremony a restarted verification goes straight to AUTHENTICATE.

Mastercard

Served at /mock/mastercard/allowance-auth and opened top-level, since the real Mastercard page cannot be embedded either.

  1. "Securing your payment information..."
  2. "Verifying your passkey (mock)..."
  3. "Verified. Returning you to {display_name}..."

It then returns through the same callback bridge production uses, which posts mastercard_verification_complete to your opener and closes itself. Your application then calls complete, which is the authoritative transition.

The redirect action's expires_at is the authentication-session window Mastercard returns, not the allowance's lifetime. It is the deadline for the cardholder to finish the hosted ceremony.

Skipping the Ceremony in API Tests

For tests that exercise API orchestration rather than browser behavior, a mock agentic-token rail accepts submit_passkey directly and activates:

POST /agentic/allowances/{allowance_id}/verify
{
"rail": "agentic-token",
"provider": "vic",
"action": "submit_passkey",
"assurance_data": { "result": "approved" }
}
Response
{
"status": "active",
"rail": "agentic-token",
"provider": "vic"
}

This shortcut exists only in test tenants; production requires the real ceremony.

Scenarios Worth Rehearsing

The scenarios use vaulted card sources, and Test Sources lists the card that selects each one. The behaviors they rehearse — provisioning outcomes, ceremony results, allowance accounting, and idempotency — are contracts of the model rather than of the card.

Visa Happy Path

Tokenize 4242424242424242, create a payment method, create an allowance, then loop verification: start returns passkey_session, submit_session returns select_otp_method, select_otp_method returns otp, and submit_otp returns a passkey action with a REGISTER context. Run the REGISTER ceremony, call start again, and the second submit_session returns a passkey action with an AUTHENTICATE context. submit_passkey returns active.

The mock offers two one-time-code methods, a masked SMS destination and a masked email address, with a five-minute expiry and three attempts. Any code is accepted unless the card scenario says otherwise, but the method_id must be one of the two the step returned.

Mastercard Happy Path

Tokenize 5555555555554444. start returns a redirect, the hosted mock runs its three screens, the callback bridge posts to your opener, and complete returns active.

Permanent Provisioning Rejection

Tokenize 5186160000000001. The payment method is still created, with its agentic-token rail in error carrying CARD_REJECTED and its spt rail enabled. Confirm your UI treats this as a payment method that is usable on one rail rather than as a failed create, and that GET /agentic/payment-methods/{payment_method_id}/errors shows the failure.

Verification Failure

Tokenize 4929980395567582 for Visa. The ceremony proceeds normally until submit_otp, which returns 400 INVALID_OTP. The step is retryable.

Tokenize 5186160000000003 for Mastercard. The hosted ceremony appears to succeed in the browser, and complete returns 422 PROVIDER_VERIFICATION_FAILED. This is the scenario that proves your integration treats complete as authoritative rather than trusting the browser message.

Partial Rail Success

Tokenize 4000000000000002. Visa provisions successfully and Stripe declines, so the payment method comes back with one enabled rail and one error rail. Retrying the spt rail is rejected again with 422 CARD_REJECTED.

Recoverable Provisioning Failure and Retry

Tokenize 4000000000000119. The spt rail fails on create and succeeds on retry:

Retry the 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 full payment method with the rail now enabled. Retrying a rail that is neither error nor pending returns 400 VALIDATION_ERROR.

Credential Issuance Failure

Tokenize 4000000000000341. Both rails provision, but a spt mint fails at the provider:

Response
{
"type": "PROVIDER_CREDENTIALS_FAILED",
"title": "Credential creation failed at provider",
"status": 422
}

Read the allowance afterwards and confirm amount_spent is unchanged and amount_reserved is back to zero: a conclusive rejection releases the reservation. Replaying the same idempotency key returns the same 422 without another provider call; a deliberate new attempt needs a new key. The agentic-token rail on the same allowance still mints normally.

Indeterminate Credential Outcome

Tokenize 4000000000009995. Both rails provision, but an spt mint simulates a response lost after provider dispatch and returns:

Response
{
"type": "CREDENTIAL_OUTCOME_UNKNOWN",
"title": "The credential provider outcome is unknown. The allowance amount was consumed and the credential payload is unavailable.",
"status": 409
}

Read the allowance and confirm the requested amount moved to amount_spent, amount_reserved is zero, and credentials_count did not increase. Replaying the key returns the same error without calling the provider, and there is no reconciliation or release operation.

Spend Limits

Create an allowance for 10.00, mint 7.00, then read the allowance:

GET /agentic/allowances/{allowance_id}
{
"amount_spent": { "value": "7.00", "currency": "USD" },
"amount_available": { "value": "3.00", "currency": "USD" },
"credentials_count": 1
}

Minting 4.00 next returns 400 ALLOWANCE_AMOUNT_EXCEEDED.

Idempotency

The full replay contract is documented in Errors and Recovery.

Mint twice with the same BT-IDEMPOTENCY-KEY and the same body. The first call returns the spendable credential. The second returns 409 CREDENTIAL_PAYLOAD_UNAVAILABLE, does not call the provider, and does not spend the allowance again.

Mint again with the same key and a different amount, and the request returns 409 IDEMPOTENCY_CONFLICT.

Payment method and allowance creates also accept BT-IDEMPOTENCY-KEY. Assert that a same-key replay returns the original resource, a different body returns 409 IDEMPOTENCY_CONFLICT, and a different key creates a distinct resource even for an identical request body.

Repeat each create and credential mint without the header. Every keyless call is a new operation: creates return distinct resource IDs, and two sequential credential mints both consume allowance capacity. Concurrent keyless mints cannot overspend; one proceeds and the other receives 409 ALLOWANCE_CREDENTIAL_IN_PROGRESS.

Credential values cannot be recovered after the response. Persist the credential only in the downstream system that will immediately present it; never log it or place it in retry state.

Local Development

Use an HTTPS tunnel for end-to-end browser testing; it exercises the iframe, origin, and popup behavior your deployed site will use. No Visa origin registration is required.

Backend-only tests, including the submit_passkey shortcut above, need no tunnel.