Errors and retries reference
One error envelope on every endpoint: each code with its HTTP status and retryable flag, how the SDKs back off, and what 410 and 426 mean.
Last updated:
When a request cannot be served, every endpoint answers the same way — one envelope, machine-readable, with enough in it to decide what to do next. The examples below authenticate like everything else here: a bb_ API key presented as a bearer token.
The envelope
Every failure is error with code, message, retryable and optionally retryAfterMs, requestId and details. Branch on retryable, never on the code: the code enum is open, and a newer server may introduce codes your switch has not seen. requestId is what support will ask for; details carries structured specifics such as a workflow's full validation error list.
How the SDKs retry
Both SDKs honour retryAfterMs and back off with full jitter when it is absent. One deliberate exception: the TypeScript SDK does not retry a POST that failed at the connection level, because a create that timed out on the way back may have succeeded server-side — retrying it blindly would make two sessions where you asked for one.
Every code
The current vocabulary — and it stays open.
| Name | Type | Description |
|---|---|---|
unauthorized
|
HTTP 401 | No key, or a key that does not exist. Not retryable. |
forbidden
|
HTTP 403 | The key is valid but not allowed to do this. Not retryable. |
not_found
|
HTTP 404 | The resource does not exist for this organisation. Not retryable. |
invalid_request
|
HTTP 400 | The body failed validation — requests are strict, so a typo'd field lands here. Not retryable. |
disallowed_browser_arg
|
HTTP 400 | An `extraArgs` entry is outside the allowlist. Not retryable. |
conflict
|
HTTP 409 | The request contradicts current state. Not retryable. |
rate_limited
|
HTTP 429 | Too many requests. Retryable, usually with `retryAfterMs`. |
quota_exceeded
|
HTTP 402 | A plan quota is exhausted. Not retryable — the fix is the plan, not a retry. |
concurrency_limit
|
HTTP 429 | Too much running at once for the organisation. Retryable once something finishes. |
queue_timeout
|
HTTP 429 | The request waited too long in the queue. Retryable. |
capacity_unavailable
|
HTTP 503 | No capacity right now — or, self-hosted, a missing key provider. Retryable. |
session_expired
|
HTTP 410 | The session reached its deadline. Not retryable — create a new session. |
session_gone
|
HTTP 410 | The session was released or its container is gone. Not retryable. |
launch_failed
|
HTTP 502 | The browser container failed to start. Retryable. |
protocol_incompatible
|
HTTP 426 | Client and server cannot negotiate a protocol version. Not retryable. |
navigation_failed
|
422 | The task API could not open `startUrl` before starting the run. Not retryable as it stands: fix the URL, or the site’s refusal. |
internal
|
HTTP 500 | Something went wrong on our side. Retryable. |
426, and the two 410s
protocol_incompatible usually means your client speaks a newer minor than the server — negotiation demands majors match exactly and the server's minor be at least yours, so upgrade the server or pin an older client; retrying changes nothing. The two 410s differ in history: session_expired says the session hit a deadline it was always going to hit, while session_gone says it was released or its container no longer exists. Both end the same way — a new session — but the first is a planning signal and the second may deserve a look at shutdownReason.
See the envelope
A GET for a session that does not exist shows the envelope exactly as every other failure returns it.
Requires: api-key
# Two refusals, one envelope shape. not_found is terminal; retryable says so.
curl -s "https://browserberg.com/v1/sessions/sess_does_not_exist" \
-H "Authorization: Bearer $BROWSERBERG_API_KEY" \
| python3 -m json.tool
{
"error": {
"code": "not_found",
"message": "No such session.",
"retryable": false,
"requestId": "req_xxxxxxxx"
}
}