Skip to main content
An Agent Checkout is a conversation wrapped around a purchase. You start a run with a URL, follow its state and messages, answer anything the agent cannot decide, and let the user steer it until the purchase finishes. By the end, you will know how to start a run, observe it through a server-side stream or polling fallback, respond to every supported input type, and send new instructions while the agent is working.

Try the Live Demo

See the complete run loop without setting up an app

Example Repository

Explore a complete implementation of this quickstart
Agent Checkouts runs in production only. Use a production API key and live authentication credentials. Staging and test credentials return 401.

Prerequisites

  • Node.js: Version 20 or later
  • Crossmint API key: A production server-side key with agent-checkouts.create, agent-checkouts.read, agent-checkouts.update, and agent-checkouts.cancel scopes
  • Buyer identifier: A stable identifier from your system for the user making the purchase

Understand the Run Loop

Your integration repeats the same loop until the run reaches a terminal status: The run and its messages are two views of the same work. Prefer the message stream for a live integration. It emits both run updates and message updates, so one connection can drive your control loop and user experience. Use polling when your runtime cannot hold an open connection.

Run a Checkout

1

Configure the client

This quickstart uses a server-side key. Send x-crossmint-user-id so the checkout, payment authorization, and protected inputs belong to the same user.
Keep the server key outside browser and mobile bundles.
Use a client-side production key (ck_production_...) in X-API-KEY and send the signed-in user’s JWT as Authorization: Bearer YOUR_USER_JWT. Register the JWT issuer under 3P Auth providers in the Crossmint Console and restrict the key to your app’s allowed origins. The request bodies and run loop stay the same.
2

Start a run

Every run needs a startUrl and maxCost. The task is optional.
  • Use a product URL when the page already identifies what to buy.
  • Use a merchant home page, category, search, cart, or any other URL when the agent must navigate before checkout.
  • Add a task whenever the URL alone does not fully describe the goal, such as the product, variant, quantity, delivery preference, or payment method.
To start from a product page without extra instructions, omit task:
The request returns 202 Accepted with a runId. The run starts as queued, then moves to running.The checkout returns blocked with code policy.max_cost_exceeded instead of spending above maxCost.
Never put card details or merchant passwords in task. Payment uses an Agent Card authorization, and passwords use protected inputs, as shown below.
3

Choose how to observe the run

Agent Checkouts supports several integration shapes. The checkout browser is managed by Crossmint and does not need to be displayed to the user.Open the stream with the same server-side headers used to start the run. It is a standard HTTP response with content type text/event-stream, so it works in Node.js, serverless runtimes that support streaming responses, workers, and long-running services.
The stream emits two event types:
For each event:
  1. Store its id as the resume cursor.
  2. On run.updated, replace your current run state only when its revision is greater than your stored run revision. If it is awaiting_input, handle its requiredAction once by requestId.
  3. On message.upsert, pass the message to the part handler described below.
  4. Reconnect with the cursor in the last-event-id header or after query parameter.
  5. If a cursor is rejected or the stream closes unexpectedly, list the message history again and reconnect from its streamCursor.
The server closes the stream after the run reaches succeeded, blocked, failed, or cancelled.
An iMessage agent, ChatGPT integration, or other conversational agent normally consumes this stream on its backend. It maps agent replies and input requests into its own channel, then sends the user’s response back through the Messages API.

Bootstrap or Resynchronize the Stream

For an existing run, list its messages before opening the stream. The final page’s streamCursor forms a lossless handoff to live events. Use it as the stream’s after value, and ignore duplicate or stale message and run revisions.Use the same procedure after an unexpected disconnect or rejected cursor:
Reconnect to GET /{runId}/messages/stream?after=${streamCursor}. Reuse answeredRequestIds in the stream handler so a replayed run.updated event does not submit the same response twice. See List Agent Checkout Messages and Stream Agent Checkout Messages for pagination and stream details.

Poll as a Fallback

