> ## Documentation Index
> Fetch the complete documentation index at: https://docs.crossmint.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Agent Checkouts

> Get your agent buying from any merchant URL in 15 minutes.

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.

<CardGroup cols={2}>
  <Card title="Try the live demo" icon="rocket" href="https://agent-checkouts.demos-crossmint.com">
    See the full flow in action without setting anything up locally.
  </Card>

  <Card title="Agent Checkouts Quickstart" icon="code" href="https://github.com/Crossmint/agent-checkout-quickstart">
    Explore the full reference implementation this guide is based on.
  </Card>
</CardGroup>

<Warning>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`.</Warning>

## 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

<Steps>
  <Step title="Get a Crossmint API key">
    Sign in to the <a href="https://www.crossmint.com/console" target="_blank">Crossmint Console</a> 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.
  </Step>

  <Step title="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 <a href="https://www.crossmint.com/console/projects/apiKeys" target="_blank">Crossmint Console</a>, 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.

    <Warning>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.</Warning>
  </Step>

  <Step title="Configure your client">
    Point your client at the production API and send both credentials on every request.

    ```typescript theme={null}
    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
    };
    ```
  </Step>

  <Step title="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.

    ```typescript theme={null}
    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
    ```
  </Step>

  <Step title="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`.

    ```typescript theme={null}
    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).
  </Step>

  <Step title="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.

    ```typescript theme={null}
    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);
    }
    ```
  </Step>

  <Step title="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.

    ```tsx theme={null}
    // 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" />;
    ```
  </Step>

  <Step title="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.

    ```typescript theme={null}
    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" /* … */ },
            }),
        });
    }
    ```

    <Warning>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.</Warning>

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

    <Accordion title="Why single-use and virtual card numbers are safe to submit, but a real PAN is not">
      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](https://usa.visa.com/dam/VCOM/global/support-legal/documents/expanded-pci-dss.pdf)).
      * **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](https://www.mastercard.com/content/dam/public/mastercardcom/globalrisk/pdf/Virtual%20Card%20Numbers%20and%20SDP%20Compliance%20FAQs%20\(15%20February%202022\).pdf)).

      The agent-card programs that issue these scoped tokens: [Visa Intelligent Commerce](https://developer.visa.com/capabilities/visa-intelligent-commerce) and [Mastercard Agent Pay](https://www.mastercard.com/us/en/business/artificial-intelligence/mastercard-agent-pay.html).
    </Accordion>
  </Step>

  <Step title="Read the result">
    When the checkout reaches a terminal state, read the outcome.

    ```typescript theme={null}
    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.
  </Step>
</Steps>

## Next Steps

<CardGroup cols={2}>
  <Card title="Provide Purchase Context" icon="address-card" href="/agents/payment-flows/agent-checkouts-buyer-context">
    Buyer profiles and the buyer request — prefill identity, shipping, and intent.
  </Card>

  <Card title="Choose a Payment Method" icon="credit-card" href="/agents/payment-flows/agent-checkouts-payment-method">
    Supported methods, and how to pass a card safely.
  </Card>

  <Card title="API Reference" icon="code" href="/api-reference/agent-checkouts/create-agent-checkout">
    The full request/response schema for every endpoint.
  </Card>
</CardGroup>
