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.
The error body
Section titled “The error body”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.
Two responses that do not use this shape
Section titled “Two responses that do not use this shape”| 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.
Transport, authentication and validation
Section titled “Transport, authentication and validation”| 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.
Not found
Section titled “Not found”| 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.
Conflicts
Section titled “Conflicts”409 covers two very different situations, and the code tells you which.
Idempotency conflicts — your bug
Section titled “Idempotency conflicts — your bug”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).
Busy conflicts — wait and retry
Section titled “Busy conflicts — wait and retry”| 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.
State conflicts — act, then retry
Section titled “State conflicts — act, then retry”| 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.
Upstream and infrastructure
Section titled “Upstream and infrastructure”| 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.
Repository review errors
Section titled “Repository review errors”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.
Failure codes inside successful responses
Section titled “Failure codes inside successful responses”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.
A retry policy that matches the server
Section titled “A retry policy that matches the server”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.
Related
Section titled “Related”- API conventions — idempotent
PUTand what “the same body” means. - Limits — the caps behind
429and413. - Follow a run live — handling
409while a run is in flight. - the API reference — the error object in the machine contract.