Skip to content

Errors and retries

The error envelope

Every 4xx and 5xx response under /api/v1/ has the same shape:

{
  "error": {
    "code": "invalid",
    "message": "Request validation failed.",
    "details": {
      "analysis_type": ["\"nope\" is not a valid choice."],
      "image_id": ["This field is required."]
    }
  }
}
code
A short, stable, machine-readable string. Branch on this and the HTTP status.
message
A sentence for people. We improve these over time, so don't match on them.
details
For a validation error (400), an object mapping each field to a list of problems. Parameter errors appear under the parameter's own name, such as details.max_iterations, and a problem with the parameters as a whole under details.__all__. Problems that aren't about one field appear under submission, upload, job or parameters. For other errors, details is {"detail": "<message>"}.

Messages never name the services or machines that run your work, and never carry a stack trace. A job or image that fails reports the solver's own explanation when it gives one, or a plain sentence such as "The analysis failed on the compute worker."

Status codes

Status error.code When What to do
400 invalid A field or parameter is missing or out of range, the image isn't ready or can't be analysed, the result isn't available yet, or no compute tier can fit the image. Fix the request. Retrying it unchanged gives the same answer.
400 parse_error The body isn't valid JSON. Fix the body.
401 not_authenticated, authentication_failed Missing or bad credentials. See Authentication.
403 permission_denied The key's owner is in this team but the key isn't valid for it, or result access is paused (see below). Use a key made by an owner of this team. See Authentication.
403 analysis_unavailable The analysis isn't released to you on this team. Check GET /analysis-types/.
404 not_found The team, image or job doesn't exist, belongs to another team, or is in a team you can't reach. Check the ID and the key.
405 method_not_allowed Wrong HTTP method for the path. —
409 idempotency_conflict The Idempotency-Key was already used for a different submission. Use a new key for a new job. See Idempotency.
415 unsupported_media_type The body's Content-Type isn't one the API reads. Send Content-Type: application/json.
429 throttled Over the rate limit. Wait Retry-After seconds.
5xx — Something went wrong on our side. Retry with backoff.

Retrying safely

Response status?DoneDon't retry:fix the requestWait Retry-After,then retryBack off exponentially,with jitterPOST /analysis-jobs/?Retry with thesame Idempotency-KeyList jobs first:it may have been createdRetry 2xx400 401 403 404 4094295xx or network erroryes, with a keyyes, without a keyno
What to do with each kind of response.
  • Reads (GET) are always safe to retry.
  • Job submission isn't safe to repeat blindly. Always send an Idempotency-Key. If a request times out, resend it with the same key: you get the original job back (200 with Idempotency-Replayed: true). Without a key, a blind retry can create a second job.
  • Upload sessions expire after one hour. If the presigned upload is rejected as expired, create a new session. Don't retry the old form. Once the file is uploaded, complete the upload within 6 days: a later completion is refused, and the file can be deleted any time after 7 days.
  • Back off. Start around 1 s, double each time, add random jitter, and give up after a few minutes.
import random
import time


def call(method, url, retries=6, **kwargs):
    """Send a request, retrying 429s and 5xx responses with exponential backoff."""
    for attempt in range(retries):
        response = session.request(method, url, **kwargs)
        if response.status_code == 429:
            time.sleep(float(response.headers.get("Retry-After", 2 ** attempt)))
            continue
        if response.status_code >= 500:
            time.sleep(2 ** attempt + random.random())
            continue
        return response
    response.raise_for_status()
    return response