Errors
Error envelope format and full status-code reference.
Datapoint uses conventional HTTP status codes. Most endpoints return a consistent JSON envelope on error, with a couple of exceptions noted below.
Error envelope
Most non-2xx responses have the same shape:
{
"detail": "<human-readable message or structured error data>",
"message": "<mirror of detail>"
}detail: for simple errors, a string; for validation errors (422), an array of field-level error objects; for402 Payment Requiredand any error that carries acodefield (content review, oversized media, unsupported media), an object.message: a mirror ofdetailfor most errors, with one deliberate exception — on a field-validation422,detailis the array of field errors whilemessageis a joined summary string. It is a plain string for simple errors and every500. Whendetailis an object (e.g.402, a content-review block, or a rejected plan — the latter two are also422s),messageholds that same object, not a string.
Status codes
| Code | Meaning | Typical cause |
|---|---|---|
400 | Bad Request | Missing or invalid field that doesn't fit field-level validation (e.g. dimensions and instruction both set, or a missing datapoints array); unknown task_type; handler-level datapoint rejection; unsupported or mismatched media; content-review invalid_media. Some 400s carry a code object. |
401 | Unauthorized | Missing, malformed, or inactive API key (e.g. "Invalid or inactive API key"). Dashboard-managed endpoints also return 401 when their credential is missing or invalid. The exact message varies by cause. |
402 | Payment Required | Not enough account credits to cover the charge (job reservation or planner fee). detail is an object with needed_credits and available_credits. |
403 | Forbidden | Signed media-URL signature invalid or not authorized for the requested resource. Returned as a bare status by the media proxy. |
404 | Not Found | Job, media, or API key doesn't exist (or belongs to another client). The media proxy returns a bare 404. |
409 | Conflict | Duplicate invite — a pending invite already exists for the email, or the email already belongs to your organization. (Re-submitting a job with a name you already used is not a conflict; it returns 200 with the existing job.) |
410 | Gone | A one-time code (e.g. device authorization) has expired or already been used. |
413 | Payload Too Large | An uploaded media file exceeds the size limit. detail is an object: {"code": "media_too_large", "max_bytes": ...}. |
422 | Unprocessable Entity | Field-level request validation failed. detail is an array of per-field errors. Content-review blocks also surface here (see Content review). |
429 | Too Many Requests | Rate limit exceeded. The actionable signal is the Retry-After header (seconds until the window resets). |
500 | Internal Server Error | Unexpected server error. detail and message are usually the string "Internal Server Error"; a few handled failures return 500 with a more specific string. |
502 | Bad Gateway | The planner could not produce a plan. The media proxy also returns a bare 502 on an upstream storage error. |
503 | Service Unavailable | Task dispatch failed (retry with a new name), or content review is temporarily unavailable (a Retry-After header is set). |
504 | Gateway Timeout | The planner timed out. |
Examples
400: task-type rejection
Sent back when a task handler rejects your payload shape (e.g. a comparison datapoint with 3 candidates instead of 2):
{
"detail": "Datapoint 0: comparison requires exactly 2 media items under 'candidates', got 3.",
"message": "Datapoint 0: comparison requires exactly 2 media items under 'candidates', got 3."
}402: insufficient balance
detail is an object carrying the shortfall in credits so you can show a clear message to your user. Because message mirrors detail, it holds the same object — not a string. The message string inside the object varies by endpoint ("Insufficient balance" for jobs; "Insufficient balance for planner fee" for the planner), but needed_credits / available_credits are always present.
{
"detail": {
"message": "Insufficient balance",
"needed_credits": 150,
"available_credits": 75
},
"message": {
"message": "Insufficient balance",
"needed_credits": 150,
"available_credits": 75
}
}Top up via POST /billing/checkout and retry.
422: request validation
Field-level validation emits one entry per offending field. The message summary joins those entries with "; ", and each entry's location path is joined with " -> " (the leading body segment is dropped):
{
"detail": [
{
"type": "missing",
"loc": ["body", "name"],
"msg": "Field required"
},
{
"type": "string_type",
"loc": ["body", "response_options", 0],
"msg": "Input should be a valid string"
}
],
"message": "name: Field required; response_options -> 0: Input should be a valid string"
}429: rate limit
{
"detail": "Rate limit exceeded",
"message": "Rate limit exceeded"
}Read the Retry-After header to know how long to back off. See Rate limits for details.
503: dispatch failed
When background task dispatch is unreachable:
{
"detail": "Failed to queue tasks. Please retry with a new name.",
"message": "Failed to queue tasks. Please retry with a new name."
}The job's reservation has already been released, so retrying with a new name is safe and won't double-charge.
Content review
Uploaded media pass through an automated content-review step at upload time, before any job can use them. Media referenced by URL are reviewed while the job's datapoints are processed, after the job is accepted — a block there marks that datapoint as blocked (visible in errors on GET /jobs/{job_id}) instead of failing the request. Three outcomes surface as request errors:
| Code | detail.code | Meaning |
|---|---|---|
422 | content_blocked | The media did not pass content review and cannot be used. |
400 | invalid_media | The media could not be reviewed because it was unreadable or malformed (corrupt file, wrong format). |
503 | — | Content review is temporarily unavailable. A Retry-After header tells you when to retry. |
A blocked upload looks like:
{
"detail": {
"code": "content_blocked",
"filename": "clip.mp4",
"reason": "This content did not pass moderation review."
},
"message": {
"code": "content_blocked",
"filename": "clip.mp4",
"reason": "This content did not pass moderation review."
}
}For content_blocked (422) and invalid_media (400), fix or replace the offending file and resubmit. For the 503 case, wait for the interval in Retry-After and retry the same request — it is safe to repeat.
Handling errors
A minimal handling pattern:
r = requests.post(url, headers=headers, json=body)
if r.status_code == 402:
short = r.json()["detail"]
raise RuntimeError(
f"Top up {short['needed_credits'] - short['available_credits']} credits to continue"
)
if r.status_code in (429, 503):
time.sleep(int(r.headers.get("Retry-After", 60)))
# retry
r.raise_for_status()Next
- Rate limits: 429 handling in depth
- Billing: how costs are reserved and refunded