Skip to main content
Let your agent buy from any merchant with a single API. Hand the Agent Checkouts API a product URL and a spending cap, and Crossmint completes the checkout on your user’s behalf — paying with any method the merchant accepts. By the end you will have a script that saves a buyer profile, creates a checkout from a product URL, polls it through its lifecycle, embeds the live browser session for the user to watch, answers each pending action, and reads the final receipt.

Try the live demo

See the full flow in action without setting anything up locally.

Agent Checkouts Quickstart

Explore the full reference implementation this guide is based on.
Agent Checkouts can only be tested in production today — there is no staging environment for it. Use a ck_production_... key and live auth credentials; test/staging credentials fail with 401.

How it works

A checkout walks a simple lifecycle with no webhooks in v1 — you poll until it reaches a terminal state:
  1. Create a checkout and get back an id.
  2. Poll GET /{id} (≈ every 1.5s) as it walks the lifecycle.
  3. Embed the live browser session in an <iframe> so the user can watch.
  4. Respond to each pendingUserAction by rendering a form from the JSON Schema it carries.
  5. Read the terminal result — a receipt on success, or a failure reason.

Prerequisites

  • Node.js 20+
  • A client-side API key (ck_production_...) with the scopes agent-checkouts.create | read | update | cancel, plus agent-checkouts.buyer-profiles.create | read | update | delete for buyer profiles
  • An external auth provider (e.g. Stytch, Auth0, Clerk) that issues JWTs for your signed-in users — the key alone is not enough

Setup

1

Get a Crossmint API key

Sign in to the Crossmint Console and create a project.Under API Keys, create a client-side key (ck_production_...) with the agent-checkouts.create | read | update | cancel and agent-checkouts.buyer-profiles.create | read | update | delete scopes, and add your app’s origin under the allowed-origins setting. The ck_ key is public by design — it authenticates against the browser’s Origin, so restrict it with allowed-origins rather than treating it as a secret.
2

Register your auth provider in Crossmint

Checkouts run on behalf of a signed-in user, so every request carries two credentials — your ck_ key and the user’s JWT.In the Crossmint Console, under 3P Auth providers, register the provider whose JWTs you want Crossmint to trust. Crossmint will then accept Authorization: Bearer <jwt> headers minted by that provider.
Without the Authorization: Bearer <jwt> header the API returns 401, even with a valid key. The JWT must come from your live provider environment — a test token is rejected. Read the token fresh on each call so refreshed sessions are picked up.
3

Configure your client

Point your client at the production API and send both credentials on every request.
const BASE_URL = "https://www.crossmint.com/api/unstable/agent-checkouts";

const headers = {
    "Content-Type": "application/json",
    "X-API-KEY": CROSSMINT_API_KEY, // ck_production_... client-side key
    Authorization: `Bearer ${jwt}`, // the signed-in user's JWT
};
4

Save a buyer profile

A buyer profile is a reusable set of the buyer’s name, contact, and shipping (there is deliberately no payment block). Save one, then attach it to any checkout so the agent fills those fields from it instead of asking every time.
const res = await fetch(`${BASE_URL}/buyer-profiles`, {
    method: "POST",
    headers,
    body: JSON.stringify({
        label: "Home",
        name: { first: "Ada", last: "Lovelace" },
        contact: { email: "ada@example.com" },
        shipping: {
            addressLines: ["1 Market St"],
            locality: "San Francisco",
            administrativeAreaCode: "US-CA", // ISO 3166-2, country-prefixed
            postalCode: "94105",
            countryCode: "US", // ISO 3166-1 alpha-2, required
        },
    }),
});

const { id: buyerProfileId } = await res.json(); // 201 — save this
5

Create a checkout

Give the agent a target.url, an optional request (natural-language instruction), and a required maxCost. Attach the saved profile with buyerProfileId, and save the returned id.
const res = await fetch(BASE_URL, {
    method: "POST",
    headers,
    body: JSON.stringify({
        target: {
            kind: "direct_url",
            url: "https://merchant.example/products/classic-tee",
            request: "buy the medium in black", // optional
        },
        buyerProfileId, // attach the saved buyer profile
        constraints: {
            maxCost: { amount: "100.00", currency: "USD" }, // required
        },
    }),
});

