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.
-
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-Keyreused 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:
-
upgradequota - 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. -
waitrate limit - Rate limited. Back off for
error.action.retry_afterseconds (theRetry-Afterheader carries the same value), then retry — or followerror.action.urlto remove the cap by signing up. -
signupreserved - For an anonymous error an account would resolve. No error body carries it today — anonymous caps surface as
wait(rate) orupgrade(quota), and the signup nudge on a successful anonymous upload rides theX-Img-Actionresponse 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.
{
"error": {
"type": "authentication_error",
"code": "unauthorized",
"message": "Invalid or missing API key"
}
}
forbidden
HTTP 403 · type: permission_error. Valid key but insufficient permissions.
{
"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 modified — PATCH (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.
{
"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.
{
"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.
{
"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:
{
"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):
{
"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.
{
"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).
{
"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.
{
"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.
{
"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.
{
"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".
{
"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.
{
"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.
{
"error": {
"type": "processing_error",
"code": "import_failed",
"message": "Import failed"
}
}
update_failed
HTTP 500 · type: api_error. A media update failed server-side.
{
"error": {
"type": "api_error",
"code": "update_failed",
"message": "Update failed"
}
}
delete_failed
HTTP 500 · type: api_error. A media deletion failed server-side.
{
"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:
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')}")