Optimagio

API Documentation

v2.0
Back to Home

Contents

Overview

The Optimagio API provides powerful image optimization capabilities. Upload images via URL or file upload, or send a full folder as an upload-first batch, and receive optimized versions with significant size reductions while maintaining visual quality.

Base URL: https://api.optimagio.dev

Key Features

  • Input support for JPEG/JPG, PNG, WebP, GIF, AVIF, TIFF, and BMP; output support for JPEG, PNG, WebP, GIF, AVIF, or auto
  • URL-based and file upload optimization
  • Upload-first batch optimization with automatic batch creation, progress events, and ZIP download
  • Configurable compression levels and output formats
  • Image resizing with dimension constraints (up to 8000px)
  • EXIF metadata is stripped by default; set keep_exif=true to opt in
  • Storage Library persistence is explicit; set storage_commit=true when an API request should save optimized results
  • Automatic format selection with auto option
  • Caching of optimized results
  • Dashboard and admin analytics for API-key traffic and batch activity
  • Multi-tier access controls with different quotas

Authentication

All API requests require authentication using an API key. Include your API key in the request headers.

Option 1: X-API-Key Header

X-API-Key: your_api_key_here

Option 2: Authorization Header

Authorization: Bearer your_api_key_here

Get your API key: Sign up for an account and generate your API key from the dashboard. API keys use the format opt_.... The plaintext key is returned exactly once when created; active key counts are tier-limited (Free 1, Starter 3, Pro 10, Business 25, Scale custom by contract).

Endpoints

Optimize Image

POST /api/optimize

Optimize an image from URL or file upload with configurable parameters.

Request Parameters

ParameterTypeRequiredDescription
urlstringYes*Image URL to optimize (required if not using file upload)
accept_filebooleanNoSet to true for file upload mode
imagefileYes*Image file (required if accept_file=true)
compressionstringNoCompression level: auto, low, medium, max (default: auto)
widthintegerNoTarget width in pixels (max 8000)
heightintegerNoTarget height in pixels (max 8000)
formatstringNoOutput format: jpeg, png, webp, gif, avif, auto
keep_exifbooleanNoPreserve EXIF metadata (default: false)
storage_commitbooleanNoSave optimized result to Storage Library (default: false)
keep_originalbooleanNoRetain original source bytes in Storage Library; requires storage_commit=true (default: false)

Response

{
  "success": true,
  "cached": false,
  "original_filename": "example.jpg",
  "original_size": 1048576,
  "optimized_size": 524288,
  "compression_ratio": "50.0% saved",
  "percent": 50,
  "data": {
    "id": "v2-a1b2c3d4-cmax-w1920-h1080-jpeg-exiftrue",
    "src_url": "https://example.com/original.jpg",
    "optimized_url": "/api/images/v2-a1b2c3d4-cmax-w1920-h1080-jpeg-exiftrue",
    "size": 524288,
    "width": 1920,
    "height": 1080,
    "format": "jpeg",
    "processed_at": "2026-04-28T10:30:00Z"
  }
}

Caching: The id is a content + parameter hash, not a UUID. Re-submitting the same image with the same parameters returns the same id and cached: true — no recomputation cost.

Storage Library: Optimize requests do not save optimized results or originals by default. Send storage_commit=true to persist the optimized result. Add keep_original=true only when you also want the original retained; keep_original=true without storage_commit=true is rejected.

URL mode constraints: Only http / https are allowed; private and internal IP ranges are blocked; downloaded source size is capped at 50 MB. The optimization request body itself is capped at 10 MB.

Optimize Batch

POST /api/v1/optimize/batch

Upload multiple image files directly. Optimagio creates the batch automatically, generates job_id values server-side, queues accepted files, and returns links for status, SSE progress, and ZIP download.

Simple multipart fields

FieldTypeRequiredDescription
files[]fileYesRepeat for each image. Up to 100 files per request.
output_formatstringNoOutput format. Alias: format.
compressionstringNoCompression level such as auto, low, medium, or max.
resize_widthintegerNoTarget width. Alias: width.
resize_heightintegerNoTarget height. Alias: height.
keep_exifbooleanNoPreserve EXIF metadata when the output encoder supports it.
storage_commitbooleanNoSave accepted optimized results to Storage Library. Alias in manifest: options.storage_commit.
keep_originalbooleanNoRetain originals for accepted files; requires storage_commit=true. Alias in manifest: options.keep_original.

