Skip to main content

Web Agentic

web-agentic

web-agentic

The @basis-theory/web-agentic package verifies Agentic Payments allowances in the customer's browser. It drives the Verify Allowance state machine, renders the customer prompts, runs the provider ceremonies, and resolves only after the API reports the selected rail active.

The SDK is framework-agnostic and has no runtime dependencies. Use it with plain JavaScript, React, Vue, Angular, or another browser framework.

For the complete payment flow around verification, start with the Agentic Payments Implementation Guide.

Before You Begin

Create a public application with agentic:allowance:verify. Public keys are designed for browser use.

Never expose a private application key in frontend code. Allowance reads, credential minting, and every other private Agentic Payments operation remain on your backend.

Your application also needs:

  • An allowance whose rail is pending_verification
  • The exact provider from that rail
  • An HTTPS top-level origin with popups enabled
  • A browser environment that allows hosted iframes
  • A Content Security Policy whose connect-src permits the selected Agentic Payments API origin

Installation

npm install @basis-theory/web-agentic

Initialization

Create one instance for the page or component lifecycle:

import { AgenticVerification } from "@basis-theory/web-agentic";

const agenticVerification = AgenticVerification({
apiKey: "<PUBLIC_API_KEY>",
displayName: "Example Agent",
});

Production uses https://api.basistheory.com/agentic by default. A test tenant uses the test API:

const agenticVerification = AgenticVerification({
apiKey: "<PUBLIC_API_KEY>",
apiBaseUrl: "https://api.test.basistheory.com/agentic",
displayName: "Example Agent",
});

Verify an Allowance

Pass the allowance ID and the exact rail and provider pair returned by the allowance:

const result = await agenticVerification.verifyAllowance("<ALLOWANCE_ID>", {
rail: "agentic-token",
provider: "vic",
});

console.log(result);
// { status: "active", rail: "agentic-token", provider: "vic" }

The SDK serializes calls on each instance. A second verifyAllowance call while one is active rejects rather than running two ceremonies against the same UI and browser resources. Calling it for an already-active rail resolves without contacting the provider.

Customize the UI

The built-in UI renders in a Shadow DOM and includes method selection, OTP entry, provider-ceremony confirmations, waiting and progress states, retry prompts, errors, and success.

These are the principal states with the default appearance; the error states are shown in Handle Errors:

Appearance and Copy

Set four CSS colors with appearance. All values must be valid CSS colors.

const agenticVerification = AgenticVerification({
apiKey: "<PUBLIC_API_KEY>",
appearance: {
primaryColor: "#1d4ed8",
secondaryColor: "#e5e7eb",
backgroundColor: "#ffffff",
fontColor: "#111827",
},
});
PropertyControls
primaryColorPrimary buttons, selected methods, focused OTP boxes, and spinner accent
secondaryColorMethod cards, OTP boxes, secondary buttons, and spinner track
backgroundColorModal surface
fontColorHeadings, labels, buttons, and derived muted text
successColorAnything that represents successful operations
errorColorAnything that represents operations in error

Override user-facing text with strings:

const agenticVerification = AgenticVerification({
apiKey: "<PUBLIC_API_KEY>",
strings: {
methodSelectTitle: "Confirm your identity",
otpTitle: "Enter the code we sent",
continueButton: "Continue securely",
successTitle: "You're verified",
},
});

Placeholders in built-in strings, such as {minutes} and {attempts} in otpHint, remain available to localized overrides.

Replace Individual Prompts

Keep ui: true and pass one or more handlers to replace only the prompts your application owns:

const agenticVerification = AgenticVerification({
apiKey: "<PUBLIC_API_KEY>",
handlers: {
collectOtp: ({ method, maxAttempts, attempt, error }) =>
openYourOtpDialog({ method, maxAttempts, attempt, error }),
},
});

The built-in UI continues to render every step without an override, plus waiting, progress, retry, error, and success states.

Bring Your Own UI

Set ui: false and provide a handler for every prompt. The SDK still owns the API state machine.

function confirmWithButton({ network, openPopup }) {
return new Promise((resolve, reject) => {
const button = document.createElement("button");
button.type = "button";
button.textContent = `Continue with ${network}`;

button.addEventListener(
"click",
() => {
button.remove();
try {
resolve(openPopup());
} catch (error) {
reject(error);
}
},
{ once: true },
);

document.body.append(button);
});
}

const agenticVerification = AgenticVerification({
apiKey: "<PUBLIC_API_KEY>",
ui: false,
handlers: {
selectOtpMethod: async (methods) => showMethodPicker(methods),
collectOtp: async ({ method, maxAttempts, attempt, error }) =>
showOtpInput({ method, maxAttempts, attempt, error }),
confirmCeremony: confirmWithButton,
},
});

The helper is intentionally minimal; render the control in your own dialog or component. Its ordering is mandatory: confirmCeremony must call openPopup() synchronously inside the click listener. Browser user activation ends when that listener returns, including after an await. Returning true without opening the popup is rejected with ConfigurationError.

