# img-src.io — Complete Documentation for LLMs > img-src.io is an image CDN service that lets developers upload images once and serve them globally with real-time transformations via URL parameters. It supports automatic format conversion (WebP, AVIF, JPEG, PNG), resizing, cropping, and quality optimization — all at the edge with 200+ Cloudflare locations worldwide. > For a shorter summary, see: https://img-src.io/llms.txt --- ## Table of Contents 1. [Overview](#overview) 2. [How Image URLs Work](#how-image-urls-work) 3. [Image Transformation Parameters](#image-transformation-parameters) 4. [Supported Image Formats](#supported-image-formats) 5. [Pricing](#pricing) 6. [Authentication](#authentication) 7. [API Reference](#api-reference) 8. [Error Codes](#error-codes) 9. [Rate Limiting](#rate-limiting) 10. [Usage Examples (curl)](#usage-examples-curl) 11. [Framework Integration Examples](#framework-integration-examples) 12. [Architecture](#architecture) 13. [FAQ](#faq) 14. [Links](#links) --- ## Overview img-src.io is a developer-focused image hosting and CDN platform. You upload an image through the dashboard or REST API, and receive a permanent CDN URL. You can then transform that image on-the-fly by appending URL parameters — no build step, no image pipeline, no configuration required. **Core capabilities:** - Upload images via web dashboard or REST API - Automatic global CDN delivery via Cloudflare (200+ edge locations) - Real-time image transformation through URL parameters - Format conversion: WebP (25-34% smaller), AVIF (50%+ smaller), JPEG, PNG - Resize, crop, and quality adjustment at the edge - Hash-based deduplication (no duplicate storage charges) - Immutable cache headers for optimal browser caching - Zero egress/bandwidth fees on all plans --- ## How Image URLs Work Every uploaded image gets a CDN URL in this format: ``` https://img-src.io/i/{username}/{filepath} ``` Alternative CDN domain: ``` https://cdn.img-src.io/{username}/{filepath} ``` Transform images by adding URL parameters: - `?w=800` — Resize width to 800px - `?h=600` — Resize height to 600px - `?w=800&h=600&fit=cover` — Resize and crop to exact dimensions - `?q=85` — Set quality to 85% - Change the file extension to convert format: `photo.webp`, `photo.avif`, `photo.jpg` Example: `https://img-src.io/i/john/photo.webp?w=800&q=85` serves a WebP image resized to 800px width at 85% quality. --- ## Image Transformation Parameters | Parameter | Description | Values | Default | |-----------|-------------|--------|---------| | `w` | Width in pixels | 1-10000 | Original width | | `h` | Height in pixels | 1-10000 | Original height | | `fit` | Resize fit mode | `cover`, `contain`, `fill`, `scale-down` | `contain` | | `q` | Quality percentage | 1-100 | 80 | | `p:name` | Apply a named preset (Pro only) | Any preset name | — | **Fit modes explained:** - **`cover`**: Resize and crop to fill exact dimensions. Parts of the image may be clipped. - **`contain`**: Resize to fit within dimensions. Maintains aspect ratio, no cropping. - **`fill`**: Stretch the image to fill exact dimensions. Aspect ratio may change. - **`scale-down`**: Like `contain`, but only shrinks — never enlarges the image. **Format conversion:** Change the file extension in the URL to convert format on-the-fly: - `photo.webp` — WebP format (default, 25-34% smaller than JPEG) - `photo.avif` — AVIF format (50%+ smaller, max 8 megapixels) - `photo.jpg` or `photo.jpeg` — JPEG format - `photo.png` — PNG format **Presets (Pro only):** Define reusable transformation configurations: ``` https://img-src.io/i/john/photo.webp?p:thumbnail ``` Presets are created via the API or dashboard. Example preset `thumbnail` = `w=200&h=200&fit=cover`. --- ## Supported Image Formats **Input (upload):** JPEG, PNG, WebP, GIF, BMP, TIFF, ICO, SVG, HEIC/HEIF, AVIF, TGA, PNM, QOI, HDR, EXR **Output (serve):** WebP (default), AVIF (max 8 megapixels), JPEG, PNG **AVIF limitations:** - Maximum 8 megapixels (~3500x2300px) - Maximum 4000px on any single dimension - Images exceeding these limits fall back to WebP **Maximum upload file size:** 5 MB --- ## Pricing ### Free Plan ($0/month) - 100 image uploads per month - 10 GB storage - 1,000 transformations per month - 1,000 API requests per month - 100 API requests per minute rate limit - Unlimited CDN requests (serving) - No bandwidth/egress fees ### Pro Plan ($5/month) Everything in Free, plus: - Unlimited image uploads per month - 50 GB storage - 10,000 transformations per month - 10,000 API requests per month - 500 API requests per minute rate limit - Access Links (signed, time-limited URLs for private images) - Image presets (e.g., `?p:thumbnail`, `?p:hero`) ### Resource Pack ($10/pack, Pro users only) - +10 GB storage - +10,000 API requests - +10,000 transformations --- ## Authentication The API supports two authentication methods: ### 1. API Key (Recommended for programmatic access) Create API keys in the dashboard under Settings > API Keys. All API keys have the `imgsrc_` prefix. ``` Authorization: Bearer imgsrc_a1B2c3D4e5F6g7H8i9J0k1L2m3N4o5P6q7R8s9T0u1V2w3X4 ``` **API key properties:** - Prefix: `imgsrc_` - Scopes: `read`, `write`, or `read,write` - Optional expiration: 1-365 days - Full key shown only once at creation ### 2. Clerk JWT (For web application sessions) JWT tokens issued by Clerk with RS256 signature. Used automatically by the web dashboard. ``` Authorization: Bearer eyJhbGciOiJSUzI1NiIs... ``` JWT claims include `username` and `email` fields. --- ## API Reference **Base URL:** `https://img-src.io/api/v1` **OpenAPI spec:** `https://docs.img-src.io/api-reference/openapi.json` All endpoints except `/health` require authentication via `Authorization: Bearer ` header. ### Health Check ``` GET /health ``` No authentication required. Returns service status. **Response:** ```json { "status": "ok", "timestamp": "2026-01-21T12:00:00Z" } ``` --- ### Upload Image ``` POST /api/v1/images Content-Type: multipart/form-data Authorization: Bearer ``` **Form fields:** | Field | Type | Required | Description | |-------|------|----------|-------------| | `file` | File | Yes | Image file (max 5 MB) | | `path` | String | No | Custom path/folder (e.g., `blog/2024/photo.png`) | | `visibility` | String | No | `public` (default) or `private` (Pro only) | **Response (201 Created):** ```json { "id": "abcdef1234567890", "hash": "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", "url": "/john/blog/2024/photo.png", "paths": ["blog/2024/photo.png"], "is_new": true, "size": 1024000, "format": "png", "dimensions": { "width": 1920, "height": 1080 }, "available_formats": { "webp": "https://img-src.io/i/john/blog/2024/photo.webp", "avif": "https://img-src.io/i/john/blog/2024/photo.avif", "jpeg": "https://img-src.io/i/john/blog/2024/photo.jpg" }, "uploaded_at": "2026-01-21T12:00:00Z", "visibility": "public", "_links": { "self": "/api/v1/images/abcdef1234567890", "delete": "/api/v1/images/abcdef1234567890" } } ``` If the same file content is uploaded again (SHA256 match), `is_new` will be `false` and a new path alias is created instead of storing a duplicate. --- ### List Images ``` GET /api/v1/images Authorization: Bearer ``` **Query parameters:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `limit` | integer | 50 | Items per page (1-100) | | `offset` | integer | 0 | Pagination offset | | `sort` | string | `newest` | Sort order: `newest`, `oldest`, `name`, `size` | | `path` | string | — | Filter by folder path | **Response (200 OK):** ```json { "images": [ { "id": "abcdef1234567890", "original_filename": "photo.jpg", "sanitized_filename": "photo.jpg", "size": 1024000, "uploaded_at": "2026-01-21T12:00:00Z", "url": "/api/v1/images/abcdef1234567890", "cdn_url": "https://img-src.io/i/john/photo.webp", "paths": ["photo.webp", "blog/photo.webp"], "visibility": "public" } ], "folders": [ { "name": "blog", "image_count": 42 } ], "total": 150, "limit": 50, "offset": 0, "has_more": true } ``` --- ### Search Images ``` GET /api/v1/images/search?q=vacation Authorization: Bearer ``` **Query parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `q` | string | Yes | Search query (matches filename) | | `limit` | integer | No | Max results (default 20) | **Response (200 OK):** ```json { "results": [ { "id": "abcdef1234567890", "original_filename": "vacation-photo.jpg", "sanitized_filename": "vacation-photo.jpg", "paths": ["photos/vacation.jpg"], "size": 1024000, "uploaded_at": "2026-01-21T12:00:00Z", "url": "/api/v1/images/abcdef1234567890", "cdn_url": "https://img-src.io/i/john/photos/vacation.jpg", "visibility": "public" } ], "total": 1, "query": "vacation" } ``` --- ### Get Image Metadata ``` GET /api/v1/images/:id Authorization: Bearer ``` **Response (200 OK):** ```json { "id": "abcdef1234567890", "metadata": { "hash": "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", "original_filename": "photo.jpg", "size": 2048576, "uploaded_at": "2026-01-21T12:00:00Z", "mime_type": "image/jpeg", "width": 1920, "height": 1080, "dominant_color": "3b82f6" }, "urls": { "original": "https://img-src.io/i/john/photo.jpg", "webp": "https://img-src.io/i/john/photo.webp", "avif": "https://img-src.io/i/john/photo.avif", "jpeg": "https://img-src.io/i/john/photo.jpg", "png": "https://img-src.io/i/john/photo.png" }, "visibility": "public", "_links": { "self": "/api/v1/images/abcdef1234567890", "delete": "/api/v1/images/abcdef1234567890" } } ``` --- ### Delete Image ``` DELETE /api/v1/images/:id Authorization: Bearer ``` **Response (200 OK):** ```json { "success": true, "message": "Image deleted", "deleted_paths": ["photo.webp", "blog/photo.webp"], "deleted_at": "2026-01-21T12:00:00Z" } ``` --- ### Create Signed URL (Pro only) ``` POST /api/v1/images/:id/signed-url Authorization: Bearer Content-Type: application/json ``` **Request body:** ```json { "expires_in_seconds": 3600 } ``` | Field | Type | Default | Description | |-------|------|---------|-------------| | `expires_in_seconds` | integer | 3600 | 60-604800 (1 min to 7 days) | **Response (200 OK):** ```json { "signed_url": "https://img-src.io/i/john/photo.webp?token=xxx&expires=1704153600", "expires_at": 1704153600, "expires_in_seconds": 3600 } ``` --- ### Update Image Visibility ``` PUT /api/v1/images/:id/visibility Authorization: Bearer Content-Type: application/json ``` **Request body:** ```json { "visibility": "private" } ``` **Response (200 OK):** ```json { "id": "abcdef1234567890", "visibility": "private", "message": "Visibility updated to private" } ``` --- ### Get User Settings ``` GET /api/v1/settings Authorization: Bearer ``` **Response (200 OK):** ```json { "settings": { "id": "user_abc123", "username": "johndoe", "email": "john@example.com", "plan": "pro", "delivery_formats": ["webp", "avif", "jpeg"], "default_quality": 80, "default_fit_mode": "contain", "default_max_width": 1920, "default_max_height": 1080, "theme": "light", "language": "en", "created_at": 1704067200, "updated_at": 1704067200, "total_uploads": 150, "storage_used_bytes": 104857600 } } ``` --- ### Update User Settings ``` PUT /api/v1/settings Authorization: Bearer Content-Type: application/json ``` **Request body (all fields optional):** ```json { "delivery_formats": ["webp", "avif", "jpeg"], "default_quality": 85, "default_fit_mode": "cover", "default_max_width": 1920, "default_max_height": 1080, "theme": "dark", "language": "ko" } ``` **Response (200 OK):** ```json { "settings": { "..." }, "message": "Settings updated successfully" } ``` --- ### List API Keys ``` GET /api/v1/settings/api-keys Authorization: Bearer ``` **Response (200 OK):** ```json { "api_keys": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Production API Key", "key_prefix": "imgsrc_a1B2c3", "scopes": "read,write", "created_at": 1704067200, "last_used_at": 1704153600, "expires_at": 1735689600, "total_requests": 1234 } ], "total": 1 } ``` --- ### Create API Key ``` POST /api/v1/settings/api-keys Authorization: Bearer Content-Type: application/json ``` **Request body:** ```json { "name": "Production API Key", "scopes": ["read", "write"], "expires_in_days": 90 } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | string | Yes | Key name (1-100 chars) | | `scopes` | array | No | `["read"]`, `["write"]`, or `["read", "write"]` (default) | | `expires_in_days` | integer | No | 1-365 days (optional) | **Response (201 Created):** ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "key": "imgsrc_a1B2c3D4e5F6g7H8i9J0k1L2m3N4o5P6q7R8s9T0u1V2w3X4", "key_prefix": "imgsrc_a1B2c3", "name": "Production API Key", "scopes": "read,write", "created_at": 1704067200, "expires_at": 1735689600 } ``` **Important:** The full `key` value is only returned once at creation time. Store it securely. --- ### Delete API Key ``` DELETE /api/v1/settings/api-keys/:id Authorization: Bearer ``` **Response (200 OK):** ```json { "success": true, "message": "API key deleted" } ``` --- ### List Presets (Pro only) ``` GET /api/v1/settings/presets Authorization: Bearer ``` **Response (200 OK):** ```json { "presets": [ { "id": "preset_abc123", "name": "thumbnail", "description": "200x200 thumbnail with cover fit", "params": { "w": 200, "h": 200, "fit": "cover", "format": "webp" }, "created_at": 1704067200, "updated_at": 1704067200, "usage_count": 42 } ], "total": 1 } ``` --- ### Create Preset (Pro only) ``` POST /api/v1/settings/presets Authorization: Bearer Content-Type: application/json ``` **Request body:** ```json { "name": "thumbnail", "description": "200x200 thumbnail with cover fit", "params": { "w": 200, "h": 200, "fit": "cover", "format": "webp" } } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | string | Yes | Preset name (1-50 chars) | | `description` | string | No | Description (max 200 chars) | | `params` | object | Yes | Transformation parameters | --- ### Update Preset (Pro only) ``` PUT /api/v1/settings/presets/:id Authorization: Bearer Content-Type: application/json ``` **Request body (all fields optional):** ```json { "name": "card-image", "description": "Card thumbnail for product listings", "params": { "w": 400, "h": 300, "fit": "cover" } } ``` --- ### Delete Preset (Pro only) ``` DELETE /api/v1/settings/presets/:name Authorization: Bearer ``` **Response (200 OK):** ```json { "success": true, "message": "Preset has been deleted" } ``` --- ### Get Usage Statistics ``` GET /api/v1/usage Authorization: Bearer ``` **Response (200 OK):** ```json { "plan": "free", "plan_name": "Free Plan", "plan_status": "active", "subscription_ends_at": null, "plan_limits": { "max_uploads_per_month": 100, "max_storage_bytes": 10737418240, "max_bandwidth_per_month": null, "max_api_requests_per_month": 1000, "max_transformations_per_month": 1000 }, "total_images": 42, "storage_used_bytes": 104857600, "storage_used_mb": 100, "storage_used_gb": 0.1, "current_period": { "period": "2026-02", "period_start": 1738368000, "period_end": 1740787200, "uploads": 15, "bandwidth_bytes": 1073741824, "api_requests": 200, "transformations": 150 }, "credits": { "storage_bytes": 0, "api_requests": 0, "transformations": 0 } } ``` --- ## Error Codes All errors follow this format: ```json { "error": { "code": "ERROR_CODE", "message": "Human-readable description", "status": 400, "path": "/api/v1/images/nonexistent" } } ``` | HTTP Status | Error Code | Description | |-------------|------------|-------------| | 400 | `VALIDATION_ERROR` | Invalid request body or parameters | | 400 | `INVALID_FILE` | File is not a valid image or exceeds 5 MB | | 401 | `UNAUTHORIZED` | Missing or invalid authentication token | | 403 | `FORBIDDEN` | Insufficient permissions or plan restrictions | | 403 | `PLAN_LIMIT_EXCEEDED` | Monthly quota exceeded | | 404 | `NOT_FOUND` | Resource does not exist | | 409 | `CONFLICT` | Resource already exists (e.g., duplicate preset name) | | 429 | `RATE_LIMIT_EXCEEDED` | Too many requests | | 500 | `INTERNAL_ERROR` | Server error | --- ## Rate Limiting | Plan | Limit | |------|-------| | Free | 100 requests/minute | | Pro | 500 requests/minute | Rate limit information is included in every API response via headers: | Header | Description | |--------|-------------| | `X-RateLimit-Limit` | Maximum requests per minute | | `X-RateLimit-Remaining` | Remaining requests in current window | | `X-RateLimit-Reset` | Unix timestamp when the rate limit resets | When rate limited, the API returns HTTP 429 with a `Retry-After` header. --- ## Usage Examples (curl) ### Upload an image ```bash curl -X POST https://img-src.io/api/v1/images \ -H "Authorization: Bearer imgsrc_YOUR_API_KEY" \ -F "file=@photo.jpg" \ -F "path=blog/2024/hero.jpg" ``` ### Upload a private image (Pro) ```bash curl -X POST https://img-src.io/api/v1/images \ -H "Authorization: Bearer imgsrc_YOUR_API_KEY" \ -F "file=@secret.png" \ -F "visibility=private" ``` ### List images ```bash curl https://img-src.io/api/v1/images?limit=20&sort=newest \ -H "Authorization: Bearer imgsrc_YOUR_API_KEY" ``` ### Search images ```bash curl "https://img-src.io/api/v1/images/search?q=vacation" \ -H "Authorization: Bearer imgsrc_YOUR_API_KEY" ``` ### Get image metadata ```bash curl https://img-src.io/api/v1/images/abcdef1234567890 \ -H "Authorization: Bearer imgsrc_YOUR_API_KEY" ``` ### Delete an image ```bash curl -X DELETE https://img-src.io/api/v1/images/abcdef1234567890 \ -H "Authorization: Bearer imgsrc_YOUR_API_KEY" ``` ### Create a signed URL (Pro) ```bash curl -X POST https://img-src.io/api/v1/images/abcdef1234567890/signed-url \ -H "Authorization: Bearer imgsrc_YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"expires_in_seconds": 3600}' ``` ### Create an API key ```bash curl -X POST https://img-src.io/api/v1/settings/api-keys \ -H "Authorization: Bearer imgsrc_YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "CI/CD Key", "scopes": ["read", "write"], "expires_in_days": 90}' ``` ### Get usage statistics ```bash curl https://img-src.io/api/v1/usage \ -H "Authorization: Bearer imgsrc_YOUR_API_KEY" ``` ### Serve a transformed image (no auth required) ```bash # Resize to 800px width, WebP format, 85% quality curl -o output.webp "https://img-src.io/i/john/photo.webp?w=800&q=85" # Convert to AVIF, resize to 400x300 with cover crop curl -o output.avif "https://img-src.io/i/john/photo.avif?w=400&h=300&fit=cover" # Use a preset (Pro) curl -o output.webp "https://img-src.io/i/john/photo.webp?p:thumbnail" ``` --- ## Framework Integration Examples ### HTML `` tag ```html Photo description ``` ### HTML `` with format fallback ```html Photo description ``` ### React `` ```jsx function Image({ username, path, width, quality = 85 }) { const src = `https://img-src.io/i/${username}/${path}?w=${width}&q=${quality}`; return ; } ``` ### Next.js `` with custom loader ```jsx // next.config.js module.exports = { images: { loader: 'custom', loaderFile: './lib/img-src-loader.js', }, }; // lib/img-src-loader.js export default function imgSrcLoader({ src, width, quality }) { return `https://img-src.io/i/${src}?w=${width}&q=${quality || 85}`; } // Usage in component import Image from 'next/image'; Photo ``` ### Responsive images with `srcset` ```html Responsive photo ``` --- ## Architecture img-src.io runs entirely on Cloudflare's edge infrastructure: - **Frontend:** React 19 on Cloudflare Pages - **Backend API:** TypeScript on Cloudflare Workers (Hono framework) - **CDN:** Cloudflare Workers + optional Rust container for native codec support (libvips) - **Storage:** Cloudflare R2 (objects), KV (cache), D1 (metadata) - **Auth:** Clerk (JWT/JWKS with RS256 signature, KV-cached public keys with 1-hour TTL) **Data flow:** - **Upload:** Frontend/API → Backend Worker → R2 (storage) + KV (cache) + D1 (metadata) - **Serve:** Browser → CDN Worker → R2 → Container (transform) → CDN cache - **Delete:** Backend Worker → R2 + KV invalidation + CDN cache purge --- ## FAQ **What counts as a transformation?** A transformation is any request that modifies the image — resize, format conversion, or quality adjustment. Cached responses from the CDN do not count toward your transformation limit. **What happens when I reach my limit?** On the Free plan, uploads and transformations are blocked until the next billing cycle. On the Pro plan, you can purchase Resource Packs to extend your quota instantly. **Can I downgrade from Pro to Free?** Yes. Cancel anytime. Your images remain accessible, but you'll be limited to Free plan quotas after the billing period ends. **Is there a bandwidth limit?** No. Cloudflare R2 has zero egress fees, so img-src.io offers unlimited bandwidth on all plans at no extra cost. **What is an Access Link?** Access Links are signed, time-limited URLs for private images. They are available on the Pro plan and allow you to share images that expire after a set duration (1 minute to 7 days). **What are image presets?** Presets are saved transformation configurations (e.g., `p:thumbnail` = `w=200&h=200&fit=cover`). Define them once in the dashboard or via API, then use `?p:name` in any image URL. Pro plan only. **How does deduplication work?** Every uploaded image is hashed with SHA256. If the same content is uploaded again, no duplicate is stored — instead, a new path alias is created pointing to the existing file. **What image formats are supported?** Input: JPEG, PNG, WebP, GIF, BMP, TIFF, ICO, SVG, HEIC/HEIF, AVIF, TGA, PNM, QOI, HDR, EXR. Output: WebP (default), AVIF (max 8MP), JPEG, PNG. --- ## Links - [Homepage](https://img-src.io) - [Pricing](https://img-src.io/pricing) - [Documentation](https://docs.img-src.io) - [API Reference](https://docs.img-src.io/api-reference) - [About](https://img-src.io/about) - [Terms of Service](https://img-src.io/terms-of-service) - [Privacy Policy](https://img-src.io/privacy-policy) - [X (Twitter)](https://x.com/img_src_io) - [GitHub](https://github.com/img-src-io) - [LinkedIn](https://www.linkedin.com/in/taehun-brian-kim/)