API Reference
Complete reference for the img.pro REST API. All requests go to https://api.img.pro/v1. Every endpoint returns the Image object.
Authenticate every request with a Bearer token — Authorization: Bearer YOUR_API_KEY for your own account, or a machine secret plus an X-Img-User header when acting for an app’s users. The create endpoint also works with no token (anonymous). Authentication covers all three modes and where to get the credential; the endpoints below are the same regardless of which you use.
POST /v1/images
Upload an image file as multipart/form-data. Returns the Image object; url in the response is the live CDN link, ready to embed or transform.
-
fileFile required - The image file. Supported: JPEG, PNG, GIF, WebP, AVIF, HEIC, SVG, BMP, ICO. Max 70 MB (10 MB for SVG). The stored filename comes from the multipart file part.
-
captionstring - Free-text caption / description for the image. Up to 5,000 characters; longer is rejected with
422. -
published_atstring - Publish date: a unix timestamp or ISO-8601 date like
2024-06-01. Backdate it to sort the image under the photo’s date. Omit to default to upload time. -
ttlstring - Time-to-live: seconds (e.g.
3600) or a duration (5m–90d). Omit for permanent storage. -
metadataobject - Your attribution fields (
author,license, …) as a nested string→string map, identical to the response — the only channel for custom fields. On multipart, JSON-encode it into a singlemetadataform field. See Custom Fields.
Custom fields are returned nested under metadata, so they can never collide with a built-in field. Send them the same way — in the nested metadata object (JSON-encoded on multipart). An unrecognized top-level field is rejected with 422, never silently stored. See Custom Fields.
curl -X POST "https://api.img.pro/v1/images" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "file=@photo.jpg" \
-F "caption=Hero shot from launch day"
{
"id": "abc12345",
"object": "image",
"url": "https://src.img.pro/4j2/abc12345.jpg",
"page_url": "https://img.pro/abc12345",
"sizes": {
"small": { "url": "https://src.img.pro/4j2/abc12345.jpg?size=s", "width": 426, "height": 320 },
"medium": { "url": "https://src.img.pro/4j2/abc12345.jpg?size=m", "width": 853, "height": 640 },
"large": { "url": "https://src.img.pro/4j2/abc12345.jpg?size=l", "width": 1440, "height": 1080 }
},
"filename": "hero-shot.jpg",
"format": "jpg",
"width": 4000,
"height": 3000,
"bytes": 245678,
"transformable": true,
"status": "ready",
"public": true,
"published_at": "2024-01-01T00:00:00Z",
"expires_at": null,
"created_at": "2024-01-01T00:00:00Z",
"caption": "Hero shot from launch day",
"metadata": {},
"labels": {},
"nsfw": false
}
Most uploads return status: "ready" immediately. Formats that need conversion (HEIC, for example) return status: "processing" with null dimensions; poll GET /v1/images/:id until ready.
Anonymous uploads
The same request works without a Bearer token:
curl -X POST "https://api.img.pro/v1/images" \
-F "file=@photo.jpg"
The response is the same Image object, with three deltas from an authenticated create: expires_at is always set (anonymous uploads have a shorter retention — they expire after 30 days), the per-file size cap is smaller (20 MB), and the endpoint is rate-limited. A signup nudge also rides the X-Img-Action response header (see Response headers), never the body:
{
"id": "abc12345",
"object": "image",
"expires_at": "2024-01-31T00:00:00Z"
// (plus the standard Image fields)
}
Import from a URL
Send Content-Type: application/json with a url instead of a multipart file, and the server fetches the image for you (it must be publicly accessible; 30s timeout). All other fields from the table above work as JSON keys, and custom fields go in a nested metadata object (see Custom Fields). The filename is derived from the URL path, and metadata.source_url is set automatically to the imported URL.
curl -X POST "https://api.img.pro/v1/images" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/image.jpg",
"caption": "Imported from example.com"
}'
Idempotent retries
To make a create safe to retry, send an Idempotency-Key header with a unique value of your choosing:
curl -X POST "https://api.img.pro/v1/images" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: order-58213-hero" \
-F "file=@photo.jpg"
A retry with the same key and body returns the original response (flagged by the Idempotent-Replayed response header — see Response headers) instead of creating a duplicate. The same key with a different body returns 409 idempotency_key_conflict, and a retry while the original is still processing returns 409 idempotency_key_in_progress (back off per the Retry-After response header). Keys are kept for 24 hours.
GET /v1/images/:id
Get a single image. Public images can be fetched without authentication.
curl "https://api.img.pro/v1/images/abc12345" \
-H "Authorization: Bearer YOUR_API_KEY"
Returns the Image object. A blocked image returns 403 media_blocked and a failed image returns 422 media_failed; see Lifecycle.
PATCH /v1/images/:id
Update editable fields. The editable structural fields are caption, public, ttl, and published_at; an omitted field is left unchanged. Your own fields go in the nested metadata (attribution) and labels (selectors) objects — the same shapes the response returns, so you can GET an image, edit either map, and PATCH it straight back. A null value inside metadata or labels deletes that key; an unrecognized top-level field is rejected with 422 (see Custom Fields and Labels). A handful of upload-time fields are fixed once at upload and are off-limits on PATCH — sending nsfw, tool, defaults, or filename returns 422.
-
captionstring - Update caption (up to 5,000 characters). Send empty string or
nullto clear. -
publicboolean - Flip the media’s public/private state.
true/1/"true"= public;false/0/"false"= private. -
ttlstring - New TTL (e.g.
7d) ornullto make permanent. -
published_atstring - Publish date: a unix timestamp or ISO-8601 date like
2024-06-01. Backdate to re-sort the item.nullreturns 422; every image keeps a publish date. -
metadataobject - Nested string→string map of your attribution fields (canonical — mirrors the response). Merged with existing metadata; a
nullvalue removes a key. See Custom Fields. -
labelsobject - Nested string→string map of your selector labels (mirrors the response). Merged with existing labels; a
nullvalue removes a key. See Labels.
curl -X PATCH "https://api.img.pro/v1/images/abc12345" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"caption": "Updated caption", "public": true, "metadata": {"alt_text": "A vivid sunset", "camera": null}}'
Returns the updated Image object.
DELETE /v1/images/:id
Permanently delete an image and all its CDN variants. Returns a tombstone confirming the deletion: the id and object, plus deleted: true. Deleting an already-deleted image returns 404.
curl -X DELETE "https://api.img.pro/v1/images/abc12345" \
-H "Authorization: Bearer YOUR_API_KEY"
{
"id": "abc12345",
"object": "image",
"deleted": true
}
GET /v1/images
List images with cursor-based pagination (cursor only, no offset). Lists carry only servable media: blocked and failed images are excluded (see Lifecycle).
-
idsstring - Comma-separated IDs to fetch specific items. Cannot be combined with
label[…]filters. -
label[key]string - Filter by a label:
?label[state]=pending. A comma-separated value isIN(?label[state]=pending,likedmatches either); distinct keys AND together. At most 50 value alternatives across all keys. A key you never set matches nothing (an empty page, not an error). Keep the same filter across every page of one walk —next_urlcarries it forward for you. -
label[!key](flag) - Match media that has no
keylabel at all (the Kubernetes!key/ does-not-exist selector). Takes no value. This is how you reach media created outside your app — a value filter can’t, since an absent key never matches a value. Composes with value filters:?label[kind]=upload&label[!state]. See Labels. -
limitinteger default 50 1–100.-
cursorstring - Opaque pagination cursor from the previous response.
curl "https://api.img.pro/v1/images?limit=20" \
-H "Authorization: Bearer YOUR_API_KEY"
# filtered by label (see Labels below)
curl "https://api.img.pro/v1/images?label%5Bstate%5D=pending&limit=20" \
-H "Authorization: Bearer YOUR_API_KEY"
{
"object": "list",
"data": [
{
"id": "abc12345",
"object": "image",
"url": "https://src.img.pro/4j2/abc12345.jpg",
"page_url": "https://img.pro/abc12345",
"sizes": {
"small": { "url": "https://src.img.pro/4j2/abc12345.jpg?size=s", "width": 426, "height": 320 },
"medium": { "url": "https://src.img.pro/4j2/abc12345.jpg?size=m", "width": 853, "height": 640 },
"large": { "url": "https://src.img.pro/4j2/abc12345.jpg?size=l", "width": 1440, "height": 1080 }
},
"filename": "hero-shot.jpg",
"format": "jpg",
"width": 4000,
"height": 3000,
"bytes": 245678,
"transformable": true,
"status": "ready",
"public": true,
"published_at": "2024-01-01T00:00:00Z",
"expires_at": null,
"created_at": "2024-01-01T00:00:00Z",
"caption": "Hero shot from launch day",
"metadata": {},
"labels": {},
"nsfw": false
}
],
"pagination": {
"has_more": true,
"next_cursor": "1704067200_42",
"next_url": "https://api.img.pro/v1/images?cursor=1704067200_42&limit=20"
}
}
The pagination object holds has_more, the opaque next_cursor, and a ready-to-call next_url for the next page. On the final page all three are terminal: has_more: false, next_cursor: null, next_url: null.
PATCH /v1/images/batch
Update editable fields on up to 100 items at once (more than 100 ids returns 422). Same field restrictions as the single-item PATCH; caption, public, ttl, and published_at apply uniformly to every id in the batch, and metadata / labels merge into each item.
-
idsstring[] required - Array of media IDs (max 100).
-
captionstring - Same caption applied to every item (empty string or
nullclears). -
publicboolean - Flip every item’s public/private state in one call.
-
ttlstring - Same TTL applied to every item (e.g.
7d) ornullto make all permanent. -
metadataobject - Nested string→string map merged into every item’s attribution metadata. A
nullvalue removes that key on each item. -
labelsobject - Nested string→string map merged into every item’s selector labels. A
nullvalue removes that key on each item. See Labels.
curl -X PATCH "https://api.img.pro/v1/images/batch" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"ids": ["abc12345", "def45678"], "ttl": "7d", "public": false}'
{
"object": "batch_result",
"data": [
{ "id": "abc12345", "object": "image", "url": "https://src.img.pro/4j2/abc12345.jpg", "public": false },
{ "id": "def45678", "object": "image", "url": "https://src.img.pro/4j2/def45678.jpg", "public": false }
],
"errors": []
}
Every batch response is one batch_result envelope. data holds one entry per successfully updated item, each a complete Image object (abbreviated above for space). errors holds one entry per item that failed.
When some items are skipped (a moderation-locked image, for example) the call returns HTTP 207: those ids appear in errors, each with a nested error object (a coarse type, a specific code, and a human-readable message — the same shape detailed under Errors below), and only the successful items appear in data.
{
"object": "batch_result",
"data": [
{ "id": "abc12345", "object": "image", "url": "https://src.img.pro/4j2/abc12345.jpg", "public": false }
],
"errors": [
{ "id": "def45678", "error": { "type": "permission_error", "code": "media_locked", "message": "Media cannot be modified" } }
]
}
DELETE /v1/images/batch
Delete up to 100 items by ID (more than 100 ids returns 422). Every requested id is reported back with a tombstone in data.
-
idsstring[] required - Array of image IDs to delete (max 100).
curl -X DELETE "https://api.img.pro/v1/images/batch" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"ids": ["abc12345", "def45678"]}'
{
"object": "batch_result",
"data": [
{ "id": "abc12345", "object": "image", "deleted": true },
{ "id": "def45678", "object": "image", "deleted": true }
],
"errors": []
}
Same batch_result envelope as batch update; here data holds a tombstone per requested id. Deletion is idempotent: every id reports deleted: true, whether it was removed now or was already gone (matching the single DELETE /v1/images/:id).
GET /v1/usage
Current quota and usage statistics.
curl "https://api.img.pro/v1/usage" \
-H "Authorization: Bearer YOUR_API_KEY"
{
"object": "usage",
"monthly": {
"uploads": 42,
"uploads_limit": 100,
"uploads_remaining": 58,
"resets_at": "2024-03-01T00:00:00Z"
},
"totals": {
"images_stored": 142,
"storage_used_bytes": 52428800,
"storage_limit_bytes": 1073741824,
"storage_remaining_bytes": 1021313024
},
"plan": "free"
}
Response headers
Every authenticated response includes quota information in headers:
-
X-Monthly-Uploads-Used - Uploads used this billing period.
-
X-Monthly-Uploads-Limit - Monthly upload limit.
-
X-Monthly-Uploads-Remaining - Uploads remaining.
-
X-Storage-Used - Storage used, in bytes.
-
X-Storage-Limit - Storage limit, in bytes.
-
X-Storage-Remaining - Storage remaining, in bytes.
Depending on the endpoint and the request, you may also see these headers:
-
X-Img-Action - JSON signup nudge on anonymous creates (the nudge never appears in the body).
-
X-RateLimit-Limit - Request limit for the current window (plus
-Dailyvariant) on anonymous flows. -
X-RateLimit-Remaining - Requests remaining in the window (plus
-Dailyvariant) on anonymous flows. -
X-RateLimit-Reset - When the window resets (plus
-Dailyvariant) on anonymous flows. -
Retry-After - Back-off seconds on
429andidempotency_key_in_progress. -
Idempotent-Replayed truewhen a create was replayed from an earlierIdempotency-Key.-
X-Processing-Time - Server processing time in milliseconds, set on every create response.
The X-Img-Action value is a JSON object, e.g. {"type":"signup","url":"https://img.pro/auth/register","message":"Sign up for permanent storage, larger files, and higher limits","label":"Create Account"}. All headers above are exposed for cross-origin reads.
Custom Fields
Your own fields live in the nested metadata object — the single channel for custom data, on input and output alike. The request shape mirrors the response, so the round-trip is symmetric: GET an image, edit metadata, and send it straight back. A custom field sent at the top level is rejected with 422, never silently stored.
Set fields
On a JSON request (URL import or PATCH), send a nested metadata object:
curl -X POST "https://api.img.pro/v1/images" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/photo.jpg",
"caption": "Sunset over the Pacific",
"metadata": { "author": "Jane Doe", "license": "cc-by-4.0" }
}'
On a multipart upload a form field can’t hold a nested object, so JSON-encode your fields into a single metadata form field. Custom fields sent as bare top-level form fields are rejected with 422:
curl -X POST "https://api.img.pro/v1/images" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "file=@photo.jpg" \
-F "caption=Sunset over the Pacific" \
-F 'metadata={"author":"Jane Doe","license":"cc-by-4.0"}'
Read fields
Custom fields come back nested under metadata. What you send is what you get back:
{
"id": "abc12345",
"object": "image",
"url": "https://src.img.pro/4j2/abc12345.jpg",
"page_url": "https://img.pro/abc12345",
"format": "jpg",
"status": "ready",
"caption": "Sunset over the Pacific",
"created_at": "2024-01-01T00:00:00Z",
"metadata": {
"author": "Jane Doe",
"license": "cc-by-4.0"
}
}
Update fields
PATCH the nested metadata object; it merges with the existing map. Set a key to null to remove it (keys you don’t mention are left unchanged):
curl -X PATCH "https://api.img.pro/v1/images/abc12345" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"metadata": {"alt_text": "A vivid sunset", "camera": null}}'
Field-name safety
The Image object’s own field names round-trip safely: you can GET a response, tweak it, and POST or PATCH it back, and those keys (id, url, format, status, …) are accepted-and-ignored rather than rejected. Any other unrecognized top-level field is rejected with 422 — custom fields belong in the nested metadata object. Internal routing markers (_team_id, _csrf, and the rest of the _-prefixed set) are stripped before storage.
metadata is a flat string→string map with limits: ≤ 50 keys, each key ≤ 64 chars and value ≤ 1024 chars. Exceeding any returns 422 validation_error with the offending field in details.
Upload-time fields (nsfw, tool, defaults, filename) are fixed at upload and rejected on PATCH; see PATCH /v1/images/:id for the editable-field rule.
Labels
metadata describes an image; labels select images. When your app needs to fetch images by machine state — a review queue, a kind, a workflow step — write a label and filter the list on it, instead of listing everything and filtering client-side.
Write labels
Labels are written exactly like metadata: a nested object on upload or PATCH, with merge semantics — keys you don’t send are preserved, and a null value clears one key. On multipart, JSON-encode them into a single labels form field.
curl -X PATCH "https://api.img.pro/v1/images/abc12345" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"labels": {"state": "liked"}}' # merges; other labels untouched
# clear one key
curl -X PATCH "https://api.img.pro/v1/images/abc12345" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"labels": {"state": null}}'
Filter a list
Add label[key]=value params to GET /v1/images. A comma-separated value matches any of the alternatives (IN); distinct keys must all match (AND):
# state == pending
curl "https://api.img.pro/v1/images?label%5Bstate%5D=pending" \
-H "Authorization: Bearer YOUR_API_KEY"
# state IN (pending, liked) — CSV is OR
curl "https://api.img.pro/v1/images?label%5Bstate%5D=pending,liked" \
-H "Authorization: Bearer YOUR_API_KEY"
# state == pending AND kind == generated — distinct keys AND
curl "https://api.img.pro/v1/images?label%5Bstate%5D=pending&label%5Bkind%5D=generated" \
-H "Authorization: Bearer YOUR_API_KEY"
Find media without a label
Prefix the key with ! to match media that has no such label — the Kubernetes !key (does-not-exist) selector. It takes no value. This is the only way to reach media created outside your app (an img.pro web or plain-API upload your app never labeled): a value filter can’t, because an absent key never matches a value. It composes with value filters, so “uploads I haven’t triaged yet” is one call:
# media with no "state" label at all
curl "https://api.img.pro/v1/images?label%5B%21state%5D" \
-H "Authorization: Bearer YOUR_API_KEY"
# kind == upload AND no "state" label — the "unlabeled inbox"
curl "https://api.img.pro/v1/images?label%5Bkind%5D=upload&label%5B%21state%5D" \
-H "Authorization: Bearer YOUR_API_KEY"
Value filters are equality + IN. Filtering a value on a key you never set returns an empty page, not an error. A filter can carry at most 50 value alternatives across all keys and 20 keys total (label[!key] counts as a key, carries no value), and cannot be combined with ids. Keep the same filter across every page of one walk — the cursor is a position in that filtered ordering, and next_url carries your filter forward automatically.
Bounds
labels is deliberately tighter than metadata — the bound is what makes it safe to promise a filter contract on: at most 20 labels per image, keys matching ^[a-z0-9_.-]{1,64}$, values ≤ 128 chars with no commas (the filter’s IN separator) and no leading/trailing whitespace (the filter trims values). Violations return 422 validation_error naming the offending key. Keep rich, human-facing data in metadata; keep short machine selectors in labels.
Errors
Every error is one nested error object: a coarse error.type, a specific error.code, a human-readable error.message, and (conditionally) error.action, error.details, or error.usage (a quota snapshot on quota_exceeded). Branch on type or switch on code; see the Error Reference for full details.
-
unauthorized401 - Missing or invalid API key.
-
forbidden403 - Key lacks the required ability (read / write).
-
quota_exceeded403 - Upload or storage limit reached.
-
media_locked403 - Image is moderation-locked and can’t be modified.
-
media_blocked403 - Image is blocked and can’t be retrieved.
-
not_found404 - Media item not found.
-
idempotency_key_conflict409 - Idempotency-Key reused with a different body.
-
idempotency_key_in_progress409 - Same Idempotency-Key still processing; wait for Retry-After.
-
validation_error422 - Invalid parameters (per-field errors in
details). -
media_failed422 - Image processing failed; the message explains why.
-
rate_limited429 - Too many requests.
-
upload_failed500 - Server error during upload.
-
import_failed500 - Server error during import.
-
fetch_failed502 - Could not fetch the import URL.