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:

http
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

Never expose your API key in client-side code or public repositories.

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 (5m90d). 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)

Request
bash
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"
Response
json
{
  "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)

Request
bash
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.

Response
json
{
  "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).

Request
bash
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.

Response
json
{
  "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.

Request
bash
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.
Request
bash
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.

Request
bash
curl -X DELETE "https://api.img.pro/v1/images/abc12345" \
  -H "Authorization: Bearer YOUR_API_KEY"
Response
json
{
  "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
Request
bash
curl "https://api.img.pro/v1/images?limit=20" \
  -H "Authorization: Bearer YOUR_API_KEY"
Response
json
{
  "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.
Request
bash
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}'
Response
json
{
  "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.

Response
json
{
  "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).
Request
bash
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"]}'
Response
json
{
  "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.

Request
bash
curl "https://api.img.pro/v1/usage" \
  -H "Authorization: Bearer YOUR_API_KEY"
Response
json
{
  "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 period
X-Monthly-Uploads-LimitMonthly upload limit
X-Monthly-Uploads-RemainingUploads remaining
X-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:

Request
bash
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:

Response
json
{
  "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:

Request
bash
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.

401unauthorizedMissing or invalid API key
403forbiddenKey lacks required permission
403quota_exceededUpload or storage limit reached
403media_lockedImage is moderation-locked and can’t be modified
404not_foundMedia item not found
409idempotency_key_conflictIdempotency-Key reused with a different body
409idempotency_key_in_progressSame Idempotency-Key still processing — wait for Retry-After
422validation_errorInvalid parameters (per-field errors)
429rate_limitedToo many requests
500upload_failedServer error during upload
500import_failedServer error during import
502fetch_failedCould not fetch import URL