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 | wait",
"url": "https://...",
"label": "Button text for humans",
"retry_after": 3600
},
"usage": {
"plan": "free",
"uploads_used": 1000,
"uploads_limit": 1000
}
}
}
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, referenced something invalid, targeted an unavailable workspace, or lost a guarded write race. Codes:
validation_error,not_found,workspace_unavailable,state_conflict,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, retried while the original is still in flight, or no longer matches its canonical create row. Codes:idempotency_key_conflict,idempotency_key_in_progress,idempotency_recovery_conflict.-
processing_error - Upload / import processing failed. Codes:
upload_failed,fetch_failed,import_failed,media_failed. -
api_error - Server-side failure or a temporarily indeterminate/unavailable server state. Retryable examples include
app_context_unavailableandcreate_outcome_ambiguous; terminal examples includeupdate_failedanddelete_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 contact support for accounts already on the highest plan. -
waitretry unchanged - The operation is temporarily blocked by a rate limit, unavailable App API authority/context, an identical create still in flight, or an indeterminate create write being reconciled. Back off for
error.action.retry_afterseconds (theRetry-Afterheader carries the same value), then retry the same operation.
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; direct GET and PATCH requests return this error instead of an Image object, and batch PATCH reports it for the affected item. 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). Accounts already on the top tier receive a support email action.
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 ($29/mo) for 10,000 uploads"
},
"usage": {
"plan": "free",
"uploads_used": 1000,
"uploads_limit": 1000,
"storage_used_bytes": 10737418240,
"storage_limit_bytes": 10737418240
}
}
}
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": "mailto:support@img.pro",
"label": "Contact support"
},
"usage": { "plan": "max", "uploads_used": 200000, "uploads_limit": 200000, "storage_used_bytes": 2147483648000, "storage_limit_bytes": 2147483648000 }
}
}
rate_limited
HTTP 429 · type: rate_limit_error. Too many requests. Includes a Retry-After HTTP header.
Back off for error.action.retry_after seconds when a wait action is present; the Retry-After header carries the same value.
{
"error": {
"type": "rate_limit_error",
"code": "rate_limited",
"message": "Too many requests. Retry later.",
"action": {
"type": "wait",
"retry_after": 2520,
"label": "Retry later"
}
}
}
validation_error
HTTP 422 · type: invalid_request_error. Invalid input: a bad TTL, a missing required field, a file that’s too large, unsupported, or not decodable by the Images binding, a non-patchable field on PATCH, a malformed public value on create or PATCH (including null or blank), an exceeded metadata or labels limit, a malformed label[…] list filter, and so on. Carries a details field with per-field messages; size/format/decoder problems report under file (for both multipart uploads and URL imports). A rejected create leaves no Image to poll or delete.
{
"error": {
"type": "invalid_request_error",
"code": "validation_error",
"message": "Validation failed",
"details": {
"file": ["This JPEG image could not be decoded for transformation. The file may be damaged, exceed image dimension limits, or use an unsupported variant."],
"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"
}
}
workspace_unavailable
HTTP 409 · type: invalid_request_error. The destination workspace is being deleted or is otherwise fenced against new writes. Do not create a replacement blindly: let the user restore or select an active workspace, then retry the request.
{
"error": {
"type": "invalid_request_error",
"code": "workspace_unavailable",
"message": "Workspace is being deleted"
}
}
state_conflict
HTTP 409 · type: invalid_request_error. A PATCH request supplied if_labels, but at least one expected label changed before the update committed. No requested fields were applied. Reload the image, then decide from its current state whether a new transition is still appropriate.
{
"error": {
"type": "invalid_request_error",
"code": "state_conflict",
"message": "Image labels changed before the update was applied"
}
}
idempotency_key_conflict
HTTP 409 · type: idempotency_error. You reused an Idempotency-Key with a different request body. Use a fresh globally unique key for each logical create, or send the exact same body again during the 24-hour retry window to recover the original. After that window, replay and duplicate prevention are not guaranteed.
{
"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 }
}
}
idempotency_recovery_conflict
HTTP 409 · type: idempotency_error. The canonical image identity for this key exists but no longer matches a safely recoverable version of the original operation. Stop automatic retries and inspect the operation or use a new key for genuinely new work.
{
"error": {
"type": "idempotency_error",
"code": "idempotency_recovery_conflict",
"message": "The original media operation can no longer be recovered safely"
}
}
create_outcome_ambiguous
HTTP 503 · type: api_error. img.pro could not yet prove whether the canonical create write committed. Honor Retry-After and retry the identical request; do not change its body or key.
{
"error": {
"type": "api_error",
"code": "create_outcome_ambiguous",
"message": "The media operation is still being reconciled. Retry the same request.",
"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 422 / 502 / 504 · type: processing_error. A URL import couldn’t fetch the source. A redirect to a disallowed address returns 422, an unavailable or invalid upstream response returns 502, and the 30-second request deadline returns 504.
{
"error": {
"type": "processing_error",
"code": "fetch_failed",
"message": "Could not fetch the URL"
}
}
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), and retryable app_context_unavailable (503). 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") == "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')}")