Response

{
  "batch_id": "1e57527d-2c52-4d40-95a4-4b43c438c06a",
  "client_batch_id": "catalog-2026-05",
  "status": "processing",
  "created": true,
  "finalized": true,
  "accepted": 3,
  "rejected": 0,
  "rejected_files": [],
  "files": [
    {
      "job_id": "c0e8bcb3-410b-485f-b5ee-2cb4493b9e78",
      "client_file_id": "file_0",
      "filename": "hero.jpg",
      "status": "queued",
      "status_url": "/api/v1/jobs/c0e8bcb3-410b-485f-b5ee-2cb4493b9e78"
    }
  ],
  "links": {
    "status": "/api/v1/batches/1e57527d-2c52-4d40-95a4-4b43c438c06a",
    "status_page": "/api/v1/batches/1e57527d-2c52-4d40-95a4-4b43c438c06a?files_limit=1000&files_offset=0",
    "events": "/api/v1/batches/1e57527d-2c52-4d40-95a4-4b43c438c06a/events",
    "download_zip": "/api/v1/batches/1e57527d-2c52-4d40-95a4-4b43c438c06a/download"
  }
}

Batch lifecycle: use GET /api/v1/batches/:id for status, files_limit / files_offset for large status reads, GET /api/v1/batches/:id/events for optional SSE progress, and GET /api/v1/batches/:id/download for the completed ZIP. These routes accept API-key auth and verify batch ownership. The ZIP is the canonical bulk retrieval path; per-file download_url values are convenience links and can expire. If any completed result is missing from storage, the ZIP endpoint returns 409 partial_batch_results_unavailable with completed, downloadable, and missing counts instead of a partial archive.

SSE update event

event: update
data: {
  "kind": "file_status",
  "batch_id": "1e57527d-2c52-4d40-95a4-4b43c438c06a",
  "job_id": "c0e8bcb3-410b-485f-b5ee-2cb4493b9e78",
  "file_status": "completed",
  "original_size": 1048576,
  "optimized_size": 421888,
  "compression_ratio": 59.8,
  "result_image_id": "v2-a1b2..."
}

Get Optimized Image

GET /api/images/:id

Download or preview an optimized image by its ID from Optimagio or an API client. No authentication required — the image ID acts as a capability token, but browser requests from cross-site pages are rejected so these URLs cannot be used as public CDN embeds.

Returns the raw image binary with appropriate Content-Type header.

Cache-Control: private, no-cache, max-age=0, must-revalidate

Account lifecycle: Your files stay in your account as long as your subscription is active, plus 60 days after cancellation. Free accounts unused for 12 months are reminded twice and then archived. Anonymous and Free saved results outside an active account still use the short cache window. Use POST /api/images/touch only for active uncommitted browser sessions, and call POST /api/images/commit when the user explicitly saves current results.

Get Usage Limits

GET /api/limits

Get your current usage and quota limits. Requires API key authentication.

Response

{
  "subscription_tier": "free",
  "is_admin": false,
  "lifecycle": {
    "state": "active",
    "reason": "",
    "previous_tier": "",
    "target_tier": "",
    "storage_bytes_at_transition": 0,
    "storage_cap_bytes_at_transition": 0,
    "warning_count": 0
  },
  "storage": {
    "used_bytes": 0,
    "included_bytes": 0,
    "remaining_bytes": 0,
    "hard_limit_bytes": 0
  },
  "limits": {
    "monthly_images": 100,
    "file_size_mb": 10,
    "bandwidth_bytes": 1073741824,
    "minute_requests": 30,
    "max_api_keys": 1,
    "team_seats": 1
  },
  "usage": {
    "monthly_images": 47,
    "bandwidth_bytes": 73400320,
    "minute_requests": 3,
    "team_seats": 1,
    "active_seats": 1,
    "pending_seats": 0,
    "month_start": "2026-04-01",
    "month_end": "2026-04-30"
  },
  "remaining": {
    "monthly_images": 53,
    "bandwidth_bytes": 1000341504,
    "minute_requests": 27,
    "team_seats": 0
  },
  "pricing": {
    "tier": "free",
    "display_name": "Free",
    "monthly_cents": 0,
    "annual_cents": 0,
    "annual_discount_percent": 15
  }
}

