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; for 402 Payment Required and any error that carries a code field (content review, oversized media, unsupported media), an object.
  • message: a mirror of detail for most errors, with one deliberate exception — on a field-validation 422, detail is the array of field errors while message is a joined summary string. It is a plain string for simple errors and every 500. When detail is an object (e.g. 402, a content-review block, or a rejected plan — the latter two are also 422s), message holds that same object, not a string.

Status codes

CodeMeaningTypical cause
400Bad RequestMissing 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.
401UnauthorizedMissing, 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.
402Payment RequiredNot enough account credits to cover the charge (job reservation or planner fee). detail is an object with needed_credits and available_credits.
403ForbiddenSigned media-URL signature invalid or not authorized for the requested resource. Returned as a bare status by the media proxy.
404Not FoundJob, media, or API key doesn't exist (or belongs to another client). The media proxy returns a bare 404.
409ConflictDuplicate 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.)
410GoneA one-time code (e.g. device authorization) has expired or already been used.
413Payload Too LargeAn uploaded media file exceeds the size limit. detail is an object: {"code": "media_too_large", "max_bytes": ...}.
422Unprocessable EntityField-level request validation failed. detail is an array of per-field errors. Content-review blocks also surface here (see Content review).
429Too Many RequestsRate limit exceeded. The actionable signal is the Retry-After header (seconds until the window resets).
500Internal Server ErrorUnexpected server error. detail and message are usually the string "Internal Server Error"; a few handled failures return 500 with a more specific string.
502Bad GatewayThe planner could not produce a plan. The media proxy also returns a bare 502 on an upstream storage error.
503Service UnavailableTask dispatch failed (retry with a new name), or content review is temporarily unavailable (a Retry-After header is set).
504Gateway TimeoutThe 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:

Codedetail.codeMeaning
422content_blockedThe media did not pass content review and cannot be used.
400invalid_mediaThe 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