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

# Error codes

> Every error code the SDK and MCP server can return, what it means, and whether retrying is safe.

Look up any error code, see whether a retry can work, and write retry logic that
never charges anyone twice.

## Overview

Every failure from the proxy or the API is a `SelfAgentPayError` with a stable,
machine-readable `code`. A few failures escape as native JavaScript errors
instead, so catch more than `SelfAgentPayError` around these calls:

* Saving a screenshot to a local file: see `screenshot(options?)` in the
  [API reference](/sdk/api-reference).
* `recipeSubmission()`: a network failure, or an abort through your `signal`,
  rejects with the native `fetch` error.
* `recipesForUrl()` and `submitRecipe()`: an abort through your `signal` while
  the response body is being read rejects with the native abort error.
* `listTokens()` (and `tokensForUrl()`, which calls it), `recipesForUrl()` and
  `buyableMerchants()`: a successful response whose body is not valid JSON
  throws a native `SyntaxError`. A body cut off mid-read, or for the two
  registry helpers JSON of the wrong shape such as `null`, can throw a native
  `TypeError`.

The signup and login helpers (`createLogin()`, `getSignupRequest()`,
`awaitSignupApproval()`, `setLoginUsername()`, `rotateLoginPassword()`) always
throw `SelfAgentPayError`. Through the MCP server, a refused call returns a tool
error carrying the same message.