const { id } = await res.json();
The checkout fails with max_cost_exceeded rather than ever spending above maxCost.The request is where you steer the agent. The more you specify up front — size, delivery option, billing address, which payment method to use — the fewer questions it stops to ask; say nothing and it surfaces a pending action for each. The one thing you can’t put here is card details — the agent always collects those through a payment user action (the step below).
6

Poll the lifecycle

There are no webhooks in v1 — poll GET /{id} (≈ every 1.5s) until the status is terminal. Swallow transient poll errors and retry rather than killing the loop.
const TERMINAL = ["succeeded", "failed", "cancelled"];

async function getCheckout(id: string) {
    const res = await fetch(`${BASE_URL}/${id}`, { headers, cache: "no-store" });
    return res.json();
}

let checkout = await getCheckout(id);
while (!TERMINAL.includes(checkout.status)) {
    // Show the user the live session (next step)…
    if (checkout.status === "awaiting_user_action") {
        await respond(checkout); // …and answer when asked (step after)
    }
    await new Promise((r) => setTimeout(r, 1500));
    checkout = await getCheckout(id);
}
7

Embed the live browser session

The view returns a browser.embedUrl so your user can watch (and, with write permission, interact with) the session the agent is driving.
// embedUrl may be relative to the Crossmint origin — resolve it against the
// API host, or an <iframe src> resolves it against your origin and 404s.
const embedUrl = new URL(checkout.browser.embedUrl, "https://www.crossmint.com").toString();

<iframe src={embedUrl} allow="clipboard-write" />;
8

Respond to user actions

When status is awaiting_user_action, the view carries a pendingUserAction with a responseSchema (a JSON Schema). Render the form dynamically from that schema — never hardcode fields — so the same code handles shipping, payment, or size selection.
async function respond(checkout) {
    const action = checkout.pendingUserAction;
    // action.responseSchema → walk .properties to render inputs
    // action.expiresAt      → respond before this or the checkout fails

    await fetch(`${BASE_URL}/${checkout.id}/actions/${action.id}`, {
        method: "POST",
        headers,
        body: JSON.stringify({
            action: "submit",
            values: { fullName: "Ada Lovelace", country: "GB" /* … */ },
        }),
    });
}
The actionId goes in the path only — including it in the body is rejected. More actions may follow (e.g. payment after shipping), so return to polling after each response.
Payment is one of these actions. When the agent reaches the payment step it raises a payment action, and you submit the card details in the values you send back — you can’t pre-supply them in the create request. Submit only an agent card or a single-use / virtual card number, never a real, reusable card number (PAN): a scoped or one-time number can’t be reused if it leaks; a reusable PAN can.
The card networks put scoped, one-time numbers outside PCI-DSS scope precisely because a leak can’t be reused:
  • Visa: single-use virtual Visa account numbers are “out of scope for PCI DSS protection requirements based on the low risk of fraud,” and EMVCo payment tokens “are not considered to be Visa account data and are not in scope for PCI DSS” — while “all other Visa primary account numbers (PANs) must be protected in accordance with PCI DSS” (Expanded Position on PCI DSS Applicability for Virtual Visa Accounts).
  • Mastercard: Single-Use Virtual Card Numbers are treated as out of PCI-DSS scope because the number is disabled after one authorization and cannot be reused (Virtual Card Numbers and SDP Compliance FAQs).
The agent-card programs that issue these scoped tokens: Visa Intelligent Commerce and Mastercard Agent Pay.
9

Read the result

When the checkout reaches a terminal state, read the outcome.
if (checkout.status === "succeeded") {
    const { total, merchantOrderId, evidence } = checkout.receipt;
}

if (checkout.status === "failed") {
    const { reason, message } = checkout.failure;
    // reason: max_cost_exceeded | user_cancelled | user_action_expired | automation_failed
}
To stop a checkout early, call DELETE /{id} — it’s accepted immediately and the status flips to cancelled on a later poll.

Next Steps

Provide Purchase Context

Buyer profiles and the buyer request — prefill identity, shipping, and intent.

Choose a Payment Method

Supported methods, and how to pass a card safely.

API Reference

The full request/response schema for every endpoint.