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
{
"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.
| Type | Meaning | Codes |
|---|---|---|
invalid_request_error | The request was malformed or referenced something invalid. | validation_error, not_found, bad_request, … |
authentication_error | Missing or invalid credentials. | unauthorized |
permission_error | Authenticated, but not allowed. | forbidden, media_locked |
rate_limit_error | Too many requests. | rate_limited |
quota_error | Plan quota exhausted. | quota_exceeded |
idempotency_error | Idempotency-Key reused with a different body, or retried while the original is still in flight. | idempotency_key_conflict, idempotency_key_in_progress |
processing_error | Upload / import processing failed. | upload_failed, fetch_failed, import_failed |
api_error | Unexpected 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.
{
"error": {
"type": "authentication_error",
"code": "unauthorized",
"message": "Invalid or missing API key"
}
}
forbidden
HTTP 403 — Valid key but insufficient permissions. type: permission_error.
{
"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.
{
"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:
{
"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):
{
"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.
{
"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.
{
"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.
{
"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.)
{
"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.
{
"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.
{
"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).
{
"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.
{
"error": {
"type": "processing_error",
"code": "import_failed",
"message": "Import failed"
}
}
update_failed
HTTP 500 — Media metadata update failed. type: api_error.
{
"error": {
"type": "api_error",
"code": "update_failed",
"message": "Update failed"
}
}
delete_failed
HTTP 500 — Media deletion failed. type: api_error.
{
"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:
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')}")