Skip to content

Errors

Match on the machine code, never on the status alone and never on a message string. Codes are stable; statuses are shared by unrelated conditions and messages are not part of the contract.

Almost every error is one field:

{ "error": "cloud_run_in_progress" }

Two codes add a second field:

{ "error": "insufficient_scope", "requiredScope": "wamp.cloud.turns:submit" }
{ "error": "cloud_rate_limit_exceeded", "retryAfterSeconds": 3 }

Treat the body as extensible: new fields may appear, so read the ones you need rather than validating the object shape.

Read the delay from the header, not the body

Section titled “Read the delay from the header, not the body”

Three domain codes — cloud_workspace_starting, cloud_session_finalizing and cloud_run_in_progress — carry a delay as a Retry-After header only. It is always 2. There is no retryAfterSeconds in their bodies.

429 cloud_rate_limit_exceeded carries the delay both as a Retry-After header and as a retryAfterSeconds body field.

So read the header if you want one code path for all of them. A client that only looks at the body will silently fall back to its own default on the three codes where the server actually told it what to do.

Case Body
Request body over 1 MB 413 with { "success": false, "error": "request entity too large", "limit": 1048576, "received": 1234567 }
Repository review failures { "success": false, "error": "<code>" } — see below

Both come from layers outside the /v1 handlers. If you parse errors generically, read error and tolerate the extra success field; the 413 error is prose rather than a machine code, so detect that one by status.

Status Code Cause Retry?
400 invalid_request A body, path id, or query parameter failed validation — including an unknown property or an unknown query parameter No. Fix the request
400 invalid_input A malformed grantId path parameter No
400 invalid_organization A malformed or repeated X-Wamp-Organization header No
400 organization_header_not_allowed X-Wamp-Organization sent together with a bearer No. Remove the header
401 bearer_credential_required No Authorization: Bearer header on a /v1 path No
401 invalid_or_expired_credential Token expired, revoked, wrong audience, or introspection rejected it Mint a new token, retry once
403 cloud_access_denied The credential lacks wamp.cloud.access No. The installation is not consented for Cloud
403 insufficient_scope The credential lacks the operation’s capability, named in requiredScope No. Re-consent is required
403 installation_principal_required A repository-grant endpoint was called with a human credential No
403 cloud_repository_grant_required The grant behind the request is missing, revoked, or refused No. Ask an administrator
406 representation_not_acceptable Accept did not match on GET /v1/openapi.json or GET /v1/docs No. Fix Accept
413 (prose) JSON body over 1 MB No. Send less
429 cloud_rate_limit_exceeded Your authority’s token bucket is empty Yes, after Retry-After
429 cloud_auth_rate_limit_exceeded The per-IP pre-authentication bucket is empty Yes, after Retry-After
500 internal_error Unexpected server fault Cautiously, with backoff
503 identity_service_unavailable Credential introspection is down. Retry-After: 2 Yes
503 cloud_rate_limit_unavailable The rate limiter faulted and fails closed. Retry-After: 1 Yes
503 account_authorization_unavailable GET /v1/capabilities could not reach the account service Yes, with backoff

401 versus 403 is the distinction to encode: 401 means this credential is no good, so re-mint and retry once. 403 means the installation is not permitted, and no amount of retrying will change it — surface it to whoever can grant the capability.

Status Code Cause
404 cloud_session_not_found No such session, or it belongs to another tenant
404 cloud_turn_not_found No such turn in this session
404 cloud_run_not_found No such run in this session
404 cloud_artifact_not_found No such artifact, or it is not publicly visible
404 cloud_publication_not_found No such publication
404 cloud_publication_merge_not_found No such merge under that publication

404 is also what you get for a resource that exists but belongs to a different organization — the API does not distinguish “absent” from “not yours”, because doing so would leak the existence of other tenants’ data. Two practical consequences: a 404 right after a successful PUT usually means an id case or typo mismatch, and a 404 on a session you are sure exists usually means the token belongs to a different installation.

409 covers two very different situations, and the code tells you which.

These mean the same caller-owned id was reused for different content. They are never transient and retrying is pointless.

Code Operation Trigger
cloud_session_conflict PUT /v1/sessions/{sessionId} Same id, different task, model, settled runtime, origin, or source
cloud_turn_conflict PUT …/turns/{turnId} Same id, different message, replyTo, or explicitly sent model / runtime
cloud_publication_conflict PUT …/publications/{publicationId} Same id, a different stored request hash
cloud_publication_merge_conflict PUT …/merges/{mergeId} Same id, a different stored request hash
cloud_interaction_conflict PUT …/turns/{turnId} with replyTo The interaction is already resolved, expired, or not the open one

The rule that keeps you out of this: an id identifies one exact command. To send different content, mint a new id. Replaying an identical body is always safe and returns 200 (or 202 with created: false for turns).

Code Meaning Retry-After
cloud_run_in_progress A run is already active in this session. Only one may be non-terminal at a time 2
cloud_session_finalizing The previous run is being closed out and its transcript captured 2
cloud_publication_in_progress A publication for this session is not terminal yet
cloud_conversation_busy Another command holds the session’s conversation
cloud_agent_locked The session’s agent or runtime is locked against this change
cloud_publication_merge_not_ready A when_ready merge is still waiting on checks or protection

