Reference
Errors
Every error code the API returns, its HTTP status, what it means and what to do.
Every failed request returns a JSON body with one error object, and an HTTP status that depends only on the code.
{
"error": {
"code": "not_found",
"message": "Session sess_us_... not found",
"details": {},
"requestId": "..."
}
}- Match on
error.code, not onmessage. The message is for people and can change. detailsdepends on the code. The column below lists the fields you can rely on.requestIdidentifies the request in our logs. Quote it when you ask for support.- A client that sees a code it does not know should treat it like
internaland use the HTTP status. - In the MCP tools a failed call is a tool result marked as an error, with the text
[code] message. - The SDK throws
WebagentApiErrorwith the samecode,status,detailsandrequestId.
| Code | HTTP | Meaning | Details | What to do |
|---|---|---|---|---|
internal | 500 | Bug or unexpected failure on our side | ||
timeout | 504 | An operation ran past its time limit | ms | Retry. If it keeps happening, make the request smaller. |
invalid_input | 400 | Request body, query or params failed validation | zod issues | Read error.details (the failed fields), fix the request, send it again. |
not_found | 404 | No such resource (or not visible to this tenant) | Check the id and the project of your key. A resource you cannot see is also reported as not found. | |
session_closed | 410 | The session has ended | Start a new session. A closed session cannot be reopened. | |
session_paused | 409 | A human took over; agent actions wait for hand-back | A person took over in the live view. Wait for hand-back, or stop the session. | |
browser_crashed | 502 | The browser process died | ||
browser_launch_failed | 503 | Chromium did not start | launch (why, retryable) | error.details.launch says why and whether a retry can help. |
page_crashed | 502 | The tab's renderer crashed; navigate/reload recovers | Run navigate or reload on the tab. The browser is still alive. | |
protocol_error | 502 | CDP/Playwright returned an error | protocol message | |
target_detached | 409 | The tab or frame went away mid-action | ||
navigation_blocked | 403 | URL refused by policy (SSRF guard, site rules) | url, reason | The address is private, loopback or otherwise refused. Use a public URL or a tunnel: see Test a local app. |
element_not_found | 422 | No element matched the target | target | |
element_not_visible | 422 | Element exists but is not visible | target | |
element_disabled | 422 | Element is disabled | target | |
capacity_exceeded | 503 | No runner capacity, or a tenant limit reached | limit | Close sessions you no longer need, then retry. error.details.limit says which limit you hit. |
runner_unavailable | 503 | The session's runner cannot be reached; retry briefly, then treat the session as lost | reason | Retry in a few seconds. If it keeps failing, treat the session as lost and start a new one. |
unauthorized | 401 | Missing or bad API key / token | Check the API key (Authorization: Bearer wa_... or x-api-key). Make a new key if it was revoked. | |
forbidden | 403 | Authenticated but not allowed | reason | Your key or account is not allowed to do this. Read the message and error.details.reason; an admin may be needed. |
conflict | 409 | State conflicts with the request (id in use) | ||
consent_required | 403 | A third-party skill needs admin consent first | skill, asks | An admin has to accept what the skill asks for first (error.details.asks). |
unsupported_on_engine | 422 | This engine cannot do the action | engine, feature | Use another engine, or drop the option. error.details.engine and error.details.feature say what is missing. |
wrong_region | 421 | The API key belongs to another region; retry on that region's API | region | Send the request to the API of the region in error.details.region. |
rate_limited | 429 | Too many requests; also sends Retry-After | retryAfterS | Wait error.details.retryAfterS seconds (also the Retry-After header), then retry. |
idempotency_key_reused | 422 | Idempotency-Key already used with a different request | Use a new Idempotency-Key for a different request. | |
idempotency_in_progress | 409 | First request with this Idempotency-Key still running; retry later | The first request is still running. Wait, then retry with the same key. | |
auth_unavailable | 503 | Identity service unreachable (dashboard sign-in); retry | Retry in a few seconds. | |
reauth_required | 401 | Sensitive action needs a sign-in within the last 120 s; sign in again | Sign in again in the dashboard, then repeat the action. | |
payload_too_large | 413 | The request body is larger than the endpoint accepts | limitBytes | Send less data. error.details.limitBytes is the limit. |
method_not_allowed | 405 | The route exists but not for this method; also sends Allow | Use another HTTP method. The Allow response header lists the ones the route accepts. | |
org_suspended | 403 | This organisation is suspended. Contact support. | Contact support. |
Two reasons share an existing code instead of having their own:
- A replayed
Idempotency-Keywhose first answer was too large to keep returnsconflictwithdetails.reason: "idempotency_not_replayable". - A dashboard sign-in by a user with no organization returns
forbiddenwithdetails.reason: "needs_org".