Error Reference

Every API error response follows the same shape. Machine-readable codes, human-readable messages, and an optional action object that tells you exactly what to do next.

Response Shape

json
{
  "error": "error_code",
  "message": "Human-readable explanation",
  "action": {
    "type": "upgrade | wait | redirect",
    "url": "https://...",
    "label": "Button text for humans",
    "seconds": 3600
  },
  "usage": {
    "plan": "free",
    "uploads_used": 100,
    "uploads_limit": 100
  }
}

The action and usage fields are only present on certain errors. The action.seconds field is only present for wait type actions.

Action Types

Type When What To Do
upgrade Quota exceeded with a higher plan available Surface action.url to the user — it’s a signed, time-limited upgrade link.
wait Rate limited Wait action.seconds before retrying.
redirect Anonymous user hit a limit, or a quota-exceeded user is already on the highest plan Surface action.url — for anonymous flows it points to registration; for top-tier customers it points to billing/contact.

Error Codes

unauthorized

HTTP 401 — Invalid or missing API key.

No action included.

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

forbidden

HTTP 403 — Valid key but insufficient permissions.

No action included.

json
{
  "error": "forbidden",
  "message": "Insufficient permissions"
}

quota_exceeded

HTTP 403 — Upload or storage limit reached.

Includes an upgrade action when there’s a higher plan to move to, or a redirect action to billing/contact for accounts already on the top tier.

Upgrade available:

json
{
  "error": "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": 5242880000,
    "storage_limit_bytes": 5368709120
  }
}

Top-tier customer (no upgrade left):

json
{
  "error": "quota_exceeded",
  "message": "Monthly upload limit reached. You are on the highest plan.",
  "action": {
    "type": "redirect",
    "url": "https://img.pro/billing",
    "label": "Contact support"
  },
  "usage": { "plan": "max", "uploads_used": 1000000, "uploads_limit": 1000000, "storage_used_bytes": 5497558138880, "storage_limit_bytes": 5497558138880 }
}

rate_limited

HTTP 429 — Too many requests. Includes a Retry-After HTTP header.

Anonymous upload / import flows are rate-limited per IP and respond with a redirect action pointing to registration. The exact limit isn’t exposed publicly; clients should rely on Retry-After for the wait window.

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

validation_error

HTTP 422 — Invalid input (bad TTL, missing required field, sent a non-patchable field on PATCH, etc.).

No action included. Has an errors field instead with per-field messages.

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

not_found

HTTP 404 — Media or resource doesn't exist.

No action included.

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

external_id_duplicate

HTTP 409 — Another media item on this team already uses the external_id you sent. Treat as success: fetch the existing item with GET /v1/media/:id, or send a different external_id.

No action included.

json
{
  "error": "external_id_duplicate",
  "message": "An image with this external_id already exists"
}

upload_failed

HTTP 500 — File processing failed (invalid format, too large, or internal error).

No action included.

json
{
  "error": "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. Returns 504 if the request timed out (30 second limit).

No action included.

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

Timeout example (HTTP 504):

json
{
  "error": "fetch_failed",
  "message": "URL fetch timeout"
}

import_failed

HTTP 500 — URL import processing failed after fetch succeeded.

No action included.

json
{
  "error": "import_failed",
  "message": "Import failed"
}

update_failed

HTTP 500 — Media metadata update failed.

No action included.

json
{
  "error": "update_failed",
  "message": "Update failed"
}

delete_failed

HTTP 500 — Media deletion failed.

No action included.

json
{
  "error": "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/upload",
        headers={"Authorization": f"Bearer {api_key}"},
        files={"file": open(filepath, "rb")},
        data={"caption": caption} if caption else {}
    )

    if response.ok:
        return response.json()

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

    if action.get("type") == "upgrade":
        # Quota exceeded with a higher plan available.
        print(f"Limit reached. {action['label']}")
        print(f"Upgrade here: {action['url']}")
    elif action.get("type") == "redirect":
        # Anonymous flow hit a rate cap, OR a top-tier customer is out of quota.
        print(f"{data['message']} -> {action['url']}")
    elif action.get("type") == "wait":
        # Rate-limited with a known retry window. Back off and retry.
        time.sleep(action.get("seconds", 60))
        return upload_image(api_key, filepath, caption)

    raise Exception(f"Upload failed: {data['message']}")