Skip to main content
An agent that can buy anything on the open web will regularly hit a wall only a person can get past. Which of the five sizes? What card? This store demands a login. A code was just texted to you. Rather than guessing, the run stops and asks — through one channel, with one shape, for every kind of question. That channel is the user action.
If the thing answering these is an LLM agent, read which questions the agent may answer itself first. variant and shipping are answerable in software; card, credentials and otp are not, and the reason is not squeamishness.

How an action arrives

An action appears on the checkout object you are already polling:
string
What is being asked for: variant, shipping, card, credentials, otp, or a slug the tier-2 agent chose. Branch on this. Omitted on actions raised before this field existed.
string
UUID. Goes in the submit URL. A new action gets a new id every time — never cache one.
string
Written for a person, in the context of this specific page. Render it as-is.
object
A JSON Schema for the object you must send back as values. Always {"type":"object","additionalProperties":false,...} with a required array and a properties map.
object
What the merchant’s own checkout says this order costs — shipping and tax included — read off the live page at the moment the run stopped to ask. { "amount": "61.40", "currency": "GBP" }.Present on card actions. Show it to your user before you answer. After that the money has moved: receipt.total only exists once the checkout has succeeded, which is too late to be a confirmation step.Best-effort, so treat it as optional. It is omitted entirely when no total could be parsed, and currency can be absent when the page shows a bare number — a confident wrong price would be worse than no price, so nothing is guessed.
string
ISO 8601. Past this instant, answering fails and the checkout fails with user_action_expired.
There is at most one pending action per checkout at a time. The orchestrator runs one invocation per checkout, so there is never a race between two open questions.

Driving a UI from the schema

The schema is a real JSON Schema subset, and it is deliberately narrow so you can render it without a schema library: Every property is type: "string", always. Send strings — the API accepts any JSON in values, but the engines read them as strings, so a number will be stringified somewhere you cannot see. required lists every property name, and additionalProperties: false means extra keys are meaningless. Send exactly the listed keys.

Identifying an action

Rendering a free-text field and a card field the same way is not acceptable — you need to know which kind of question this is. pendingUserAction.key is that answer: variant, shipping, card, credentials, otp. It is set by the engine raising the action, not inferred from the merchant’s markup. The reliable read:
key is set by the engine when it raises the action, so it tells you what is being asked for regardless of what the merchant’s form happened to call its fields. Branch on it. Actions raised before this field existed have no key. If you need to handle those, or want a belt-and-braces check before putting a card number on the wire, the card action is the only one whose properties include card_number:

The action keys

Raised by both engines, always with exactly this schema. Clients key on its shape, so it is pinned in code rather than generated — on the tier-2 path the model’s own field list is discarded in favour of it.
card_expiry is typed into the merchant’s field character by character, so send it in a form a human would type: 12 / 30 or 12/30. Spaces and slashes are normalised before verification.You are never asked for a cardholder name. It comes from the buyer profile.Reaching this action is the guarantee that nothing has been charged yet: the code path that fills a card and clicks pay is only reachable from a resume that carries card values.

Submitting

The response is the full checkout object, re-serialized after the update — the same shape as GET /v1/checkouts/{id}, with pendingUserAction now gone. The request body is a discriminated union on action, and nothing else is accepted:
values is required for submit, even if empty. A body that is neither shape gets 400 with the Zod issues attached.

Declining

Declining is a first-class outcome, not an error path. The run stops immediately, the checkout goes to failed with failure.reason: "user_cancelled", and the browser is torn down. reason is optional, free text, and stored on the action for your own audit. Use it whenever you are asked for something you will not supply — a login you do not have, an OTP nobody can read, a price you do not like. Declining is much better than letting the action expire: same terminal outcome, ten minutes sooner, and the reason is recorded.

Expiry

Actions default to a 10-minute TTL, and expiresAt on the action is authoritative. When it passes:
  1. The orchestrator’s wait times out.
  2. The action row is marked expired.
  3. The checkout fails with failure.reason: "user_action_expired".
  4. The browser is torn down.
There is no extension, no grace period, and no late answer. Submitting to an already-resolved action returns 409:
The same 409 covers Action already submitted and Action already declined — which is also your idempotency signal. If a retry of your own submit returns 409 Action already submitted, the first one landed; poll the checkout rather than treating it as a failure. An action is also marked expired when the checkout goes terminal for any other reason — you cancelled it, or the run died while the question was open. That is what guarantees a succeeded, failed or cancelled checkout never comes back still advertising a pendingUserAction. If your submit races a cancellation, expect the 409.
Ten minutes is chosen against the merchant, not against you. A parked browser holds a real merchant checkout session, and those expire on their own timetable — a longer window would mostly produce runs that resume onto a dead cart.

What happens to submitted values

This is the part worth reading carefully before you put a card number in a values object.
1

A redacted copy is written to the action's audit record

Redaction is driven by the action’s key, not by what the fields happen to be called.
  • card, credentials and otpevery submitted value becomes "[redacted]", whatever its field name.
  • Every other key — values whose field name looks card-shaped (card, cc-num, pan, cvc, cvv, security code, expir) or secret-shaped (password, passcode, secret, token, otp, one-time, verification code, auth code, pin) are redacted; the rest are stored as sent.
Keying off the action is the reliable half. Field names inside a credentials action are chosen by a model reading a merchant’s login form, so they are not ours to predict — name-matching alone let plaintext passwords through, which is why it is now a backstop rather than the primary defence.
2

The raw values go to a transient, delete-on-read store

A separate table keyed by action id, with a short TTL of its own. This is the only place raw values exist.
3

The resume event carries no values at all

The orchestrator’s event bus is a third-party system with durable event payloads and durable step outputs. Values never enter it. The event carries the action id and the outcome; the values are read inside the one step that types them.
4

The row is deleted once that step completes

Not on read — a mid-flight retry would otherwise be stranded with nothing to type. It is deleted after the step that used it succeeds, and a background sweep clears anything left past its TTL regardless.
Redaction protects our stores; it does not make a password reusable-safe. A merchant password still travels over the wire, still exists in a transient row for the length of one step, and is still typed into a real login form. Use throwaway or scoped credentials where the merchant allows it, and prefer buyer sessions so a password is supplied once rather than on every run.
Submitted values are never returned by any API endpoint. pendingUserAction disappears once resolved, and no endpoint exposes resolved actions or their values — the audit copy is internal.

Cards specifically

Send single-use or agent-issued card numbers. The posture above is designed so that a database dump does not yield reusable PANs, and it holds — but “the raw number existed in a row for the duration of one checkout step” is a very different claim from “we are a card vault”, and we are not making the second one. Values are also scrubbed on the way to the timeline: on the tier-2 path, agent tool inputs pass through a filter that masks both the exact values you submitted and anything PAN-shaped, so a card number typed through a generic “fill this field” tool cannot surface in progressItems.