Skip to content

Errors

Every error, from every endpoint, has the same shape:

{
"error": {
"code": "quota_exceeded",
"message": "storage limit reached for the Developer plan (100 GiB); upgrade to add more",
"request_id": "8c1f2e4a-3b7d-4e9f-a1c2-5d6e7f8a9b0c",
"details": {
"reason": "storage_quota",
"limit": 107374182400,
"current": 106300000000
}
}
}
Field Notes
code Stable, machine-readable. Switch on this, never on message.
message Human-readable. Wording may change between releases.
request_id Same value as the X-Request-Id header. Quote it to support.
details Present on quota denials and some validation errors. Structured.
Status code Meaning Retry?
400 bad_request Malformed JSON, invalid parameter, or an unknown field no
401 unauthorized Missing, invalid, expired or revoked credential no
403 forbidden Authenticated, but role/override/scope insufficient, or the account is suspended no
404 not_found No such resource, or no permission to know it exists no
405 Method not allowed on that path no
409 conflict State conflict — a duplicate slug, for instance no
402 quota_exceeded A plan limit was hit. See details.reason. no
429 rate_limited Over 30 req/s sustained (burst 60) yes
500 internal Server-side failure yes
503 Not ready (/readyz), or storage unreachable yes

402 carries a structured details object:

{ "reason": "storage_quota", "limit": 107374182400, "current": 106300000000 }
details.reason What was hit Unit
max_repositories Repository count count
max_members Organization members count
max_tokens Active API tokens count
max_object_size A single object’s size bytes
storage_quota Total physical storage bytes
max_storage_connections BYOS storage connections count

Switch on reason to show the right upgrade prompt — this is what the dashboard and the CLI do.

try:
client.artifacts.upload(...)
except QuotaExceededError as e:
reason = (e.details or {}).get("reason")
if reason == "max_object_size":
print("This file is larger than your plan allows.")
elif reason == "storage_quota":
print("Out of storage. Delete something or upgrade.")

Limits per plan: Plans & quotas.

Meaning Fix
401 “I don’t know who you are.” Check the token exists, isn’t revoked, hasn’t expired, and is spelled correctly.
403 “I know who you are, and no.” Check your role, any repository override, and the token’s scope.

A token that used to work and now gives 403 almost always means your role changed or you’re calling something the scope doesn’t cover — remember scopes are a hard cap. Debugging checklist in Roles & permissions.

Retry 429, 5xx and transport errors. Never retry 4xx other than 429 — the request is wrong and will stay wrong.

Four attempts, exponential backoff, full jitter is the convention every official client uses:

import random, time
import httpx
def request_with_retry(client, method, url, **kw):
last = None
for attempt in range(4):
try:
r = client.request(method, url, **kw)
if r.status_code < 500 and r.status_code != 429:
return r
last = r
except httpx.TransportError as e:
last = e
if attempt < 3:
retry_after = 0
if isinstance(last, httpx.Response):
retry_after = float(last.headers.get("Retry-After", 0) or 0)
delay = retry_after or min(2 ** attempt + random.random(), 30)
time.sleep(delay)
raise RuntimeError(f"gave up after 4 attempts: {last}")
Operation Safe to retry?
Any GET always
Create upload session yes with Idempotency-Key
Authorize objects yes — idempotent by nature
PUT to a presigned URL yes — rewind and resend
Complete object yes — idempotent
Finalize yes — idempotent
Create snapshot yes with Idempotency-Key
POST /organizations no — you’d create a second organization
POST …/graph/edges no — you’d create a duplicate edge

Not from the API, but part of the contract every client implements:

Condition Convention
Downloaded bytes don’t hash to the oid ChecksumError — delete the temp file, never write to the destination
Presigned PUT or GET fails UploadError / DownloadError
A path escapes the destination on directory expansion reject before writing
  1. Read code, not message. The code is the contract.
  2. Save request_id. It’s the only way support can find your specific request.
  3. Reproduce with curl — takes the client out of the picture:
    Terminal window
    curl -isS -X POST "https://cavsnode.com/api/v1/repositories/$REPO/uploads" \
    -H "Authorization: Bearer $CAVS_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"expected_objects": 1}'
    -i shows the headers, including X-Request-Id.
  4. Check identity: GET /users/me — which orgs and roles does this credential see?
  5. Check the token: is it in the org’s token list, unexpired, with the scope you expect?
  6. Check platform health: curl -sS https://cavsnode.com/readyz.
  7. cav doctor if you’re using the CLI — it checks the whole chain.

Every official SDK maps status codes to typed errors identically:

HTTP Python / TypeScript
401 AuthenticationError
403 AuthorizationError
404 NotFoundError
409 ConflictError
402 / 413 QuotaExceededError
429 RateLimitError (.retry_after)
5xx / network after retries TemporaryServiceError
checksum mismatch ChecksumError
transfer failure UploadError / DownloadError

All derive from CAVSError.

CLI exit codes are separate and documented in CLI — exit codes.