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
{
"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.
{
"error": "unauthorized",
"message": "Invalid or missing API key"
}
forbidden
HTTP 403 — Valid key but insufficient permissions.
No action included.
{
"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:
{
"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):
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"error": "fetch_failed",
"message": "URL returned 404"
}
Timeout example (HTTP 504):
{
"error": "fetch_failed",
"message": "URL fetch timeout"
}
import_failed
HTTP 500 — URL import processing failed after fetch succeeded.
No action included.
{
"error": "import_failed",
"message": "Import failed"
}
update_failed
HTTP 500 — Media metadata update failed.
No action included.
{
"error": "update_failed",
"message": "Update failed"
}
delete_failed
HTTP 500 — Media deletion failed.
No action included.
{
"error": "delete_failed",
"message": "Delete failed"
}
Handling Errors in Code
Here's a comprehensive example showing how to handle errors, including action-based responses:
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']}")