Errors
The envelope
Section titled “The envelope”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 codes
Section titled “Status codes”| 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 |
Quota denials
Section titled “Quota denials”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.
401 vs 403
Section titled “401 vs 403”| 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 policy
Section titled “Retry policy”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, timeimport 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}")Retrying safely
Section titled “Retrying safely”| 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 |
Client-side errors
Section titled “Client-side errors”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 |
Debugging a failure
Section titled “Debugging a failure”- Read
code, notmessage. The code is the contract. - Save
request_id. It’s the only way support can find your specific request. - 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}'-ishows the headers, includingX-Request-Id. - Check identity:
GET /users/me— which orgs and roles does this credential see? - Check the token: is it in the org’s token list, unexpired, with the scope you expect?
- Check platform health:
curl -sS https://cavsnode.com/readyz. cav doctorif you’re using the CLI — it checks the whole chain.
Client error taxonomy
Section titled “Client error taxonomy”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.
- API overview — conventions and limits.
- Troubleshooting — symptom-first diagnosis.

