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.

TypeMeaningCodes
invalid_request_errorThe request was malformed or referenced something invalid.validation_error, not_found, bad_request, …
authentication_errorMissing or invalid credentials.unauthorized
permission_errorAuthenticated, but not allowed.forbidden, media_locked
rate_limit_errorToo many requests.rate_limited
quota_errorPlan quota exhausted.quota_exceeded
idempotency_errorIdempotency-Key reused with a different body, or retried while the original is still in flight.idempotency_key_conflict, idempotency_key_in_progress
processing_errorUpload / import processing failed.upload_failed, fetch_failed, import_failed
api_errorUnexpected server-side failure.update_failed, delete_failed, …

Action Types

Type When What To Do
upgrade Quota exceeded — a higher plan (or billing/contact, for top-tier accounts) resolves it Surface error.action.url — a signed, time-limited upgrade link, or the billing page for accounts already on the highest plan.
signup Anonymous flow — create an account / API key for higher limits and permanent storage Surface error.action.url — it points to registration. (On a successful anonymous upload the same nudge is returned in the X-Img-Action response header, not the body.)
wait Rate limited Back off for error.action.retry_after seconds, then retry (or follow error.action.url to remove the cap by signing up). The Retry-After header carries the same value.

Error Codes

unauthorized

HTTP 401 — Invalid or missing API key. type: authentication_error.

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

forbidden

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

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

media_locked

HTTP 403 — The image is moderation-locked and can’t be modified or deleted. type: permission_error. In a batch operation 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"
  }
}

quota_exceeded

HTTP 403 — Upload or storage limit reached. type: quota_error. Includes an upgrade action — error.action.url is the signed upgrade link when there’s a higher plan, 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?t=abc&exp=1709337600&sig=hmac...",
      "label": "Upgrade to Pro ($19/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 — Too many requests. type: rate_limit_error. Includes a Retry-After HTTP header.

Anonymous upload / import flows are rate-limited 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 exact limit isn’t exposed publicly; rely on retry_after for the wait window.

json
{
  "error": {
    "type": "rate_limit_error",
    "code": "rate_limited",
    "message": "Anonymous upload rate limit reached. Sign up for an API key to remove the cap.",
    "action": {
      "type": "wait",
      "retry_after": 2520,
      "url": "https://img.pro/auth/register?from_cta=api",
      "label": "Create API Key"
    }
  }
}

validation_error

HTTP 422 — Invalid input (bad TTL, missing required field, sent a non-patchable field on PATCH, exceeded a metadata limit, etc.). type: invalid_request_error. Carries a details field with per-field messages.

json
{
  "error": {
    "type": "invalid_request_error",
    "code": "validation_error",
    "message": "Validation failed",
    "details": {
      "file": ["File is required"],
      "ttl": ["TTL must be at least 5 minutes (300 seconds)"]
    }
  }
}

not_found

HTTP 404 — Media or resource doesn't exist. type: invalid_request_error.

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

idempotency_key_conflict

HTTP 409 — You reused an Idempotency-Key with a different request body. type: idempotency_error. 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. (To attach an ID from your own system, store it as a metadata field such as external_id — it’s saved for your reference and never affects idempotency.)

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 — A request with this Idempotency-Key is still being processed; a concurrent retry arrived before the original finished. type: idempotency_error. 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 — File processing failed (invalid format, too large, or internal error). type: processing_error.

json
{
  "error": {
    "type": "processing_error",
    "code": "upload_failed",
    "message": "File too large: 150.00 MB. Maximum file size is 70 MB."
  }
}

fetch_failed

HTTP 502 / 504 — URL import couldn't fetch the source. type: processing_error. Returns 504 if the request timed out (30 second limit).

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

import_failed

HTTP 500 — URL import processing failed after fetch succeeded. type: processing_error.

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

update_failed

HTTP 500 — Media metadata update failed. type: api_error.

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

delete_failed

HTTP 500 — Media deletion failed. type: api_error.

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

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')}")