Skip to main content

Accept Cards from Browser Agents

Your checkout can accept card entry from a browser agent without replacing its shopper-facing flow or changing processors. Basis Theory's combined card element registers an enter_card WebMCP tool when you opt in. The tool runs inside the secure card iframe; the merchant page receives the card's validation state, not the card number.

Choose the path that matches your checkout:

Your checkout todayAdd for agentsKeep
You already use a Basis Theory card elementTurn on agentTools on that elementYour existing card form and payment flow
Shoppers use another processor's card formMount a hidden Basis Theory card element for agentsThe visible form and your existing processor

WebMCP is experimental. Chrome currently requires the WebMCP flag or an origin trial; in browsers without WebMCP, agentTools does nothing. This feature supports the combined card element, not separate card-number, expiry, and CVC elements.

Already use Basis Theory Elements​

With Web Elements v3, enable the tool on the card you already mount:

const card = bt.createElement('card', { agentTools: true });
await card.mount('#card');

For React Elements, set agentTools on <CardElement />. The agent can call enter_card; the same validation and change events run as when a shopper types. Keep your existing charge path and Pay button. To let an agent complete checkout, your page must also register an order-summary tool and a payment tool that invokes the same charge function as the Pay button. Return the processor's actual payment outcome, not just a Basis Theory token ID. See Register your checkout tools below for the tool lifecycle and an example.

Keep your existing processor's card form​

Leave the shopper's form in place. Mount a Basis Theory card for agents in a container with the HTML hidden attribute (or display: none):

<div id="agent-card" hidden></div>
const bt = await BasisTheory('<PUBLIC_API_KEY>', { allowHttpClient: true });
const card = bt.createElement('card', { agentTools: true });
await card.mount('#agent-card');

When the agent submits payment, send the element references to your existing processor with bt.client.post. For example, if your processor accepts card data at a browser-callable endpoint:

const processorResponse = await bt.client.post(
'https://api.processor.example/payment_methods',
{ card: { number: card.number, expiration: card.expiryDate, cvc: card.cvc } }
);

The references resolve inside the secure iframe. Your page receives the destination's response, not the card values. Adapt the URL, payload, and browser-safe authorization to your processor's contract. If this endpoint only creates a payment method, your checkout must still charge it and return the real charge result to the agent. Do not treat a token or payment-method reference as a successful payment.

bt.client needs allowHttpClient: true at initialization and the processor host on the Elements destination allowlist. Contact support to add a host before integrating. The HTTP client requirements explain the request behavior and errors. Register your checkout tools for the order and payment steps; the card element supplies enter_card, not the entire checkout.

Register your checkout tools​

The card element provides enter_card. Your page can register its own order-summary and payment tools with document.modelContext.registerTool. The example below adds the payment tool to an existing Elements checkout; adapt its /api/charge endpoint and button selector to your own checkout. For a processor sidecar, use the same registration lifecycle but replace charge() with your processor flow above, including the actual charge.

Register submit_payment only while the card is complete, and have it run the same function as your pay button. An agent is then offered payment exactly when a shopper could pay. Return the outcome of the charge, not of tokenization: a token alone is not a payment, and the agent treats the tool's result as what happened.

const card = bt.createElement('card', { agentTools: true });
await card.mount('#card');
const payButton = document.querySelector('#pay-button');

const charge = async () => {
const token = await bt.tokens.create({
type: 'card',
data: {
number: card.number,
expiration_month: card.expiryDate,
expiration_year: card.expiryDate,
cvc: card.cvc,
},
});

// Your backend charges the token and reports the outcome
const response = await fetch('/api/charge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tokenId: token.id }),
});
if (!response.ok) throw new Error('The payment was declined');

return response.json(); // e.g. { status: 'paid', orderId: 'ord_123' }
};

const submitPayment = {
name: 'submit_payment',
description: 'Pays for the order with the card entered in the card form.',
annotations: { consequentialHint: true },
execute: async () => ({
content: [{ type: 'text', text: JSON.stringify(await pay()) }],
}),
};

let registration = null;
let cardComplete = false;
let paying = false;
let paymentAttempt = null;
let completedPayment = null;

// Offer submit_payment while the card is complete, but leave it alone while a
// payment runs: tokenizing clears the card, and unregistering a tool during its
// own call makes Chrome discard the result
const syncSubmitPayment = () => {
if (paying || !document.modelContext?.registerTool) return;

if (cardComplete && !registration) {
registration = new AbortController();
document.modelContext?.registerTool(submitPayment, { signal: registration.signal });
} else if (!cardComplete && registration) {
registration.abort();
registration = null;
}
};

card.on('change', ({ detail }) => {
cardComplete = detail.isValid;
syncSubmitPayment();
});

const pay = () => {
if (completedPayment) return Promise.resolve(completedPayment);
if (paymentAttempt) return paymentAttempt;
if (!cardComplete) return Promise.reject(new Error('Enter a valid card first'));

paying = true;
// Set the shared attempt before starting tokenization. Both the button and
// WebMCP tool reuse it, even if called in the same turn.
paymentAttempt = Promise.resolve()
.then(charge)
.then((result) => {
completedPayment = result;
return result;
})
.finally(() => {
paymentAttempt = null;
paying = false;
setTimeout(syncSubmitPayment);
});
return paymentAttempt;
};

payButton.addEventListener('click', () => {
void pay().catch((error) => {
// Show this error in your checkout UI; the agent receives its own rejection.
console.error('Payment failed', error);
});
});

The button and tool share one in-flight attempt. A completed result is reused so another click or tool call cannot charge the same order again. Chrome discards the result of a tool that is unregistered while it is running. Tokenizing clears the card, which fires a change event with isValid: false during the call, so the example holds the registration still until pay() returns. On failure, the card must be valid again before retrying. This browser guard is not a substitute for a server-side order idempotency key: use the same key on retries, including when the network fails after the processor accepts a charge.


Test the flow​

Enable WebMCP in Chrome and use the DevTools WebMCP pane to invoke enter_card with a test card. Verify the card becomes valid, the agent's payment tool uses the same order total and charge path as your checkout, and its result reflects the processor's response. Test the normal shopper flow in a browser without WebMCP too.

The agent supplies the card data when it calls enter_card and remains responsible for how it handles that data. The merchant page does not receive the full card number from the tool. See security and browser support before going live.