selectOtpMethod returns a method ID. Treat each method's masked value as opaque display text. Returning null, returning undefined, or throwing from that handler cancels selection. collectOtp returns the code and is called again with error set after INVALID_OTP; return null or undefined to cancel code entry. confirmCeremony cancels when it returns null or false without opening.

Headless mode has no built-in retry prompt: a retryable failure rejects the verifyAllowance promise, and your application decides whether to call it again.

Use with React

No React wrapper is required. Keep one instance for the component lifecycle and dispose it on unmount:

import { AgenticVerification } from "@basis-theory/web-agentic";
import { useEffect, useMemo, useState } from "react";

function VerifyButton({ allowanceId, rail, provider, publicApiKey }) {
const [error, setError] = useState(null);
const agenticVerification = useMemo(
() => AgenticVerification({ apiKey: publicApiKey }),
[publicApiKey],
);

useEffect(
() => () => agenticVerification.dispose(),
[agenticVerification],
);

async function verify() {
setError(null);
try {
await agenticVerification.verifyAllowance(allowanceId, {
rail,
provider,
});
} catch (verificationError) {
setError(verificationError);
}
}

return (
<>
<button onClick={verify}>Verify allowance</button>
{error && <p role="alert">{error.message}</p>}
</>
);
}

Factory Options

AgenticVerification(options) returns collectDeviceContext, verifyAllowance, and dispose.

OptionTypeDefaultDescription
apiKeystringNonePublic key with agentic:allowance:verify. Required unless verify is provided
apiBaseUrlstringhttps://api.basistheory.com/agenticAgentic Payments API base URL
displayNamestringAgent1 to 60 characters shown on the provider's ceremony screens
platformTypestringWEBForwarded in device context. Common values are WEB, MOBILE, and NATIVE
uibooleantrueSet to false when all prompts use custom handlers
appearanceobjectDark paletteBuilt-in UI colors
stringsobjectEnglish copyUser-facing string overrides
handlersobjectNoneMixed or headless prompt handlers
otpLengthnumber6Number of OTP entry boxes in the built-in UI
openRedirect(uri) => voidBrowser popupLaunch override for redirect ceremonies (e.g. Mastercard) in webviews or native shells
completePollAttemptsnumber10Poll attempts for ceremonies that finish through complete (e.g. Mastercard)
completePollDelayMsnumber2000Delay between complete poll attempts in milliseconds
requestTimeoutMsnumber30000Timeout for each built-in API request in milliseconds
onEvent(event) => voidNoneReceives state_change, ceremony_open, restart, error, and success events
verifyfunctionBuilt-in public transportCustom verification transport for backend routing
getAllowancefunctionNoneOptional private allowance read for provider discovery and final refresh with a custom transport
bridgeOriginsstring[]Basis Theory API origins plus apiBaseUrlOrigin allowlist for ceremony callback messages (e.g. Mastercard's callback bridge). Override only for an API origin you control

verifyAllowance Options

OptionTypeDefaultDescription
providervic or agentpayNoneRequired with the built-in public transport
railstringagentic-tokenThe only rail with a browser ceremony. Any other value is rejected with ConfigurationError
displayNamestringFactory valuePer-call display-name override
timeoutMsnumber300000Overall ceremony budget in milliseconds
signalAbortSignalNoneCancels the active verification

The promise resolves to { status, rail, provider, allowance? }. The built-in public transport cannot read allowances, so allowance is present only when a custom private getAllowance transport completes the optional final refresh.

Collect Device Context

collectDeviceContext(overrides?) returns the object the SDK sends with start. client_device_id is stable per device using local storage, with an in-memory fallback, and client_reference_id is new on every call. language_code and time_zone are omitted when the browser cannot report a valid value.

The browser cannot reliably discover its public IP address or outgoing Accept header, so the SDK never invents ip_address or accept_header. If a custom backend transport adds them, use an IPv4 address and the browser's actual Accept value.

Calling collectDeviceContext() yourself does not mutate the next SDK-managed request. Use its return value only when a custom transport needs to merge backend-supplied fields.

Route API Calls Through Your Backend

The direct public-key transport is the default. If policy requires Agentic API calls to pass through your backend, provide verify and optionally getAllowance. Both receive the active AbortSignal; preserve it in your fetch so cancellation and the overall ceremony timeout still work.

import {
AgenticVerification,
ApiError,
} from "@basis-theory/web-agentic";

async function readOrThrow(response) {
if (response.ok) return response.json();

const problem = await response.json().catch(() => null);
throw new ApiError(
problem?.detail ?? `Agentic API returned ${response.status}`,
{
status: response.status,
problem,
traceId: response.headers.get("bt-trace-id") ?? undefined,
},
);
}

const agenticVerification = AgenticVerification({
displayName: "Example Agent",
verify: async (allowanceId, body, { signal } = {}) => {
const response = await fetch(
`/api/agentic/allowances/${encodeURIComponent(allowanceId)}/verify`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
signal,
},
);
return readOrThrow(response);
},
});

