Errors & rate limits
The canonical error envelope, the full error code catalogue, billing-block codes, and per-key rate limits.
Type: Reference · Audience: Anyone integrating against
/api/v1
Every error response uses the same envelope:
{ "error": "Human-readable message", "code": "MACHINE_CODE" }
error is always present and safe to surface to end users. code is present on
actionable failures — branch on it, never on the error string (which may be
reworded without notice). Some responses add context fields (e.g.
available_cents, required_cents, maxAllowed, action_url).
Error code catalogue
code |
HTTP | Meaning | What to do |
|---|---|---|---|
UNAUTHORIZED |
401 | Missing/invalid API key or session | Check the Authorization: Bearer kc_live_… header |
INSUFFICIENT_SCOPE |
403 | Key authenticated but lacks the required scope | Create a key with the needed scope (jobs:write, etc.) |
TEST_KEY_LIVE_BILLING |
403 | A kc_test_ key was used on POST /v1/jobs/submit |
Use a kc_live_ key — test keys can't place billable jobs |
NO_PAYMENT_METHOD |
402 | No active subscription and no credits | Add credits (POST /v1/credits/topup) or subscribe |
INSUFFICIENT_CREDITS |
402 | Credit balance is below the job's estimated cost | Top up; see available_cents / required_cents |
COST_LIMIT_EXCEEDED |
422 | Estimated cost exceeds the per-job ceiling | Reduce job size; see maxAllowed. Contact support for higher limits |
SUBMISSION_IN_PROGRESS |
409 | A submit with this Idempotency-Key is still in flight |
Retry after Retry-After seconds |
UNSUPPORTED_SIMULATOR |
400 | simulator is not on the allowlist |
Use one of the listed simulator ids |
UNKNOWN_TIER |
400 | tierId is not a valid top-up tier |
Use a documented tier id |
INVALID_CHECKSUM |
400 | sha256 is not a 64-char hex digest |
Send a valid SHA-256, or omit it |
PAYLOAD_TOO_LARGE |
413 | Declared/assembled size exceeds the upload ceiling | Use resumable multipart (POST /v1/uploads); see Large-file uploads |
UNSUPPORTED_MEDIA_TYPE |
415 | contentType is denied (active content) or not in the allowlist |
Upload a supported asset type |
SIZE_MISMATCH |
422 | Assembled object size ≠ declared size at completion | Re-upload missing parts; the upload is incomplete/corrupt |
FORBIDDEN_ASSET_KEY |
403 | A storage key you referenced belongs to another account | Reference only keys your own upload or your own job produced |
INVALID_ASSET_KEY |
400 | A storage key is malformed — absolute, or containing a .. path segment |
Send the key exactly as the upload returned it |
RATE_LIMITED |
429 | Too many requests in the window | Back off; honour Retry-After |
SERVICE_UNAVAILABLE |
503 | A dependency (database/provider) is down | Retry with backoff |
INTERNAL_ERROR |
500 | Unexpected server error | Retry; if persistent, contact support |
Billing-block codes
When access is blocked by the billing layer, code carries the specific
reason so you can route the user correctly. Billing reasons return 402 so the
user can add or update a payment method:
code |
HTTP | Meaning |
|---|---|---|
stripe_past_due |
402 | Subscription payment past due |
no_payment_method |
402 | No subscription or payment method on file |
Rate limits
Limits are per API key (key-authenticated calls) or per user/IP (session and public calls), using a sliding 60-second window.
| Class | Limit | Applies to |
|---|---|---|
| Write | 10 / min | POST /v1/jobs/submit, POST /v1/credits/topup |
| Public | 60 / min | POST /v1/public/estimate (per IP) |
Every rate-limited response includes:
| Header | Meaning |
|---|---|
X-RateLimit-Limit |
Requests allowed in the window |
X-RateLimit-Remaining |
Requests remaining |
Retry-After |
Seconds to wait, on a 429 only |
Limits may be raised per plan over time. Treat the headers as authoritative rather than hard-coding the numbers above, and implement exponential backoff on
429.
Upload size limits
There is no 25 MB-class request-body limit on uploads: file bytes never go through the API, only through presigned URLs to object storage.
| Method | Ceiling | Notes |
|---|---|---|
POST /v1/jobs/upload-url (single PUT) |
5 GiB (MAX_UPLOAD_BYTES) |
One-shot, not resumable; capped at the provider single-object limit |
POST /v1/uploads (multipart) |
200 GiB default (MAX_MULTIPART_UPLOAD_BYTES) |
Resumable; raise per plan |
Exceeding the ceiling returns 413 PAYLOAD_TOO_LARGE. For anything large or on
an unreliable connection, use the resumable flow — see
Large-file uploads.
Storage keys are owner-scoped
Every key the API mints belongs to exactly one account, and its owner is encoded in the key itself:
| Prefix | Written by | Example |
|---|---|---|
uploads/{userId}/… |
your uploads | uploads/8f3c…/scene/receptor.pdb |
outputs/{userId}/{jobId}/… |
your completed jobs | outputs/8f3c…/job-42/frame_001.png |
Every key you send is checked against the account that authenticated the
request — sceneFile, each entry in attachments[], and any key you ask us to
mint a download URL for. A key belonging to another account is refused with 403 FORBIDDEN_ASSET_KEY; a malformed one (absolute, or containing a .. segment) is
refused with 400 INVALID_ASSET_KEY.
Two consequences worth knowing:
- Send keys back verbatim. Take the
keyfrom the upload response and pass it through unchanged. Do not rewrite, join, or normalise it. - A batch fails whole. If you ask for download URLs for several keys and any one of them is foreign, the entire request is refused rather than returning partial results — otherwise the per-key outcomes would let a caller probe for the existence of another account's objects.