Error Codes
Errors are returned as JSON: { "error": "<human-readable message>" }, with a
machine-readable code field present on some errors. The per-endpoint status
codes are in the OpenAPI specification.
HTTP Status Codes
Section titled “HTTP Status Codes”| Code | Meaning |
|---|---|
| 200 | OK |
| 201 | Created |
| 202 | Accepted — the work runs asynchronously (e.g. project/account deletion) |
| 302 / 303 | Redirect — form-encoded requests to form-friendly endpoints redirect instead of returning JSON |
| 400 | Bad Request |
| 401 | Unauthorized |
| 403 | Forbidden |
| 404 | Not Found |
| 409 | Conflict — merge staleness and merge conflicts (see codes below) |
| 410 | Gone — the resource was deleted |
| 413 | Payload Too Large — a request body exceeded its cap (PAYLOAD_TOO_LARGE; the git and /mcp paths have their own caps) |
| 422 | Unprocessable — the request was understood but cannot be applied |
| 429 | Rate Limited |
| 500 | Server Error |
| 501 | Not Implemented — an optional integration is not configured on this instance (e.g. GitHub/Google OAuth) |
| 502 | Bad Gateway — an upstream provider call failed |
| 503 | Service Unavailable — a dependency is unhealthy, or an optional binding is absent (see DEPLOY_QUEUE_UNAVAILABLE) |
| 504 | Gateway Timeout — an operation exceeded its bound (see PUSH_TIMEOUT) |
Machine-readable error codes
Section titled “Machine-readable error codes”- Caller authentication failures carry no machine-readable code: a
401is{"error": "Invalid token"}(or similar) with nocodefield. Branch on the status, not a code. (AUTH_ERRORexists internally but surfaces only when the instance’s outbound GitHub credential fails during a sync push — it never describes your own authentication.) 404s likewise usually carry no code —{"error": "..."}only — and an unauthorized private project is deliberately indistinguishable from a missing one. There is noPROJECT_NOT_FOUNDcode; where a code does appear on a 404 it is the genericNOT_FOUND.- Rate limiting carries no machine-readable code: a
429has{"error": "Too many requests"}plusRetry-After,X-RateLimit-Limit, andX-RateLimit-Remainingheaders TOKEN_SCOPE_INSUFFICIENT— areadtoken was used for a write. Returned403, checked before routing on the HTTP method, on API routes. The git smart-HTTP router enforces the same rule on the resolved operation (sogit pushand itsgit-receive-packadvertisement are refused too) but answers with its deliberate plain-text404— never this code — so an unauthorized caller cannot distinguish “no permission” from “no such repo”TOKEN_LIMIT_REACHED—409; the account already holds 20 active tokens. Revoked tokens do not count toward the limitSESSION_REQUIRED—403; the endpoint accepts the browser session cookie only. Applies to token management (/api/users/me/tokens,legacy-token/disable) and torotate-tokenwhen called with a scoped tokenSESSION_EXPIRED—401; the browser session is no longer validADMIN_REQUIRES_DIRECT_CREDENTIAL—403; an MCP/CLI OAuth grant was used on/api/admin/*. Refused before routing, even when the account behind the grant is the instance administratorCSRF—403; aPOST/PUT/PATCH/DELETEarrived with anOriginorRefererthat is missing, malformed, or names a different host. Enforced for session-cookie callers and for the few unauthenticated endpoints that must not be forgeable (magic-link verify); bearer-token callers are unaffectedSYNC_DIVERGED— the upstream and the Stratum copy have genuinely diverged in content. The fetch window is deepened incrementally first, so this is a real conflict rather than a too-shallow clonePINNED_SHA_UNREACHABLE— the pinned commit is not reachable in the clonePUSH_TIMEOUT—504during restore. The push may still land — do not assume it failedSTALE_BASE— the change’s recorded base is behind the project HEAD (merge.requireFreshBase); re-evaluate on the new baseSTALE_WORKSPACE— the workspace advanced after evaluation; the merge is rejected so unevaluated commits can never landSTALE_PROJECT—409; the project’s default branch advanced between the point a manual conflict resolution was evaluated and the point it would have been committed. The same principle asSTALE_WORKSPACEfrom the other side: the resolution would otherwise have landed on a base no evaluator saw. Nothing is committed — re-resolve against the current revisionWORKSPACE_UNVERIFIABLE— the workspace state could not be verified against what was evaluatedMERGE_CONFLICT— the merge produced conflicts; the response includes a conflict id forPOST /api/projects/conflicts/{id}/resolvePROTECTION_BLOCKED—403(not a 409, despite sitting beside the staleness codes); the merge is blocked by branch protection and the response lists thereasons. A policy that names an evaluator whose binding is absent — most commonlysandbox, whose[[sandboxes]]binding is commented out by default — fails closed rather than being skipped, so it shows up here permanently until the binding is enabled or the evaluator is dropped from the policyTARGET_DELETING—409; the project (or its owner) is being deletedNOT_REDRIVABLE—409; the deletion job is not in an incomplete stateIMPORT_IN_PROGRESS—409; the project’s import has not finished, so the requested operation would race itGONE—410; the resource was deletedINVALID_PATH—422; the requested path is invalid (e.g. a conflict resolution naming a path outside the repo)AMBIGUOUS_REF—409; a?ref=names something that is both a branch and a tag, so the read is refused rather than guessingBRANCH_EXISTS—409; a branch of that name already existsBRANCH_NAME_CONFLICT—409; the name collides with an existing ref path (releasewhenrelease/2.xexists — git stores refs as paths)NO_DEFAULT_BRANCH—409; the configured default branch is not advertised by the remote, so there is nothing to default a start point toDEFAULT_BRANCH_PROTECTED—409; the default branch cannot be deletedSUBMODULES_UNSUPPORTED— a gitlink entry (any depth) and/or a root-level.gitmodulesfile was found on import, or in the workspace a change is being created from — a gated push andPOST /api/projects/{name}/changesalike, since both run the same scan; git submodules are not supported (see Importing from GitHub). This code is internal and is not returned to API clients as a code:POST /api/projects/{name}/changesreports it as a plain 400 carrying the explanatory message, a gated push reports it over the git protocol as a per-refngreason, and an import records afailedqueue job. Match on the message, not on this identifier.
Post-merge deployments
Section titled “Post-merge deployments”DEPLOY_QUEUE_UNAVAILABLE—503; the instance has noDEPLOY_QUEUEbinding, so deployments are not enabled here. Checked before the row’s status is changed, so approving or retrying is still possible once it is configuredDEPLOYMENT_NOT_PENDING—409; the deployment is not awaiting approval (it already ran, or someone else approved it first)DEPLOYMENT_NOT_RETRYABLE—409; only a finished deployment can be retried. Aqueued,runningorpending_approvalrow still has a future, and retrying one would publish the same commit twiceDEPLOYMENT_RETRY_EXISTS—409; that attempt number already exists — someone retried first, and their row is the live one
Manual conflict resolution
Section titled “Manual conflict resolution”POST /api/projects/conflicts/{id}/resolve runs the same gates a change does,
and reports them with its own codes:
SECRET_DETECTED—422; the secret scan rejected the resolution; the response carriesissuesEVALUATION_FAILED—422; the evaluator suite rejected the resolution; the response carriesissuesEVAL_PREP_FAILED—502; the diff the evaluators would judge could not be builtINVALID_INPUT—400/422; the resolution body is malformed, names an unknown strategy, or carries a file over the 10 MB per-file cap
Generic codes
Section titled “Generic codes”Anything thrown as an AppError and not matched above surfaces with its own
code and status: VALIDATION_ERROR (400), NOT_FOUND (404), FORBIDDEN
(403), CONFLICT (409), PAYLOAD_TOO_LARGE (413),
EXTERNAL_SERVICE_ERROR / GITHUB_ERROR / GIT_PROTOCOL (502), and
INTERNAL_ERROR (500). Treat these as families, not as a stable contract for
a specific endpoint — the coded errors above are the ones worth branching on.
Separately, an import job records a failure category on the job row —
NETWORK_ERROR, TIMEOUT, AUTH_ERROR, NOT_FOUND, RATE_LIMITED,
UNSUPPORTED_CONTENT, STORAGE_ERROR, GIT_ERROR, CANCELLED,
UNKNOWN_ERROR — classified from the failure message. As categories these are
not API error responses: they appear on the admin metrics API and in the failure
notification, not as a code on a request you made.
Two of those names do also exist as response codes in their own right, so match on the endpoint rather than on the name alone:
GIT_ERROR—500; a git operation the request depended on could not be completed. Returned byPOST /api/projects/conflicts/{id}/resolvewhen the project’s current revision cannot be read, so the evaluated base cannot be verified. The request was well formed; retrying can succeedAUTH_ERROR—401; the repository credential was rejected or has expired