Skip to main content
Every error response follows the same JSON shape:
{
  "success": false,
  "error": "<error_code>",
  "message": "<human_readable>"
}
Handle this shape in your client — the HTTP status code tells you the category, the error field tells you the exact reason, and message is safe to surface to end users (no stack traces, no internal info).

HTTP status codes

CodeCategoryTypical
200SuccessSync result returned
201Resource createdFile uploaded, key minted, webhook subscribed
202Accepted (async)Task queued; poll /tasks/{id}
204No contentImage/key/webhook deleted
302RedirectGET /runs/{id}/download
400Bad requestValidation or preflight error
401UnauthorizedBad / missing API key
402Payment requiredOut of credits
403ForbiddenKey valid but not permitted
404Not foundRun / file / image doesn’t exist or belongs to another org
408TimeoutSync ceiling exceeded; response tells you to retry async
409ConflictIdempotency collision or invalid state
410GoneResource existed but was deleted / expired
413Payload too largeUpload exceeds tier limit
422ValidationField-level rejection on the request body
429Rate limitedSee Retry-After header
500Server errorOur bug. Gets paged.
501Not implementedEndpoint exists but feature isn’t live yet
502Upstream errorA dependency we rely on (model provider, storage, or rendering pipeline) failed
503Service unavailableCapacity exhausted or maintenance

Error codes

Authentication

CodeWhenRemedy
invalid_api_keyMissing, malformed, or revoked sk_live_*Check Authorization header; rotate if compromised
forbiddenKey OK but lacks permission (suspended org, wrong tier)Check org status on GET /verify

Billing

CodeWhenRemedy
insufficient_creditsOrg balance below the format’s minimumTop up via dashboard or contact sales

Validation

CodeWhenRemedy
invalid_requestMissing required field, bad enum valueFix payload per error message
validation_errorPydantic field-level errorError message has JSON pointer to the bad field
preflight_failedLLM validator rejected (missing assets)Attach the missing assets or rephrase the prompt
payload_too_largeFile exceeds tier limitCompress or upgrade tier

Resources

CodeWhenRemedy
not_foundRun / file / image / webhook ID doesn’t exist OR belongs to another orgCheck ID spelling; confirm you created it under the same org
goneResource expired (30-day retention)Start a new run
conflictIdempotency key reused with different body, or resource in wrong stateDifferent key, or wait for terminal state

Runtime

CodeWhenRemedy
sync_timeout_upgrade_asyncSync request hit 300s ceilingResubmit with async: true; poll /tasks/{id}
rate_limit_exceededOver per-key tier limitRetry after the Retry-After header seconds
internal_errorUnhandled server exceptionInclude X-Request-Id in support ticket
upstream_errorLLM provider / storage outageRetry with exponential backoff
service_unavailablePool saturated, scheduled maintenanceRetry after 30-60s
not_implementedEndpoint exists (for shape compatibility) but the feature isn’t available on your tier / deploymentContact support

Handling errors in code

import requests

resp = requests.post(f"{API}/excel/generate", headers=headers, json=body)
if not resp.ok:
    err = resp.json()
    code = err.get("error", "unknown")
    msg = err.get("message", "")

    if code == "preflight_failed":
        # Fix the prompt or attach missing assets
        print(f"Rejected: {msg}")
    elif code == "insufficient_credits":
        # Show a billing prompt to the user
        redirect_to_billing()
    elif code == "rate_limit_exceeded":
        # Honor Retry-After
        retry_after = int(resp.headers.get("Retry-After", "30"))
        time.sleep(retry_after)
        # then retry
    elif resp.status_code >= 500:
        # Transient — retry with exponential backoff
        retry_with_backoff()
    else:
        # Permanent — don't retry
        raise RuntimeError(f"{code}: {msg}")

Debugging failed runs

Capture the run_id and task_id from any response (successful or not) and include them on support tickets — they’re the fastest way for us to pull the matching ledger / trace rows on our side. For terminal failed states on async tasks, the error field on GET /tasks/{task_id} carries the structured failure:
{
  "task_id": "task_...",
  "status": "failed",
  "error": {
    "code": "internal_error",
    "message": "AgentLoopError: max_tool_calls=30 exceeded"
  },
  "credits_used": 0,
  "duration_ms": 45123
}

Rate limits

Every response includes rate-limit headers:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1713312345
And on 429:
Retry-After: 42
Limits are per API key, per minute. Tiers:
TierRequests/minConcurrent runs
free102
pro6010
enterprise60050
Rate limiting kicks in at the gateway, before any agent / pool work. It’s effectively free to hit rate limits (no credits deducted).