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

# Handle Buyer Inputs

> Render ordinary and protected fields and submit one complete form response

An Agent Checkout form can contain ordinary and protected fields. Render ordinary fields with your own controls. Render each field whose `handling` is `"protected"` with `CrossmintProtectedInput`; your application receives a reference instead of its value.

## Prerequisites

* **Checkout integration**: Complete the [Agent Checkouts quickstart](/agents/agent-checkouts-quickstart).
* **React SDK**: Install `@crossmint/client-sdk-react-ui` version 4.9.0 or later.
* **API key**: Use a production key with `agent-checkouts.read` and `agent-checkouts.update`. Add `protected-inputs.create` to the client-side key when collecting protected fields.
* **User authentication**: Pass the buyer's JWT from your [external authentication integration](/wallets/guides/bring-your-own-auth). Use the same project, environment, and user as the checkout run.

## Collect and Submit Answers

<Steps>
  <Step title="Read the requested fields">
    Use an open `input_request` part from the Messages API, or `requiredAction.request` when polling the run. Both expose the same `interaction.fields` descriptors. A protected field belongs to a `"form"` interaction.

    ```json theme={null}
    {
        "type": "input_request",
        "requestId": "req_456",
        "status": "open",
        "question": "Enter your email and password to sign in.",
        "expiresAt": "2026-10-02T15:30:00.000Z",
        "interaction": {
            "kind": "form",
            "fields": [
                {
                    "key": "email",
                    "label": "Email",
                    "required": true,
                    "handling": "standard",
                    "input": { "kind": "text", "autoComplete": "email" }
                },
                {
                    "key": "password",
                    "label": "Password",
                    "required": true,
                    "handling": "protected",
                    "input": {
                        "kind": "text",
                        "display": "masked",
                        "autoComplete": "current-password"
                    }
                }
            ]
        }
    }
    ```

    Pass each protected descriptor unchanged as the component's `field`. It supplies the key, accessible label, requiredness, and supported input hints. `display: "masked"` controls appearance; `handling: "protected"` controls how the value is collected. A standard text field can also be masked.
  </Step>

  <Step title="Render ordinary fields with your controls">
    Keep the request's field order and use each `key` as its answer key. Display its `label`, respect `required`, and pass supported presentation hints to your controls. For a standard single-line text descriptor:

    ```tsx StandardTextField.jsx theme={null}
    export function StandardTextField({ field, value, onChange }) {
        return (
            <label>
                {field.label}
                <input
                    name={field.key}
                    value={value}
                    required={field.required}
                    type={field.input.display === "masked" ? "password" : "text"}
                    placeholder={field.input.placeholder}
                    autoComplete={field.input.autoComplete}
                    inputMode={field.input.inputMode}
                    onChange={(event) => onChange(event.target.value)}
                />
            </label>
        );
    }
    ```

    Your parent component owns the value and `onChange` callback. For other input kinds, choose a control and submit the corresponding typed value:

    | Input | Ordinary Control | Answer |
    | - | - | - |
    | Single-line text | Text input; use a password input for `display: "masked"` | String, preserving leading zeros |
    | Multiline text | Textarea | String |
    | Number / integer | Numeric input; parse before submission | Finite number / safe integer |
    | Boolean | Checkbox or toggle | `true` or `false`; required means an answer must be present, not necessarily `true` |
    | Choice, `selection.kind: "one"` | Select or radio group | One option's `value` |
    | Choice, `selection.kind: "many"` | Multiple select or checkbox group | Array of option values, respecting `selection.min` and optional `max` |

    Use choice labels for display and values for answers. Placeholder options are not answers. Disabled options cannot be newly selected; preserve already-selected disabled options. Browser form serialization omits disabled controls, so include those locked selections explicitly when constructing answers.

    For example, an ordinary response can contain `email: "buyer@example.com"`, `quantity: 2`, `giftWrap: false`, and `colors: ["navy", "black"]`. Submit only keys requested by this form. Omit unanswered optional keys; `null` is not an answer.
  </Step>

  <Step title="Render and collect protected fields">
    This example handles one protected field. Your page owns the visible label, button, and error text. The iframe contains only the input. Call `ref.collect()` when the buyer chooses to continue; an HTML `<form>` is optional.

    <Snippet file="client-sdk-react-ui-installation-cmd.mdx" />

    ```tsx ProtectedField.tsx theme={null}
    import { useRef, useState } from "react";
    import {
        CrossmintProvider,
        CrossmintProtectedInput,
        type CrossmintProtectedInputRef,
        type ProtectedInputField,
    } from "@crossmint/client-sdk-react-ui";

    type Props = {
        apiKey: string;
        jwt: string;
        field: ProtectedInputField;
        onCollected: (answer: { protectedInputId: string }) => Promise<void>;
    };

    export function ProtectedField({ apiKey, jwt, field, onCollected }: Props) {
        const ref = useRef<CrossmintProtectedInputRef>(null);
        const [busy, setBusy] = useState(false);
        const [error, setError] = useState("");

        async function collect() {
            if (busy || ref.current === null) {
                return;
            }
            setBusy(true);
            setError("");
            try {
                const result = await ref.current.collect();
                if (result.status !== "collected") {
                    setError(result.message);
                    return;
                }
                await onCollected(result.input);
            } catch {
                setError("Could not complete collection or submission. Try again.");
            } finally {
                setBusy(false);
            }
        }

        return (
            <CrossmintProvider apiKey={apiKey}>
                <div>{field.label}</div>
                <CrossmintProtectedInput
                    ref={ref}
                    jwt={jwt}
                    field={field}
                    disabled={busy}
                    invalid={error !== ""}
                />
                {error !== "" && <p role="alert">{error}</p>}
                <button type="button" disabled={busy} onClick={collect}>
                    Continue
                </button>
            </CrossmintProvider>
        );
    }
    ```

    Pass your client-side API key as `apiKey`, the buyer's JWT as `jwt`, and the descriptor from `interaction.fields` as `field`. Mount this request's UI with `key={requestId}` so a replacement request resets its state. If your page already has a `CrossmintProvider`, use that provider instead of nesting another one. External authentication does not require `CrossmintAuthProvider`.

    For a form with multiple protected fields, the parent owns one Continue button and one ref per field. Collect them together with `Promise.all`, then submit the combined answers. Check every result before submitting. Each result is independent: correcting one field does not discard an unchanged field's successful reference.

    | Result | Your next action |
    | - | - |
    | `collected` | Use `result.input` as the answer for that field key. |
    | `invalid` | Show `result.message` and let the buyer correct the input. |
    | `unavailable` | Show `result.message` and offer a retry. |
    | `superseded` | Collect again because the field or its authentication changed. |

    `collect()` validates and registers the value before returning a reference. Numeric parsing runs during collection, so invalid numeric text is reported before you submit an answer. Unchanged successful collections reuse their reference until it expires; editing or clearing the field invalidates that reuse.
  </Step>

  <Step title="Submit the complete form answer">
    Combine the collected reference with ordinary answers under the keys from the request. For the email and password example, send this body to `POST /api/unstable/agent-checkouts/{runId}/messages`:

    <Snippet file="agent-checkouts-protected-input-response.mdx" />

    Send all required answers in one `response.answers` object, using the field keys from the request. Ordinary answers are strings, booleans, numbers, or choice values; protected answers are `{ protectedInputId }`. Answers do not repeat `handling`. Omit unanswered optional field keys.

    Follow [response and retry handling](/agents/checkouts/follow-run#respond-or-steer): retain one message ID and body until acknowledged, and use a new ID after correcting a definitive validation rejection. If the request closes or expires, load the current request.

    An accepted answer resumes the run; it does not confirm that the merchant accepted the value. Follow subsequent messages for application results or another request.
  </Step>
</Steps>

## Choose the Input Type and Presentation

| Input | Protected support | Representation |
| - | - | - |
| Single-line text | Yes | Preserves the string, including leading zeros. Use for passwords, verification codes, and identifiers. |
| Number | Yes | A finite numeric value; permits fractions. |
| Integer | Yes | A safe whole-number value. |
| Multiline text, boolean, choice | No | Render ordinary controls for fields with `handling: "standard"`. |

Use `appearance` to supply fonts and supported theme variables for the input. Style labels, buttons, and error messages in your own page. `disabled` and `invalid` update the mounted input dynamically. Numeric fields use numeric keyboard hints; a text code can specify `inputMode: "numeric"` without losing leading zeros.

Protected references last 24 hours by default and at most 7 days. The component's optional `expiresAt` sets the reference lifetime; the request's `expiresAt` is the separate deadline to answer the request.

## Use a Chat or Native Handoff

A messaging agent can collect ordinary answers, then send the buyer to an authenticated page that renders only the protected fields. Your application associates that page with the run and request, merges the references with the ordinary answers, and submits the complete response. A native app can use the same page in a webview.

Collection keeps plaintext out of your application JavaScript and answer messages. After application to a merchant page, page content, screenshots, or recordings can expose values. Do not rely on the collection component to mask browser evidence.

## Next Steps

<CardGroup cols={2}>
  <Card title="Reuse Merchant Sessions" icon="user-lock" href="/agents/checkouts/merchant-sessions">
    Reuse the buyer's merchant session on later checkouts
  </Card>

  <Card title="Send a Message" icon="message" href="/api-reference/agent-checkouts/send-agent-checkout-message">
    Review form, payment, and steering message formats
  </Card>
</CardGroup>
