> ## 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 as there is no staging environment for it. Use a production key and **live** auth credentials; test/staging credentials fail with `401`.</Warning>

## Server-side or client-side

Every endpoint in this guide accepts both call styles, so pick the one that matches your app and use the same style for every request:

|             | Server-side                                                | Client-side                                                    |
| ----------- | ---------------------------------------------------------- | -------------------------------------------------------------- |
| Key         | Server key (`sk_production_...`)                           | Client key (`ck_production_...`)                               |
| User auth   | None, or `x-crossmint-user-id` to act on behalf of a user  | `Authorization: Bearer <jwt>` from your auth provider          |
| Use it when | Your backend or an agent buys for itself or for your users | Your browser app calls Crossmint directly for a signed-in user |

You choose once in [Setup](#setup); every call after that is identical. Server-side is the shorter path because it requires no external auth provider. Checkouts and buyer profiles are isolated per project and subject, so resources created under one subject are only visible to that subject.

## 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 production **API key** server-side (`sk_production_...`) or client-side (`ck_production_...`) with the scopes `agent-checkouts.create | read | update | cancel`, plus `agent-checkouts.buyer-profiles.create | read | update | delete` for buyer profiles
* For client-side calls only: an **external auth provider** (e.g. Stytch, Auth0, Clerk) that issues JWTs for your signed-in users.

## Setup

Pick your call style once — the rest of the guide is the same for both.

<Tabs>
  <Tab title="Server-side">
    <Steps>
      <Step title="Get a server-side 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 **server-side** key (`sk_production_...`) with the `agent-checkouts.create | read | update | cancel` and `agent-checkouts.buyer-profiles.create | read | update | delete` scopes. Keep it secret and never ship it to a browser or a mobile app.

        There is no auth provider to register: a server key authenticates on its own, and checkouts run under a fixed service subject for your project.
      </Step>

      <Step title="Configure your requests">
        Point your code at the production API and send the same headers 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_SERVER_API_KEY, // sk_production_... server-side key
            "x-crossmint-user-id": "YOUR_USER_ID", // optional — act on behalf of one of your users
        };
        ```

        Send `x-crossmint-user-id` to run the checkout on behalf of a specific end user, using your own identifier for that user. The subject is namespaced under your project, so a server key can only address its own project's subjects; omit the header and everything runs under the shared service subject.
      </Step>
    </Steps>
  </Tab>

  <Tab title="Client-side">
    <Steps>
      <Step title="Get a client-side 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">
        A client-side call runs *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 `ck_` 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 requests">
        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_CLIENT_API_KEY, // ck_production_... client-side key
            Authorization: `Bearer ${jwt}`, // the signed-in user's JWT
        };
        ```
      </Step>
    </Steps>
  </Tab>
</Tabs>

## Run a checkout

Every step below reuses the `BASE_URL` and `headers` from setup, so the code is identical for both call styles.

<Steps>
  <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. When you poll server-side, forward this URL to your frontend to render the same iframe.

    ```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 | automation_timeout | llm_spend_exceeded
    }
    ```

    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>
