Overview
Every failure from the proxy or the API is aSelfAgentPayError 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. recipeSubmission(): a network failure, or an abort through yoursignal, rejects with the nativefetcherror.recipesForUrl()andsubmitRecipe(): an abort through yoursignalwhile the response body is being read rejects with the native abort error.listTokens()(andtokensForUrl(), which calls it),recipesForUrl()andbuyableMerchants(): a successful response whose body is not valid JSON throws a nativeSyntaxError. A body cut off mid-read, or for the two registry helpers JSON of the wrong shape such asnull, can throw a nativeTypeError.
createLogin(), getSignupRequest(),
awaitSignupApproval(), setLoginUsername(), rotateLoginPassword()) always
throw SelfAgentPayError. Through the MCP server, a refused call returns a tool
error carrying the same message.
Branch on code first
Read the message for overloaded codes
card_blocked, error), check the merchant’s own order state instead.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.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.
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
Spending, limits and approval
limit_reached
limit_reached
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.
autoPauseOnLimitis on for every agent card created through the dashboard. Any breach of the card’s own limits, per transaction included, flips it tolimit, 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.
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.checkout_total_unknown
checkout_total_unknown
plan_quota_reached
plan_quota_reached
- 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.
approval_timeout
approval_timeout
-
Card fill. The purchase was at or above the agent card’s
confirmAbovethreshold, 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.
awaitSignupApprovalorgetSignupRequestagain with the samerequestId. An approval you could not read is already a login, so the next poll finds it andlistTokens()lists it. Never callcreateLoginagain: that opens a second, unrelated request and spends another of the account’s five outstanding slots.
request_not_found, or any other 4xx)
does not arrive here. awaitSignupApproval throws it at once with its own code.approval_denied
approval_denied
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.Card and merchant refusals
account_paused
account_paused
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.card_paused
card_paused
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.merchant_blocked
merchant_blocked
- 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.
card_blocked
card_blocked
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 and the agent card’s spend. Retrying is futile until the account holder or operator corrects the card.
login_blocked
login_blocked
- 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.
paired_cvv_refused
paired_cvv_refused
- 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.
numberLanded on the error says which variant you have:card_live_fill_unsupported
card_live_fill_unsupported
insecure_page
insecure_page
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.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
selectOptioncan 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.
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.Security code (CVV)
cvv_entry_required
cvv_entry_required
plan_quota_reached. Check
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.Challenges and handoff
challenge_required
challenge_required
- The page is showing a 3-D Secure, one-time-code or CAPTCHA step. The account holder has to act first; see Handling 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.
handoff_not_applicable and challenge_scan_incomplete
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.Authentication and scope
scope_denied
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.token_not_found
token_not_found
- 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
missingarray 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 inmissing.
Page and fill
insecure_site
insecure_site
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).”
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.clipboard_blocked
clipboard_blocked
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.fill_rejected
fill_rejected
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.invalid_choice
invalid_choice
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.unknown_target
unknown_target
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:unanswered
unanswered
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 backunanswered, 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.
Backend and protocol
malformed_response
malformed_response
- 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.
card_resolution_uncertain instead.card_resolution_uncertain
card_resolution_uncertain
error
error
- 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
errorafter 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 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
errorafter 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.Attested checkout
These codes come frombrowser.attestedCheckout(). The same table, with the
feature’s status, is on
Attested detokenization.
attested_fill_failed, attested_fill_unreversed, attested_fill_incomplete
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.
invalid_plan
invalid_plan
- A malformed plan. Retrying the same plan fails the same way, so fix it.
One of:
- A
screenshotstep (refused for SDK callers). - A step type other than
goto,click,fill,fillSecret,waitFororwait. - A step missing a required field:
click,waitForandfillSecretneed a non-emptyselector;fillneedsselectorand a stringvalue;gotoneeds a non-emptyurl;waitneeds a finitems. - A destination that fails the host check.
- A plan that never fills the secret.
- A plan where
fillSecretis not followed by exactly oneclickand then onlywaitorwaitForsteps.
- A
- 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.
Page hints
hint_unresolved and hint_uncertain
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 and TOTP.Catalog and registry calls
These codes come from the HTTP response rather than the proxy.rate_limited
rate_limited
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.retryAfterSecondsis set: wait that long. A refused fill leaves the session open, so retry it on the same page. See Limits. - The catalog (
listTokens()): a per-key and per-account, fixed one-minute window. The response carriesRetry-Afterin whole seconds, surfaced asretryAfterSeconds. Wait that long. The window is fixed, so retrying early is refused again but does not push it out. ARetry-Afterstripped or unparseable on the way leavesretryAfterSecondsundefined too; do not read that as the recipe budget. - Recipe submissions (
submitRecipe()): a rolling 24-hour budget with noRetry-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 carriesRetry-After, surfaced asretryAfterSeconds; retry after it. Comparedaily_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.Signup and login writes
These come fromcreateLogin, getSignupRequest, awaitSignupApproval,
setLoginUsername and rotateLoginPassword (MCP: create_login,
get_signup_request, wait_for_signup_approval, set_login_username,
rotate_login_password). See Agent signup.
too_many_pending, login_limit and daily_limit
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.not_found and not_rotatable: who may change a login, and for how long
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.request_not_found
request_not_found
awaitSignupApproval does not absorb this one: it stops the wait immediately
with this code rather than polling a request that does not exist.username_already_set, signup_denied and signup_expired
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.write_outcome_unknown
write_outcome_unknown
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:Two reasons that never reach the agent
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.Session and connection
After a hard_timeout
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.Hosted MCP server
These apply only to the hosted MCP server. How hosted sessions, handles and retries work is on On the hosted server.Discovery responses
Your MCP client reads two discovery documents to find the Agent Vault sign-in page. Both answerGET and HEAD without sign-in.
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.invalid_token challenge looks like this, with your deployment’s
address in place of the placeholder:
What the user sees after invalid_token
What the user sees after invalid_token
- If the client holds a refresh token, it may get a new access token without asking the user, and the call goes through.
- 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
/mcpand choose Agent Vault; in Codex, runcodex mcp login agent-vault. See Connect to the hosted server.
access_not_chosen, choose access for the new row on
Connected apps.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. A404 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 first. A
JSON-RPC error body looks like
{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"Request too large"}}.
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, andsubmit_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.