When you cannot keep a stream open, poll the run for status and requiredAction. Start with a short interval while the agent is active, increase it while nothing changes, and reset it after a status change or response.The handleRequiredAction dispatcher is implemented below because its form, payment, and protected-input branches connect to the appropriate user surface.
A poll immediately after your response can still contain the same action. Track each requestId so you answer it only once.The run’s browser object is null until a browser session exists. When present, a web app can optionally render browser.embedUrl in an <iframe> so the user can watch the read-only session. Other integrations can ignore it.
4

Handle message parts

Both listed history and message.upsert events use the same message shape. Store messages by id, replace a stored message only when a higher revision arrives, and use role to identify the speaker.A message revision orders versions of one message ID. A run revision orders complete run snapshots from polling and run.updated events. Revision values may skip and have no meaning beyond ordering; the stream cursor is the separate replay position used to reconnect.Handle every part in a message because one message can contain several parts.
5

Handle required actions

Every requiredAction points to an open input_request. Dispatch on requiredAction.request.interaction.kind: form, payment, or protected.
First, use one helper for every response. Generate a stable message id before the call and reuse it if you retry the same HTTP request.

Handle a Form Request

A form request carries responseSchema and uiSchema. Render the fields from those schemas instead of hardcoding them; the same path handles shipping details, product options, contact information, and merchant-specific questions.
Submit the keys defined by that request’s responseSchema, and answer before expiresAt.

Handle a Payment Request

A payment request carries the amount and merchant domain. Follow Choose a Payment Method to save or select the card, create an order intent for the exact amount, and complete any required verification.When that process returns an orderIntentId, submit it as the response:
The order intent authorizes a secure, purchase-scoped card credential. Raw card details never appear in the run request, message history, or response.

Handle a Protected Input Request

A protected request currently has purpose: "password". Follow Buy from Authenticated Stores to create the secure user handoff and render CrossmintProtectedInput.When the component returns a protectedInputId, submit it as the response:
The password never reaches your app, your server, the model, or the message history.If the user does not want to submit the requested value, send either:
  • action: "alternative" with free-text text, such as "Check out as a guest instead"
  • action: "decline" to refuse the request
After every response, return to the run loop. The agent can ask for several inputs during one purchase.
6

Let the user steer

The initial task is not final. Send a text message at any time before the run finishes to change preferences, answer a conversational question, or redirect the agent.
The API returns 202 Accepted when it accepts the message. This does not mean the agent has consumed it yet. Read the message again and check its delivery field when your UI needs to show that distinction.
7

Read the result

The terminal status tells you how the run ended. Use the structured result for application logic and the final result message for the user-facing summary.
  • succeeded: The purchase was confirmed. result.purchase contains a captured receipt or reports that the merchant confirmed the order without exposing one.
  • blocked: The agent stopped safely because of a policy, product, fulfillment, merchant, or payment condition. Use result.code for logic and result.summary for display.
  • failed: The run could not continue because of an input timeout, model, browser, accounting, or runtime failure. Use reason for recovery logic.
  • cancelled: Your app or the user cancelled the run.
To cancel an active run, call POST /{runId}/cancel. The request returns 202 Accepted; continue following the run until its status becomes cancelled.

Launching in Production

Agent Checkouts already uses the production API. Before serving users:
  1. Create a production server-side or client-side API key with the required Agent Checkouts scopes.
  2. Keep one stable user subject across the run, order intent, and protected input.
  3. Route card authorization through an order intent and passwords through CrossmintProtectedInput; never place secrets in text or form messages.
  4. Treat message IDs as idempotency keys, resume the SSE stream from its cursor, and retry transient read failures.

Learn More

Provide Purchase Context

Prefill identity, shipping, and intent so the agent asks fewer questions

Choose a Payment Method

Authorize a secure card credential with an order intent

Buy from Authenticated Stores

Collect passwords securely and reuse merchant logins

Create a Checkout

Review the complete run request and response schemas

Send a Message

Review every message your app can send