Error reference

Every API error is one nested error object. Branch on error.type (coarse category) or switch on error.code (specific), with a human-readable error.message and an optional error.action that tells you exactly what to do next.

On this page

Response shape

json
{
  "error": {
    "type": "rate_limit_error",
    "code": "rate_limited",
    "message": "Too many requests. Retry later.",
    "action": { "type": "wait", "retry_after": 60 }
  }
}

The action and details fields live inside the error object and are only present on certain errors. action.retry_after (seconds) is only present for wait actions; the Retry-After HTTP header carries the same value. Success responses never carry an error or an action.

Error types

error.type is a coarse, stable category. Branch on it to handle a whole class of errors without enumerating every code.

invalid_request_error
The request was malformed, named something that isn’t there, wrote to storage that is being deleted, or lost a race with another change to the same image. Codes: validation_error, not_found, workspace_unavailable, state_conflict, bad_request, …
authentication_error
Missing or invalid credentials. Codes: unauthorized.
permission_error
Authenticated, but not allowed. Codes: forbidden, media_locked, media_blocked, app_blocked.
rate_limit_error
Too many requests. Codes: rate_limited.
quota_error
The App is at a limit of its plan. Codes: quota_exceeded.
idempotency_error
Idempotency-Key reused with a different body, retried while the original is still in flight, or belonging to an earlier upload that can no longer be replayed. Codes: idempotency_key_conflict, idempotency_key_in_progress, idempotency_recovery_conflict.
processing_error
Upload / import processing failed. Codes: upload_failed, fetch_failed, import_failed, media_failed.
api_error
A failure on img.pro’s side (server_error, update_failed, delete_failed), or something img.pro couldn’t confirm in time, which comes with a wait action (app_context_unavailable, create_outcome_ambiguous, and server_error on GET /v1/billing/status).

Action types

When present, error.action tells you exactly what to do next. The type is a closed enum:

wait retry unchanged
The request can’t go through yet: a rate limit, a key whose App img.pro couldn’t check in time, an identical create still in flight, an upload img.pro can’t confirm yet, or a user’s renewal img.pro is still confirming (GET /v1/billing/status). Back off for error.action.retry_after seconds (the Retry-After header carries the same value), then retry the same operation.

Error codes

unauthorized

HTTP 401 · type: authentication_error. The request sent no API key, or one img.pro doesn’t know. Every endpoint needs one, GET /v1/images/:id included; an image file itself loads from its url without a key.

A key that worked before stops when it is revoked, or when the person who created it leaves the App’s team or is no longer an Admin there, since a key belongs to its creator. Create a new key on the App’s API Keys page and send that one instead.

json
{
  "error": {
    "type": "authentication_error",
    "code": "unauthorized",
    "message": "Invalid or missing authentication"
  }
}

forbidden

HTTP 403 · type: permission_error. The key is valid but not allowed to do this: it lacks the permission (read or write), or it reads GET /v1/billing/status without naming a connected user. The message says what to send.

json
{
  "error": {
    "type": "permission_error",
    "code": "forbidden",
    "message": "Insufficient permissions"
  }
}

GET /v1/billing/status read with an App-wide key and no X-Img-User:

json
{
  "error": {
    "type": "permission_error",
    "code": "forbidden",
    "message": "GET /v1/billing/status changed on September 29, 2026: it now reads one user's subscription to your App's plans, so send X-Img-User with their id. Your App's img.pro plan and usage are at GET /v1/usage."
  }
}

media_locked

HTTP 403 · type: permission_error. The image is moderation-locked and can’t be modified: PATCH (single or batch) returns this. Deletion is not blocked: an owner can still DELETE a locked image. In a batch update, the locked id appears in the errors array while the rest of the batch still applies.

json
{
  "error": {
    "type": "permission_error",
    "code": "media_locked",
    "message": "Media cannot be modified"
  }
}

media_blocked

HTTP 403 · type: permission_error. The image was blocked by moderation and can’t be retrieved. Blocked images never appear in lists; this error is what a direct GET /v1/images/:id returns instead of the object. The message includes the coarse reason, one of exactly four values: content_policy, dmca, spam, or terms.

json
{
  "error": {
    "type": "permission_error",
    "code": "media_blocked",
    "message": "This image was blocked (content_policy) and is no longer available."
  }
}

quota_exceeded

HTTP 403 · type: quota_error. The App holds its plan’s images limit (5,000 on Free, 50,000 on Pro), counting its own storage and every connected user’s, so a new upload or URL import is refused and stores nothing (the response carries X-Limits-Enforced: true). Everything already stored keeps serving. Retrying does not help until the App holds fewer images (delete some) or is on a higher plan (Pro, or Unlimited, which has no limits); its owner upgrades it in Billing. The refusal is not kept as an Idempotency-Key’s answer, so the same key creates once the App has room, and a retry of a create that already stored its image returns that image. GET /v1/usage shows how close the App is (limits.images).