Manage Stored Results

GET /api/my/storage/usage
GET /api/my/storage/items?limit=50&offset=0
POST /api/my/storage/items/:object_id
DELETE /api/my/storage/items/:object_id

Authenticated accounts can inspect stored-result usage, list committed objects, pin an existing cloud-stored optimized result into persistent account storage, or unpin/delete one stored result to reclaim bytes. Pin and unpin are dashboard mutations and require action intents. Private Browser Mode results stay local and cannot be pinned because no Optimagio object exists.

List Response

{
  "items": [
    {
      "id": "abc123",
      "image_id": "abc123",
      "filename": "hero.webp",
      "byte_size": 248391,
      "pinned_at": "2026-05-08T12:00:00Z",
      "tier_at_commit": "starter",
      "lifecycle_state": "active",
      "download_url": "/api/images/abc123"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}

Bulk Download (ZIP)

POST /api/images/download-zip

Stream a ZIP archive containing many already-optimized images. No authentication required — access is controlled by the capability ids you provide. Up to 5,000 ids per request; the resulting archive is capped at 500 MB. Source ids must still be live (see retention note above).

Request Body (JSON)

{
  "images": [
    { "id": "v2-a1b2c3d4-cmax-w1920-h1080-jpeg-exiftrue", "filename": "hero.jpg",  "format": "jpeg" },
    { "id": "v2-e5f6g7h8-cmax-w0800-h0600-webp-exiftrue", "filename": "thumb.webp", "format": "webp" }
  ]
}

Response

Streams application/zip with Content-Disposition: attachment; filename=optimized_images.zip and Cache-Control: no-store. Missing/expired ids are skipped; if every requested id is gone, the endpoint returns 409 no_downloadable_images.

Refresh Active Results

POST /api/images/touch

Refresh active uncommitted browser-session results before they are saved. Committed results keep the retention window captured at commit time. Up to 500 ids per request.

Request Body (JSON)

{
  "ids": [
    "v2-a1b2c3d4-cmax-w1920-h1080-jpeg-exiftrue",
    "v2-e5f6g7h8-cmax-w0800-h0600-webp-exiftrue"
  ]
}

Response

{ "touched": 2 }

Supported Formats

GET /public/formats

Authoritative list of input/output formats and per-format capabilities. No authentication required.

Response

{
  "supported_formats": [
    {
      "name": "WebP",
      "extension": "webp",
      "mime_type": "image/webp",
      "description": "Modern format with superior compression",
      "features": ["Superior compression", "Lossless/Lossy", "Transparency", "Animation"]
    }
    // ... JPEG, PNG, GIF, AVIF
  ],
  "input_formats":  ["JPEG", "PNG", "WebP", "GIF", "TIFF", "BMP"],
  "output_formats": ["JPEG", "PNG", "WebP", "GIF", "AVIF"],
  "max_file_size":  "50MB",
  "max_dimensions": "10000x10000"
}

Pricing Plans

GET /public/pricing

List of marketing plan previews with pricing and feature breakdowns. No authentication required. Useful for public pricing pages and access-request flows.

Response

{
  "plans": [
    {
      "code": "free",
      "name": "Free",
      "currency": "USD",
      "period": "month",
      "monthly_price": 0,
      "annual_price": 0,
      "requests_limit": 100,
      "size_limit_mb": 10,
      "bandwidth_gb": 1,
      "rate_limit_per_minute": 30,
      "rate_limit_per_key_per_minute": 15,
      "optimized_retention_days": 0,
      "included_storage_bytes": 0,
      "api_keys": 1,
      "included_seats": 1,
      "features": [...]
    },
    { "code": "starter",  "name": "Starter",  "monthly_price": 9,   ... },
    { "code": "pro",      "name": "Pro",      "monthly_price": 29,  "popular_plan": true, ... },
    { "code": "business", "name": "Business", "monthly_price": 89,  ... },
    { "code": "scale",    "name": "Scale",    "monthly_price": 399, "contact_sales": true, ... }
  ],
  "annual_discount_percent": 15,
  "contact": { "sales": "[email protected]", "support": "[email protected]" }
}

The plan names returned by this endpoint are marketing labels. The internal subscription_tier field controls API rate limits, quotas, and access only; it is not proof of a paid subscription or billing consent. Values are free, starter, pro, business, scale — see Access Tiers.

Health Check

GET /api/health

Check API service status. No authentication required.

{
  "status": "ok",
  "service": "optimagio-api",
  "version": "2.0.0"
}

Batch Uploads

For external API clients, POST /api/v1/optimize/batch is the primary batch API. Send files first; the server creates or extends the batch, generates job IDs, and returns the tracking links. For 5000 files, send 50 chunks of up to 100 files with one stable client_batch_id and a unique Idempotency-Key per chunk.

Simple upload

curl -X POST https://api.optimagio.dev/api/v1/optimize/batch \
  -H "X-API-Key:${OPTIMAGIO_API_KEY}" \
  -H "Idempotency-Key: catalog-may-2026-part-1" \
  -F "output_format=webp" \
  -F "compression=auto" \
  -F "storage_commit=true" \
  -F "resize_width=2000" \
  -F "files[]=@products/shoes/nike-001.jpg" \
  -F "files[]=@products/shoes/nike-002.png"

When no manifest is supplied, file names come from the multipart parts and the batch is finalized automatically. Use this mode for small batches and quick integrations.

Advanced manifest

Send a JSON manifest field when you need stable client IDs, folder paths in the ZIP, checksums, chunking, or an expected total.

{
  "client_batch_id": "catalog-2026-05",
  "name": "May product catalog",
  "total_files": 1842,
  "finalize": false,
  "options": {
    "output_format": "webp",
    "compression": "auto",
    "resize_width": 2000,
    "keep_exif": false,
    "storage_commit": true,
    "keep_original": false
  },
  "files": [
    {
      "field": "file_0",
      "client_file_id": "products/shoes/nike-001.jpg",
      "relative_path": "products/shoes/nike-001.jpg",
      "sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
    }
  ]
}
curl -X POST https://api.optimagio.dev/api/v1/optimize/batch \
  -H "X-API-Key:${OPTIMAGIO_API_KEY}" \
  -H "Idempotency-Key: catalog-2026-part-0001" \
  -F 'manifest={"client_batch_id":"catalog-2026-05","total_files":1842,"finalize":false,"options":{"output_format":"webp","compression":"auto"},"files":[{"field":"file_0","client_file_id":"products/shoes/nike-001.jpg","relative_path":"products/shoes/nike-001.jpg"}]}' \
  -F "file_0=@products/shoes/nike-001.jpg"

Chunked upload

StepManifest fieldsBehavior
First chunkclient_batch_id, total_files, finalize=falseCreates the batch and returns batch_id. total_files is required when creating an unfinished upload.
Middle chunksclient_batch_id or batch_id, finalize=falseAppends more files to the same batch. Prefer the same client_batch_id so clients do not need to persist batch_id between chunks. Each chunk should use a new Idempotency-Key.
Final chunkclient_batch_id or batch_id, finalize=trueFinalizes upload only when uploaded file rows match total_files. Finalized batches reject further appends.

Idempotency

Use Idempotency-Key on every upload attempt or chunk. Retrying the same key with the same fingerprint returns the saved response. A different payload with the same key returns 409 idempotency_conflict. A duplicate while the first request is still running returns retryable 409 idempotency_in_progress with Retry-After.

client_batch_id is scoped to the authenticated user and helps find the same batch after a retry or process restart. It is not a substitute for per-chunk idempotency.

Polling and retrieval

  • Use GET /api/v1/batches/:id?files_limit=1000&files_offset=0 as the primary integration path for status and result visibility.
  • Advance files_offset through large batches; the response reports whether more file rows are available.
  • SSE at /api/v1/batches/:id/events remains available for live UI progress, but polling is the recommended external API baseline.
  • Use download_zip for completed batch retrieval. Completed file rows can include download_url, but those per-file links can expire; the batch ZIP returns 409 partial_batch_results_unavailable if any completed output is missing.

Limits, paths, and accounting

  • Each upload-first request accepts up to 100 files and uses the bundle upload protection bucket, not the 10 MB sync optimize bucket.
  • A batch can contain up to 50,000 files; 5000 files should be split into 50 requests with 100 files each.
  • Per-file size limits are tier-aware. Oversized files are rejected before queueing, recorded as failed batch rows, and consume zero optimization credits.
  • Batch semantics are per_item: invalid relative_path, invalid sha256, checksum mismatches, and oversized files go to rejected_files while accepted files continue processing.
  • Monthly image quota is enforced by workers. A chunk can queue more files than the remaining quota; extra jobs become quota_paused and can resume after upgrade or reset.
  • Cache hits are free for monthly image credits but still appear in analytics. Result bandwidth is counted separately where bandwidth limits apply.
  • Batch ZIP download preserves validated relative_path folder structure. Without a safe path, files use the flat optimized filename behavior.

Path safety: folder paths must be relative, use forward slashes, and avoid empty segments, ., .., control characters, absolute paths, and excessive length.

Advanced/internal-compatible flow

Most integrations should use POST /api/v1/optimize/batch. The explicit batch and bundle routes remain available for dashboard internals, older preflight flows, pause/resume/cancel before upload, and custom enterprise automation.

POST /api/v1/batches
POST /api/v1/optimize/async/upload-bundle
GET  /api/v1/batches/{batch_id}
GET  /api/v1/batches/{batch_id}/events
GET  /api/v1/batches/{batch_id}/download

Examples

URL-based Optimization

cURL

curl -X POST https://api.optimagio.dev/api/optimize \ 
  -H "X-API-Key:${OPTIMAGIO_API_KEY}" \
  -F "url=https://example.com/image.jpg" \
  -F "compression=medium" \
  -F "storage_commit=true" \
  -F "width=1920" \
  -F "height=1080"

File Upload Optimization

cURL

curl -X POST https://api.optimagio.dev/api/optimize \ 
  -H "X-API-Key:${OPTIMAGIO_API_KEY}" \
  -F "accept_file=true" \
  -F "compression=max" \
  -F "format=webp" \
  -F "storage_commit=true" \
  -F "image=@/path/to/your/image.jpg"

Upload-First Batch Optimization

cURL

curl -X POST https://api.optimagio.dev/api/v1/optimize/batch \
  -H "X-API-Key:${OPTIMAGIO_API_KEY}" \
  -H "Idempotency-Key: product-batch-001" \
  -F "output_format=webp" \
  -F "compression=auto" \
  -F "storage_commit=true" \
  -F "resize_width=2000" \
  -F "files[][email protected]" \
  -F "files[][email protected]"

JavaScript

const batchForm = new FormData();
const clientBatchId = 'catalog-2026-05';
batchForm.append('output_format', 'webp');
batchForm.append('compression', 'auto');
batchForm.append('storage_commit', 'true');
batchForm.append('resize_width', '2000');
batchForm.append('manifest', JSON.stringify({
  client_batch_id: clientBatchId,
  total_files: fileInput.files.length,
  finalize: true
}));

for (const file of fileInput.files) {
  batchForm.append('files[]', file, file.name);
}

const batchResponse = await fetch('https://api.optimagio.dev/api/v1/optimize/batch', {
  method: 'POST',
  headers: {
    'X-API-Key': 'your_api_key_here',
    'Idempotency-Key': clientBatchId + '-chunk-0001'
  },
  body: batchForm
});

const batch = await batchResponse.json();
const status = await fetch('https://api.optimagio.dev' + batch.links.status_page, {
  headers: { 'X-API-Key': 'your_api_key_here' }
});
console.log(batch.client_batch_id, await status.json(), batch.links.download_zip);

JavaScript (Fetch API)

// URL-based optimization
const urlForm = new FormData();
urlForm.append('url', 'https://example.com/image.jpg');
urlForm.append('compression', 'medium');
urlForm.append('format', 'webp');
urlForm.append('storage_commit', 'true'); // optional: save optimized result

const response = await fetch('https://api.optimagio.dev/api/optimize', {
  method: 'POST',
  headers: {
    'X-API-Key': 'your_api_key_here'
  },
  body: urlForm
});

const result = await response.json();
console.log(result);

// File upload optimization
const formData = new FormData();
formData.append('accept_file', 'true');
formData.append('compression', 'max');
formData.append('storage_commit', 'true'); // optional: save optimized result
formData.append('image', fileInput.files[0]);

const uploadResponse = await fetch('https://api.optimagio.dev/api/optimize', {
  method: 'POST',
  headers: {
    'X-API-Key': 'your_api_key_here'
  },
  body: formData
});

const uploadResult = await uploadResponse.json();

Python (requests)

import requests

# URL-based optimization
response = requests.post(
	'https://api.optimagio.dev/api/optimize',
    headers={'X-API-Key': 'your_api_key_here'},
    data={
        'url': 'https://example.com/image.jpg',
        'compression': 'medium',
        'format': 'webp',
        'storage_commit': 'true'  # optional: save optimized result
    }
)

result = response.json()
print(result)

# File upload optimization
with open('image.jpg', 'rb') as f:
    files = {'image': f}
    data = {
        'accept_file': 'true',
        'compression': 'max',
        'format': 'webp',
        'storage_commit': 'true'  # optional: save optimized result
    }
    
    response = requests.post(
	'https://api.optimagio.dev/api/optimize',
        headers={'X-API-Key': 'your_api_key_here'},
        files=files,
        data=data
    )
    
    result = response.json()

Error Codes

Status CodeErrorDescription
400Bad RequestInvalid request format, missing parameters, file size limit exceeded, or keep_original=true without storage_commit=true
401UnauthorizedInvalid, missing, or expired API key
402Payment RequiredMonthly image, bandwidth, or stored-result cap exhausted — upgrade or wait until reset
403ForbiddenAccount suspended or insufficient permissions
409ConflictIdempotency conflict, in-progress duplicate upload, or append to a finalized batch
404Not FoundRequested image not found or expired
413Payload Too LargeFile exceeds your tier's max file size — upgrade for larger uploads
429Too Many RequestsPer-minute rate limit hit — wait, then retry
500Internal Server ErrorServer error during image processing
503Service UnavailableWorker pool saturated or readiness probe reports overload

Error response format

Quota and rate-limit responses (402, 413, 429) carry a rich JSON body so clients can show the right remediation without parsing strings.

Generic error (4xx / 5xx)
{
  "error": "error_code",
  "message": "Human-readable explanation"
}
Anonymous or global rate limit hit (429)
{
  "error": "rate_limit_exceeded",
  "message": "Rate limit exceeded: 60/60 requests this minute. Try again in 24 seconds.",
  "limit": 60,
  "used": 60,
  "remaining": 0,
  "reset_at": "2026-04-29T12:35:00Z",
  "retry_after_seconds": 24,
  "current_tier": "free",
  "next_tier": "starter"
}

Plus headers X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After.

Monthly quota exhausted (402)
{
  "error": "monthly_quota_exceeded",
  "message": "Monthly image quota exhausted: 100/100 images used.",
  "limit": 100,
  "used": 100,
  "remaining": 0,
  "reset_at": "2026-05-01T00:00:00Z",
  "retry_after_seconds": null,
  "current_tier": "free",
  "next_tier": "starter",
  "upgrade_url": "https://optimagio.com/pricing"
}

Bandwidth quota uses "error": "bandwidth_quota_exceeded" with the same fields. It is based on accepted optimized result bytes handled by Optimagio.

Stored-result cap reached (402)
{
  "error": "storage_cap_reached",
  "message": "Storage cap reached for your current tier.",
  "limit": 10737418240,
  "used": 10737418240,
  "remaining": 0,
  "cap_bytes": 10737418240,
  "used_bytes": 10737418240,
  "current_tier": "starter",
  "next_tier": "pro",
  "upgrade_url": "https://optimagio.com/pricing"
}

This limit applies when committing optimized results into Optimagio storage. The cap is a hard limit; upgrade your plan or delete older results to free space.

Idempotency conflict (409)
{
  "error": "idempotency_conflict",
  "message": "Idempotency-Key was already used with a different request payload."
}

In-progress duplicates return "error": "idempotency_in_progress", "retryable": true, and a Retry-After header.

File too large (413)
{
  "error": "file_size_exceeded",
  "message": "File size 50000000 bytes exceeds free tier limit of 10485760 bytes.",
  "limit": 10485760,
  "used": 50000000,
  "current_tier": "free",
  "next_tier": "starter"
}

Backoff guidance: on 429 wait retry_after_seconds before retrying. On 402 / 413 retrying won't help — surface the message and link upgrade_url if present. On 503 read the Retry-After header.

Rate Limits

Throughput is protected by four layered systems. Each runs independently; whichever rejects first wins.

API-key optimization is always cloud/accounted. In the web UI, users on paid access tiers can choose Cloud only or Private Browser Mode. Private Browser Mode stays on-device with no dashboard history, server analytics, platform result, or cloud fallback. Verified Reports use Cloud only platform results. Any result accepted by Optimagio's optimization endpoints counts as a platform result.

Upload-first batches use the bundle upload protection profile so one request can carry multiple files. This is separate from the 10 MB sync optimize body cap. Individual files still must fit the caller's tier.

Layer 1 — Edge / per-IP protection

Each route family has a per-IP cap with sustained-violation IP blocking. Anonymous/global rate-limit hits return 429 with Retry-After when a retry delay is available.

Layer 2 — Per-account result window

A strict fixed-minute window per user. Successful cloud optimizations consume rate_limit_per_minute slots and monthly image credits. Cache hits are recorded in analytics but are free for monthly image credits. When the window is full, authenticated work waits or moves to the queue instead of returning a user-facing rate error.

Batch uploads follow partial drain quota policy: accepted chunks may queue more files than the remaining monthly image quota, but workers do not process or bill beyond the limit. Extra jobs stay quota_paused until quota resets or the account receives more capacity.

Layer 3 — Per-API-key bucket

In addition to the per-user window, each API key has its own cap at ~50% of the user-level rate by default. One bombing key won't starve the user's other keys.

Layer 4 — Worker pool / readiness

When the worker pool's queue load goes over 90%, the /api/healthz/ready probe returns 503 so upstream load balancers route around the node. Free-tier jobs may also have their compression auto-bumped under load (adaptive quality).

Per-minute result rate by tier

  • Free: 30 / min · 15 / min per key
  • Starter: 300 / min · 150 / min per key
  • Pro: 1,000 / min · 500 / min per key
  • Business: 3,000 / min · 1,500 / min per key
  • Scale: 10,000 / min · 5,000 / min per key

Rate-limit headers

Responses from rate-limited routes include current counters:

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 47
X-RateLimit-Reset: 1761696000
Retry-After: 24            (only on 429)

Backoff guidance: authenticated optimize work is throttled by waiting or queueing. On anonymous/global 429, wait for Retry-After. On 402 retrying won't help — the user must upgrade or wait until reset_at. On 503 read the Retry-After header.

Async optimization

For URL-based optimization that may take seconds (large remote fetches, heavy AVIF encodes, queue contention), submit the job asynchronously and poll for status. Frees up the HTTP connection immediately and the same tier limits and quotas apply.

For file batches, prefer Upload-First Batch Optimization. The older explicit batch + upload-bundle flow remains available as an advanced/internal-compatible path for clients that need preflight control.

Submit a job

POST /api/v1/optimize/async
X-API-Key: your_key
Content-Type: application/json

{
  "url": "https://example.com/photo.jpg",
  "compression": "auto",
  "format": "webp",
  "width": 1200
}

Returns 202 Accepted with the job id:

{
  "job_id": "8f3c6d2e-1b4a-4d3e-9c8f-a4b3e2d1c0e1",
  "status": "queued",
  "status_url": "/api/v1/jobs/8f3c6d2e-1b4a-4d3e-9c8f-a4b3e2d1c0e1"
}

Poll for status

GET /api/v1/jobs/{'{'}job_id{'}'}
X-API-Key: your_key

Status progression: queuedprocessingcompleted | failed. Batch file rows can also report quota_paused when monthly image quota is exhausted before the worker starts that file.

{
  "id": "8f3c6d2e-...",
  "status": "completed",
  "progress": 100,
  "result_image_id": "abc123...",
  "result_url": "/api/images/abc123...",
  "created_at": 1761696000,
  "updated_at": 1761696012
}

Polling cadence: 1–2 seconds is a reasonable interval. Jobs auto-expire after 24 hours. The result image is served via /api/images/{id} just like sync responses.

Advanced/internal-compatible file bundle route

Low-level clients can explicitly create a batch and submit a preallocated bundle, but this is not the main external API path.

POST /api/v1/batches
POST /api/v1/optimize/async/upload-bundle

Reports and Site Audit

Report and Site Audit APIs use dashboard JWT sessions. Shared report reads are public only for callers holding the generated token. Optimization reports store metadata, not optimized image bytes.

Create a metadata report

POST /api/v1/reports
Authorization: Bearer dashboard_jwt
Content-Type: application/json

{
  "title": "Homepage batch",
  "processing_mode": "browser_only",
  "report_sync_mode": "sync",
  "files": [
    {
      "name": "hero.jpg",
      "original_size": 1048576,
      "optimized_size": 421888,
      "status": "completed",
      "processed_by": "local_wasm"
    }
  ]
}
{
  "status": "generated",
  "reused": false,
  "report": {
    "id": "6d1f2c6a-4d2c-4216-8c99-c3f9ef119a10",
    "title": "Homepage batch",
    "verification_status": "browser_reported",
    "processing_mode": "browser_only",
    "file_count": 1,
    "saved_bytes": 626688,
    "cloud_credits_used": 0,
    "cloud_credits_saved": 1
  }
}

Create a report share link

POST /api/v1/reports/{report_id}/share
Authorization: Bearer dashboard_jwt
{
  "id": "72b0c734-c1af-4be8-a44c-3e3cd64f28b7",
  "report_id": "6d1f2c6a-4d2c-4216-8c99-c3f9ef119a10",
  "token": "show-once-share-token",
  "url": "/reports/shared/show-once-share-token",
  "created_at": "2026-05-07T10:30:00Z"
}

The plaintext token is returned only in this create response. Public shared report payloads expose an allowlist of summary and file metadata; internal session IDs, result image IDs, hashes, and raw options are omitted.

Submit a Site Audit

POST /api/v1/site-audits
Authorization: Bearer dashboard_jwt
Content-Type: application/json

{
  "url": "https://example.com",
  "max_pages": 10
}
{
  "id": "9b4e6762-334b-4977-87fb-6d5e77db13f1",
  "status": "queued",
  "status_url": "/api/v1/site-audits/9b4e6762-334b-4977-87fb-6d5e77db13f1",
  "max_pages": 10,
  "pages_limit": 25
}

Read Site Audit status

GET /api/v1/site-audits/{audit_id}
Authorization: Bearer dashboard_jwt
{
  "id": "9b4e6762-334b-4977-87fb-6d5e77db13f1",
  "root_url": "https://example.com",
  "status": "completed",
  "stage": "finalizing",
  "score": 74,
  "pages_scanned": 5,
  "images_found": 42,
  "fixable_images": 18,
  "potential_savings_bytes": 8388608,
  "can_optimize": true,
  "images": [],
  "recommendations": [],
  "top_issues": []
}

Public audits use POST /api/public/site-audits and return a tokenized status URL. Authenticated concurrency hits return 429 audit_concurrency_exceeded with Retry-After.

Access / Quota Tiers

Your access tier determines monthly image quota, monthly bandwidth, stored-result cap, max file size, and rate limits. The canonical internal identifier returned by GET /api/limits is subscription_tier; it is an access and quota field, not billing consent or proof of a paid subscription. Values are: free, starter, pro, business, scale. Monthly bandwidth means accepted optimized result bytes handled by Optimagio. Included delivery is EU/NA Standard or approved Volume; high-cost global Standard delivery requires explicit Global Delivery opt-in; metered quota billing is not available. MB, GB, and TB quota labels use decimal SI bytes; JSON byte fields stay raw bytes.

FeatureFreeStarterProBusinessScale
Identifierfreestarterprobusinessscale
Monthly images10010,00050,000100,000500,000+
Monthly bandwidth1 GB50 GB300 GB1 TB3 TB+ contract
Stored results0 GB included; 1h cache5 GB hard cap50 GB hard cap250 GB hard cap2 TB+ contract
Max file size10 MB25 MB100 MB150 MBcustom
Result rate30/min300/min1,000/min3,000/min10,000/min
Per-key rate15/min150/min500/min1,500/min5,000/min
API keys131025custom

Check your current usage and limits via GET /api/limits with an API key, GET /api/my/limits from an authenticated dashboard session, or from the dashboard UI. Live values come from GET /public/pricing. Legacy tier names (premium, enterprise, unlimited) still resolve correctly to their modern replacements but are no longer documented in plans.