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

# Follow a Run

> Observe checkout updates, recover after disconnects, and respond or steer through messages

Follow a checkout from its returned `runId` until it reaches a terminal status. The run exposes current status and `requiredAction`; its messages expose conversation, progress, activity, input requests, and results.

## Prerequisites

* A checkout started through the [quickstart](/agents/agent-checkouts-quickstart)
* A production API key with `agent-checkouts.read` and `agent-checkouts.update`; add `agent-checkouts.cancel` to cancel runs
* The same buyer identity used to create the run

## Authenticate Each Request

For a backend integration, keep the server key outside client bundles and send the buyer's identifier on every call:

```typescript theme={null}
const BASE_URL = "https://www.crossmint.com/api/unstable/agent-checkouts";
const runId = "YOUR_RUN_ID";
const headers = {
    "Content-Type": "application/json",
    "X-API-KEY": process.env.CROSSMINT_API_KEY!,
    "x-crossmint-user-id": "YOUR_USER_ID",
};
```

For browser or native calls, use a production client-side key and `Authorization: Bearer YOUR_USER_JWT` instead of `x-crossmint-user-id`. Register your JWT issuer under **3P Auth providers** in the <a href="https://www.crossmint.com/console/projects/apiKeys" target="_blank">Crossmint Console</a> and allow your app's origins. The request bodies are the same.

## Observe and Recover

<Steps>
  <Step title="Choose the observation strategy">
    | Strategy | Use it when |
    | - | - |
    | Backend SSE stream | Your server can keep a connection open. Relay updates to your website, native app, messaging agent, or voice agent through its existing channel. |
    | Direct client stream | Your browser or native client calls Crossmint with a client key and JWT. Use a fetch-based SSE client; native `EventSource` cannot attach the authentication headers. |
    | Polling | Your runtime cannot keep a stream open. Read the run for status and requests, and list messages for conversation details. |

    Crossmint recommends streaming for live production integrations. The [quickstart](/agents/agent-checkouts-quickstart) uses polling to keep its example small.
  </Step>

  <Step title="Open the message stream">
    The stream is an HTTP response with content type `text/event-stream`. It carries both run snapshots and message changes:

    ```bash theme={null}
    curl --no-buffer \
        --url "https://www.crossmint.com/api/unstable/agent-checkouts/YOUR_RUN_ID/messages/stream" \
        --header "Accept: text/event-stream" \
        --header "X-API-KEY: YOUR_SERVER_API_KEY" \
        --header "x-crossmint-user-id: YOUR_USER_ID"
    ```

    ```text theme={null}
    id: cursor_01
    event: run.updated
    data: {"runId":"...","revision":3,"status":"awaiting_input","requiredAction":{...}}

    id: cursor_02
    event: message.upsert
    data: {"id":"msg_...","revision":2,"role":"assistant","parts":[...]}
    ```

    Store each event's `id` as the resume cursor. Keep the greatest run `revision`, and the greatest message `revision` for each message ID. Replayed or stale snapshots must not replace newer state. Revisions can skip; they order snapshots, while the cursor identifies the stream replay position.

    Handle open requests once by `requestId`, whether they arrive in `requiredAction` or a message part. Dispatch `interaction.kind: "form"` to [Handle Buyer Inputs](/agents/checkouts/buyer-inputs), and `"payment"` to [Authorize Payments](/agents/checkouts/payments).

    Reconnect with the cursor in the `last-event-id` header or `after` query parameter. If the cursor is rejected or recovery needs a fresh snapshot, use the next step.
  </Step>

  <Step title="Bootstrap or resynchronize from history">
    List all message pages before opening a stream for an existing run. The final page's `streamCursor` provides the handoff to live updates. Read the current run after history so you also discover a pending action.

    ```typescript theme={null}
    // Uses BASE_URL, runId, and headers from the authentication section.
    const messages = new Map();
    let cursor: string | null = null;
    let streamCursor: string | null = null;

    do {
        const query = new URLSearchParams({ limit: "100" });
        if (cursor !== null) {
            query.set("cursor", cursor);
        }
        const response = await fetch(`${BASE_URL}/${runId}/messages?${query}`, { headers });
        if (!response.ok) {
            throw new Error(`Could not list messages: ${response.status}`);
        }
        const page = await response.json();
        for (const message of page.data) {
            const existing = messages.get(message.id);
            if (!existing || message.revision > existing.revision) {
                messages.set(message.id, message);
            }
        }
        cursor = page.nextCursor;
        streamCursor = page.streamCursor;
    } while (cursor !== null);

    const response = await fetch(`${BASE_URL}/${runId}`, { headers, cache: "no-store" });
    if (!response.ok) {
        throw new Error(`Could not read checkout: ${response.status}`);
    }
    const run = await response.json();
    const streamUrl = new URL(`${BASE_URL}/${runId}/messages/stream`);
    if (streamCursor !== null) {
        streamUrl.searchParams.set("after", streamCursor);
    }
    ```

    Render the recovered messages and current run. If `run.status` is `"awaiting_input"` and its request has not been answered, handle `run.requiredAction` before connecting to `streamUrl` with the same authentication. Retain your request deduplication state across reconnects. See [List Messages](/api-reference/agent-checkouts/list-agent-checkout-messages) and [Stream Messages](/api-reference/agent-checkouts/stream-agent-checkout-messages) for pagination and replay details.
  </Step>

  <Step title="Poll when streaming is unavailable">
    Read `GET /{runId}` with the same headers and `cache: "no-store"`. Poll every 2 seconds while active, increase the interval while nothing changes, and reset it after a status change or response. Stop at a terminal status.

    Retry transient read failures with bounded backoff. For `429`, respect the response's `retryAfterSeconds`. This read helper illustrates the retry behavior:

    ```typescript theme={null}
    // Uses BASE_URL, runId, and headers from the authentication section.
    async function readRun() {
        const retryable = new Set([408, 429, 500, 502, 503, 504]);
        for (let attempt = 0; attempt < 5; attempt++) {
            let response: Response | undefined;
            try {
                response = await fetch(`${BASE_URL}/${runId}`, { headers, cache: "no-store" });
            } catch (error) {
                if (attempt === 4) {
                    throw error;
                }
            }
            if (response?.ok) {
                return response.json();
            }
            if (response && (!retryable.has(response.status) || attempt === 4)) {
                throw new Error(`Could not read checkout: ${response.status}`);
            }
            const body = response?.status === 429 ? await response.json().catch(() => null) : null;
            const seconds = body?.retryAfterSeconds;
            const serverDelay = typeof seconds === "number" && Number.isFinite(seconds) && seconds > 0
                ? seconds * 1_000 : 0;
            await new Promise((resolve) => setTimeout(resolve, Math.max(1_000 * 2 ** attempt, serverDelay)));
        }
        throw new Error("Could not read checkout after retries");
    }
    ```

    A poll immediately after a successful response can still contain the same action. Track answered request IDs so you do not submit again. For resumed integrations, recover message history rather than relying only on an in-memory set.
  </Step>