json
{
  "error": {
    "type": "quota_error",
    "code": "quota_exceeded",
    "message": "This App is at the Free limit of 5,000 images, so it accepts no new uploads. Upgrade it to Pro in Billing: https://img.pro/billing"
  }
}

On Pro the message names Pro’s limit and Unlimited: “This App is at the Pro limit of 50,000 images, so it accepts no new uploads. Upgrade it to Unlimited in Billing: https://img.pro/billing”. A connected user’s own key reads only that the App takes no new uploads.

media_failed

HTTP 422 · type: processing_error. The image’s processing failed and it will never become servable. Failed images never appear in lists; direct GET and PATCH requests return this error instead of an Image object, and batch PATCH reports it for the affected item. The message explains what went wrong so you can correct it and re-upload.

json
{
  "error": {
    "type": "processing_error",
    "code": "media_failed",
    "message": "This HEIC image could not be read."
  }
}

rate_limited

HTTP 429 · type: rate_limit_error. Too many requests. Includes a Retry-After HTTP header.

Back off for error.action.retry_after seconds when a wait action is present; the Retry-After header carries the same value.

json
{
  "error": {
    "type": "rate_limit_error",
    "code": "rate_limited",
    "message": "Too many requests. Retry later.",
    "action": {
      "type": "wait",
      "retry_after": 2520
    }
  }
}

validation_error

HTTP 422 · type: invalid_request_error. Invalid input: a bad TTL, a missing required field, a file that’s too large, unsupported, or one our image processor can’t decode, a non-patchable field on PATCH, an unsupported or malformed user selector, the retired public field from November 1, 2026 (until then it is accepted and ignored), an exceeded metadata or labels limit, a malformed label[…] list filter, and so on. Carries a details field with per-field messages; size, format and decoding problems report under file (for both multipart uploads and URL imports). A rejected upload stores nothing, so there is no Image to poll or delete.

For URL imports, a disallowed destination or redirect returns this code with details.url. Fix the URL before retrying.

json
{
  "error": {
    "type": "invalid_request_error",
    "code": "validation_error",
    "message": "Validation failed",
    "details": {
      "file": ["This JPEG image could not be decoded for transformation. The file may be damaged, exceed image dimension limits, or use an unsupported variant."],
      "ttl": ["TTL must be at least 5 minutes (300 seconds)"]
    }
  }
}

not_found

HTTP 404 · type: invalid_request_error. The media or resource doesn’t exist or isn’t in the storage your key reaches. An image in another App’s storage answers exactly like one that doesn’t exist.

json
{
  "error": {
    "type": "invalid_request_error",
    "code": "not_found",
    "message": "Media not found"
  }
}

workspace_unavailable

HTTP 409 · type: invalid_request_error. The storage this create writes to is being deleted, so it takes no new images: your App storage (the App, or its owner’s account, is being deleted) or a connected user’s storage (their account, or their connection, is being deleted). Don’t retry into it, and don’t create somewhere else in its place without asking the person. It takes images again only if the deletion is undone, for example when an account is restored.

json
{
  "error": {
    "type": "invalid_request_error",
    "code": "workspace_unavailable",
    "message": "The storage this request writes to is being deleted, so it takes no new images."
  }
}

state_conflict

HTTP 409 · type: invalid_request_error. A PATCH request supplied if_labels, but at least one expected label changed before the update committed. No requested fields were applied. Reload the image, then decide from its current state whether a new transition is still appropriate.

json
{
  "error": {
    "type": "invalid_request_error",
    "code": "state_conflict",
    "message": "Image labels changed before the update was applied"
  }
}

idempotency_key_conflict

HTTP 409 · type: idempotency_error. You reused an Idempotency-Key with a different request body. Use a fresh globally unique key for each logical create, or send the exact same body again during the 24-hour retry window to recover the original. After that window, replay and duplicate prevention are not guaranteed.

json
{
  "error": {
    "type": "idempotency_error",
    "code": "idempotency_key_conflict",
    "message": "Idempotency-Key was already used with a different request body"
  }
}

idempotency_key_in_progress

HTTP 409 · type: idempotency_error. A request with this Idempotency-Key is still being processed; a concurrent retry arrived before the original finished. The key is claimed before the work runs, so a retry backs off instead of creating a duplicate. Wait for Retry-After (mirrored in error.action.retry_after) and retry the same request.

