API Reference
Complete reference for the img.pro REST API. All requests go to https://api.img.pro/v1.
Authentication
Include your API key as a Bearer token:
Authorization: Bearer YOUR_API_KEY
Sign up at img.pro, then create a key from the dashboard.
Keys carry one or both abilities: read (the GET endpoints) and write (POST / PATCH / DELETE). Calling an endpoint your key isn’t scoped for returns 403 forbidden — choose the abilities when you create the key.
The create endpoint works without authentication. Anonymous uploads expire after 30 days and are rate-limited. Include a Bearer token for ownership, permanent storage, and higher limits.
Keep your API key secret
The Image object
Every endpoint returns this same object. The two fields you’ll reach for most are url (the image’s direct CDN link) and sizes (ready-made responsive variants); the rest carry the image’s metadata and state.
| Field | Type | Presence / notes |
|---|---|---|
| id | string | Always. The image id — also its URL slug. |
| object | "image" | Always. |
| url | string | Always (every plan). The image itself: a direct, embeddable, transformable CDN URL. |
| page_url | string | Always. The shareable viewer page — the link you send to a person to open the image in a browser. |
| sizes | object | Always (every plan; {} for non-transformable sources). Responsive variants small / medium / large, each { url, width, height }. For a social/OG card, add ?size=social to the image url. |
| filename | string | Always. Original filename; falls back to {id}.{format}. |
| format | string | Always. The output format, normalized — a stored HEIC serves as jpg. |
| width | number|null | Always. null while processing. |
| height | number|null | Always. null while processing. |
| bytes | number|null | Always. null while processing. |
| transformable | boolean | Always. false for sources that can’t be CDN-transformed (SVG/BMP/ICO). |
| status | ready|processing|failed | Always. |
| public | boolean | Always. |
| published_at | string|null | Always. Publish/display date, ISO-8601 UTC; null = draft. |
| expires_at | string|null | Always. ISO-8601 UTC; null = permanent. |
| created_at | string | Always. ISO-8601 UTC (e.g. 2024-01-01T00:00:00Z). |
| caption | string|null | Always. null when unset. |
| metadata | object | Always (may be {}). Your open field map; nested, so it never shadows a core field. |
| nsfw | boolean | Always. true when flagged, else false. |
| blocked | boolean | Always. true when moderation-locked (visible to any viewer), else false. |
| block_reason | string|null | Always. null unless locked AND the authenticated owner. Coarse: content_policy / dmca / spam / terms. |
| failure_reason | string|null | Always. null unless status="failed" — friendly text. |
sizes gives three ready-made responsive variants (included, every plan). For arbitrary dimensions, format, effects, or the social/OG card (?size=social), apply transform params to url — see Transformations.
POST /v1/images
Create an image — one endpoint for both file uploads and URL imports. The server negotiates by Content-Type: multipart/form-data uploads a file, application/json imports from a url. Authentication optional. The filename is taken from the multipart file part (uploads) or derived from the URL path (imports) — clients can’t override it.
Send either a file (multipart) or a url (JSON) — never both. The request Content-Type selects the mode.
| Parameter | Type | Required | Description |
|---|---|---|---|
| file | File | Yes | Image file — multipart mode. Send this or url, exactly one. Supported: JPEG, PNG, GIF, WebP, AVIF, HEIC, SVG, BMP, ICO. Max 70 MB (10 MB for SVG, 20 MB anonymous). |
| url | string | Yes | Image URL to import — JSON mode. Send this or file, exactly one. Must be publicly accessible, 30s timeout. |
| caption | string | No | Free-text caption / description for the image. |
| published_at | string | No | Publish date — a unix timestamp or ISO-8601 date 2024-06-01 (backdate to sort under the photo’s date). Omit to default to upload time. |
| ttl | string | No | Time-to-live: seconds (e.g., 3600) or duration (5m–90d). Omit for permanent storage. |
| Idempotency-Key | header | No | Opt-in retry-safety. Same key + body replays the original response (Idempotent-Replayed: true); same key + different body → 409. Retained 24h. |
| any field | string | No | Any other field (including your own external_id) is stored as attribution metadata (e.g., author, license, source_url) |
Upload a file (authenticated)
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": {},
"nsfw": false,
"blocked": false,
"block_reason": null,
"failure_reason": null
}
url is the image itself — a direct CDN URL you can apply transforms to, available on every plan (including anonymous). page_url is the shareable viewer page you send to a person, and sizes holds three ready-made responsive variants.
For retry-safety, send an Idempotency-Key header: a replay with the same key + body returns the original response (Idempotent-Replayed: true); the same key with a different body returns 409. Keys are retained 24h. To tag an image with an ID from your own system, send it as a custom field such as external_id — it’s stored under metadata for your reference and doesn’t affect idempotency.
Anonymous (no auth)
curl -X POST "https://api.img.pro/v1/images" \
-F "file=@photo.jpg"
Returns the same Image object (the body stays a pure resource), with one difference: expires_at is non-null (anonymous uploads expire after 30 days). A signup nudge rides the X-Img-Action response header — not the body. Anonymous uploads are rate-limited — sign up for an API key to remove the cap.
{
"id": "abc12345",
"object": "image",
"expires_at": "2024-01-29T00:00:00Z"
// (plus the standard Image fields)
}
// Response header:
// X-Img-Action: {"type":"signup","url":"https://img.pro/auth/register?from_cta=api","label":"Create Account"}
Import from a URL (JSON body)
Send Content-Type: application/json with a url instead of a multipart file. The filename is derived from the URL path — clients can’t override it. Same fields as the upload form above (set them as JSON keys).
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",
"external_id": "ext-12345"
}'
external_id (and any other custom field) is stored as metadata — it comes back nested under metadata, never at the top level.
{
"id": "def45678",
"object": "image",
"filename": "image.jpg",
"caption": "Imported from example.com",
"metadata": { "source_url": "https://example.com/image.jpg", "external_id": "ext-12345" }
// (plus the standard Image fields)
}
Returns the Image object. The filename is derived from the URL path, and metadata.source_url is set automatically to the imported URL.
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.
PATCH /v1/images/:id
Update editable fields. Only the params below (plus arbitrary attribution metadata) are accepted — sending nsfw, tool, defaults, or filename returns 422.
| Parameter | Type | Required | Description |
|---|---|---|---|
| caption | string | No | Update caption. Send empty string or null to clear. |
| public | boolean | No | Flip the media’s public/private state. true / 1 / "true" = public; false / 0 / "false" = private. |
| ttl | string | No | New TTL (e.g., 7d) or null to make permanent. |
| published_at | string | No | Publish date — a unix timestamp or ISO-8601 date 2024-06-01. Backdate to re-sort the item. null returns 422 — every image keeps a publish date. |
| any field | string|null | No | Merged with existing attribution metadata (your external_id included). null removes the field. |
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, "camera": null}'
Returns the Image object.
DELETE /v1/images/:id
Permanently delete an image and all its CDN variants. Returns a tombstone confirming the deletion (the id + object, plus deleted: true). Idempotent — 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).
| Parameter | Type | Required | Description |
|---|---|---|---|
| ids | string | No | Comma-separated IDs to fetch specific items |
| limit | integer | No | 1–100 (default: 50) |
| cursor | string | No | Opaque pagination cursor from the previous response |
curl "https://api.img.pro/v1/images?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": {}
}
],
"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.
| Parameter | Type | Required | Description |
|---|---|---|---|
| ids | string[] | Yes | Array of media IDs (max 100). |
| caption | string | No | Same caption applied to every item (empty string or null clears). |
| public | boolean | No | Flip every item’s public/private state in one call. |
| ttl | string | No | Same TTL applied to every item (e.g. 7d) or null to make all permanent. |
| any field | string|null | No | Merged into every item’s attribution metadata. null removes the field. |
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 (the same shape a single-item PATCH returns; abbreviated above for space). errors holds one entry per item that failed.
When some items are skipped — e.g. moderation-locked — the call returns HTTP 207: those ids appear in errors (each a nested error object), 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 — a tombstone in data for each.
| Parameter | Type | Required | Description |
|---|---|---|---|
| ids | string[] | Yes | 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"
}
Quota Headers
Every authenticated response includes quota information in headers:
X-Monthly-Uploads-UsedUploads used this billing periodX-Monthly-Uploads-LimitMonthly upload limitX-Monthly-Uploads-RemainingUploads remainingX-Storage-UsedStorage used (bytes)X-Storage-LimitStorage limit (bytes)X-Storage-RemainingStorage remaining (bytes)Other response headers you may see: X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset (plus -Daily variants) on anonymous flows; Retry-After on 429 and idempotency_key_in_progress; Idempotent-Replayed: true on an idempotency replay; and X-Img-Action (the JSON signup nudge) on anonymous creates. All are exposed for cross-origin reads.
Custom Fields
Any field you send that isn’t an Image-object field name is stored as attribution metadata.
Set fields
Include them alongside other parameters — no wrapper needed:
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 "author=Jane Doe" \
-F "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 merges with existing attribution metadata. Set a field to null to remove it:
curl -X PATCH "https://api.img.pro/v1/images/abc12345" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"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 without any of those keys (id, url, format, status, …) leaking into your metadata map. Any other field becomes metadata. 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.
A handful of upload-time fields are off-limits on PATCH and return 422 if you send them: nsfw, tool, defaults, and filename are fixed once at upload. The editable structural fields are caption, public, ttl, and published_at — and your external_id (or any custom field) merges into metadata.
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.
unauthorizedMissing or invalid API keyforbiddenKey lacks required permissionquota_exceededUpload or storage limit reachedmedia_lockedImage is moderation-locked and can’t be modifiednot_foundMedia item not foundidempotency_key_conflictIdempotency-Key reused with a different bodyidempotency_key_in_progressSame Idempotency-Key still processing — wait for Retry-Aftervalidation_errorInvalid parameters (per-field errors)rate_limitedToo many requestsupload_failedServer error during uploadimport_failedServer error during importfetch_failedCould not fetch import URL