> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agentvault.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Tool reference

> Every Agent Vault MCP tool, with its parameters, what it returns and when it refuses.

Look up any of the 29 Agent Vault MCP tools, plus
[`get_pending_result`](#get_pending_result) on the hosted server: what it does,
what it takes, what it returns and when it refuses.

## Overview

Tools are grouped in the order a run uses them:

| Stage | Tools |
| - | - |
| [Discover](#discover) | `get_token_catalog`, `get_checkout_recipes`, `submit_checkout_recipe`, `get_recipe_submission` |
| [Open an account](#open-an-account) | `create_login`, `get_signup_request`, `wait_for_signup_approval`, `set_login_username`, `rotate_login_password` |
| [Drive the page](#drive-the-page) | `open_page`, `click`, `press`, `wait_for_selector`, `count`, `wait_for_popup`, `close_popup` |
| [Fill](#fill) | `fill_checkout`, `fill_login`, `declare_page_hint`, `list_page_hint_kinds` |
| [Look at the page](#look-at-the-page) | `observe_page`, `snapshot_page`, `list_frames`, `detect_challenge`, `get_network_log` |
| [Hand off](#hand-off-a-challenge) | `request_challenge_handoff` |
| [Confirm and finish](#confirm-and-finish) | `get_session_outcome`, `get_run_report`, `close_session` |

### Conventions

* **Needs an open page.** Page tools act on the browser that `open_page`
  launches. Called first, they refuse and tell you to call `open_page`. The
  discovery and account tools need no browser.
* **Errors.** A refused call returns an MCP tool error (`isError: true`) whose
  text is the proxy's message. The underlying codes and whether to retry are on
  [Error codes](/sdk/error-codes).
* **Hosted server.** On the hosted server, every tool that uses your account
  is refused until the account holder chooses what the connection may use on
  **Connected apps**. Only `get_checkout_recipes` works before then. Every tool
  also takes an optional `session` argument, which the parameter lists below
  leave out. A result from a call that ran on a session ends with a line
  naming its handle, such as `session: s_8f3k...`. Pass that handle as
  `session` on every later call in the task. The server sends no
  `Mcp-Session-Id` header, so the argument is the only way back to the
  session. Without it, a call after
  `open_page` cannot reach the open browser: a page tool is
  refused with "call open\_page first", and another `open_page` starts a new
  browser. See [On the hosted server](#on-the-hosted-server) and
  [Hosted MCP server errors](/sdk/error-codes#hosted-mcp-server).
* **Selectors.** Any selector Playwright accepts, unless a tool says otherwise.
  A `frame` parameter is the CSS selector of the iframe the element lives in.
* **Timeouts** are in milliseconds.
* **During a handoff**, page tools are refused with `challenge_required` while
  the account owner has the page. `close_session` still works, and
  `get_run_report` touches no page.

### Status line

Every prose answer's first text block, `content[0].text`, starts with one short,
machine-readable line, then a newline, then the answer as before. A client that
reads only the start of the first text block still gets what it must act on.
The line is `key=value` pairs separated by spaces, with `status` first, and it
is at most 120 characters:

```text theme={null}
status=ok filled=3/3
status=error code=limit_reached filled=0/1
status=ok session=s_8f3k...
status=pending wait=w_4fec... waiting_on_owner=true reason=new_destination
```

| Field | When |
| - | - |
| `status` | Always: `ok`, `error` or `pending`. |
| `code` | On an error that has a code: the same code the error carries, such as one on [Error codes](/sdk/error-codes). An error with no code has none. |
| `filled` | On `fill_checkout` and `fill_login`: fields filled out of the batch, also when the batch failed partway. |
| `session` | On the hosted server, when the answer names a session handle. |
| `session_ended` | On the hosted server, when the session this call used has ended. |
| `wait`, `waiting_on_owner`, `reason`, `new_request` | On a `pending` answer: the same values as its `structuredContent`. |

An answer whose text is a JSON object (`observe_page`, `get_run_report`,
`close_session`, `count`, `list_frames`, `detect_challenge`,
`get_session_outcome`, `get_token_catalog`, `get_checkout_recipes` and the
others that answer JSON) has no line before it, so `JSON.parse(content[0].text)`
works as it is. It carries the same fields as its first keys instead, compact or
indented as the JSON already was:

```json theme={null}
{"status":"ok","session":"s_8f3k...","url":"https://shop.example.com/checkout",...}
```

When the object already has a key of the same name, it keeps its own value and
the status field goes in with a leading underscore. `close_session` already has a
`status` key (`"Session closed"`), so its answer starts
`{"_status":"ok","status":"Session closed",...}`. A JSON array
(`list_page_hint_kinds`) is not wrapped in an object: the status line is a text
block of its own, `content[0]`, and the array is `content[1]`.

`get_pending_result` returns the finished call's own status, as a line or as
keys. The hosted server also
gives a schema refusal (`code=invalid_params`) a status line. The local stdio
server does not, because the MCP SDK answers those before a tool runs.

## On the hosted server

The [hosted server](/mcp/setup#connect-to-the-hosted-server) has the same 29
tools as the local one, with the same parameters, results and errors, plus
[`get_pending_result`](#get_pending_result) for a call that is still running.
Two things differ: every tool takes one more parameter, and a browser belongs to a session
you name on each call, not to the server process. The hosted browser never
tunnels through your machine: its traffic leaves from the deployment's
[residential egress](/sdk/tunnel#residential-egress) where one is configured,
and from the proxy's address otherwise. A residential egress session is HTTPS
only.

### The `session` argument

<ParamField path="session" type="string">
  The session handle an earlier result in this task returned, such as
  `s_8f3k...`. At most 200 characters. Pass it on every call so the call reaches
  the same browser. Leave it out only to start a new session.
</ParamField>

A result from a call that ran on a session ends with one more text item:

```text theme={null}
session: s_8f3k... (pass this as "session" on every later call in this task)
```

The handle is only in the result text. The server sends no `Mcp-Session-Id`
response header: an MCP client takes that header as the id of its own
connection, and ends it with an HTTP `DELETE` when it reconnects, which used to
close the browser in the middle of a task. A call without a `session` argument
still uses a handle in the `Mcp-Session-Id` request header, from a client that
kept one from before. A handle belongs to the connection that started it. On
any other connection it reads as a session that has ended.

### Calls without a session

| Tools | Without `session` |
| - | - |
| `open_page` | Starts a session with a browser and returns its handle. If it fails without opening a page, the session is closed and no handle comes back |
| `get_token_catalog`, `create_login`, `get_signup_request`, `wait_for_signup_approval`, `set_login_username`, `rotate_login_password` | Starts a session without a browser and returns its handle. Pass that handle to `open_page` and every later call |
| `get_checkout_recipes`, `submit_checkout_recipe`, `get_recipe_submission` | Runs on its own and returns no handle. Given a handle, it runs on that session |
| Every other tool | Refused before it runs, for example: "click needs a browser session: call open\_page first, then pass the `session` it returns on this call." |

Read the catalog on the session you fill on. The session keeps what
`get_token_catalog` returned and uses it to check your fills, and it records
login writes for `get_run_report`. So start a task with `get_token_catalog` (or a
login tool) and pass its handle to `open_page`, rather than letting `open_page`
start a second session.

A first call that fails starts no session, with one exception: when a login
tool itself returns an error, its session is kept and the handle returned,
because a write may have landed before the error.

### When a session ends

A session ends when you call `close_session`, after 13 minutes with no call
that reaches its browser, 56 minutes after its browser opened, a minute before
its credential lapses, when the gateway closes it to make room at the account's
session limit, or when the gateway restarts. The age check skips a session
while a call is in flight, so a long call can still be running at the proxy's
60-minute limit and fail with `hard_timeout`. The credential check does not: the
session ends then, and its browser closes once the running call finishes. See
[Limits](/reference/limits#hosted-mcp-server).

A call naming an ended session is refused, and nothing runs. The message is one
of two:

| Message begins | Meaning |
| - | - |
| "This session has ended: the gateway closed it because" | The gateway still has a record of it. The message names the reason, then after "What its close found:" gives the run `reports`, `coversWholeRun`, `vaultWrites` and any `teardownError`. Read it before starting again |
| "This session has ended: it was closed, timed out, or the gateway restarted." | No record is left. The session ended over an hour ago, more than 100 ended sessions ago on this connection, or before a gateway restart, or it belongs to another connection |

Both end with "Call open\_page without `session` to start a new one." The reasons
the first message names are:

* `it was idle too long`
* `its browser was near the proxy's one-hour limit for a session`
* `its credential was about to lapse`
* `the account was at its browser session limit and another session of this connection needed the slot`
* `no call had used it in the 2 minutes since it started, and this account's hosted apps needed its browser session slot`
* `it was asked to (close_session)`

When the close could read nothing, what it found is a `note`: "No report could
be read for that session. Do NOT read this as nothing having been filled."

<Warning>
  A gateway restart forgets every session and every record of one. A later call
  gets only the second message, with no report. Before you repeat anything the
  ended session may have done, check the account holder's
  [**Activity**](/dashboard/activity) and **Logins** pages, or the merchant's own
  order state.
</Warning>

When the handle came only from the `Mcp-Session-Id` header, and the tool can
start a session (see [Calls without a session](#calls-without-a-session)), the
call starts a new session instead and returns the new handle. A client that
kept a handle from before keeps sending it, so this keeps it from being stuck
on a dead one. If the gateway had a record of the old session, the result adds a line
beginning "The earlier session", with what its close found.

### Closing a hosted session

`close_session` needs the handle. It takes the session out of use at once, so no
new call can enter it, then closes the browser and gives back the session's
credential. Giving it back is best effort: if Agent Vault cannot be reached at
that moment, the credential stays counted toward the connection's 50 until it
expires, at most 75 minutes after it was issued. A call that failed with
`unavailable` can also leave a credential that no session owns, which
`close_session` cannot give back. See [Limits](/reference/limits#ceilings).

* Sent again for the same handle, it returns the first answer again.
* A call still running on the session is ended. If it finishes after the close,
  the reply adds a line beginning "Calls still running when the session closed
  finished afterwards", with what the final close found.
* Refused for its arguments, it still closes the session, and the reply adds
  "The session is closed anyway."
* If the close itself fails, the reply reads "close\_session hit an internal
  error, but the session is closed." followed by what the close found.

An HTTP `DELETE` closes nothing: the server answers it `405`, and a session
ends only with `close_session`, after being idle, or at one of the limits
above. See [Protocol responses](/sdk/error-codes#protocol-responses).

### Repeated and retried calls

* **The same call at the same time.** While a call is running, a second call
  with the same tool and the same arguments on the same session joins it and
  gets the same answer, instead of running twice. A first call without a session
  joins the same way on one connection. Once the first has finished, the same
  call runs again, so polling works.
* **A retransmitted call.** Calls that change something are kept for 2 minutes:
  `open_page`, `click`, `press`, `fill_checkout`, `fill_login`,
  `declare_page_hint`, `close_popup`, `wait_for_popup`,
  `request_challenge_handoff`, `create_login`, `set_login_username`,
  `rotate_login_password`, `submit_checkout_recipe`, and `get_session_outcome`
  with `confirmsCheckout: true`. The same call again (same tool, arguments,
  session and JSON-RPC id) within those 2 minutes gets the first answer back and
  does not run. A new id runs it again.
* **A retransmitted first call.** One that started a session never starts a
  second. `get_token_catalog`, `get_signup_request` and
  `wait_for_signup_approval` run again on the session the first call started, so
  they read the current state. The login writes get the first answer back.
* **What is not kept.** A refusal made before the tool ran: a sign-in or
  credential refusal, arguments that fail the schema, a missing or ended
  session, or a restarting gateway. See
  [Gateway messages](/sdk/error-codes#gateway-messages). Sending that call again
  runs it.
* **How many are kept.** The latest 200 answers per connection. A call that
  started a session counts twice. Past 200 within 2 minutes, the oldest are
  dropped early, and a retransmission of a dropped call runs again. A gateway
  restart drops them all. After a burst of calls or a restart, check the page or
  `get_run_report` before you resend a call that changes something.

### Call deadlines

A call still running after 25 seconds, or as soon as the proxy reports that it
is waiting on the owner (a signup wait keeps the 25 seconds), answers `pending`
instead of waiting, with a `wait` id, and keeps running
on the server. Fetch its answer with
[`get_pending_result`](#get_pending_result). Do not repeat the call: that starts
a second one. A call this slow can be waiting on the account owner, for
example the first fill on a new site, which waits up to 15 minutes for them to
approve the site. The pending answer names the wait once the proxy reports it,
and `waiting_on_owner` is `true` only then. When it names one, tell the user
right away rather than polling in silence. When it does not, no owner wait is
being reported for this call: do not tell the user a new approval is waiting
(one they were already told about may still be open in their email or
dashboard). Either way, keep calling
`get_pending_result` in the same turn: an agent that ends its turn is never
called back, so the answer goes uncollected and the checkout stalls.

A hosted call gets 4 minutes, and `request_challenge_handoff` gets 12. Past that
the call answers with the tool's name, for example "click did not finish within
4 minutes. Its outcome is unknown: check before repeating it." The call may still
finish on the server. Check the page or `get_run_report` before you repeat it.

### `get_pending_result`

Hosted server only. Get the answer to a call that came back `pending`.

<ParamField path="wait" type="string" required>
  The `wait` id from the pending answer.
</ParamField>

Waits up to about 25 seconds, and answers early when the proxy reports a new
wait on the owner. If the call has finished, you get its answer. If it is still running,
you get `pending` again, with the same `wait` id: call `get_pending_result`
again. When the pending answer names a wait on the account owner, tell the user
what they need to approve, and where: the email Agent Vault sent them or the
banner on their dashboard. Then keep polling in the same turn; do not end it to
wait for them. A pending answer that names no wait reports no owner wait for
this call: do not tell the user a new approval is waiting.

When the reason is known, the pending answer names it. For example, a first
`fill_checkout` on a site the account has not authorized answers: "fill\_checkout
is waiting for the account owner to approve a new site ([www.site.com](http://www.site.com)) … they can
approve it from the email titled 'Authorize new site: [www.site.com](http://www.site.com)' or the banner
on their Agent Vault dashboard. It expires at 2026-10-14T14:32:00Z." Each wait
has its own sentence: a new site, a purchase over the card's approval threshold,
a card security code to enter, a challenge to complete, or a signup to approve.
The same facts are in `structuredContent`:

| Field | Meaning |
| - | - |
| `waiting_on_owner` | `true` while the call is waiting on a request the proxy reported to the owner, `false` otherwise. Always present |
| `reason` | `new_destination`; a purchase approval (`over_threshold`, `every_purchase`, `amount_mismatch`, `amount_unknown`, `amount_unconvertible`, `amount_unknown_strict`); `cvv_entry`; `challenge_handoff`; or `signup_approval` |
| `site` | The host the owner is asked about, when known |
| `expires_at` | When the wait ends, in UTC, if the proxy said. A `signup_approval` has none |
| `new_request` | `true` when the call waits on something other than the last wait an answer named, for example a security code after a purchase approval. It is a separate request: tell the user about it too. The text then opens with "NEW REQUEST". Absent otherwise |

Until the proxy reports a wait, the answer says no request to the owner has
been reported, `waiting_on_owner` is `false`, and the other fields are absent.
Once the wait ends and the call keeps running, the answer says the call stopped
waiting on the owner and its answer will say what came of the request.

## Discover

These tools need no open page.

### `get_token_catalog`

List the mock tokens this key may use: identity, addresses, cards and logins.

<ParamField path="url" type="string">
  Narrows cards and logins to one site, for example
  `https://shop.example.com/checkout`. Identity and addresses always come back
  unfiltered.
</ParamField>

**Returns** the catalog: `identity`, `addresses`, `cards`, `logins`, and, under
a `url`, `relatedLogins`. It never contains a real card number or CVV. Full
shape: [Token catalog](/reference/token-catalog).

How the `url` filter behaves:

* A card marked `anySite: true` is not scoped to a list of sites. It is always
  included, after any card registered for that site. An empty `cards` under a
  `url` means no card for that site is visible to this key: a key limited to
  **Only selected** cards drops every other card before the `url` filter runs.
  Ask the account holder to check the key's card access on **API keys** before
  giving up on the checkout.
* A `url` that does not parse matches no site. Site-scoped cards and every login
  drop out, and the `anySite` cards are still returned.
* A returned card is a candidate, not a guarantee. The owner's rules still apply
  at fill time and can refuse it with `merchant_blocked`, or with
  `destination_unauthorized` on a site that is not an authorized destination.
* A login appears only for its own site. A login on a sibling host of the same
  site (`us.example.com` while you are on `www.example.com`) is listed in
  `relatedLogins`, hosts only. It will not fill here: navigate to that
  `website` to sign in. An empty `logins` beside an empty `relatedLogins`
  means no login for this site is visible to this key. A key limited to
  **Only selected** logins does not see logins outside its selection, so ask the
  account holder to check **Logins**, and widen the key's access if one exists,
  before calling `create_login`.

**Errors:** `invalid_api_key`, `rate_limited` (wait for `Retry-After`).

```json Example arguments theme={null}
{ "url": "https://shop.example.com/checkout" }
```

### `get_checkout_recipes`

Look up the open checkout recipe registry for a page before improvising a
checkout. Each registry call here and in `submit_checkout_recipe` and
`get_recipe_submission` gets 20 seconds, then fails with "The recipe registry
did not answer in time."

<ParamField path="url" type="string" required>
  The absolute product or checkout URL, such as
  `https://shop.example.com/product/thing`. The query, fragment and any
  hosted-checkout token are stripped before the lookup. A string that does not
  parse as a URL is sent as is, so never pass one that contains a session token.
</ParamField>

**Returns** four slots, each `null` when nobody has recorded coverage for that
host yet:

<ResponseField name="merchant" type="object | null">
  The recipe for this host: selector overrides, gotchas, `preconditions`, and a
  `deadEnd` classification when unattended checkout is impossible there. A
  `deadEnd` means do not attempt the checkout, and says why.
</ResponseField>

<ResponseField name="platform" type="object | null">
  The platform recipe the merchant names: detection, `cardSurface`, step-by-step
  fill instructions and, where recorded, frame selectors under `selectors`.
</ResponseField>

<ResponseField name="signup" type="object | null">
  How this host's signup form behaves.
</ResponseField>

<ResponseField name="signupPlatform" type="object | null">
  The identity provider the signup recipe names (for example Auth0,
  Okta, Cognito or Shopify customer accounts).
</ResponseField>

Read `preconditions` first. Each is a step to take, not a reason to stop:

| Precondition | What to do |
| - | - |
| `delivery_location` | Choose a delivery address or postcode first. Fill it from your address tokens |
| `region_select` | Pick a country storefront first |
| `account_required` | There is no guest path. Sign in or open an account |

A signup recipe's `ownerSteps` lists what the account owner does after submit.
For each `email_code`, `sms_code` or `captcha`, call
`request_challenge_handoff`. For an `email_link`, stop and tell the owner which
site is waiting. A repeated step is the site asking again, not a rejected code.

<Note>
  A signup recipe describes a form. It is not permission to open an account,
  which always goes through `create_login` and the owner's approval, given per
  request or in advance on their [**Pre-approved signups**](/dashboard/logins#pre-approved-signups) list.
</Note>

```json Example arguments theme={null}
{ "url": "https://shop.example.com/product/thing" }
```

### `submit_checkout_recipe`

Contribute what you learned about a site to the registry, after solving a new
site or hitting a dead end the registry does not know about.

<ParamField path="recipe" type="object" required>
  The recipe body. A merchant recipe carries `id` (the primary host),
  `kind: "merchant"`, `hosts`, `platform` and `status`, plus optional `cardSurface`,
  `overrides`, `gotchas`, `notes`, `deadEnd`, `exampleProductUrl` and
  `preconditions`. Signup and signup platform recipes are accepted too. See
  [Recipe format](/registry/recipe-format).
</ParamField>

<ParamField path="note" type="string">
  Free text for the maintainer, such as how you tested it. Never personal data.
  With `verificationRequested`, an approved note is published inside the recipe
  as the evidence, word for word.
</ParamField>

<ParamField path="verificationRequested" type="boolean">
  `true` when a real purchase completed (or, for a signup recipe, an account was
  actually created) and `note` holds the sanitized evidence.
</ParamField>

**Returns** the submission `id`, its `status` and its `verdict`. The call waits
up to about 45 seconds for screening. If screening has not finished by then,
`status` is still `pending` and `verdict` is empty: poll the same `id` with
`get_recipe_submission`. Do not resubmit.

Rules the registry enforces:

* Never claim `status: "verified"`. That status is maintainer gated. Use
  `partial`, `unverified` or `dead-end`, and request verification instead.
* `preconditions` is a closed vocabulary. A merchant recipe accepts exactly
  `delivery_location`, `region_select` and `account_required`, with no repeats.
  A signup recipe accepts exactly `region_select`, `consent_gate` and
  `mode_toggle`. Omit the field rather than sending an empty array.
* A submission for the same store under another host is combined with the
  existing recipe.

**Errors:** `invalid_recipe` (fix it, do not resend unchanged), `rate_limited`
(the budget of 20 in any 24 hours: stop submitting until the oldest submission
in the last 24 hours turns 24 hours old).

```json Example arguments theme={null}
{
  "recipe": {
    "id": "shop.example.com",
    "kind": "merchant",
    "hosts": ["shop.example.com"],
    "platform": "shopify",
    "status": "partial",
    "gotchas": ["Cookie banner intercepts the first click."]
  },
  "note": "Reached a filled card form; did not submit."
}
```

### `get_recipe_submission`

Read the verdict of a submission that was still pending. Poll this rather than
resubmitting.

<ParamField path="id" type="string" required>
  The submission id `submit_checkout_recipe` returned.
</ParamField>

**Returns** the submission `id`, its `status` and its `verdict`. `status` is
one of:

* `pending`: screening is still running, even after `submit_checkout_recipe`
  gave up its 45 second wait. `verdict` is empty. Poll the same `id` again
  shortly, and do not resubmit.
* `approved`: the recipe is live.
* `merged`: it was combined with an existing recipe for the same store.
* `rejected`: `verdict` names the rule it broke.
* `needs_human`: a maintainer will decide. No action needed.

**Errors:** `not_found` (the id will not start existing).

```json Example arguments theme={null}
{ "id": "SUBMISSION_ID" }
```

## Open an account

`create_login`, `set_login_username` and `rotate_login_password` write to the
credential store, so they need a key the owner has allowed to create logins.
That permission is off by default per key; without it those tools refuse with
`signup_not_enabled`. `get_signup_request` and `wait_for_signup_approval` only
read, and answer only the key that filed the request. None needs an open page.

```mermaid theme={null}
flowchart LR
    A[get_token_catalog] -->|no login visible to this key| B[create_login]
    B -->|pending| C[wait_for_signup_approval]
    B -->|approved: pre-approved site| H[get_signup_request]
    H --> D
    C -->|still_pending or unknown| C
    C -->|approved| D[fill_login on the signup form]
    C -->|denied or expired| E[Tell the user and stop]
    D --> F[Submit, confirm the site accepted the handle]
    F --> G[set_login_username, if the site asked for one]
```

### `create_login`

Open an account on a site. Returns a **request handle**, not a credential.
Usually the request is pending until the account owner approves it. If the owner
has put this site and this exact email on their [pre-approved signup list](/dashboard/logins#pre-approved-signups), the
login is created at once and the handle comes back already `approved`.

<ParamField path="website" type="string" required>
  The site's registrable domain, for example `doordash.com`. Not the signup URL.
  The credential is locked to this domain and its subdomains, so a sign-in that
  later moves to `identity.doordash.com` still works. A bare suffix such as
  `com` or `co.uk` is refused.
</ParamField>

<ParamField path="email" type="string" required>
  A mailbox the agent controls. Never invent one. It is stored readable, and the
  owner sees it before deciding.
</ParamField>

<ParamField path="username" type="string">
  The site's separate handle. Pass it only when the site has **already accepted**
  that exact handle. Approval stores whatever you pass, and only the first handle
  sticks. Otherwise omit it and record one later with `set_login_username`.
</ParamField>

<ParamField path="minLength" type="integer">
  The site's stated minimum password length, when longer than the default of 24.
  At most 64, the longest password the vault generates. A higher value is
  refused with `invalid_policy`: on such a site, ask the account holder to add
  the login themselves.
</ParamField>

<ParamField path="maxLength" type="integer">
  The site's stated maximum password length. Below 12 is refused with
  `password_too_short`.
</ParamField>

<ParamField path="noSymbols" type="boolean">
  `true` only when the site rejects punctuation. It weakens the password, so do
  not set it speculatively.
</ParamField>

<ParamField path="symbols" type="string">
  The exact punctuation the site accepts. Applied as a filter over the vault's
  own safe set, `!@#$*+-_=.?`: characters outside that set are ignored. If none
  of them is in the set, the call is refused with `invalid_policy`. Pass
  `noSymbols` instead when the site takes no punctuation at all.
</ParamField>

<ParamField path="noRepeats" type="boolean">
  `true` when the site rejects three identical characters in a row.
</ParamField>

Pass only rules the form actually states. Each rule narrows the password.

The generated password is 12 to 64 characters long, 24 by default. It always
holds at least one lowercase letter, one uppercase letter and one digit, plus
one symbol unless `noSymbols` is set. A `minLength` greater than `maxLength` is
refused with `invalid_policy`.

**Returns** `requestId`, `status`, `expiresAt` and a reminder `note`. Nothing in
it is safe to type into a form. `status` is one of:

| `status` | Meaning | Next step |
| - | - | - |
| `pending` | The owner has not decided yet | Wait with `wait_for_signup_approval`, or poll `get_signup_request` |
| `approved` | The site is on the owner's pre-approved list, so the login already exists | Call `get_signup_request` (or `wait_for_signup_approval`) on the `requestId` now. The first answer carries the tokens. Do not wait for a decision |

Agent Vault still tries to notify the owner of every account opened from the
pre-approved list (best effort). Pre-approval completes at once for at most 2
signups per listed site and, by default, 60 per account in a 24-hour window.
Past either ceiling, or when another approval check fails, the request falls back
to `pending`, so handle both statuses. See [Limits](/reference/limits#ceilings).
The 500-login ceiling is the exception: an account that already holds 500 logins
gets `login_limit` straight away, pre-approved site or not, and no request is
created.

<Warning>
  A `pending` result stops `create_login`, not the signup. Wait with
  `wait_for_signup_approval` or poll `get_signup_request` on the same
  `requestId`. Calling `create_login` again opens a second, unrelated request and
  spends another of the account's five outstanding-request slots.
</Warning>

**Errors:** `signup_not_enabled`, `too_many_pending`, `login_limit` (the
account already holds 500 logins, counting ones the account holder added; no
request is created), `invalid_website`, `invalid_email`, `invalid_username`,
`invalid_policy`, `password_too_short`, `rate_limited` (over 20 credential writes this minute for
this key; wait, then call again), `write_outcome_unknown` (never retry blind).

```json Example arguments theme={null}
{ "website": "shop.example.com", "email": "agent-inbox@example.com", "minLength": 12 }
```

### `get_signup_request`

Check a signup request once, without blocking.

<ParamField path="requestId" type="string" required>
  The `requestId` `create_login` returned.
</ParamField>

**Returns** the request's `status`:

| `status` | Meaning | Next step |
| - | - | - |
| `pending` | Nobody has decided yet | Check back, or wait with `wait_for_signup_approval`. Do not call `create_login` again |
| `approved` | The login exists and its tokens are ready | Fill them into the signup form with `fill_login` |
| `denied` | The owner declined. Final for this request | Tell the user and stop |
| `expired` | The request's own lifetime ended before a decision | Tell the user and stop |

On `approved` the reply is
`{ status, login, passwordPolicy, passwordOwnerManaged }`:

<ResponseField name="login" type="object">
  The new login's [catalog entry](/reference/token-catalog),
  exactly as `get_token_catalog` will list it. Its fields:

  <Expandable title="login">
    <ResponseField name="login.label" type="string">
      The login's label, used by `set_login_username` and
      `rotate_login_password`.
    </ResponseField>

    <ResponseField name="login.tokens" type="object">
      `login.tokens.password` always. `login.tokens.email`, the registered
      mailbox, usually. `login.tokens.username` only when you passed a
      `username` to `create_login`. The password itself is never returned to the
      agent or to the account holder.
    </ResponseField>

    <ResponseField name="login.hasEmail, login.hasUsername" type="boolean">
      Which identifier tokens the login actually carries. Check these rather
      than assuming: the owner can clear the email after approving, leaving
      `login.tokens.username` and no `login.tokens.email` (they cannot clear
      both). If the form needs an email and there is no `login.tokens.email`,
      ask the owner rather than inventing one.
    </ResponseField>

    <ResponseField name="login.website" type="string">
      The site the login fills on, with its subdomains.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="passwordPolicy" type="object">
  What the vault generated (length, character classes, symbol alphabet), so you
  can answer a "must contain" validator without knowing the value.
</ResponseField>

<ResponseField name="passwordOwnerManaged" type="boolean">
  `true` when the owner has since replaced the password from their dashboard.
  `passwordPolicy` then describes a password that no longer exists, and
  `rotate_login_password` is permanently refused for this login. The token still
  fills the owner's password. If the site rejects it, tell the owner.
</ResponseField>

**Errors:** `request_not_found` (re-read the id; do not guess),
`scope_denied` (the owner has since removed the approved login from this key's
scope).

```json Example arguments theme={null}
{ "requestId": "REQUEST_ID" }
```

### `wait_for_signup_approval`

Block until the owner decides, instead of polling yourself.

<ParamField path="requestId" type="string" required>
  The `requestId` `create_login` returned.
</ParamField>

<ParamField path="timeout" type="number" default="45000">
  How long to wait. Must be greater than 0: `0` or a negative value is refused
  before the wait starts, with a tool error beginning `MCP error -32602`.
  Anything over 45000 is clamped to 45000, because most MCP hosts abandon a
  tool call at about 60 seconds. Call again to keep waiting.
</ParamField>

**Returns** a `status` and a `note` for every decision, and for a wait that
ran out:

| `status` | Meaning | Next step |
| - | - | - |
| `approved` | The login exists. The reply carries `login`, `passwordPolicy` and `passwordOwnerManaged`, exactly as `get_signup_request` returns them, so the tokens are under `login.tokens` | Fill with `fill_login` |
| `denied` | The owner declined | Tell the user and stop |
| `expired` | The request itself expired | Tell the user. Only a new `create_login` opens another, and it notifies the owner again |
| `still_pending` | This wait ran out. The request is still live. The reply adds `waitedMs` | Call again with the same `requestId` |
| `unknown` | The last poll failed, so the owner may already have approved. The reply adds `waitedMs` and `reason`, naming the failed poll | Call again with the same `requestId`, and check `get_token_catalog` |

An owner usually takes longer than one wait, so expect to call this several
times. A wait that ends leaves the request untouched. On no result should you
call `create_login` again for the same site.

**Errors:** a refusal no further poll can change ends the wait at once as a tool
error, not a `status`:

* `request_not_found`: the id is unknown, malformed or belongs to another key,
  or (rarely) the approved login was since deleted. Re-read the id; do not guess.
* `scope_denied`: the request was approved, but the owner has since removed that
  login from this key's or connection's access. Ask the owner.
* A key refusal such as `invalid_api_key` or `key_revoked`. See
  [Error codes](/sdk/error-codes#authentication-and-scope).

A dropped connection, a `429` or a `5xx` on one poll does not end the wait. The
next poll asks again, and if none answers before the wait runs out the status is
`unknown`.

```json Example arguments theme={null}
{ "requestId": "REQUEST_ID", "timeout": 45000 }
```

### `set_login_username`

Record the handle a site has **already accepted** on a login opened with only an
email.

<ParamField path="label" type="string" required>
  The login's label: `login.label` from the approved request, or the label in
  the token catalog.
</ParamField>

<ParamField path="username" type="string" required>
  The handle the site accepted.
</ParamField>

**Returns** an ordinary success.

<Warning>
  Call this **last**: type the handle into the site's form, submit, and confirm
  the site accepted it. This sets a username once and cannot be undone by the
  agent. Re-sending the **same** handle is idempotent and is the safe recovery
  when an answer went missing. A **different** handle is refused with
  `username_already_set`; tell the owner which handle the site accepted.
</Warning>

Only the API key that created the login can set its username, and only within
seven days of the login being created. Seven days is the default; a deployment
can set a different window. Any other key, or a later call, gets `not_found`,
the same answer as an unknown label. Re-sending the handle already stored still
succeeds after the seven days.

**Errors:** `username_already_set`, `not_found` (no such login, or not one this
key may change), `invalid_username`, `signup_not_enabled`, `daily_limit` (the
account's 25 changes in a 24-hour window are spent; it resets 24 hours after the
first counted change),
`rate_limited` (over 20 credential writes this minute; wait, then call again),
`write_outcome_unknown` (re-send the same handle).

```json Example arguments theme={null}
{ "label": "shop-example", "username": "agent_handle_42" }
```

### `rotate_login_password`

Mint a replacement password for a login an agent created, when the site rejected
the generated one at signup, or on a change form that asks only for a new
password.

<ParamField path="label" type="string" required>
  The login's label.
</ParamField>

<ParamField path="minLength" type="integer">
  The site's stated minimum length. At most 64: a higher value is refused with
  `invalid_policy`, and the account holder has to set the password themselves.
</ParamField>

<ParamField path="maxLength" type="integer">
  The site's stated maximum length. Below 12 is refused with
  `password_too_short`.
</ParamField>

<ParamField path="noSymbols" type="boolean">
  `true` only when the site rejects punctuation.
</ParamField>

<ParamField path="symbols" type="string">
  The punctuation the site accepts, filtered to the vault's safe set
  `!@#$*+-_=.?`. None of them in that set is refused with `invalid_policy`.
</ParamField>

<ParamField path="noRepeats" type="boolean">
  `true` when the site rejects three identical characters in a row.
</ParamField>

The new password follows the same length and character rules as for
`create_login`: 12 to 64 characters, 24 by default.

**Returns** success. The mock tokens do not change: re-fill the same password
token and the new password goes in. The password is generated on the server,
stored encrypted and never returned.

<Warning>
  Do not use this on a form that asks for the **current** password. There is one
  password token per login, so after rotating it fills the new value into both
  fields, the site refuses the change, and the vault holds a password the site
  does not know. A forced change on a live account is the owner's to handle.
</Warning>

Only the API key that created the login can rotate its password, and only within
seven days of the login being created. Seven days is the default; a deployment
can set a different window.

**Errors:** `not_rotatable` (a login the owner saved, one whose password they
replaced, one another key created, or one created more than seven days ago; this
tells you nothing about whether such a login exists),
`not_found`, `invalid_policy`, `password_too_short`, `signup_not_enabled`,
`daily_limit`, `rate_limited` (over 20 credential writes this minute; wait, then
call again), `write_outcome_unknown` (stop and hand it to the owner).

```json Example arguments theme={null}
{ "label": "shop-example", "maxLength": 20, "noSymbols": true }
```

## Drive the page

### `open_page`

Launch the proxied browser on the first call, then navigate. Later calls reuse
the same page, so you can walk a multi-step checkout.

<ParamField path="url" type="string" required>
  Absolute URL, for example `https://shop.example.com/checkout`.
</ParamField>

**Returns** `Opened <url>`. If the proxy connection had dropped, `open_page`
launches a replacement session; the run report then covers only what happened
after the restart (see [`get_run_report`](#get_run_report)).

On the hosted server, an `open_page` without `session` starts a new browser
every time. To go to another page, pass the `session` you already have. When
the connection already has other browsers open, the answer names them, so you
can reuse or close them rather than load the site in several at once.

In a [residential egress](/sdk/tunnel#residential-egress) session, which includes
every session on the hosted server where the deployment has one, an `http://`
address is opened as `https://` on the same host, and so is any `http://` page a
link, form or popup leads to.

**Errors:**

* **`insecure_site`.** The site would not load over HTTPS, or it redirected to
  an `http://` address. The message begins "This site doesn't support HTTPS" or
  "This site redirected to a plain http\:// address", and ends "Nothing was sent
  over plain http." The page did not change: the tab is still where it was, and
  what you filled there is still there. Do not call `open_page` on the same
  address again, since it fails the same way. Choose another site, or tell the
  user this one cannot be used. When a `click` or `press` leads to such a page,
  it returns the browser's own navigation error instead, such as
  `net::ERR_SSL_PROTOCOL_ERROR`, and the tab stays where it was.
* **`egress_unavailable`.** The first `open_page` could not start a session,
  because residential egress is required (`SELF_AGENT_PAY_RESIDENTIAL_EGRESS=1`)
  and the deployment has none, or its provider is not answering for the
  account's country. Nothing was opened. The message says how many seconds to
  wait before trying again.
* **`egress_lost`.** The session's residential egress stopped working. Every
  later call that reaches the browser is refused with it, including
  `get_session_outcome`. `get_run_report` and `close_session` still work. Read
  the report, call `close_session`, then `open_page` again.

See [Error codes](/sdk/error-codes#page-and-fill).

```json Example arguments theme={null}
{ "url": "https://shop.example.com/checkout" }
```

### `click`

Click an element, for example the place-order button.

<ParamField path="selector" type="string" required>
  The element to click.
</ParamField>

**Returns** `Clicked <selector>`. When the click opened a **new tab**, the reply
says so: call `wait_for_popup` before anything else, because every tool still
targets the page you were on.

A reply without that notice does not prove no tab opened. The proxy reports
only a tab that arrives within a short window after the click. A tab the site
opens later, for example a 3-D Secure or wallet window after a server round
trip, is still queued for `wait_for_popup` with no notice. When the checkout
plainly moved to another tab, call `wait_for_popup` anyway.

When a click times out because something covers the target (an expanded payment
iframe, a cookie wall, a modal scrim), the error says so and names the fix.
Usually that is `press` with `Enter`, or `Space` for a checkbox or radio. Where
no keypress activates the target, the error asks you to clear the overlay.

<Warning>
  `<tag> intercepts pointer events` in the call log is not that signal on its own.
  When `performing click action` or `click action done` follows it, the click
  landed and the timeout was the wait after it. The order may already be placed:
  read the page instead of clicking again.
</Warning>

```json Example arguments theme={null}
{ "selector": "#place-order" }
```

### `press`

Send a keypress to an element instead of clicking it. Use it when an overlay
swallows pointer events, or when a form only submits on Enter.

<ParamField path="selector" type="string" required>
  The element to press the key on.
</ParamField>

<ParamField path="key" type="string" required>
  Playwright key syntax: `Enter`, `Tab`, `Escape`, `ArrowDown`, or a character.
</ParamField>

<ParamField path="frame" type="string">
  The iframe the element is inside, for a hosted checkout that submits on Enter.
</ParamField>

**Returns** `Pressed <key> on <selector>`, and a new-tab notice like `click`.

**Errors:** `clipboard_blocked` for copy, cut and paste chords (`Control+c`,
`Control+v`, `Control+x` and their `Meta` spellings) once any token has been
filled. Click, or press a plain key, instead.

```json Example arguments theme={null}
{ "selector": "#password", "key": "Enter" }
```

### `wait_for_selector`

Wait for an element to reach a state.

<ParamField path="selector" type="string" required>
  The element to wait for.
</ParamField>

<ParamField path="state" type="string">
  One of `attached`, `detached`, `visible`, `hidden`. Defaults to the page's
  default (appear).
</ParamField>

<ParamField path="timeout" type="number" default="30000">
  How long to wait, in milliseconds.
</ParamField>

<ParamField path="frame" type="string">
  Wait inside a hosted iframe, for example to check a card field is there before
  filling it.
</ParamField>

**Returns** `Found <selector>`.

<Note>
  A wait that **returns** counts as an action between a card number and its
  security code. Do your waiting before the number goes in. See
  [Card order](#card-order).
</Note>

```json Example arguments theme={null}
{ "selector": "input[name='cvc']", "frame": "#card iframe", "timeout": 3000 }
```

### `count`

How many elements match a selector right now, and how many of those are
visible. Never waits, so probing several candidate selectors costs no timeout.

<ParamField path="selector" type="string" required>
  The selector to count.
</ParamField>

<ParamField path="frame" type="string">
  Count inside a hosted iframe.
</ParamField>

**Returns** `{ count, visible }`.

| Result | Read it as |
| - | - |
| `count: 0` | Not present at this instant. For something that mounts later, use `wait_for_selector` |
| `count: 1, visible: 0` | In the markup, but nothing can type into it right now. On a stepped form, usually a step you are past. If the page is still animating, give it a short `wait_for_selector` with `state: "visible"` |
| `count: 2` or more | A selector to narrow, whatever `visible` says. A framed fill refuses it; an unframed fill silently takes the first match |
| No `visible` key | An older proxy, too many matches to check (narrow it), or the page navigated mid-read (ask again) |

A nonzero `visible` is not a promise the fill will work: a visible input can
still be disabled or read-only. Set `fill_checkout`'s `timeout` regardless.

`count` is the one probe that is safe between a card number and its security
code.

After any token fill, a selector that tests a field's value, or puts a digit in
a text match, is refused with `sensitive_data_blocked`. Point with an id, class,
`[name=…]`, `[type=…]` or text without digits. Text without digits is allowed
whatever value the page is showing, so the non-digit characters of any value a
page echoes as text stay testable. See
[What the agent can see](/security/agent-context#count-and-waitforselector).

```json Example arguments theme={null}
{ "selector": "iframe[title='Secure payment input frame']" }
```

### `wait_for_popup`

Wait for a popup or new tab this page opened (a 3-D Secure window, a wallet
sign-in, a hosted checkout) and make it the current page.

<ParamField path="timeout" type="number" default="10000">
  How long to wait. Anything over 60000 is clamped to 60000, because this wait
  holds the session. Call again to keep waiting.
</ParamField>

**Returns** a confirmation. From here, `fill_checkout`, `click`, `press`,
`wait_for_selector` and `open_page` all target the popup. If the popup closed
itself and had opened a tab of its own, you land on that **child** tab and the
reply says so. It is a different document, so read it before acting.

**Errors:** fails if no popup opens within the timeout. A late popup is queued,
so one retry is worthwhile.

```json Example arguments theme={null}
{ "timeout": 30000 }
```

### `close_popup`

Close the popup and make the page that opened it current again. Safe even if the
popup already closed itself.

This tool takes no parameters.

**Returns** where you landed: the opener; a sibling tab queued under it; a child
tab the popup opened; or, if every page that opened it had closed, a blank page
in the same browser with cookies intact (navigate back with `open_page`). Read
the page before assuming your checkout is still on screen.

## Fill

### `fill_checkout`

Fill an ordered list of fields with mock tokens or real values.

<ParamField path="fields" type="array" required>
  The fields to fill, in order.

  <Expandable title="field">
    <ParamField path="selector" type="string" required>
      CSS selector for the input.
    </ParamField>

    <ParamField path="value" type="string | number" required>
      A mock token, or a real value such as the expiry month and year from the
      catalog. Numbers are accepted.
    </ParamField>

    <ParamField path="kind" type="string" default="input">
      `input` types the value. `select` picks an option in a `<select>` control
      such as a country or expiry dropdown.
    </ParamField>

    <ParamField path="frame" type="string">
      The iframe the field lives in, for example a Stripe Elements card frame.
    </ParamField>

    <ParamField path="cardField" type="boolean">
      Marks one of the card's own controls, typically an expiry that appears
      only after the number is entered, so the batch moves it after the security
      code. Only values you typed can move. A vault value (an identity or card
      token) never moves, even when marked.
    </ParamField>
  </Expandable>
</ParamField>

<ParamField path="timeout" type="number" default="30000">
  How long **each field** gets to become fillable before the call fails with the
  reason. Must be 1 or more: `0` means no timeout, not an immediate failure. It
  does not count time spent waiting on the account owner (a purchase approval or
  a live CVV entry).
</ParamField>

**Returns** `Filled N field(s)`. When the account owner was pulled in to approve
the purchase or type the security code, the reply says so. A plain reply means
nobody was asked.

When the key is verification-only (see [API keys](/dashboard/api-keys)) and
the batch filled a card field, the reply also carries a `card_fields_verified`
note. The proxy typed a public test card, not the owner's card. Nothing was
reserved or charged, and the owner was not alerted for that card fill. The test card does not
force a merchant to refuse the order, so submit only on a checkout meant for
testing. Treat the fill as a probe of the form, not a purchase.

<Tip>
  Set `timeout` whenever a miss is possible. With the 30 second default, a field
  that never becomes fillable times out at the same moment as most MCP clients,
  and the proxy's explanation (for example `locator resolved to hidden <input>`)
  is lost. A few seconds gets the explanation back. The budget is per field, so
  keep fields multiplied by timeout under your client's deadline.
</Tip>

#### Card order

Fill **expiry and any billing field first**, then the **card number**, then the
**security code immediately after it**.

Filling the number authorizes the security code that follows it exactly once.
The next **action** spends that authorization: another field, a click, or a wait
that returns. The account owner is then asked to approve the same purchase a
second time. **Reads do not spend it:** `observe_page`, `detect_challenge` and
`count` leave it in place, and a wait that timed out observed nothing.

The tool enforces this:

* A batch that puts any other field **straight after** the card number is
  refused before anything is typed. Reorder it. Only `cardField` fields move.
* A vault value between the number and the code is refused, so fill billing
  first.
* When a batch **ends** on the card number, the next `fill_checkout` is refused
  unless it starts with that card's security code.

Filling the number is also where the order total is read and checked against
the owner's limits. Put country or postcode edits that recompute the total in an
**earlier** call, so the page has settled.

**Errors:** see [Spending, limits and approval](/sdk/error-codes#spending-limits-and-approval)
and [Card and merchant refusals](/sdk/error-codes#card-and-merchant-refusals).
`cvv_entry_required` means the security code was not supplied. If the owner
did not enter it in time, or the prompt could not be started, report it and let
the user retry. If the message says "Live CVV entry is not yet available", every retry
fails the same way: stop and tell the user that this card cannot be used on this
deployment for a checkout that asks for the security code. See
[Live CVV](/guides/live-cvv#if-something-goes-wrong).

```json Example arguments theme={null}
{
  "fields": [
    { "selector": "#exp-month", "value": 12, "kind": "select" },
    { "selector": "#exp-year", "value": 2030, "kind": "select" },
    { "selector": "input[name='number']", "value": "av_card_1_number_u7x2k", "frame": "iframe[title='Secure payment input frame']" },
    { "selector": "input[name='cvc']", "value": "av_card_1_cvv_u7x2k", "frame": "iframe[title='Secure payment input frame']" }
  ],
  "timeout": 5000
}
```

Take the expiry from the same catalog card whose `number` and `cvv` tokens you
fill: its `expiryMonth` and `expiryYear`, not fixed values like the ones above.
Both are numbers, with a four-digit year. Match the format the control expects,
for example `11` and `2029`, or `1129` for a single `MMYY` field.

### `fill_login`

Fill a sign-in or signup form's identifier and password with their mock tokens.
Each half is optional.

<ParamField path="usernameSelector" type="string">
  The identifier field. Pass with `usernameToken`.
</ParamField>

<ParamField path="usernameToken" type="string">
  Choose by the field's visible label and `type`, not its `name`. For an email
  field, `tokens.email`. If the entry has no `tokens.email`, ask the account
  holder rather than filling `tokens.username`, which may be a handle. For a
  field that asks for the site's own handle in so many words, `tokens.username`. See
  [Pick the identifier token](/guides/sign-in).
</ParamField>

<ParamField path="passwordSelector" type="string">
  The password field. Pass with `passwordToken`.
</ParamField>

<ParamField path="passwordToken" type="string">
  The login's password token.
</ParamField>

<ParamField path="confirmPasswordSelector" type="string">
  A repeat-password field on a signup form, filled with the same password token.
  Needs the password pair in the same call.
</ParamField>

**Returns** which halves were filled, for example
`Filled username and password`.

* **Email-first sign-ins** (Microsoft, Okta, Google) take two
  calls: the identifier pair, submit, wait for the password field, then the
  password pair. Passing both before the password field exists fills nothing
  and says so. Each field gets 3 seconds to appear and be visible before the
  call refuses with "Nothing was filled".
* **Do not choose the token from the field's name.** A field named `username`
  (in its `name`, `id`, `data-testid` or `autocomplete`) is very often the email
  field. Read its `type` and visible label instead.
* **Authenticator codes.** If this sign-in then asks for an authenticator-app
  code and the login has `tokens.totp`, fill that token with `fill_checkout` and
  submit promptly. Use it only for this login's authenticator step.

```json Example arguments theme={null}
{
  "usernameSelector": "input[type='email']",
  "usernameToken": "av_login_github_email_u7x2k",
  "passwordSelector": "input[type='password']",
  "passwordToken": "av_login_github_password_u7x2k"
}
```

### `declare_page_hint`

Tell the vault where something is on the current page when it cannot find it by
convention. The main use is the order total on a capped card.

<ParamField path="kind" type="string" required>
  The hint kind, for example `order.total`. See `list_page_hint_kinds`.
</ParamField>

<ParamField path="selector" type="string" required>
  Plain CSS only. No `text=`, `:has-text()`, `>>`, XPath, `_react=` or
  `:visible`: a hint points at an element, it does not search for one. At most
  512 characters. An `order.total` selector matching more than 8 elements, or an
  element over 4,000 characters, adds nothing and answers `accepted: false`.
</ParamField>

<ParamField path="frame" type="string">
  The iframe the element lives in. The only way to reach a total inside an
  overlay checkout such as Paddle or Lemon Squeezy. Plain CSS, at most 512
  characters. For `order.total` the vault also reads the frame's whole text and
  every other frame on the page. Over 20,000 characters in the frame, over
  16,384 counting the page's own text, or more than 32 frames on the page, the
  hint is dropped and answers `accepted: false` even though the selector
  matched. Point at a total outside the frame if there is one. Otherwise a card
  with an approval threshold asks the account holder, and a card with spending
  limits is refused with `checkout_total_unknown`.
</ParamField>

<ParamField path="read" type="string" default="text">
  `text` or `attribute`. An attribute's value must name a currency: a bare
  `49.99` is refused.
</ParamField>

<ParamField path="attribute" type="string">
  Which attribute to read when `read` is `attribute`, for example `aria-label`,
  `content` or `data-total`.
</ParamField>

<ParamField path="login" type="string">
  For `login.totp_secret`: the label of the stored login the secret belongs to.
</ParamField>

**Returns** `{ kind, accepted, checked }`, plus `resolved`, `amount` and
`currency` when `checked` is `true`, or `untrusted` naming why a figure was
refused. A `login.totp_secret` hint returns `stored`, `digits` and `period`.
After any token fill the reply is `{ accepted: true, checked: false }`: not a
rejection, because the hint is kept and used at the card fill. That applies only
to a `selector` and `frame` built from ids, classes, element names and
presence-only attribute tests such as `[data-total]`. One that compares an
attribute value, such as `[data-testid="total"]`, is refused with `invalid_hint`
after a fill, and one declared before the fill is skipped when the card fill
reads the total. `accepted: false`
means the hint was dropped, so declare it again. See
[What changes after the first fill](/guides/page-hints#what-changes-after-the-first-fill).
You supply a location, never a value. Declare `order.total`
**before** filling the card, and again after a navigation.

**Errors:** `invalid_hint`, `hint_unresolved`, `hint_uncertain`. A `selector`
or `frame` over 512 characters is refused with `invalid_hint` before anything is
recorded. A page holds
at most 4 hints of each kind: a fifth distinct hint of the same kind on that page
is refused with `invalid_hint`. Repeating a hint does not count, and any
navigation of the page clears the count. A
`login.totp_secret` hint can also fail its preconditions first:
`sensitive_data_blocked` (this session has not signed in as that login) or
`insecure_page` (the page is not on `https://`, and is not a loopback
`http://` page). Full guide:
[Page hints](/guides/page-hints).

```json Example arguments theme={null}
{ "kind": "order.total", "selector": ".summary__grand-total" }
```

### `list_page_hint_kinds`

List the hint kinds this deployment accepts, what each is for and which
attributes it may read. Kinds are added server side, so ask rather than assume.

This tool takes no parameters. It needs the session `open_page` starts, because
the list comes from the vault.

## Look at the page

None of these tools return a filled value, and none spends the card
authorization.

### `observe_page`

A fixed set of proxy-computed answers about the page. This is how you tell a
page that rejected your input from one that moved on, including after a fill,
when screenshots are blacked out.

This tool takes no parameters.

**Returns:**

<ResponseField name="passwordField" type="boolean">A password field is on screen.</ResponseField>
<ResponseField name="identifierField" type="boolean">A field the page declares as its sign-in identifier.</ResponseField>
<ResponseField name="contactField" type="boolean">An email or phone input. A shipping form has one too.</ResponseField>
<ResponseField name="codeSent" type="boolean">The page says a code was sent.</ResponseField>
<ResponseField name="error" type="boolean">A validation or error message is showing.</ResponseField>
<ResponseField name="rateLimited" type="boolean">The page is refusing further attempts.</ResponseField>
<ResponseField name="reason" type="string">Which signals matched. Value free.</ResponseField>

<ResponseField name="errorText" type="string[]">
  Short strings saying why the page rejected you, in its own words, for example
  `["Enter a valid postcode"]`. At most 5, each cut to 200 characters, drawn
  only from elements the page marks as validation output and scrubbed of filled
  values and anything card, email, phone or token shaped.
</ResponseField>

<ResponseField name="inconclusive" type="true">
  Present only when the scan did not finish.
</ResponseField>

* **Read `errorText` whenever it is non-empty**, not only when `error` is true,
  then fix your input and fill again. Do not branch on the wording or paste it
  to the user.
* **An empty `errorText` does not mean nothing was rejected.** Some merchants
  style errors without marking them up. It is also withheld for the rest of a
  session in which the owner drove the page during a handoff.
* **A live region also announces success.** Read what it says.

<Warning>
  With `inconclusive: true`, every `false` means unknown, not absent, and a `true`
  may be an off-screen panel. Look again before acting. A finished scan **omits**
  the flag, so test for its absence, not for `false`.
</Warning>

### `snapshot_page`

The accessibility tree as YAML: every field, its accessible name and a
`[ref=eN]` handle, reaching into iframes including cross-origin ones. Call it
first on a page you have not seen.

<ParamField path="depth" type="number" default="12">
  How deep to walk. The proxy clamps it to 30.
</ParamField>

<ParamField path="cursor" type="number">
  Line offset for the next page of a truncated result. Pass it with the depth
  the truncation note names.
</ParamField>

**Returns** YAML:

```yaml theme={null}
- heading "Sign in" [level=1] [ref=e2]
- textbox "Email" [ref=e5]
- button "Continue" [ref=e9]
```

* **Pre-fill only.** Once any token is filled it is refused with
  `sensitive_data_blocked` for the rest of the session, because the page chooses
  accessible names. Snapshot while orienting, then use `observe_page` and
  `count`.
* **A ref is a selector.** Pass `aria-ref=e7` to `count`, `wait_for_selector`,
  `click` or `press`. Refs survive DOM changes but not a navigation. **A fill
  refuses a ref**: fill by CSS selector.
* **Field selectors.** The snapshot ends with a `# Field selectors` comment that
  names, for each field ref, the CSS `selector` to pass to `fill_checkout`, and the
  iframe's `frame` selector for a field inside one. Use those rather than guessing.
* **Values are never in it.** A blank-looking field says nothing about whether
  a fill landed.
* **Paging.** A result holds at most 24,000 characters. If it was cut, its last line names a cursor and depth. Pass
  both: a cursor is an offset into a capture at that depth. Lower the depth
  instead (and drop the cursor) only when what overflowed is nesting, not width.

```json Example arguments theme={null}
{ "depth": 12 }
```

### `list_frames`

This page's frames as `{ name, url }`, to find which iframe holds the payment
form. Never frame content.

This tool takes no parameters.

**Returns** `{ frames: [{ name, url }] }`. A cross-origin frame may report an
empty name or URL.

**Errors:** `sensitive_data_blocked` once any token has been filled, for the
rest of the session.

Use it when a page already shows its payment surface and you have filled
nothing. On a multi-page checkout the card frame usually appears only after
sign-in, so plan on targeting it by CSS instead: get the selector from
`get_checkout_recipes`, or use the known Stripe titles:

| Integration | Frame selector |
| - | - |
| Stripe Payment Element | `iframe[title="Secure payment input frame"]` |
| Stripe legacy Card Element | `iframe[title="Secure card number input frame"]`, with `iframe[title="Secure expiration date input frame"]` and `iframe[title="Secure CVC input frame"]` |
| Shopify (from its platform recipe) | `iframe[name*="card-fields-number"]` |

Settle every card frame before the number goes in.

### `detect_challenge`

Whether the page is showing a 3-D Secure prompt, a one-time-code step, a
CAPTCHA or bot wall, or a silent block. Call it first whenever a click or fill
seems to do nothing: an invisible bot wall looks exactly like a stuck overlay.

This tool takes no parameters.

**Returns** `{ detected, type, types, reason, signature?, deliveredCode?, ambiguousOnly?, inconclusive? }`.
`type` is `three_ds`, `otp`, `captcha` or `blocked`. Never page text.

`blocked` means the site stopped answering without saying why. `signature`
names the shape: `stalled_search` (a search that never loads), `search_bounce`
(a click bounced back to the previous page with an error) or `error_reference`
(an error dialog with a reference code). `deliveredCode: true` on an `otp`
means the page says the code was sent by SMS or email.

| Result | Next step |
| - | - |
| `three_ds`, `captcha` or `blocked` | `request_challenge_handoff`. For `blocked`, never retry, reload or search again first |
| `otp` with `deliveredCode: true` | `request_challenge_handoff`. Never fill `tokens.totp`, even on a sign-in you just performed |
| `otp` on the authenticator step of a sign-in you just performed, login has `tokens.totp` | Fill `tokens.totp` and submit promptly |
| Any other `otp` (SMS, email, checkout verification) | `request_challenge_handoff` |
| `ambiguousOnly: true` | Not handoff material. Fill `tokens.totp` if it is your own login's authenticator step, otherwise report the step as unresolved |
| `inconclusive: true` | Call `detect_challenge` again before concluding anything |

<Note>
  `ambiguousOnly` means the page has a field that **might** be a code input,
  flagged only because this sign-in's windows (this page, its opener, or a popup
  it opened) recently took a login password and no checkout token since.
  `request_challenge_handoff` refuses it with `handoff_not_applicable`.
</Note>

### `get_network_log`

A diagnostic that is off unless the deployment enables it, typically on
staging. Lists the current page's last 25 document, xhr and fetch requests:
method, host, path (cut to 200 characters), status, and how long ago.

This tool takes no parameters.

**Returns** one line per request, oldest first, or a message saying it is not
available on this deployment. No bodies, headers or query strings are ever
returned. After a vault value has been filled, the host is only `this site` or
`another site` and no path is shown.

Use it when a submit appears to do nothing: a silent server refusal shows as a
4xx on the form's request.

## Hand off a challenge

### `request_challenge_handoff`

Hand the page to the account owner so they complete a challenge you must not,
then resume.

This tool takes no parameters.

The proxy re-detects before summoning anyone, then asks the owner with a
dashboard banner, plus an email, desktop or background alert on each channel
they turned on while their **A checkout needs me to clear a bank or
verification step** switch is on, with a link that streams the live page. The call
**blocks** until they finish, the wait times out (about 10 minutes), or the
session ends.

**Returns** `{ outcome, challengeType, durationMs }`, never what the owner
typed.

| `outcome` | Meaning | Next step |
| - | - | - |
| `solved` | The proxy re-detected after release and the wall is down | Carry on |
| `completed` | Released, but the proxy could not check | Look at the page first. Hand off again only if a wall is still there |
| `released_unsolved` | A challenge is still on the page | `detect_challenge`, then hand off again |
| `failed` | The owner answered and it failed | Stop |
| `abandoned` | The owner walked away | Stop |
| Anything else | Unrecognised | Stop |

**Errors:** `handoff_not_applicable` (an `ambiguousOnly` detection; the page is
still yours), `challenge_scan_incomplete` (nobody was notified; detect again),
`challenge_required` for other page tools while the handoff is parked, and
`unknown_target` in two variants. `Unknown pageId` means the page was gone before
the handoff started: nobody was notified, so call `close_popup` or move to another
live page and detect again. `No challenge is present on this page` means the
proxy re-detected on a live page and found nothing: carry on.

<Note>
  Most MCP hosts cancel a tool call at 60 seconds. That does **not** cancel the
  handoff: the owner still has the page. Re-orient with `wait_for_selector` or
  `get_session_outcome` once they are done, rather than calling this again.
</Note>

## Confirm and finish

### `get_session_outcome`

After submitting, race a success selector against an optional failure selector.
Use this instead of scraping a confirmation page, which may echo card digits.

<ParamField path="successSelector" type="string" required>
  Appears on success, for example `#order-success`.
</ParamField>

<ParamField path="failureSelector" type="string">
  Appears on failure, for example `.checkout-error`.
</ParamField>

<ParamField path="timeout" type="number" default="15000">
  Maximum 30000; larger values are clamped. `0` or a negative value means the
  default, not "no timeout". A handoff waits for this check, so it
  is also the longest the owner can be kept from the page. To watch longer, call
  again.
</ParamField>

<ParamField path="confirmsCheckout" type="boolean">
  `true` only when the success selector confirms a completed **checkout**, so the
  session is recorded as confirmed. Leave it off for logins.
</ParamField>

<ParamField path="submitSelector" type="string">
  Required with `confirmsCheckout`. The control you clicked or pressed (with
  `Enter`) to place the order, exactly as you passed it, and the latest click or
  press on the page. Call this right after the submit.
</ParamField>

**Returns** `outcome` (`success`, `failure`, `timeout` or `cancelled`) and
`matched`, the selector that decided it. `timeout` means neither selector
appeared yet, not that the checkout failed. `cancelled` means a handoff took
the page over mid-check: ask again once it returns.

With `confirmsCheckout: true`, the confirmation is recorded only when it is
corroborated:

* the submit control has left the screen,
* no tab this page opened is queued or waiting for `wait_for_popup`,
* the page shows no error, rate limit, sign-in step, sent-code notice or
  challenge step,
* the success selector is still there and the failure selector is not.

The reply then says `corroborated: true` and the recording is started (best
effort; `get_run_report` is the record). Otherwise it says `corroborated: false`
with `notCorroboratedBecause`, and nothing is recorded. Do not report an order
on it.

<Note>
  A redacted receipt image is captured only for a session that filled no mock
  tokens, such as a saved-payment checkout. A normal token-filled checkout is
  marked confirmed with no receipt image.
</Note>

```json Example arguments theme={null}
{
  "successSelector": "#order-success",
  "failureSelector": ".checkout-error",
  "confirmsCheckout": true,
  "submitSelector": "#place-order"
}
```

### `get_run_report`

What the session actually did, from the proxy's own record. **Read this before
telling anyone whether a card was charged.**

This tool takes no parameters. It needs an open session, and is safe to call
while a handoff is parked.

**Returns:**

<ResponseField name="filledFieldKinds" type="string[]">
  The distinct token kinds filled, for example `["identity", "address", "card", "cvv"]`.
</ResponseField>

<ResponseField name="deniedCount" type="number">
  How many fills the proxy refused (paused card, merchant block, scope, URL lock).
  A burst of `rate_limited` refusals counts once a minute, not once each.
</ResponseField>

<ResponseField name="confirmationDetected" type="boolean">
  Whether a checkout confirmation was recorded.
</ResponseField>

<ResponseField name="orderTotal" type="string">
  The total on that confirmation page, when there was one.
</ResponseField>

<ResponseField name="credentialWrites" type="string[]">
  What the session changed in the vault, as `label:change`, for example
  `["doordash:signup_requested", "doordash:created", "doordash:totp_enrolled"]`.
  These outlive the session. Tell the owner about them even when the checkout
  failed.
</ResponseField>

<ResponseField name="coversWholeRun" type="boolean">
  `false` when `open_page` had to start a new proxy session partway through the
  run, for example after the connection dropped. The numbers then cover only
  what happened after the restart, and a `note` says so. A short record is not
  evidence of a quiet run.
</ResponseField>

<ResponseField name="earlierReports" type="object[]">
  Only when `open_page` replaced this run's proxy session partway through (a
  lost tab, say) and the old session's record could still be read. One object
  per earlier proxy session, oldest first, each with the same
  `filledFieldKinds`, `deniedCount`, `confirmationDetected`, `orderTotal` and
  `credentialWrites` fields as the report. An `earlierReportsNote` comes with it.
</ResponseField>

`timeout` from `get_session_outcome` plus `card` in `filledFieldKinds` means the
card **was** entered and you did not see the confirmation. Say that.

#### After a restart

With `earlierReports`, the run is split into segments: each earlier proxy
session, then the current one. The top-level fields describe only the current
one. Check every segment before you tell anyone nothing was charged. `card` in
any segment's `filledFieldKinds`, or `confirmationDetected: true` in any
segment, holds for the whole run.

When the current proxy session cannot be read but earlier segments were kept,
the reply has no top-level report fields. It carries `earlierReports`,
`currentReport: null`, `coversWholeRun: false`, a `currentReportNote`, and
`credentialWrites` when the run changed the vault:

```json theme={null}
{
  "earlierReports": [
    {
      "confirmationDetected": false,
      "deniedCount": 0,
      "filledFieldKinds": ["identity", "address", "card", "cvv"],
      "credentialWrites": []
    }
  ],
  "earlierReportsNote": "Earlier proxy sessions in this run, replaced partway through. ...",
  "currentReport": null,
  "coversWholeRun": false,
  "currentReportNote": "No report for the current proxy session: it could not be read. ..."
}
```

`currentReport: null` does not mean nothing was filled since the restart. It
means Agent Vault could not ask. The earlier segments still count.

Kinds and counts only, never a filled value. A read it cannot complete, with no
earlier segments to report, refuses and says so rather than answering with
zeros. The proxy gets 5 seconds to answer this read, here and in
`close_session`.

### `close_session`

Close the browser and end the proxy session. Call it when the task is done.

This tool takes no parameters.

**Returns** `status` and `report`, the same object `get_run_report` returns, so
your last act is reading what the run did. If the browser did not close cleanly,
`status` says so and `teardownError` is set. When `open_page` replaced a proxy
session partway through, the reply also carries `earlierReports` and
`earlierReportsNote`, as in [`get_run_report`](#after-a-restart). Count every
segment with `report`.

If no session was ever opened, or its connection had already died, there is no
report and the reply says so. Do not read that as nothing having been filled.
Any `credentialWrites` made through the server are still listed, and so are any
`earlierReports`, with `coversWholeRun: false`.

## Related

<CardGroup cols={2}>
  <Card title="Rules of engagement" icon="scale-balanced" href="/mcp/rules-of-engagement">
    The rules your agent follows when it uses these tools.
  </Card>

  <Card title="Error codes" icon="triangle-exclamation" href="/sdk/error-codes">
    Every refusal and whether retrying is safe.
  </Card>

  <Card title="Handling challenges" icon="shield-halved" href="/guides/challenges">
    3-D Secure, one-time codes and CAPTCHAs.
  </Card>

  <Card title="Confirming outcomes" icon="receipt" href="/guides/confirming-outcomes">
    What counts as proof an order went through.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.