json
{
  "error": {
    "type": "idempotency_error",
    "code": "idempotency_key_in_progress",
    "message": "A request with this Idempotency-Key is still being processed. Retry shortly.",
    "action": { "type": "wait", "retry_after": 2 }
  }
}

idempotency_recovery_conflict

HTTP 409 · type: idempotency_error. This Idempotency-Key belongs to an earlier upload that can no longer be replayed. Retrying it can’t help: stop retrying, check whether the earlier upload is in your list, and use a new key for new work.

json
{
  "error": {
    "type": "idempotency_error",
    "code": "idempotency_recovery_conflict",
    "message": "This Idempotency-Key belongs to an earlier upload that can no longer be replayed. Use a new key for new work."
  }
}

create_outcome_ambiguous

HTTP 503 · type: api_error. img.pro can’t confirm yet whether this upload was saved. Honor Retry-After and retry the identical request, with the same body and key: it returns the image if it was saved, and never makes a duplicate.

json
{
  "error": {
    "type": "api_error",
    "code": "create_outcome_ambiguous",
    "message": "img.pro is still confirming whether this upload was saved. Retry the same request after Retry-After.",
    "action": { "type": "wait", "retry_after": 2 }
  }
}

upload_failed

HTTP 500 · type: processing_error. Internal processing failure (an unexpected error while storing or transforming a valid upload). Caller-correctable problems, like too-large or unsupported files, are validation_error (422), not this; upload_failed means "retry later".

json
{
  "error": {
    "type": "processing_error",
    "code": "upload_failed",
    "message": "Upload failed"
  }
}

fetch_failed

HTTP 502 / 504 · type: processing_error. A URL import couldn’t fetch the source. An unavailable or invalid upstream response returns 502, and the 30-second request deadline returns 504.

json
{
  "error": {
    "type": "processing_error",
    "code": "fetch_failed",
    "message": "Could not fetch the URL"
  }
}

import_failed

HTTP 500 · type: processing_error. A URL import failed after the fetch succeeded.

json
{
  "error": {
    "type": "processing_error",
    "code": "import_failed",
    "message": "Import failed"
  }
}

server_error

HTTP 500 · type: api_error. Something unexpected failed on img.pro’s side, not in your request or your account. Retry in a moment.

json
{
  "error": {
    "type": "api_error",
    "code": "server_error",
    "message": "Something went wrong on img.pro's side. Retry in a moment."
  }
}

HTTP 503 on GET /v1/billing/status, with Retry-After and a wait action: img.pro is still confirming this user’s renewal. Keep the access you last read for the user and retry after Retry-After, so a moment of doubt never reads as no access.

json
{
  "error": {
    "type": "api_error",
    "code": "server_error",
    "message": "img.pro is still confirming this user's renewal. Keep the access you last read, and retry after Retry-After.",
    "action": { "type": "wait", "retry_after": 30 }
  }
}

update_failed

HTTP 500 · type: api_error. A media update failed server-side.

json
{
  "error": {
    "type": "api_error",
    "code": "update_failed",
    "message": "Update failed"
  }
}

delete_failed

HTTP 500 · type: api_error. A media deletion failed server-side.

json
{
  "error": {
    "type": "api_error",
    "code": "delete_failed",
    "message": "Delete failed"
  }
}

App API codes

App-wide keys (with optional X-Img-User) can also see a few App-specific codes: invalid_code (422), user_forbidden (403), app_suspended (403), and retryable app_context_unavailable (503). They use the same envelope; see Build an App → Errors and lifecycle for what each means and what to do.

An App-storage key can see app_blocked (403) on a write: the App is blocked, so every write (any method but GET, HEAD or OPTIONS) is refused until the block is lifted, while reads keep working. Write to support@img.pro. App-wide keys get app_suspended (403) to every request instead, as they do while the App is paused. A connected user’s own key is not the App’s, and keeps working as it does while the App is paused.

Handling errors

Here’s a comprehensive example showing how to handle errors, including action-based responses:

python
import requests
import time

def upload_image(api_key, filepath, caption=None):
    response = requests.post(
        "https://api.img.pro/v1/images",
        headers={"Authorization": f"Bearer {api_key}"},
        files={"file": open(filepath, "rb")},
        data={"caption": caption} if caption else {}
    )

    if response.ok:
        return response.json()

    err = response.json().get("error") or {}
    action = err.get("action") or {}

    # Branch on the coarse category, or the specific code.
    if action.get("type") == "wait":
        # Rate-limited with a known retry window. Back off and retry.
        time.sleep(action.get("retry_after", 60))
        return upload_image(api_key, filepath, caption)

    raise Exception(f"Upload failed [{err.get('type')}/{err.get('code')}]: {err.get('message')}")