</Steps>

## Process Message Parts

A message can contain several parts. Store it by `id`, retain its greatest `revision`, and use `role` to identify the speaker.

| Part | What Your App Does |
| - | - |
| `text` | Show the instruction or reply. A user message's `delivery` distinguishes accepted from consumed. |
| `progress` | Show the agent's current progress. |
| `activity` | Optionally show operations and their running, completed, incomplete, or uncertain status. |
| `input_request` | Handle an `open` request according to `interaction.kind`. |
| `input_response` | Show the projected application outcome, alternative, or decline. Acceptance is separate from application at the merchant. |
| `result` | Show `outcome` and `summary`, with the receipt or blocked code when present. |

Your UI can also display the read-only checkout browser in an iframe using `run.browser.embedUrl`. `browser` is `null` until a session exists; displaying it is optional.

## Respond or Steer

Submit the [form answer](/agents/checkouts/buyer-inputs) or [payment authorization](/agents/checkouts/payments) to `POST /{runId}/messages`. Generate the message `id` once and retain its exact body until acknowledged. If acknowledgement is lost, retry the same ID and body. After a definitive validation rejection, correct the response with a new ID. Do not reuse an ID for changed content.

Respond before the request's `expiresAt`. If it closes or expires, load the current request. After a response, continue observing: the agent may ask several questions during a purchase.

To choose another path, send an alternative instead of the requested answer:

```json theme={null}
{
    "id": "msg_alternative_123",
    "parts": [{
        "type": "input_response",
        "requestId": "req_123",
        "action": "alternative",
        "text": "Check out as a guest instead"
    }]
}
```

Use `action: "decline"` without `text` to refuse a request. To steer without answering a particular request, send a text part:

```json theme={null}
{
    "id": "msg_steering_123",
    "parts": [{ "type": "text", "text": "Use standard shipping. Choose navy if black is sold out." }]
}
```

A `202 Accepted` confirms message acceptance, not that the agent consumed it. Follow its `delivery` and subsequent run updates.

## Finish or Cancel

| Run Status | Result |
| - | - |
| `succeeded` | The merchant confirmed the purchase. `result.purchase` contains a captured receipt or reports confirmation without a receipt. |
| `blocked` | The agent stopped because of a policy, product, fulfillment, or merchant condition. Use `result.code` for logic and `result.summary` for display. |
| `failed` | The run could not continue. Use `reason` for recovery logic. |
| `cancelled` | Cancellation completed. |

For a succeeded run with `result.purchase.kind: "receipt_captured"`, read `result.purchase.receipt` for the total and merchant order ID. The result message supplies the user-facing summary.

Cancel an active run with `POST /{runId}/cancel`, using the same authentication. It returns `202 Accepted`; keep observing until the run reaches a terminal status. The stream closes after terminal updates.

## Connect Your User Channel

| Channel | Integration |
| - | - |
| Web | Render inputs and authorization in your application. |
| Native | Render ordinary controls natively; use an authenticated webview for protected collection or card verification. |
| Messaging or voice | Map messages into the conversation, collect ordinary answers there, and send an authenticated application link when a protected field or verification requires a web surface. |
| Server agent | Consume updates on your backend and involve a buyer surface when input or verification requires it. |

Your application associates each handoff with the buyer, run, and request. See [Handle Buyer Inputs](/agents/checkouts/buyer-inputs#use-a-chat-or-native-handoff) for combining answers collected across channels.

## Next Steps

<CardGroup cols={2}>
  <Card title="Handle Buyer Inputs" icon="keyboard" href="/agents/checkouts/buyer-inputs">
    Render and submit ordinary and protected answers
  </Card>

  <Card title="Authorize Payments" icon="credit-card" href="/agents/checkouts/payments">
    Turn a payment request into an order intent response
  </Card>
</CardGroup>