<Steps>
  <Step title="Branch on code first">
    Messages are prose and get reworded. No control flow should depend on their
    exact wording.
  </Step>

  <Step title="Read the message for overloaded codes">
    Some codes cover situations whose retry answers differ, and the message is the
    only signal. They are marked **Overloaded** below. Match loosely on a
    distinguishing phrase, treat "no match" as the more conservative branch, and
    re-check when you upgrade. Where a wrong guess could charge someone twice
    (`card_blocked`, `error`), check the merchant's own order state instead.
  </Step>

  <Step title="Treat a missing code as an unknown outcome">
    `code` is optional. Errors raised inside the SDK around a proxy call, such as
    configuration and tunnel failures, and HTTP failures whose body carries no code,
    have none. Apply [the double-charge rule](#the-double-charge-rule).
  </Step>
</Steps>

The overloaded codes are `insecure_page`, `approval_denied`, `card_blocked`,
`login_blocked`, `unknown_target`, `malformed_response`, `limit_reached`,
`challenge_required`, `cvv_entry_required`, `card_live_fill_unsupported`,
`attestation_required`, `token_not_found`, `attested_checkout_unavailable`,
`auth_failed`, `invalid_plan`, `rate_limited`, `approval_timeout` and `error`.

### The double-charge rule

A missing code, or a connection-level code, means **the request got no verdict,
not that it never landed**. Any close rejects every in-flight call, and the
connection can drop after a click or submit reached the merchant.

* On a **read**, it is safe to repeat.
* On anything that could have **changed the checkout**, treat it as an unknown
  outcome. Confirm against durable merchant state (the order or confirmation
  page, an account order list, a confirmation email) before replaying.

`browser.outcome()` is not durable merchant state: a
`confirmationDetected: false` there is inconclusive, because only your own
`recordOutcome()` call sets it. See [Sessions and timeouts](/sdk/sessions-and-timeouts#when-an-outcome-is-unknown).

### What a refusal costs

A refusal **before** anything is recorded costs the account nothing, and so does
one whose reversal succeeded: the plan's payment quota counts only fills that
released a value. Where a refusal comes **after** the transaction was recorded,
the proxy reverses it on a best-effort basis. If that reversal fails, the
recorded transaction keeps counting against quota and the card's limits. The
detail for each code says when this can happen.

Retrying a permanent failure wastes the run. Giving up on a temporary one
abandons a checkout the account holder was about to approve. Retrying one whose reversal
quietly failed can record the purchase twice.

### Retry legend

| Value | Meaning |
| - | - |
| **Yes** | Retrying the same call can succeed |
| **Once** | One retry is worth it. If the same error returns, stop |
| **After fix** | Change the call (selector, value, plan, arguments), then retry |
| **After account holder** | Stop and tell the account holder. Once they act, retry in the same session |
| **Later** | Clears on its own after a wait |
| **No** | Retrying cannot work |
| **Verify first** | The action may have reached the merchant. Check durable merchant state before repeating |
| **Reconcile first** | A transaction may be recorded with nothing to show for it. Check the dashboard before anything else |
| **Depends** | Overloaded. Read the message |

## Spending, limits and approval

| Code | Meaning | Retry | What to do |
| - | - | - | - |
| `limit_reached` | An agent card spending limit the account holder set stopped the purchase. Older deployments also send it for the plan's quotas | Depends | Shrink the cart, but check whether the card auto-paused |
| `checkout_total_unknown` | The order total could not be established, so nothing was compared to any limit | Once | Retry once the total renders, or declare a page hint |
| `plan_quota_reached` | A quota on the account plan, not a card setting | Depends | Tell the account holder. The payment allowance frees up over 30 days; the agent card quota never clears by waiting |
| `approval_timeout` | A purchase approval, or a signup wait, ended with no decision | Yes | Card fill: retry. Signup: wait again on the same `requestId` |
| `approval_denied` | The account holder refused, or an approved fill failed its checks at fill time | Depends | Abandon on a refusal. Retry on a revalidation failure. On a host change, reconcile first |
| `approval_pending` | Reserved. Not currently returned | No | Handle `approval_denied` and `approval_timeout` instead |
| `charge_exceeds_authorized` | The checkout now asks for more than the card fill was authorized for | No | Do not re-fill the card. Tell the account holder the new total |

<AccordionGroup>
  <Accordion title="limit_reached">
    A spending limit the account holder set on the agent card stopped the purchase:
    per transaction, or a daily, weekly or rolling 30-day window. The rolling windows
    apply on production deployments only; preview, development, test and staging
    deployments check only the per-transaction cap.

    The plan's quotas send `plan_quota_reached`, or `limit_reached` on an older
    deployment (see the note below).

    A total that could not be established has its own code,
    `checkout_total_unknown`.

    This is Agent Vault's own limit, not the account holder's bank's: a refusal
    charges and holds nothing at the bank. The account holder raises a cap in the
    Agent Vault dashboard.

    Agent Vault **counts a card-number fill's amount the moment it lands**, before
    any order exists. A fill whose order was never placed still counts
    until the window rolls over, so re-filling in one session can reach a cap with
    nothing bought. When that is the case for a window cap, the message
    says so.

    **Retry depends on the cause:**

    * **Caps, with auto-pause on.** `autoPauseOnLimit` is on for every agent card
      created through the dashboard. Any breach of the card's own limits, per
      transaction included, flips it to `limit`, and the next attempt is refused
      before the new total is even evaluated. The account holder has to resume the
      card. The exception is a funding card with **another** active agent card: a
      fill goes through when any active agent card permits the merchant, so a
      smaller cart or an expired window can succeed with nobody resuming anything.
    * **Caps, with auto-pause off.** A per-transaction cap is recomputed against the
      new total, so shrinking the cart works straight away. A window cap is
      recomputed from trailing history, so the same purchase succeeds later.
    * **A plan quota, on an older deployment:** behaves like `plan_quota_reached`.
      The agent card quota never clears on its own, so do not loop on it.

    So shrink the cart by all means, but check whether the card auto-paused before
    assuming that is enough. Only a cart you cannot shrink below the cap needs the
    account holder for the cap itself.

    <Note>
      On a deployment that predates `plan_quota_reached`, the plan quota cases arrive
      as `limit_reached`. Branch on `plan_quota_reached` where you have it and keep
      handling the plan case under `limit_reached` as the fallback. Never treat an
      unrecognised code as retryable.
    </Note>
  </Accordion>

  <Accordion title="checkout_total_unknown">
    The order total could not be established, so the purchase was never checked
    against the account holder's controls. **This is not a limit being exceeded.**
    Either the page's total could not be read, or its currency could not be converted into the
    denomination the caps use. It fires only on a card that has some limit
    configured; an uncapped card completes the same checkout.

    **Retry:** worth one immediate attempt. A total that had not finished rendering,
    or a refreshed exchange rate, can make the next attempt succeed. If it does not
    clear at once, it is not a rendering delay. The message names which of three
    things happened:

    | Message says | Meaning | Answer |
    | - | - | - |
    | Reading the page failed | Usually transient. Also given for a page with more than 16,384 characters of text, which the vault does not parse at all | Retry once the page settles. If it fails the same way again, the page is too long to read: use a shorter page, or hand back to the human |
    | A figure was found and refused | The page was read, but the vault could not confirm the figure is this order's charge | Retrying unchanged fails the same way. Declare a [page hint](/guides/page-hints) pointing at the total, or use a page that states the total plainly |
    | No order total was found in the page's text | A total in an image, a canvas or an embedded frame reads as absent | Declare a [page hint](/guides/page-hints), or use a page that states it as text |

    Two things not to conclude. **Raising a limit will not clear it**, so do not
    send the account holder to the dashboard. And it is **not** a promise the card
    has room: no cap was checked, so the total may still exceed one once it can be
    read.

    If the message names a currency problem, no page hint will help. For an
    unsupported currency, use a checkout in a supported one. For stale rate data,
    retry later: the rates refresh once a day.
  </Accordion>

  <Accordion title="plan_quota_reached">
    A quota on the account **plan**, not a card setting. The message names which of
    two causes:

    * **The payment allowance is exhausted.** It counts only payments that released
      a value; denials and reversals do not count. The window is a **trailing 30
      days**, not a calendar month, so it clears as the oldest payment ages out. An
      upgrade may raise the ceiling but is not a reset: the count is kept across plan
      changes. Free and Plus have this cap; Enterprise has none.
    * **The agent card is beyond the plan's agent card allowance.** This never
      clears by waiting. It needs an upgrade, or enough **older** cards removed.
      Only agent cards linked to a live funding card hold slots, and slots go to the
      oldest of those first, so deleting a newer one, or one with no funding card,
      changes nothing.

    **Retry:** neither clears by retrying this checkout. Do not send the account
    holder to check their card.
  </Accordion>

  <Accordion title="approval_timeout">
    **Overloaded.** Two unrelated waits share this code.

    * **Card fill.** The purchase was at or above the agent card's `confirmAbove`
      threshold, so the fill waited for the account holder's approval, and that wait
      ended with no decision. No real value was filled. Retry the fill, which asks
      the account holder again.
    * **Signup** (`awaitSignupApproval`). The SDK's own wait ended; the request is
      untouched and still live. The message says which of two shapes:

      * The polls were answering and the request was still awaiting a decision.
      * The last poll **failed** (a dropped connection, a deadline, a 5xx, or a
        response this build could not classify), so the wait established nothing
        and the account holder may already have approved. The error carries the
        failure as `cause`.

      Either way, call `awaitSignupApproval` or `getSignupRequest` again with the
      **same** `requestId`. An approval you could not read is already a login, so
      the next poll finds it and `listTokens()` lists it. Never call `createLogin`
      again: that opens a second, unrelated request and spends another of the
      account's five outstanding slots.

    A poll failure that can never come good (`request_not_found`, or any other 4xx)
    does not arrive here. `awaitSignupApproval` throws it at once with its own code.
  </Accordion>

  <Accordion title="approval_denied">
    **Overloaded.** Four cases, and only the first is a refusal:

    | Case | Retry |
    | - | - |
    | The account holder refused the purchase | No. Abandon this total |
    | An approved fill failed revalidation at fill time: the total became unreadable, changed currency, increased, or newly crossed the threshold | Yes. A retry asks again with the current total |
    | The page **changed host** after approval, or between resolving the token and the fill | Reconcile first. Nobody refused anything, but the recorded transaction is reversed only best effort. Check [Activity](/dashboard/activity), then restart on the intended host |
    | A card number or security code (CVV) token was passed to `selectOption`, which is refused outright | After fix. The checkout is not lost: fill the card number or security code through the normal fill path |

    The `selectOption` refusal exists because that path has no approval gate: a
    merchant-controlled dropdown listing every code from 000 to 999 could otherwise
    harvest a real security code.

    On a host change, the proxy attempts to reverse the recorded transaction, best
    effort. If the backend was unavailable, the transaction survives and a retry
    records a second one. If the error returns, or the spend looks higher than
    expected, reconcile in the dashboard rather than retrying.
  </Accordion>

  <Accordion title="charge_exceeds_authorized">
    The total on the page has risen above the figure the account holder's controls
    saw when the card number went in (or, on a saved card, when the security code
    did). On a card with a spending limit, a code filled on its own gets
    `limit_reached` instead. Usually a shipping or tax recompute on the last step, an upsell a click accepted,
    or a changed quantity or plan.

    The action that would submit is refused, and so is every other click, key press,
    typed value and dropdown choice on that checkout while the total stays above the
    authorized figure.

    **Retry:** not by re-filling the card. The number is already in the merchant's
    form, and a second fill types a second number and records a second charge for
    one purchase. If the checkout
    brings the total back down by itself, the next action goes through. If not, stop
    and tell the account holder the new total so they can decide.

    On a saved-card checkout, where the security code was the only real value filled,
    filling that code again re-runs the check against the total the page shows now.
  </Accordion>
</AccordionGroup>

## Card and merchant refusals

| Code | Meaning | Retry | What to do |
| - | - | - | - |
| `account_paused` | The account holder's account-wide stop is on. Nothing resolves for any agent | After account holder | Stop and tell the account holder. Retry in the same session once they resume |
| `destination_unauthorized` | This site is not an authorized destination for the account. `decision` says what came of asking the account holder | After account holder | `timed_out`: tell the account holder now, and retry once they authorize the site. `denied`: do not retry |
| `card_paused` | None of this card's agent cards is active, and at least one is paused | After account holder | Retry in the same session once the account holder resumes it |
| `merchant_blocked` | The agent card's merchant rules do not permit this site | No | Try another in-scope card, or tell the account holder the host |
| `card_blocked` | The card number matched several cards, or a revealed card failed its binding checks | No | Never retry blind. Use another card or ask the account holder |
| `login_blocked` | The login is locked to another site, has no website set, or its iframe moved to another origin | Depends | Sign in on the login's own site. If it has never worked anywhere, ask the account holder |
| `paired_cvv_refused` | The card number was typed; only the security code was withheld | No | Never re-fill the whole card. Follow the message |
| `card_live_fill_unsupported` | Live card fills are off here, or the vault refused the reveal | Depends | Read the HTTP status in the message |
| `insecure_page` | The page, form or frame could not be shown to be TLS protected | Depends | Read the message. Usually: move to `https` |

<AccordionGroup>
  <Accordion title="account_paused">
    The account holder used the account-wide stop (**Pause all agents**) on the
    dashboard. While it is on, **nothing** resolves for **any** agent on the account: card
    numbers, security codes, login passwords, authenticator codes, identity fields
    and every saved address. It is not a verdict about the token you used. Compare
    `card_paused`, which is one funding card's agent cards being paused.

    **Retry:** no, and not with any other token either: a different card, login or
    address fails the same way. Stop the run and tell the account holder their
    stop is on; only they can lift it. Your session stays open and the key is not revoked, so
    **once they resume, retry in the same session**.
  </Accordion>

  <Accordion title="destination_unauthorized">
    The site is not an authorized destination for this account. This is account
    level and applies to every secret type (card, security code, identity field and
    address), so it is not a verdict about the token you used. Compare
    `merchant_blocked`, which is one agent card's own allow or block list.

    **Retry:** not until the account holder authorizes the site, and not with a
    different token either. Before this error, the first fill on a new site opens a
    new-site approval request and waits up to 15 minutes while the account holder
    approves or denies it from the email or the dashboard banner. Tell the account
    holder as soon as that wait starts, so they know to approve it.

    When the fill is refused, the error's `decision` field and its message say what
    came of the request:

    | `decision` | Meaning | What to do |
    | - | - | - |
    | `timed_out` | The account holder was asked and the request expired unanswered; it can no longer be approved from the email or the banner | Tell the account holder. A retry in the same session is refused again until they authorize the site on the **Destinations** page; a retry in a new session asks them again |
    | `unconfirmed` | The account holder was asked, but the fill lost track of the request before an answer arrived; it may still be open | Tell the account holder they can still approve it from the email or the banner, then retry |
    | `denied` | The account holder declined the site | Do not retry. Tell the account holder |
    | `unasked` | The request could not be sent | Tell the account holder which site; they can authorize it on the **Destinations** page |
    | `approved` | The account holder approved, and the retry was still refused | Do not ask for approval again. Tell the account holder the authorization did not take; retry once the site is listed on the **Destinations** page |

    Once the site is authorized, **retry in the same session**: the account's
    authorized sites are read fresh on the next fill. See
    [New-site approvals and destinations](/dashboard/approvals-and-notifications#new-site-approvals-and-destinations).
  </Accordion>

  <Accordion title="card_paused">
    None of this funding card's agent cards is active, and at least one is paused.
    Not necessarily all of them: the code is `card_paused` whenever any of its agent
    cards is paused, even when the others sit at `limit`.

    **Retry:** not while it is paused. Once the account holder resumes it, **retry
    in the same session**: the proxy reloads the card's controls on the next fill, so no
    new key or session is needed.

    Ask the account holder to check all of this card's agent cards on the dashboard
    rather than resuming the first paused one. With a mix of paused and `limit` cards,
    resuming an arbitrary one can leave the one that permits this merchant still at
    `limit`. The error does not say which card that is.
  </Accordion>

  <Accordion title="merchant_blocked">
    The site is not permitted by the agent card's merchant rules: either an allow
    list that does not name it, or a block list that does. The code is the same for
    both.

    **Retry:** not with this card on this site while the rules stay as they are.
    The checkout itself may still be possible:

    * The merchant question is decided from **active** agent cards only. A card
      whose rules do permit this site is ignored while it is paused or at `limit`,
      and resuming it can make the same card work immediately. Check that first.
    * If `listTokens()` offers another in-scope funding card, its rules may permit
      this merchant.
    * Failing that, a different permitted site may still work.
    * If the account holder wants this site allowed, tell them the host and let
      them decide how. Adding it to an allow list does nothing while it is on an
      active block list, and removing it from a block list does nothing while an
      allow list omits it.
  </Accordion>

  <Accordion title="card_blocked">
    **Overloaded.** Two cases, neither an enclave failure (those have their own
    `attested_fill_*` codes):

    * **An ambiguous card number** matching several differently configured cards.
      Resolution is deterministic, so the same token collides on every attempt. Use
      a different card, or have the account holder remove the collision or narrow
      the key's scope.
    * **A binding mismatch** on the live vault fill: the vault token's owner does
      not match the card's account, or the revealed last four does not match the
      card's. Nothing was typed, and the proxy attempts to reverse the transaction,
      best effort. If the backend was also unavailable, the transaction survives:
      if the run looks like it spent quota, check the recorded transactions on
      [Activity](/dashboard/activity) and the agent card's spend. Retrying is futile
      until the account holder or operator corrects the card.

    **Retry:** never blindly.
  </Accordion>

  <Accordion title="login_blocked">
    **Overloaded.** Three cases. The code does not reveal which:

    * The login is URL locked and this page is not its registered site.
    * The login has no website set at all.
    * The login resolved for the right iframe, but that frame navigated to another
      origin before the value was typed.

    | Case | Retry |
    | - | - |
    | Wrong site | No, not here. The correct site still works |
    | No website set | After account holder. **No site will ever work** until the account holder sets the login's website on the dashboard. If the login has never resolved anywhere, assume this case |
    | The login resolved for the right iframe, then that frame navigated to another origin before the value was typed ("Login iframe navigated to a different origin before the fill") | Once. Nothing was typed. Wait for the registered-origin frame to settle and retry |
  </Accordion>

  <Accordion title="paired_cvv_refused">
    **The card number was already typed into the merchant's form.** Only the
    security code was withheld. Whether the merchant completed the charge anyway
    cannot be established from here.

    Two things can cause it.

    * The checkout could no longer be proven to be the authorized one: the total
      became unreadable, changed, or changed currency, or the page moved to
      another site or loaded a new document.
    * The pairing between the number fill and the code fill was lost: a
      navigation, a step that replaced the document, or another fill in between.
      The code then needed a fresh approval, and that approval was declined, ran
      out, or could not be settled.

    The message says which, and what became of the number's transaction. A recorded
    one is **not** reversed. The code also covers a number fill that recorded
    nothing, so do not assume a charge the message does not name.

    `numberLanded` on the error says which variant you have:

    | `numberLanded` | Meaning | Recovery |
    | - | - | - |
    | `confirmed` (or absent) | The number is in the form | Fill **only** the security code once the total is readable again. That fill asks the account holder for approval again. Or abandon |
    | `unconfirmed` | A character-by-character type failed before landing was confirmed. The number may be partly typed | Do not fill only the code. Clear the field and fill the number again through the ordinary path (which records a charge), then the code. Or abandon |
    | `displaced` | Another card's number fill went into the form after this one | Same as `unconfirmed` |

    **Retry:** never the whole card fill into a field that holds a number: it types a
    second number and records a second charge. After a declined approval, abandon
    and tell the account holder. If you abandon for any reason, tell the account
    holder a card number was entered at this merchant. Never report it as "no card data was entered".
  </Accordion>

  <Accordion title="card_live_fill_unsupported">
    **Overloaded.** Either this deployment has not enabled live card fills, or it
    has and the vault answered the reveal with a non-2xx status (the message carries
    the status). One cause carries **no status**: a successful response whose value
    is empty or masked, meaning the vault cannot reveal this card or the token is
    not a card. The proxy fails closed rather than type a mask.

    | Cause | Retry |
    | - | - |
    | Not enabled on this deployment | No. Checked before anything is recorded, so there is nothing to reconcile |
    | A persistent 401 or 403 | No. A credential or access misconfiguration |
    | No status (masked value) | No. A configuration problem: the account holder may need to re-add the card. This refusal comes after the transaction was recorded, so check [Activity](/dashboard/activity) |
    | A 429 or 5xx | Yes, after a backoff. The vault is rate limiting or briefly failing, and the checkout can still complete |

    The status cases are raised **after** the transaction was recorded, and the
    reversal is best effort. If the backend was also unavailable, the transaction
    survives and a retry records a second one. If the dashboard shows more spend
    than the run accounts for, reconcile instead of retrying.
  </Accordion>

  <Accordion title="insecure_page">
    **Overloaded.** Four cases, and read the message rather than assuming it is one
    of these:

    | Case | Retry |
    | - | - |
    | The page or iframe is served over **plaintext** on a non-loopback host, so no real value was substituted | After fix. Navigate to the `https://` URL first |
    | The page is HTTPS but the **form would post in clear**: its `action`, or a submit button's `formaction`, is an `http://` URL. The proxy reads this off the live form just before typing | No, not against this form. Use a checkout whose form posts to `https://`, or tell the account holder |
    | The form this field submits with could not be identified at all, because the page tampered with the DOM's own methods | No. Stop and tell the account holder rather than trying another field on the same page |
    | The target iframe could not be identified yet, so it could not be shown to be TLS protected | Once. "could not be identified yet" is a loading delay. "its origin could not be established" is either a transient lookup or a genuinely opaque `null` origin on an `about:` or sandboxed frame, which fails every time. If the same message returns, target a frame with a verifiable HTTPS origin |

    On plaintext, anyone on the network path between the proxy and the merchant
    could read a card number, password or authenticator code. Plaintext is never
    relayed through your machine; see [Egress and tunnelling](/sdk/tunnel).

    A loopback `http://` page (`localhost`, a `*.localhost` name, `127.0.0.0/8` or
    `::1`) is not refused, so a local demo works. On a deployment that uses a
    static upstream proxy, there is no loopback exception: those pages are refused
    too. See
    [URL locking](/security/url-locking#plaintext-pages-are-refused).

    **Two exceptions to "nothing was substituted"**, on `type`,
    `pressSequentially` and `selectOption`:

    * If the field is replaced while the value is going in, the proxy retries on the
      replacement. A plaintext form there is refused, but some characters may
      already be on the page. The field cannot be read back, so do not re-send blind
      once the form is fixed. Recovery depends on the value:
      * **Card:** clear the field, then fill the number again through the ordinary
        fill. This records a fresh charge. Or hand the checkout to the account
        holder.
      * **Password, identity or address:** nothing was charged. Clear the field
        with `fill(selector, "")` and re-send the same token.
      * **Select:** selecting again replaces the previous choice.
      * **Authenticator code:** the page may already have consumed it. Re-read the
        step before sending another.
    * A tokenized `selectOption` can have its control replaced mid-call. By then the
      choice has reached the page, so it counts as a fill in the session's outcome
      whatever the refusal says. The matching Activity entry is best effort.

    **A card's reservation is released on a best-effort basis**, never promised. A
    hold may still stand, so filling the card again can consume its limit twice.
    Reconcile as for `card_resolution_uncertain` rather than assuming the refused
    attempt cost nothing.

    On a security-code fill, read `pairingHeld` on the error. `true` means the card
    number's one-shot pairing is still armed, so the retried code is the next fill to
    send. `false` means the refusal came after the pairing was spent, and the code
    will ask the account holder again.
  </Accordion>
</AccordionGroup>

## Security code (CVV)

| Code | Meaning | Retry | What to do |
| - | - | - | - |
| `cvv_entry_required` | The security code needed the account holder's live entry, and it did not arrive | Depends | Read which ending the message names |

<Accordion title="cvv_entry_required">
  **Overloaded.** The security code is never stored, so its token cannot resolve
  to a stored value. Where live security code entry is enabled, the fill pauses
  for the account holder, and this code covers five endings. The message says
  which. The pause lasts about 3 minutes, or less while the proxy is shutting
  down, and the account holder's link stops accepting a code at the same moment
  the pause ends.

  | Ending | Retry |
  | - | - |
  | The prompt could not be started | Yes. A retry raises a fresh prompt |
  | The account holder did not answer in time | Yes |
  | The proxy **is shutting down** and could not wait | Reconcile first. Check **Activity**, then retry in a new session. See below |
  | **Superseded** by a newer fill | **No.** Another request is already in front of the account holder, and retrying replaces the one they may be answering right now. Wait for it |
  | **Cancelled** because the session closed | Reconcile first. Not in this session: check **Activity**, then open a new one. See below |

  Restarting in a new session fills the card number again. If the number was
  already filled in the failed session, that fill is still recorded: it counts
  toward the card's spend limits and the account's payment quota, and it is not
  reversed. The new fill records a second spend attempt for the same purchase, and
  it can hit a spending cap or `plan_quota_reached`. Check
  [Activity](/dashboard/activity) or reconcile before you restart.

  On a deployment without live security code entry, the message says "Live CVV
  entry is not yet available". Do not retry: every attempt fails the same way.
  Stop and tell the account holder that this card cannot be used on this
  deployment for a checkout that asks for the security code. See
  [Live CVV entry](/guides/live-cvv).
</Accordion>

## Challenges and handoff

| Code | Meaning | Retry | What to do |
| - | - | - | - |
| `challenge_required` | A challenge is showing, a handoff suspended this action, or a handoff timed out or was superseded | Depends | Retry after the handoff finishes. Never re-request a superseded one |
| `handoff_not_applicable` | The only thing detected was a field that might be a code input (`ambiguousOnly`). A policy refusal, not a lost page | No | Nothing to recover. The tab is still live and yours |
| `challenge_scan_incomplete` | The handoff re-detects before summoning anyone, and that scan could not finish | Yes | Detect again on the same page. Hand off again if a challenge is still reported |
| `wait_cancelled` | A wait was ended because a handoff took the page over | Yes | Ask again once the handoff returns |

<AccordionGroup>
  <Accordion title="challenge_required">
    **Overloaded.** Three cases:

    * The page is showing a 3-D Secure, one-time-code or CAPTCHA step. The account
      holder has to act first; see [Handling challenges](/guides/challenges).
    * A handoff is in progress and this action was suspended. Actions refused during
      a takeover are meant to be retried once the account holder is done.
    * A `requestChallengeHandoff()` timed out or was superseded.

    A **superseded** handoff means a newer one for the same key is already live,
    quite possibly the one the account holder is completing now. Re-requesting
    supersedes that one, so wait for the active handoff instead. Only a **timed-out** handoff
    should be restarted.
  </Accordion>

  <Accordion title="handoff_not_applicable and challenge_scan_incomplete">
    `handoff_not_applicable`: the only thing detected was a field that **might** be
    a code input. It was flagged because of fills you made during this sign-in, not
    because a challenge is showing, so there is nothing for the account holder to
    act on. Do not discard or recreate the tab. Fill `tokens.totp` if this is the
    authenticator step of a sign-in you just performed; otherwise report
    the step as unresolved.

    `challenge_scan_incomplete`: a frame detached during the scan, or a probe ran
    past its budget. Nobody was notified. If detection keeps returning
    `inconclusive`, the page is not settled: give it a moment before deciding
    anything.
  </Accordion>
</AccordionGroup>

## Authentication and scope

| Code | Meaning | Retry | What to do |
| - | - | - | - |
| `invalid_api_key` | `listTokens()` (`/v1/tokens`) could not use the key. Returned for a mistyped key **and** for a revoked one, so this endpoint cannot be used to test whether a key exists | No | Check the key's status on the **API keys** page before assuming a typo |
| `key_revoked` | From the proxy, when a session opens or a token resolves: the account holder revoked the key. The session closes with 4001 | No | Ask for a new key |
| `key_auto_revoked` | Revoked automatically after repeated cross-account token denials | No | Treat as an agent bug |
| `scope_denied` | The resource exists but is outside this key's allowed scope. Also under [Signup and login writes](#signup-and-login-writes), for a login removed from the key | After account holder | Refresh the catalog first. Only if nothing fits does the account holder need to widen the scope |
| `cross_user_denied` | The token belongs to a different account than the session | No | Treat as an agent bug. Three within five minutes revokes the key |
| `token_not_found` | No record for that token, a token of the wrong kind, or an identity field the account holder has not filled in | Depends | Refresh the catalog once. If the same token returns, stop. For an identity field listed in `missing`, ask the account holder to fill it in and retry once it leaves `missing` |
| `unauthorized` | A credential that is missing or malformed, rather than rejected. Also under [Sign-in responses](#sign-in-responses) and [Tool errors](#tool-errors), with different meanings | No | Fix the client |
| `auth_failed` | The key was presented and the session could not be opened | Once | Retry with backoff. If it repeats, check the key |

<AccordionGroup>
  <Accordion title="scope_denied">
    `listTokens()` returns only the cards and logins this key already allows. A token
    that went stale after the account holder narrowed the key is answered by a fresh one, and
    another allowed card or login may suit the same checkout. So refresh the catalog
    first.

    Only when nothing returned fits do you need the account holder. Once they widen the scope,
    it takes effect on the very next attempt, with no new key or session.
  </Accordion>

  <Accordion title="token_not_found">
    **Overloaded.**

    * **A token of the wrong kind.** The attested checkout accepts only a card
      **number** token (for example `av_card_1_number_u7x2k`) and refuses anything
      else, including that card's own CVV token, before any lookup. This is an agent
      bug: re-reading the catalog returns the same token. Pass a card number token.
    * **A stale token.** The account holder deleted or replaced the resource, or removed the
      field it stood for (a login's email, username or authenticator secret). Re-read the
      catalog once: it is rebuilt from the current cards and logins, so you get a
      fresh, valid token and the checkout continues.
    * **An unvaulted card.** The catalog lists every in-scope card regardless of
      vault status, so a refresh returns the same token. The account holder has to re-add
      the card on the dashboard.
    * **An empty identity field.** The account holder has not filled in that field (a last
      name, a phone) of the identity you used; the message says so, and the
      identity's `missing` array lists the field. Identity tokens never change, so
      do not compare tokens here: ask the account holder to fill it in on their profile, or
      on that labelled identity, then refresh and retry once the field is no
      longer in `missing`.

    For the other cases, refresh once. If the token comes back identical, stop.
  </Accordion>

  <Accordion title="unauthorized and auth_failed">
    `unauthorized` reaches you in two places:

    * **On the socket:** the first frame was not an auth message, or the key was
      empty. Every call in flight when the proxy closes with 4001 also gets this
      code.
    * **From the recipe helpers:** a missing or malformed `Authorization: Bearer`
      header. `recipeSubmission()` hits this when no key is configured, because it
      still sends an empty bearer. Set `SELF_AGENT_PAY_API_KEY` or pass `apiKey`.

    `listTokens()` maps every 401 to `invalid_api_key`, so you never see
    `unauthorized` there.

    `auth_failed` wraps **every** failure of the backend auth call, including a
    network error, a non-2xx response or a malformed reply, not only a rejected key.
    A transient backend blip looks identical, so one retry with backoff is worth it.
  </Accordion>
</AccordionGroup>

## Page and fill

| Code | Meaning | Retry | What to do |
| - | - | - | - |
| `evaluate_disabled` | `page.evaluate` is not available to agent sessions | No | By design |
| `sensitive_data_blocked` | Frames, snapshots and digit-testing selectors are refused once a sensitive value was filled | No | Inspect frames and snapshots before filling, or open a new session. For a selector, use an id, class or text without digits. See [What the agent can see](/security/agent-context#count-and-waitforselector) |
| `clipboard_blocked` | A copy, cut or paste chord was pressed after a real value was filled | No | Do not try another chord. Click and type, or use `fill` |
| `fill_rejected` | The fill was applied, and the field did not keep it | After fix | Fill the field the merchant actually wants, in the format it accepts |
| `invalid_choice` | A `selectOption` named no option | After fix | Pass a string, `{ label }`, `{ value }` or `{ index }` |
| `target_not_typable` | The field will not take input; refused before the card gate. Nothing typed or recorded | After fix | Make the field editable or aim at the right one. The pairing is intact |
| `not_a_card_number_field` | A card number was aimed at an element that is not a card-number field; refused before the card gate. Nothing typed or recorded | After fix | Aim at the card-number field (`input[autocomplete="cc-number"]` when the page marks it). The pairing is intact |
| `not_a_security_code_field` | A security code was aimed at an element that is not a security-code field; refused before the card gate. Nothing typed or recorded | After fix | Aim at the security-code field (`input[autocomplete="cc-csc"]` when the page marks it). The pairing is intact |
| `frame_target_timeout` | The proxy could not settle on the field inside a frame. Nothing typed | After fix | Check **both** halves of the selector. The pairing survives |
| `unknown_target` | The target is gone, or the field could not be resolved | Depends | See the details |
| `popup_pending` | `recordOutcome()` refused: a tab this page opened is still queued | Yes | Call `waitForPopup()`, read that tab, then record |
| `unanswered` | The proxy stopped waiting for one call. The session is intact | Depends | Read or count: once. Action: only after closing the tab |
| `insecure_site` | A `goto()` in a residential egress session stopped: the site would not load over HTTPS, or it redirected to an `http://` address. Nothing was sent over plain `http`, and the tab stayed where it was | No | Choose another site, or tell the account holder this one cannot be used. Do not treat the navigation as possibly landed |

<AccordionGroup>
  <Accordion title="insecure_site">
    A session on the deployment's [residential egress](/sdk/tunnel#residential-egress)
    is HTTPS only. An `http://` address you pass to `goto()` is loaded as
    `https://` on the same host and path, and on the same port when the address
    names one. When that HTTPS attempt fails (no
    TLS on the host, a certificate the browser will not accept, no answer), or when
    the site's server redirects to an `http://` address, the navigation stops with
    this code. It never falls back to `http`. The message says which case it was:

    * "This site doesn't support HTTPS, so Agent Vault won't load it (insecure\_site)."
    * "This site redirected to a plain http\:// address, so Agent Vault didn't follow it (insecure\_site)."

    Both end "Nothing was sent over plain http." Nothing committed: the tab is
    still on the page it was on, and anything typed there is still there. Retrying
    the same address fails the same way every time, in this session or a new one.

    Only `goto()` returns this code. When a `click()` or `press()` starts a
    navigation that is stopped the same way, it gets the browser's own navigation
    error, such as `net::ERR_SSL_PROTOCOL_ERROR`, and the tab stays on its page. A
    navigation the page starts by itself is stopped silently.

    A name that does not resolve, a slow host that runs past your `timeout`, and a
    failure of the residential provider itself are not this code: they keep their
    ordinary error, and a provider that keeps failing ends the session with
    `egress_lost`. An `insecure_site` does not mark the session's outcome as an
    error on its own, so a run that goes on to check out elsewhere still finishes
    normally.
  </Accordion>

  <Accordion title="clipboard_blocked">
    A modifier chord (`Control+c`, `Control+v`, `Control+x` and their `Meta` and
    `ControlOrMeta` spellings) was pressed after a real value was substituted into
    the page. A chord could move a filled card number or password somewhere the
    agent controls, so it is refused once the session is tainted. An untainted
    session is unaffected.

    The refusal covers the whole class and fails closed on modifier names it does not
    recognise, so probing spellings will not find a way through. Plain keys (Tab,
    Enter, arrows, characters) still work. To clear an input, click it and type over
    it rather than reaching for `Control+a`.
  </Accordion>

  <Accordion title="fill_rejected">
    A token fill is verified after it is applied. The field did not keep what was
    written: the page emptied it, or a `maxlength` or input mask cut it short. The
    value still reached the page, so it counts as a token use, and any transaction
    and card-fill alert stand. The site's own submit will read something else.

    A control that moves the value elsewhere (a one-time-code widget spreading a code
    across boxes, a tag input lifting it into a hidden field) is not a rejection. A
    tokenized `selectOption` is verified the same way: the page's own handlers put a
    different option back.

    | Control | Retry |
    | - | - |
    | Text field | After fix. Not the same value into the same field: fill the field the merchant wants, in the format it accepts (an expiry split across two inputs, a card widget's own iframe). For a phone field, try `phone_national` |
    | Select | Once, after the page settles. If it reverts again, the page is choosing that option itself |

    Either way, check the merchant's own state before repeating anything that could
    charge.
  </Accordion>

  <Accordion title="invalid_choice">
    The choice carried none of `label`, `value` and `index`, or its label or value
    was empty. Left to the browser, an empty choice matches a placeholder option and
    reports success with the dropdown unchanged. Nothing was selected.

    The SDK raises the same code before sending for an object with none or more than
    one of those keys, and for `{ index }` or `{ value }` on a proxy that does not
    support them, rather than risk selecting the wrong option.
  </Accordion>

  <Accordion title="unknown_target">
    **Overloaded.** Several causes, and two want opposite responses:

    | Cause | What to do |
    | - | - |
    | A popup that **closed itself** (a 3-D Secure, wallet or sign-in window) | Ordinary merchant behaviour, not an agent bug, and **not** evidence of success. Do not rebuild the id. Return to the parent page and read the merchant's own state |
    | A fabricated or stale id | An agent bug |
    | `waitForPopup()` timed out | Once. A late popup is queued, and the next call reads it |
    | A sensitive fill's target could not be resolved yet, or is disabled or read-only | Wait for the checkout to render and try **once**. If the field is still missing, re-discover the selector rather than looping |
    | `requestChallengeHandoff()` on a page that was already gone (`Unknown pageId`) | Nobody was notified. Switch to the page that opened it, or another live page, and detect again there |
    | `requestChallengeHandoff()` re-detected and found no challenge (`No challenge is present on this page`) | Not a target problem. The challenge is gone: carry on with the checkout |

    On the character-by-character paths (`page.type`, locator `type`,
    `pressSequentially`), the target can vanish **after** the transaction was
    recorded, and it is **not** reversed even though no character landed. Retrying
    records a second transaction. If the field disappeared mid-run, reconcile
    rather than retry blind.

    For a lost page the error carries `targetLoss`:

    | `targetLoss` | Meaning |
    | - | - |
    | `pre_lookup` | The tab was already gone when the call arrived. Nothing was resolved or typed |
    | `in_flight` | It went away with the operation running. Part of a value may have landed |
    | Absent | Assume the fill may have landed |
  </Accordion>

  <Accordion title="unanswered">
    The proxy stopped waiting for this one call: a click, press, count or other page
    call that Playwright never answered inside the proxy's deadline (120 seconds, or
    the call's own `timeout` plus 30 seconds, capped at an hour). Unlike
    `request_timeout`, the transport is still up and the browser, pages and cookies
    are intact. The call was abandoned, not cancelled, so an action may still land
    later. The message says which kind of call it was.

    * **A read, wait or count:** retry once.
    * **An action:** close that page first (`page.close()` stops the action; if the
      close itself comes back `unanswered`, call it again). Then read the merchant's
      order state, and only if no order is there continue in a new tab of the same
      context.

    A stalled screenshot counts as an action. A stalled card fill may already have
    recorded its transaction: check the dashboard before filling again. Full
    procedure: [Recover from unanswered](/sdk/sessions-and-timeouts#recover-from-unanswered).
  </Accordion>
</AccordionGroup>

## Backend and protocol

| Code | Meaning | Retry | What to do |
| - | - | - | - |
| `unknown_method` | The call is not part of the protocol | No | Treat as an agent bug |
| `parse_error` | The frame was not valid JSON | No | Treat as an agent bug |
| `bad_request` | A frame arrived with no method, or an HTTP body was not JSON or too large | No | Treat as an agent bug |
| `internal` | The proxy failed to set up the session | Yes | Retry with a new session. Report it if it repeats |
| `unavailable` | The proxy instance is shutting down. Arrives before any browser or transaction exists. The hosted MCP server has its own [`unavailable`](#tool-errors) | Yes | Reconnect at once rather than retrying on this socket |
| `malformed_response` | A backend response had an unexpected shape, or the enclave filled a card whose last4 is not the authorized one | Once | Nothing recorded for the shape case. The wrong-card case: reconcile |
| `card_resolution_uncertain` | The call that records the transaction timed out or answered unreadably. No card value was entered | Reconcile first | The outcome is unknown, not failed |
| `error` | The catch-all: usually a Playwright failure, also backend trouble | Verify first | See the details |

<AccordionGroup>
  <Accordion title="unavailable">
    The instance you reached is draining and refused to start a session. It arrives
    on the auth frame, before any browser, tunnel or transaction exists, and the
    socket then closes with 1013 ("Try Again Later"). Nothing was allocated or
    spent. A fresh connection lands on an instance that can serve it. Treat a repeat
    as a deployment rolling out and back off a little.
  </Accordion>

  <Accordion title="malformed_response">
    **Overloaded.**

    * **The shape case** is always before anything is recorded, so a retry cannot
      charge twice. Whether it helps depends on the cause the message names:
      transient corruption clears, a version mismatch or a field the backend never
      sent fails every time. Retry once; if the message is unchanged, it is a
      deployment problem for the account holder.
    * **The wrong-card case** on the attested path is a critical anomaly: a payment
      may already have been attempted with another card. Reconcile rather than
      retry.

    A shape failure **after** the transaction was recorded arrives as
    `card_resolution_uncertain` instead.
  </Accordion>

  <Accordion title="card_resolution_uncertain">
    The call that records the transaction timed out with its outcome unknown, or
    returned a response that could not be read after the record was written. The
    transaction may exist with no id to reverse it. **No card value was entered**:
    the reveal happens after this call. Raised on both the attested path and the
    ordinary fill.

    **Retry:** no. Reconcile first. Retrying either records the purchase twice or
    consumes a cap for money never spent. There is no safe variant of this code.
  </Accordion>

  <Accordion title="error">
    **Overloaded.** The catch-all for any failure the proxy could not classify under
    a more specific code.
    Usually a Playwright failure, but it also covers backend trouble (a failed
    fetch, a non-2xx response, unparseable JSON, a misconfigured deployment) during
    token resolution or approval, before any value reaches the page.

    **Verify before retrying.** "Backend failure" is not by itself a licence to
    retry:

    * A genuinely pre-execution failure never touched the merchant. A network blip
      clears with backoff; a misconfiguration will not.
    * **A live vault fetch that threw** (DNS failure, connection reset, its own
      timeout) escapes as `error` **after** the transaction was recorded. The
      reversal is best effort. Before repeating a checkout that failed while the
      card was being revealed, check the recorded transactions on
      [Activity](/dashboard/activity) and the agent card's spend.
    * **On the attested path**, an enclave service failure (a 5xx, a mid-response
      reset, a timeout, a malformed success) arrives as `error` after the enclave
      may already have typed the card. The proxy keeps the transaction and marks the
      outcome unknown. Treat it as unverified.
    * A failure while waiting for a selector is safe to repeat. A click or submit
      that timed out may already have reached the merchant: check the merchant's
      order state first.

    `browser.outcome()` helps only if it returns `confirmationDetected: true`, and
    that only repeats your own earlier `recordOutcome()` call. This is the one code
    where the message is all you have.
  </Accordion>
</AccordionGroup>

## Attested checkout

These codes come from `browser.attestedCheckout()`. The same table, with the
feature's status, is on
[Attested detokenization](/security/attested-detokenization).

| Code | Meaning | Retry | What to do |
| - | - | - | - |
| `attestation_required` | The process could not prove it runs in the enclave. Fails closed | Once | One retry after a backoff covers a transient launcher or key fetch failure. Otherwise configuration |
| `attested_checkout_unavailable` | Seven causes. Six refuse up front with nothing recorded; the seventh is reversed | Depends | Read the message. See the details |
| `attested_checkout_busy` | A concurrency ceiling: per session, key, account or instance. Nothing recorded | Later | Back off and retry. Do not open more sessions |
| `attested_fill_failed` | Failed **before the card was entered**, and the transaction was reversed | Yes | The retry-safe attested failure |
| `attested_fill_unreversed` | Failed before the card was entered, but the transaction could **not** be reversed | Reconcile first | Tell the account holder. Not agent recoverable |
| `attested_fill_incomplete` | Failed **after the card was entered**, or the outcome is unknown. The transaction is kept | Verify first | Check the merchant, and tell the account holder yourself |
| `invalid_plan` | The plan was malformed, or a required tunnel dropped during the approval wait | Depends | Fix the plan. For the lost tunnel: relaunch and run the same plan |

<AccordionGroup>
  <Accordion title="attested_checkout_unavailable">
    **Overloaded.** Seven causes share this code:

    | Cause | Retry |
    | - | - |
    | No enclave is configured on this deployment | No. Configuration |
    | The enclave is configured over a plaintext, non-local connection | No. Configuration |
    | The enclave's egress is not declared filtered | No. Configuration |
    | The enclave's identity is not declared verified | No. Configuration |
    | The endpoint failed to prove it is an enclave Agent Vault operates | Depends. A token that verifies but is wrong: no. A transient failure to reach the endpoint or fetch its keys: once, after a short backoff |
    | This session **required** a tunnel (`tunnel: true`) and the deployment cannot relay the enclave through it | After fix: open a session without `tunnel: true`, or fill the card token on your own page |
    | The enclave accepted the checkout but did not use the egress relay it was granted, during a rolling deploy. The card was **not** entered and the transaction is reversed | Later, once the deploy settles. If the reversal fails you get `attested_fill_unreversed` instead |

    Nothing was spent in any case, so nothing needs reconciling. Tell the account holder
    which configuration cause the message names.
  </Accordion>

  <Accordion title="attested_fill_failed, attested_fill_unreversed, attested_fill_incomplete">
    * **`attested_fill_failed`**: no card value was entered, no budget was
      consumed, no counters moved. The proxy uses this code only once it has
      confirmed the reversal landed. Also covers the run exceeding its overall time
      budget. **Retry is safe.**
    * **`attested_fill_unreversed`**: the same failure with that promise withdrawn,
      usually a backend outage. The recorded transaction counts against the card's
      limits and the account's quota until someone reconciles it. Also raised when
      the enclave filled a card whose last4 is not the authorized one **and** the
      reversal failed, which is a critical anomaly: another card may have been
      charged. Surface it to the account holder.
    * **`attested_fill_incomplete`**: the card may already have reached the page and
      the order may exist. The proxy attempts to notify the account holder, but that is best
      effort and can fail silently, so **tell the account holder yourself**. Only the
      merchant's own order state can settle it. A blind retry is how you get two
      orders.
  </Accordion>

  <Accordion title="invalid_plan">
    **Overloaded.** Refused **before** any transaction was recorded.

    * **A malformed plan.** Retrying the same plan fails the same way, so fix it.
      One of:
      * A `screenshot` step (refused for SDK callers).
      * A step type other than `goto`, `click`, `fill`, `fillSecret`, `waitFor` or
        `wait`.
      * A step missing a required field: `click`, `waitFor` and `fillSecret` need a
        non-empty `selector`; `fill` needs `selector` and a string `value`; `goto`
        needs a non-empty `url`; `wait` needs a finite `ms`.
      * A destination that fails the host check.
      * A plan that never fills the secret.
      * A plan where `fillSecret` is not followed by exactly one `click` and then
        only `wait` or `waitFor` steps.
    * **A lost tunnel.** A session that required tunnel egress lost its tunnel while
      the account holder was deciding the approval ("its tunnel is no longer available"). The
      plan is fine. Relaunch with `chromium.launch({ tunnel: true })` and run the
      same plan.
  </Accordion>
</AccordionGroup>

## Page hints

| Code | Meaning | Retry | What to do |
| - | - | - | - |
| `invalid_hint` | Malformed: an unknown kind, a selector outside plain CSS, a `selector` or `frame` over 512 characters, an attribute value comparison declared after a token fill, an unreadable attribute, or a fifth distinct hint of one kind on one page (the limit is 4 per kind, per page). Nothing recorded | After fix | The message names what was wrong |
| `hint_unresolved` | A storing hint found nothing usable where it pointed | After fix | Point at a different element, for causes you can fix |
| `hint_uncertain` | The write reached the vault and its answer was lost | No | Tell the account holder to check the login on their **Logins** page |

<Accordion title="hint_unresolved and hint_uncertain">
  `hint_unresolved` is **deliberately ambiguous**: "matched nothing" and "matched,
  but not a usable secret" are one outcome with no counts attached, because a
  caller that could tell them apart could read a filled value back a bit at a
  time. Retrying the same hint changes nothing. Fixable causes: reveal the "can't
  scan the QR code?" text option, or re-check the `login` label and the site.

  For `login.totp_secret` it also covers refusals nothing can clear: a second
  login registered for this site, an `otpauth://` link naming an account that is
  not the login's stored username, or a login stored with no username. The account holder
  then enrols two-factor from the dashboard.

  `hint_uncertain`: the secret may already be stored. Enrolment never overwrites an
  existing secret, so a retry would be refused with `hint_unresolved` and look like
  a second failure. Do not retry. Ask the account holder to check the login and
  finish two-factor setup there if the secret is missing. See
  [Page hints](/guides/page-hints) and [TOTP](/security/totp).
</Accordion>

## Catalog and registry calls

These codes come from the HTTP response rather than the proxy.

| Code | Source | Meaning | Retry |
| - | - | - | - |
| `rate_limited` | `listTokens()` | The catalog's per-key or per-account request rate. Also under [Signup and login writes](#signup-and-login-writes) and [Sign-in responses](#sign-in-responses) | Later: wait `Retry-After` seconds |
| `rate_limited` | `submitRecipe()` | The account's recipe submission budget: 20 in any 24 hours, by default | Later: a place frees 24 hours after each counted submission |
| `invalid_recipe` | `submitRecipe()` | Not JSON, oversized, or failed validation. `validationErrors` is set | After fix |
| `not_found` | `recipeSubmission(id)` | No submission with that id, or one from another account. `setLoginUsername` also answers `not_found`: see [Signup and login writes](#signup-and-login-writes) | No |

<Accordion title="rate_limited">
  **Overloaded.** Four different sources share this code; tell them apart by which
  call threw, never by a missing `retryAfterSeconds`.

  * **The proxy** (`chromium.launch()` or a fill): too many new sessions, or too
    many token resolutions, for this key or its account in a fixed one-minute
    window. Nothing was opened, filled or charged. `retryAfterSeconds` is set: wait
    that long. A refused fill leaves the session open, so retry it on the same
    page. See [Limits](/reference/limits#rate-limits).
  * **The catalog** (`listTokens()`): a per-key and per-account, fixed one-minute window. The
    response carries `Retry-After` in whole seconds, surfaced as
    `retryAfterSeconds`. Wait that long. The window is fixed, so retrying early is
    refused again but does not push it out. A `Retry-After` stripped or
    unparseable on the way leaves `retryAfterSeconds` undefined too; do not read
    that as the recipe budget.
  * **Recipe submissions** (`submitRecipe()`): a rolling 24-hour budget with no
    `Retry-After`. Each submission counts for 24 hours after it was made, so a
    place frees when the oldest one in the last 24 hours turns 24 hours old. Stop
    submitting until then rather than polling.
  * **Credential writes** (`createLogin`, `setLoginUsername`,
    `rotateLoginPassword`): 20 writes per key in a fixed one-minute window,
    shared by all three. It carries `Retry-After`,
    surfaced as `retryAfterSeconds`; retry after it. Compare `daily_limit`, which clears only 24 hours after the first change counted in its window.

  `recipesForUrl()` and `buyableMerchants()` set no code at all on failure: a
  non-2xx reports the status in its message. Branch on which call threw.
</Accordion>

## Signup and login writes

These come from `createLogin`, `getSignupRequest`, `awaitSignupApproval`,
`setLoginUsername` and `rotateLoginPassword` (MCP: `create_login`,
`get_signup_request`, `wait_for_signup_approval`, `set_login_username`,
`rotate_login_password`). See [Agent signup](/guides/agent-signup).

| Code | Meaning | Retry | What to do |
| - | - | - | - |
| `signup_not_enabled` | The key may not create or change agent-made logins. Off by default, set per key | After account holder | Ask the account holder to turn on "Let this key create logins" |
| `too_many_pending` | The account already has 5 signup requests awaiting a decision | Later | No fixed wait. Tell the account holder requests are already waiting |
| `login_limit` | The account holds 500 logins, the ceiling for agent-made additions. Every login counts, including ones the account holder added | No | Clears only when the account holder deletes logins. The account holder's own **New login** form is not capped |
| `request_not_found` | The `requestId` names no live request for this key | No | Re-read the id from `createLogin`. Do not guess |
| `scope_denied` | The account holder removed the approved login from this key's scope | After account holder | Ask them to restore access |
| `username_already_set` | The login already carries a **different** handle | No | Tell the account holder which handle the site accepted |
| `not_found` | From `setLoginUsername`: no such login, or not one this key may change. That covers a login another key created, one created more than seven days ago (by default), one whose password the account holder replaced, and one outside the key's access. The answer is the same for each | No | Check the `label`. If it is right, only the account holder can set the username, from their dashboard |
| `not_rotatable` | Only the key that created the login can rotate it, only within seven days of creating it (by default), and only while the account holder has not replaced its password. Every other case, including an unknown label, gets this same answer | No | Check the `label`. If it is right, this key cannot rotate the password: tell the account holder why you wanted to rotate it (for example, the site rejected the password) so they can change it from their dashboard |
| `signup_requires_approval` | The retired synchronous signup endpoint. Answered 410 | No | Use `createLogin` |
| `invalid_website` | The website was not a usable domain | After fix | Pass the registrable domain, for example `doordash.com` |
| `invalid_email` | Failed a plausibility check: 4 to 254 characters, exactly one `@` with text on both sides, no whitespace or control characters. No mailbox is contacted | After fix | Correct the address |
| `invalid_username` | The handle is under 4 characters or contains control characters | After fix | Correct the handle |
| `invalid_policy` | The password rules cannot be satisfied together: a `minLength` above 64 (the longest password the vault generates) or above `maxLength`, a length that is not a whole number, or a `symbols` that shares no character with the vault's set `!@#$*+-_=.?` | After fix | Pass only rules the page actually states. A site that needs more than 64 characters cannot be served: ask the account holder to add the login |
| `password_too_short` | The site's `maxLength` is below the shortest password the vault will generate | No | Sign in with an existing credential, or ask the account holder to add the login |
| `daily_limit` | The account's budget for agent-made credential changes, 25 in a 24-hour window by default, is spent | Later | The window starts at the first counted change and resets 24 hours after it, not at midnight. There is no `Retry-After`. Nothing clears it sooner |
| `rate_limited` | The key's 20 credential writes this minute are spent, shared by `createLogin`, `setLoginUsername` and `rotateLoginPassword` | Later | Wait `retryAfterSeconds`, then call again. Nothing was written |
| `signup_denied` | The account holder declined. Raised by the SDK | No | Final for this request |
| `signup_expired` | The request's own lifetime ended before a decision. Raised by the SDK | No | The request is gone |
| `write_outcome_unknown` | The write may or may not have taken effect | Verify first | Never retry blind. See the details |

<AccordionGroup>
  <Accordion title="too_many_pending, login_limit and daily_limit">
    `too_many_pending` is per **account**, not per key, because it protects the account
    holder's attention. It has deliberately no `Retry-After`, which would disclose
    when the oldest request was filed. It clears as requests are decided, expire or
    are cleared by the account holder. Do not poll tightly.

    `login_limit` is a standing ceiling, not a rate. It is checked when
    `createLogin` opens the request **and** again at approval, because the count can
    move during the pending window. At the first check, `createLogin` fails with
    `login_limit` (HTTP 403) and no request is created, even for a pre-approved site.
    A request that passed the first check can still stall as `pending`, with the
    reason shown to the account holder.

    `daily_limit` reaches the agent directly from `setLoginUsername` and
    `rotateLoginPassword`. The same check runs when the account holder approves a
    signup request; the change counts against the budget only then. A request that
    fails it stays `pending`. The agent never sees `daily_limit` for it, so a
    request stuck at `pending` longer than expected may be waiting on this budget.

    The budget is 25 changes by default, in a 24-hour window that starts at the
    first counted change and resets 24 hours after it. It does not reset at
    midnight. A refused change is not counted and does not move the window.
  </Accordion>

  <Accordion title="not_found and not_rotatable: who may change a login, and for how long">
    `setLoginUsername` and `rotateLoginPassword` change only a login that the
    calling API key created through a signup request. Another key on the same
    account is refused, even one with the signup permission. Both also stop working
    seven days after the login was created, which is when the signup was approved.
    Seven days is the default; a deployment can set a different window.

    The two calls refuse with different codes. `setLoginUsername` answers
    `not_found` (HTTP 404) and `rotateLoginPassword` answers `not_rotatable` (HTTP
    409\). Each gives that one answer for every reason, so the code does not tell you
    which rule failed or whether the label exists. One exception: the creating key
    re-sending the exact handle a login already carries still succeeds after the
    seven days, because it writes nothing.
  </Accordion>

  <Accordion title="request_not_found">
    One code for five misses: an unknown id, a malformed id, an id that belongs to a
    different kind of record, a request that belongs to another key, or (rarely) a
    since-deleted login behind an approved request. They are collapsed so a key
    cannot use the poll to probe other keys' requests.

    `awaitSignupApproval` does not absorb this one: it stops the wait immediately
    with this code rather than polling a request that does not exist.
  </Accordion>

  <Accordion title="username_already_set, signup_denied and signup_expired">
    `username_already_set`: `setLoginUsername` sets a username, it does not rename
    one. The existing handle may come from an earlier call, a `username` passed to
    `createLogin`, or the account holder. Re-sending the handle the login already carries is
    **not** this error: an exact replay is idempotent and returns success. The
    refusal does not say which handle is on file, so a second guess only makes the
    record worse.

    `signup_denied`: opening a new request for the same site costs another
    outstanding slot and another notification. Do it only if something about the
    request genuinely changed.

    `signup_expired`: distinct from `approval_timeout`, which means the SDK's wait
    ended on a request that is still alive. A new `createLogin` opens a new request
    and notifies the account holder again, so do not loop on it.
  </Accordion>

  <Accordion title="write_outcome_unknown">
    Raised by the SDK on `createLogin`, `setLoginUsername` and
    `rotateLoginPassword` when no usable answer arrived: the connection dropped, the
    30 second deadline expired, the server answered 5xx (a gateway error can land
    **after** the change committed and the account holder was notified), or a success arrived
    without the promised fields. Never raised by the read-only polls.

    `error.writeSettled` separates two shapes. `true`: a status line arrived, so the
    write is done or refused for good and only its description was lost. `false`:
    the request may still be in flight.

    What to do depends on the call:

    | Call | Do this |
    | - | - |
    | `createLogin` | Do **not** retry: a second request is a second approval prompt and another outstanding slot, and can lock you out with `too_many_pending`. Tell the account holder: their dashboard lists every pending request, and an approval you could not poll still appears as a login in `listTokens()` |
    | `setLoginUsername` | Re-send the **same** handle. An exact replay is idempotent, so success means the handle is stored. Never a different handle |
    | `rotateLoginPassword` | Stop putting that login's password on pages and hand it to the account holder. Rotating again would replace a password the site may already have |
  </Accordion>

  <Accordion title="Two reasons that never reach the agent">
    `scope_full` (the key's allowed-logins list is at its cap) and
    `label_unavailable` (the account has run out of numbered labels for a site's
    name) are real failure reasons, but they are checked only at signup **approval**
    and are the account holder's business. To the agent, the request simply stays `pending`.

    Do not write a branch expecting either. The visible symptom is a request that
    stays `pending` until it ends as `signup_expired`. Only the account holder can fix it, by widening the
    key's scope or deleting a login that holds one of the site name's 50 labels, then
    approving again. See [Limits](/reference/limits#labels-per-site-name).
  </Accordion>
</AccordionGroup>

## Session and connection

| Code | Meaning | Retry | What to do |
| - | - | - | - |
| `request_timeout` | Raised by the SDK: the call went unanswered past its deadline, so the transport is closed | No | Not this call, not this session. Fresh `chromium.launch()`. It does not say whether the operation ran |
| `connection_closed` | Raised by the SDK: the socket closed while this call was in flight, or a call was made on a closed transport | No | Same recovery and same unknown-outcome rule |
| `idle_timeout` | 15 minutes with no message and no call in flight (close code 4008). A call the proxy is still working on keeps the session alive | Verify first | Usually fine with a new session. Verify if a mutating call was in flight just before |
| `hard_timeout` | 60 minutes total (close code 4009). Fires on its own schedule | Verify first | Opening a new session is fine. Repeating the last action is not, until you know it did not land |
| `action_timeout` | Raised by the SDK: the call ran past your `actionTimeoutMs`. **Not terminal**: the session stays open, but the call may still be running in the proxy | Depends | Do not relaunch. Recover as for `unanswered`: a read can be retried, an action may have landed. See [The per-action bound](/sdk/sessions-and-timeouts#the-per-action-bound) |
| `connection_resumed` | Raised by the SDK: the connection dropped and the same session was resumed. **Not terminal**: browser, pages, cookies and fill state are intact. Only the call in flight failed | Verify first | Do not relaunch. Retry a read. For an action, check the page on the same browser before repeating it. See [Resume after a dropped connection](/sdk/sessions-and-timeouts#resume-after-a-dropped-connection) |
| `egress_unavailable` | `chromium.launch({ residentialEgress: true })` was refused (close code 4030): this deployment has no residential egress, or the provider is down or has no exit in the session's country (see [Residential egress country](/sdk/tunnel#residential-egress-country)). Nothing was started, and the session slot is freed | Later | Wait `retryAfterSeconds`, then launch again. The session never falls back to another address |
| `egress_lost` | The session's residential egress stopped working mid-session. Every call except `outcome()` and the close calls is refused | No | Not in this session. Close it and launch a new one, which may first answer `egress_unavailable` |
| `session_limit` | `chromium.launch()` was refused (close code 4029): the account already has as many live proxy sessions as its plan allows (2, 5 or 10). Refused, not queued. Nothing was opened or spent | After fix | Close a session you no longer need, or wait for one to finish, then launch again with backoff. `browser.close()` returns before the proxy has released the slot, so an immediate relaunch can still get `session_limit`. A slot whose release failed, or a crashed proxy's slot, frees itself within 3 minutes. See [Limits](/reference/limits#concurrent-sessions) |

<Accordion title="After a hard_timeout">
  `browser.outcome()` cannot help here, for two reasons: it runs over the socket
  that just closed, and even in an open session a `false` is inconclusive. The check has to
  be the merchant's own order state.

  Rebuilding browser state that changes nothing (navigations, waits, reads) is
  safe. "Replay the run" is not: an order-submit click that completed earlier is
  still completed, and repeating it because a later read timed out submits it
  twice.

  See [Sessions and timeouts](/sdk/sessions-and-timeouts).
</Accordion>

## Hosted MCP server

These apply only to the [hosted MCP server](/mcp/setup#connect-to-the-hosted-server).
How hosted sessions, handles and retries work is on
[On the hosted server](/mcp/tool-reference#on-the-hosted-server).

### Discovery responses

Your MCP client reads two discovery documents to find the Agent Vault sign-in page.
Both answer `GET` and `HEAD` without sign-in.

| Request | Answer |
| - | - |
| `/.well-known/oauth-protected-resource`, or the same with the server's path appended, such as `/.well-known/oauth-protected-resource/mcp` | Always `200` with `resource` (the MCP server URL), `authorization_servers` (the sign-in domain), `bearer_methods_supported: ["header"]` and `resource_name: "Agent Vault"`. Clients may cache it for 5 minutes |
| `/.well-known/oauth-authorization-server` | `200` with a copy of the sign-in service's own authorization server metadata, for clients that look for it on the server's origin. The copy is refreshed at most every 5 minutes |

When the server holds no fresh copy and cannot get a usable one from the sign-in service
(the fetch fails, the sign-in service answers with an error or something other than JSON, or
the document names a different issuer), the second request answers:

| Status | Body | Meaning | What to do |
| - | - | - | - |
| 502 | `{"error":"authorization_server_unavailable"}` | The server could not fetch or check the sign-in service's metadata. Not a problem with your token, your account or your client. There is no `Retry-After` header | Retry discovery after a few seconds. A failure is never cached, so every retry asks the sign-in service again. Sign-in cannot start from this document until it clears. If it persists for minutes, the sign-in service may be down: try again later |

This is separate from token checks. A client that already holds a valid access
token is not affected, and a client that follows `authorization_servers` from
the first document reads the sign-in service's metadata directly instead.

### Sign-in responses

The hosted server checks the access token on every request, before any
tool runs. When the check fails, the server answers at the HTTP level, not with
a tool error. Your MCP client handles these responses itself. The agent never
sees a tool result for the call.

| Status | Body | Meaning | What to do |
| - | - | - | - |
| 401 | `{"error":"unauthorized"}` | The request carried no `Authorization: Bearer` token. The `WWW-Authenticate` header is a bare `Bearer` challenge with a `resource_metadata` URL and no `error` | The client starts sign-in. It reads `resource_metadata` to find the Agent Vault sign-in page |
| 401 | `{"error":"invalid_token"}` | A token was sent and refused: it expired, is malformed, has a bad signature, was issued by another sign-in service, was issued for another server (wrong audience), or lacks the `sub`, `sid` or `exp` claim. The `WWW-Authenticate` header carries `error="invalid_token"`. Also a [tool error](#tool-errors), with a different meaning | Sign in again. Do not retry the tool: the same token is refused the same way every time |
| 429 | `{"error":"rate_limited","error_description":"Too many requests without a valid token"}` | Where the deployment turns this limit on: more than 30 requests in a minute from one address without a valid token. Requests with a valid token are never limited here | Wait the `Retry-After` seconds. Fix the missing or refused token first: every request without a valid token counts against the limit |
| 503 | `{"error":"temporarily_unavailable"}` | The server could not load the sign-in service's signing keys, so it could not check the token. Not a problem with the token | Retry after the `Retry-After` interval, 5 seconds. Do not sign in again |

The full `invalid_token` challenge looks like this, with your deployment's
address in place of the placeholder:

```text theme={null}
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token", error_description="The access token is invalid or expired", resource_metadata="https://mcp.agentvault.io/.well-known/oauth-protected-resource"
```

<Accordion title="What the user sees after invalid_token">
  The client needs a new access token. Retrying the tool call cannot fix this.

  1. If the client holds a refresh token, it may get a new access token without
     asking the user, and the call goes through.
  2. Otherwise the client marks the Agent Vault server as needing sign-in, and its
     tools fail until the user signs in again. Some clients open the Agent Vault
     sign-in page in the user's browser by themselves. Others wait for the user to
     start it, the same way
     as the first sign-in: in Claude Code, run `/mcp` and choose **Agent Vault**;
     in Codex, run `codex mcp login agent-vault`. See
     [Connect to the hosted server](/mcp/setup#connect-to-the-hosted-server).

  A full sign-in can create a new connection, which starts with nothing granted.
  If the next call answers `access_not_chosen`, choose access for the new row on
  **Connected apps**.
</Accordion>

There is no `insufficient_scope` response. The server does not read the token's
scopes. What a connection may use is decided on **Connected apps**, and a
connection without access gets the `access_not_chosen` tool error below.

### Protocol responses

The server answers these itself, at the HTTP level. A `404` or `405` comes
before the token is checked. The JSON-RPC errors come after it, so a request
without a valid token gets a [sign-in response](#sign-in-responses) first. A
JSON-RPC error body looks like
`{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"Request too large"}}`.

| Status | Body | When |
| - | - | - |
| 404 | `Not found`, as plain text | A path other than the MCP server URL and the discovery documents |
| 405 | `Method not allowed`, as plain text, with `Allow: POST` | Any method but `POST` on the MCP server URL. `GET`: there is no standalone event stream to open. `DELETE`: the server does not let a client end a session this way, and nothing is closed; end one with `close_session`. MCP clients carry on after either |
| 400 | `-32700` "Parse error" | The body is not JSON |
| 400 | `-32600` "Batched requests are not supported" | The body is a JSON array. Send one message per request |
| 400 | `-32600` "Invalid request" | The body is JSON but not an object |
| 400 | `-32600` "tools/call needs an id" | A `tools/call` sent without an `id`, as a notification. Nothing runs and no session starts. Send it with an id |
| 413 | `-32600` "Request too large" | The body is over 1 MiB. Nothing runs |
| 408 | `-32600` "Request body too slow" | The whole body did not arrive within 30 seconds. Nothing runs |
| 500 | `-32603` "Internal error" | The server failed while handling the request. For a `tools/call` the error carries the request's own `id`. The call may have run part way: verify before you repeat one that changes something |

Arguments that fail a tool's schema are not a protocol response: they come back
as a tool error whose text begins `MCP error -32602`, and the tool does not run.

### Tool errors

These errors can occur whenever a call needs a new credential: any call that
starts a session, and `submit_checkout_recipe` or `get_recipe_submission` when
called without a session. A call on a session that is already open uses that
session's credential and is not refused this way. Nothing ran and no session
was started.

The agent gets a tool error carrying the message only, not the code. Match
loosely on the phrases quoted below, and treat anything else as the conservative
case.

| Code | Meaning | Retry | What to do |
| - | - | - | - |
| `access_not_chosen` | The account holder has not yet chosen what this connection may use. A new connection starts with nothing granted. The message reads "Choose what this app may use in Agent Vault before it can act", sometimes followed by a link | After account holder | Ask the account holder to click **Choose access** on the connection's row on **Connected apps**, then click **Save access**. Then call again |
| `connection_revoked` | The account holder revoked this connection, or it lapsed after going unused. The message reads "This connection was revoked in Agent Vault. Connect the app again to continue." | No | Sign in again. That creates a new connection, which needs access chosen again |
| `too_many_connections` | The account already holds 200 hosted MCP connections, counting revoked and lapsed ones. Only a new sign-in is refused. Existing connections keep working. The message reads "This Agent Vault account has reached its limit of connected apps. Contact support to clear old connections." | No | Revoking does not free a place. The account holder contacts [team@self.xyz](mailto:team@self.xyz) to clear old connections |
| `too_many_credentials` | This connection already holds 50 live credentials. Every open session holds one until it closes. That includes the browserless session that `get_token_catalog` and the login tools start. The message ends "Retry in about N seconds", the time until the earliest one expires | Later | Call `close_session` on sessions you have finished with. If that does not clear it, wait the seconds the message gives (at most 75 minutes), then call again. Why a credential can outlive its session: [Limits](/reference/limits#ceilings) |
| `not_configured` | The deployment's hosted MCP server is not fully set up on the Agent Vault side, so Agent Vault cannot issue any credential. Every call that needs a new credential gets this error, on every connection. The message reads "The hosted MCP gateway is not configured on this deployment." | No | Nothing you or the account holder can change fixes it, and signing in again does not help. Only whoever runs the deployment can. Report the message to them and stop |
| `unauthorized` | Agent Vault did not accept the hosted MCP server itself as a trusted caller, so it issued no credential. This is not about your sign-in, and not the HTTP 401 `unauthorized` [sign-in response](#sign-in-responses). Every call that needs a new credential gets this error, on every connection. The message reads "Unauthorized" | No | Nothing you or the account holder can change fixes it, and signing in again does not help. Only whoever runs the deployment can. Report the message to them and stop |
| `unknown_user` | The account you signed in with has no Agent Vault account. The message says to sign up at Agent Vault first. | No | Sign in to the Agent Vault dashboard once with the same account, which creates your Agent Vault account. Then call again |
| `account_deleting` | The Agent Vault account is being deleted. The message reads "This Agent Vault account is being deleted." | No | Nothing the agent can do. See [Data and deletion](/dashboard/data-and-deletion) |
| `invalid_token` | Rare: the access token passed the server's check but expired before Agent Vault issued the session's credential. The message says the access token is invalid or expired. | Yes | Call again. If the token has expired, that request gets the HTTP 401 `invalid_token` above and the client signs in again |
| `unavailable` | Hosted only: Agent Vault did not answer the credential request within 10 seconds, could not be reached, or answered without a usable credential. Not the proxy's [`unavailable`](#backend-and-protocol). The message reads "Agent Vault could not be reached. Retry in a moment.", "Agent Vault answered 502." (with the status it got), or says the signing keys could not be loaded | Yes | Retry the same tool after a few seconds, with backoff. Nothing ran. Do not sign in again or ask the account holder to change anything. A tight retry loop can end in `too_many_credentials`: see [Limits](/reference/limits#ceilings) |

### Gateway messages

The hosted server answers these itself, as tool errors. None carries a code.
The first three refuse the call before any tool runs, and are not kept for
[retries](/mcp/tool-reference#repeated-and-retried-calls), so sending the call
again runs it.

| Message | When | What to do |
| - | - | - |
| "click needs a browser session: call open\_page first, then pass the `session` it returns on this call." (with the tool's name) | A tool that needs a browser was called without a `session` | Call `open_page`, passing the handle you already hold from `get_token_catalog` or a login tool. Then pass the `session` that `open_page` returns on this call |
| "This session has ended: ..." | The call named a session that has closed. Two forms, one with the reason and the run report | See [When a session ends](/mcp/tool-reference#when-a-session-ends) |
| "The gateway is restarting. Retry in a moment." | A call that would start a session arrived while the server was shutting down. No session was started | Retry in a few seconds. Every session from before the restart has ended, with no report kept: check **Activity** before you repeat anything |
| "click did not finish within 4 minutes. Its outcome is unknown: check before repeating it." (with the tool's name, and 12 minutes for `request_challenge_handoff`) | The call ran past its deadline. It may still finish on the server | Verify first: check the page or `get_run_report` before you repeat it |
| "close\_session hit an internal error, but the session is closed." | The close failed part way. Any report it read follows the message | Do not call `close_session` again. Read what follows, and check **Activity** if nothing does |

## Related

<CardGroup cols={2}>
  <Card title="Sessions and timeouts" icon="clock" href="/sdk/sessions-and-timeouts">
    Which errors end a session, and the deadlines.
  </Card>

  <Card title="Troubleshooting" icon="life-ring" href="/reference/troubleshooting">
    Symptoms and fixes.
  </Card>
</CardGroup>


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