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 | signup | wait",
      "url": "https://...",
      "label": "Button text for humans",
      "retry_after": 3600
    },
    "usage": {
      "plan": "free",
      "uploads_used": 100,
      "uploads_limit": 100
    }
  }
}

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 or referenced something invalid. Codes: validation_error, not_found, 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, or retried while the original is still in flight. Codes: idempotency_key_conflict, idempotency_key_in_progress.
processing_error
Upload / import processing failed. Codes: upload_failed, fetch_failed, import_failed, media_failed.
api_error
Unexpected server-side failure. Codes: update_failed, 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 the billing/contact page for accounts already on the highest plan.
wait rate limit
Rate limited. Back off for error.action.retry_after seconds (the Retry-After header carries the same value), then retry — or follow error.action.url to remove the cap by signing up.
signup reserved
For an anonymous error an account would resolve. No error body carries it today — anonymous caps surface as wait (rate) or upgrade (quota), and the signup nudge on a successful anonymous upload rides the X-Img-Action response header, not the body. Handle it defensively (the example below does) so the branch stays valid if a future error adopts it, but don’t rely on receiving it.

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; this error is what a direct GET /v1/images/:id returns instead of the object. 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) — or the billing/contact page for accounts already on the top tier.

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 ($9/mo) for 1,000 uploads"
    },
    "usage": {
      "plan": "free",
      "uploads_used": 100,
      "uploads_limit": 100,
      "storage_used_bytes": 1073741824,
      "storage_limit_bytes": 1073741824
    }
  }
}

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": "https://img.pro/billing",
      "label": "Contact support"
    },
    "usage": { "plan": "max", "uploads_used": 100000, "uploads_limit": 100000, "storage_used_bytes": 1073741824000, "storage_limit_bytes": 1073741824000 }
  }
}

rate_limited

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

Anonymous upload / import flows are rate-limited per hour and per day, and respond with a wait action: back off for error.action.retry_after seconds (mirrored by the Retry-After header), or follow error.action.url to sign up and remove the cap. The current limits surface at runtime: the 429 message names which window tripped, and anonymous success responses carry X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset headers (plus -Daily variants). The concrete numbers vary and aren’t documented; rely on retry_after for the wait window rather than caching the cap.

json
{
  "error": {
    "type": "rate_limit_error",
    "code": "rate_limited",
    "message": "Hourly limit of <hourly> uploads reached. Resets in 42 minutes. Get an API key for higher limits.",
    "action": {
      "type": "wait",
      "retry_after": 2520,
      "url": "https://img.pro/auth/register",
      "label": "Create API Key"
    }
  }
}

validation_error

HTTP 422 · type: invalid_request_error. Invalid input: a bad TTL, a missing required field, a file that’s too large or in an unsupported format, a non-patchable field on PATCH, an exceeded metadata or labels limit, a malformed label[…] list filter, and so on. Carries a details field with per-field messages; size/format problems report under file (for both multipart uploads and URL imports).

json
{
  "error": {
    "type": "invalid_request_error",
    "code": "validation_error",
    "message": "Validation failed",
    "details": {
      "file": ["File too large. Maximum file size is 70 MB."],
      "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"
  }
}

idempotency_key_conflict

HTTP 409 · type: idempotency_error. You reused an Idempotency-Key with a different request body. Each key is locked to the first request body it’s used with (for 24 hours); use a fresh key for a different request, or send the exact same body again to get the original response back.

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

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. Returns 504 if the request timed out (30 second limit), or 422 if the URL redirected to a disallowed address.

json
{
  "error": {
    "type": "processing_error",
    "code": "fetch_failed",
    "message": "URL returned 404"
  }
}

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), already_subscribed (409), and invalid_target_url (422). 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") == "signup":
        # Anonymous flow: create an account / API key to lift the limit.
        print(f"{err['message']} -> {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')}")