Only the first two carry a delay. For the rest, back off on your own schedule — a few seconds is right for cloud_publication_in_progress and cloud_conversation_busy; a when_ready merge is a background wait measured in minutes, so poll it slowly.

Code Meaning What to do
cloud_turn_unresolved A turn was submitted while an interaction is open Answer the interaction first, with replyTo
cloud_workspace_expired The sandbox lease has ended Read continuation on the session to see whether work can resume
cloud_workspace_expiring The lease is too close to its deadline to start this work Retry shortly; a new lease will be taken
cloud_resume_unavailable The session cannot be continued from its last checkpoint Start a new session
cloud_turn_execution_lost The execution backing this turn disappeared Re-submit as a new turn id
cloud_session_limit_reached (429) The organization is at its active-session cap Archive finished sessions, then retry

cloud_session_limit_reached is 429 rather than 409 because it is a quota, not a state conflict — but no amount of waiting clears it. Archive something.

Status Code Meaning
410 cloud_artifact_pruned The artifact’s bytes were reclaimed. The manifest is still readable

410 appears on exactly one endpoint, GET /v1/sessions/{sessionId}/artifacts/{artifactId}/content, and it means something specific: the artifact existed, you are allowed to read it, and the content is permanently unavailable. It is not 404, because the record is still there — GET …/artifacts/{artifactId} continues to return the manifest with state: "pruned", including its size and sha256.

Never retry a 410. If you need artifact bytes durably, copy them into your own storage while state is available; the sha256 in the manifest lets you verify what you stored.

Status Code Meaning Retry?
502 cloud_turn_start_failed The turn was admitted but starting execution failed Yes — re-PUT the same turn id
502 cloud_run_control_failed A control operation on the run (such as cancel) failed downstream Yes, with backoff
503 cloud_workspace_starting The sandbox is being provisioned. Retry-After: 2 Yes
503 cloud_workspace_unavailable No sandbox could be provisioned Yes, with backoff
503 cloud_runtime_outdated The requested runtime is too old for this session No. Create a new session
503 cloud_runtime_account_unavailable No pooled account is free for the requested runtime Yes, later. Check availability.retryAt from discovery

The 502 codes are the ambiguous ones: the command may or may not have taken effect. This is exactly what caller-owned ids are for — re-send the identical PUT and the response tells you which world you are in.

GET /v1/sessions/{sessionId}/repository/review can fail for repository-level reasons, and those responses use { "success": false, "error": "<code>" }:

Status Code Meaning
409 no_changes The agent changed nothing. There is nothing to publish
409 not_repository The session has no repository checkout
409 repository_conflict The checkout is not in a state that can be reviewed
409 review_changed The tree moved while the review was being computed
409 branch_conflict The working branch conflicts with what is on the remote
413 review_too_large The diff exceeds the review budget
404 not_found The workspace or repository is gone
404 repository_not_found The repository is not reachable with the current grant
403 repository_read_only The grant does not permit the requested operation
401 not_connected / authorization_expired The GitHub connection behind the grant needs an administrator’s attention
503 not_configured GitHub integration is not configured on this deployment
502 (other) An upstream GitHub failure

no_changes is the common one and it is not really an error — it is the honest answer when an agent decided nothing needed changing. Handle it as a normal outcome in your product.

Not every failure is an HTTP error. Long-running commands report their own outcome in a 200 body:

Field Where
run.lastError.code GET …/runs/{runId}
publication.lastError.code GET …/publications/{publicationId}
merge.lastError.code GET …/merges/{mergeId}
data.errorCode wamp.publication.failed, wamp.publication.merge_failed events

These are diagnostic strings drawn from several internal error families and the set is open. Known values worth handling: dispatch_attempts_exhausted on a run; no_changes, branch_conflict, review_changed, publish_failed on a publication; merge_not_ready, merge_not_allowed, pull_request_changed, pull_request_closed, repository_read_only on a merge. Treat an unrecognized value as a permanent failure of that command and report it verbatim — see Open a pull request.

Note that lastError can be present on a non-failed resource: a waiting merge carries lastError.code: "merge_not_ready" while it is still retrying. Read status first, lastError second.

const RETRY_AFTER_HEADER = new Set([
'cloud_workspace_starting', // 503
'cloud_session_finalizing', // 409
'cloud_run_in_progress', // 409
'cloud_rate_limit_exceeded', // 429
'cloud_auth_rate_limit_exceeded',
'identity_service_unavailable',
'cloud_rate_limit_unavailable',
]);
const BACKOFF = new Set([
'cloud_workspace_unavailable',
'cloud_runtime_account_unavailable',
'cloud_publication_in_progress',
'cloud_conversation_busy',
'cloud_run_control_failed',
'internal_error',
'account_authorization_unavailable',
]);
function plan(status, code, retryAfterHeader) {
if (status === 401) return { action: 'remint-once' };
if (RETRY_AFTER_HEADER.has(code)) {
return { action: 'retry', delayMs: (Number(retryAfterHeader) || 2) * 1000 };
}
if (BACKOFF.has(code)) return { action: 'retry-backoff' };
// 502 on an idempotent PUT: re-send the SAME id.
if (status === 502) return { action: 'resend-same-id' };
return { action: 'fail' };
}

Everything not in those sets is terminal. In particular: never retry a 400, a 403, a 404, a 410, or any …_conflict — each of them means the request itself was wrong, and a retry loop on them is how an integration turns a bug into a rate limit.