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.

Response Shape

json
{
  "error": {
    "type": "quota_error",
    "code": "quota_exceeded",
    "message": "Human-readable explanation",
    "action": {
      "type": "upgrade | wait",
      "url": "https://...",
      "label": "Button text for humans",
      "retry_after": 3600
    },
    "usage": {
      "plan": "free",
      "uploads_used": 1000,
      "uploads_limit": 1000
    }
  }
}

The action, usage, 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, referenced something invalid, targeted an unavailable workspace, or lost a guarded write race. 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.
rate_limit_error
Too many requests. Codes: rate_limited.
quota_error
Plan quota exhausted. Codes: quota_exceeded.
idempotency_error
Idempotency-Key reused with a different body, retried while the original is still in flight, or no longer matches its canonical create row. 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
Server-side failure or a temporarily indeterminate/unavailable server state. Retryable examples include app_context_unavailable and create_outcome_ambiguous; terminal examples include update_failed and delete_failed.

Action types

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

upgrade quota
A quota was exceeded and a higher plan resolves it. Surface error.action.url — the directly-followable upgrade surface for the team (a signed upgrade link, the dashboard billing page, or an app’s co-branded billing page, depending on the team kind), or contact support for accounts already on the highest plan.
wait retry unchanged
The operation is temporarily blocked by a rate limit, unavailable App API authority/context, an identical create still in flight, or an indeterminate create write being reconciled. 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. Invalid or missing API key.

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

forbidden

HTTP 403 · type: permission_error. Valid key but insufficient permissions.

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

media_locked

HTTP 403 · type: permission_error. The image is moderation-locked and can’t be modifiedPATCH (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. When the request is authenticated as the image’s owner, the message includes the coarse reason. The reason is always 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."
  }
}

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."
  }
}

quota_exceeded

HTTP 403 · type: quota_error. Upload or storage limit reached. Includes an upgrade action: when there’s a higher plan, error.action.url is the directly-followable upgrade surface for the team — a signed upgrade link (personal accounts), the dashboard billing page (workspaces), or the app’s co-branded billing page (App API teams). Accounts already on the top tier receive a support email action.

Upgrade available:

json
{
  "error": {
    "type": "quota_error",
    "code": "quota_exceeded",
    "message": "Monthly upload limit reached.",
    "action": {
      "type": "upgrade",
      "url": "https://img.pro/upgrade/pro?team=4j2&ts=1709337600&sig=hmac...",
      "label": "Upgrade to Pro ($29/mo) for 10,000 uploads"
    },
    "usage": {
      "plan": "free",
      "uploads_used": 1000,
      "uploads_limit": 1000,
      "storage_used_bytes": 10737418240,
      "storage_limit_bytes": 10737418240
    }
  }
}

Top-tier customer (no upgrade left):

json
{
  "error": {
    "type": "quota_error",
    "code": "quota_exceeded",
    "message": "Monthly upload limit reached. You are on the highest plan.",
    "action": {
      "type": "upgrade",
      "url": "mailto:support@img.pro",
      "label": "Contact support"
    },
    "usage": { "plan": "max", "uploads_used": 200000, "uploads_limit": 200000, "storage_used_bytes": 2147483648000, "storage_limit_bytes": 2147483648000 }
  }
}

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,
      "label": "Retry later"
    }
  }
}

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 not decodable by the Images binding, a non-patchable field on PATCH, a malformed public value on create or PATCH (including null or blank), an exceeded metadata or labels limit, a malformed label[…] list filter, and so on. Carries a details field with per-field messages; size/format/decoder problems report under file (for both multipart uploads and URL imports). A rejected create leaves no Image to poll or delete.

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 accessible.

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

workspace_unavailable

HTTP 409 · type: invalid_request_error. The destination workspace is being deleted or is otherwise fenced against new writes. Do not create a replacement blindly: let the user restore or select an active workspace, then retry the request.

json
{
  "error": {
    "type": "invalid_request_error",
    "code": "workspace_unavailable",
    "message": "Workspace is being deleted"
  }
}

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. The canonical image identity for this key exists but no longer matches a safely recoverable version of the original operation. Stop automatic retries and inspect the operation or use a new key for genuinely new work.

json
{
  "error": {
    "type": "idempotency_error",
    "code": "idempotency_recovery_conflict",
    "message": "The original media operation can no longer be recovered safely"
  }
}

create_outcome_ambiguous

HTTP 503 · type: api_error. img.pro could not yet prove whether the canonical create write committed. Honor Retry-After and retry the identical request; do not change its body or key.

json
{
  "error": {
    "type": "api_error",
    "code": "create_outcome_ambiguous",
    "message": "The media operation is still being reconciled. Retry the same request.",
    "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 422 / 502 / 504 · type: processing_error. A URL import couldn’t fetch the source. A redirect to a disallowed address returns 422, 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"
  }
}

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

Apps acting on their users (machine secret + 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 Building an app → Errors for what each means and what to do.

Handling Errors in Code

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") == "upgrade":
        # Quota exceeded: surface the signed upgrade link (or billing page).
        print(f"Limit reached. {action['label']} -> {action['url']}")
    elif 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')}")