The application-owned backend route must authenticate the browser, forward the body to the Agentic Payments Verify Allowance endpoint with a server-held key carrying agentic:allowance:verify, and return the Problem Details body plus bt-trace-id. The provider ceremonies still run in the browser; a custom transport changes only the API hop.

Dispose the Instance

Call dispose() when the owning page or component permanently unmounts:

agenticVerification.dispose();

Disposal aborts an active run and removes every SDK-owned iframe, popup, message listener, timer, and UI host. A disposed instance cannot be reused.

Handle Errors

SDK errors extend AgenticVerificationError. API errors use the RFC 7807 response type as an open set and preserve the bt-trace-id header as traceId.

The built-in terminal-error screen shows customer-safe copy only. API error codes, HTTP statuses, field errors, and trace IDs never reach it — read them from the rejected promise's ApiError, or from onEvent. In-progress states render a Cancel button that aborts the verification, and Escape does the same; success and terminal-error states render a Close button that dismisses the modal without changing the result. There is no separate close icon.

The built-in UI renders three kinds of error state: a recoverable inline message on the ceremony prompt (for example, a blocked popup), a retryable error with a Try Again button, and a terminal error with only Close:

import {
ApiError,
PopupBlockedError,
VerificationCancelledError,
VerificationNotSupportedError,
} from "@basis-theory/web-agentic";

try {
await agenticVerification.verifyAllowance("<ALLOWANCE_ID>", {
rail: "agentic-token",
provider: "vic",
});
} catch (error) {
if (error instanceof VerificationCancelledError) {
showTryAgainLater();
} else if (error instanceof PopupBlockedError) {
askCustomerToAllowPopups();
} else if (error instanceof VerificationNotSupportedError) {
offerAnotherActiveRailOrPaymentMethod();
} else if (error instanceof ApiError) {
console.error(error.type, error.status, error.traceId);
}
}
ErrorMeaning
ApiErrorBasis Theory Problem Details response or a synthetic NETWORK_ERROR
VerificationCancelledErrorThe customer or an AbortSignal cancelled the run
PopupBlockedErrorThe browser blocked the ceremony window
PopupClosedErrorA ceremony window closed before completion (e.g. the Visa popup)
CeremonyTimeoutErrorThe overall run, an API request, a hosted frame, or a completion poll exceeded its budget. For a hosted-frame timeout (e.g. Visa's), check ad/content blockers, privacy tools, browser security settings, network filtering, and CSP frame-src
VerificationNotSupportedErrorThe source cannot complete verification on the selected rail
ConfigurationErrorThe SDK options or integration contract are invalid

Network failures such as offline, DNS, TLS, or CORS errors have no HTTP response. They surface as ApiError with status: 0, type: "NETWORK_ERROR", and no trace ID.

The SDK's retry behavior is deterministic:

API typeBuilt-in SDK behavior
INVALID_OTPKeeps the same code-entry step open and calls collectOtp again with the error
Any 409 ending in _IN_PROGRESSRetries the same action four times with bounded exponential backoff; built-in UI then offers a restart
VERIFICATION_STATE_INVALIDRestarts once automatically; a repeat is offered through built-in retry UI
PROVIDER_VERIFICATION_FAILEDBuilt-in UI offers a restart from start; headless mode rejects
VERIFICATION_NOT_SUPPORTEDRejects with terminal VerificationNotSupportedError for this rail
Any other 409Rejects without retrying
NETWORK_ERRORBuilt-in UI offers a restart; headless mode rejects the retryable ApiError
Any other typeRejects with terminal ApiError

Include traceId when contacting Basis Theory Support. Treat API error types as an open set and do not automatically retry an unknown type.

Browser Support

The SDK supports current Chrome, Edge, and Firefox, plus Safari 16.4 or newer. It supports iOS Safari and Android Chrome; ceremony popups may appear as new tabs on mobile.

For a native shell or webview, set platformType to MOBILE or NATIVE. Mastercard can use openRedirect to launch an authentication session or custom tab because the SDK treats the callback message only as a cue and still polls complete. Visa requires a WebAuthn-capable webview loading your HTTPS page and allowing its hosted iframe; fully native Visa ceremonies are not supported by this SDK.

Test the Integration

Use a test tenant and set:

apiBaseUrl: "https://api.test.basistheory.com/agentic";

Test-tenant ceremonies use Basis Theory-hosted mocks of the provider ceremonies, speaking the production message protocols with no provider traffic. See Agentic Payments Testing for the test card numbers and failure scenarios.

Own the Protocol

The SDK is the recommended path even when your application supplies every visual prompt. If you must also own the verification state machine, iframe and popup protocols, origin and source validation, polling, and cleanup, follow Own the Browser Verification Flow.