# IMG.LY Gateway — Integration Guide for AI Coding Agents > Base URL: `https://gateway.img.ly` > Protocol: REST + Server-Sent Events (SSE) > Auth: Bearer token (API key or gateway JWT) The IMG.LY Gateway is an AI generation API for image, video, audio, and text generation. It handles authentication, credit metering, input validation, and asset storage so integrators only deal with one unified API. --- ## Authentication Two authentication methods, both via the `Authorization` header: ### 1. API Key (server-side) Use for server-to-server calls and minting short-lived JWTs. ``` Authorization: Bearer sk_live_... ``` API keys start with `sk_`. Keep them secret — never expose in client-side code. Optional: set `X-End-User-Id: ` to attribute usage to a specific end user. ### 2. Gateway JWT (client-side, recommended) Short-lived tokens minted from an API key via `POST /v1/tokens`. Use in browser clients. ``` Authorization: Bearer eyJhbGciOiJIUzI1NiIs... ``` Typical flow: 1. Your backend calls `POST /v1/tokens` with the API key to get a JWT 2. Your frontend uses the JWT for all gateway calls 3. JWT expires in 5-15 minutes — fetch a new one when needed --- ## Endpoints ### POST /v1/tokens — Mint a JWT Requires API key auth. Returns a short-lived JWT for client-side use. **Request:** ```http POST /v1/tokens Authorization: Bearer sk_live_... Content-Type: application/json { "expires_in": 900 } ``` - `expires_in` (optional): Token lifetime in seconds. Clamped to 300-900. Default: 900. **Response:** ```json { "token": "eyJhbGciOiJIUzI1NiIs..." } ``` The JWT payload contains: - `kid` — API key ID - `iss` — Organization ID - `sub` — End-user ID (from `X-End-User-Id` header, if set) - `scp` — Scopes - `iat`, `exp` — Issued at, expiration --- ### GET /v1/models — List available models Returns models the caller's scopes permit. **Request:** ```http GET /v1/models Authorization: Bearer ``` **Query parameters:** - `capability` (optional): Filter by capability — `text2image`, `image2image`, `text2video`, `image2video`, `text2speech`, `text2text` - `groupBy` (optional): Set to `capability` to group results **Response (flat):** ```json [ { "id": "bfl/flux-2", "name": "FLUX.2", "creator": "Black Forest Labs", "capability": "text2image" }, { "id": "google/veo-3.1-fast", "name": "Veo 3.1 Fast", "creator": "Google", "capability": "text2video" }, { "id": "anthropic/claude-sonnet-4.6", "name": "Claude Sonnet 4.6", "creator": "Anthropic", "capability": "text2text" } ] ``` **Response (grouped by capability):** ```json { "text2image": [ { "id": "bfl/flux-2", "name": "FLUX.2", "creator": "Black Forest Labs" } ], "text2video": [ { "id": "google/veo-3.1-fast", "name": "Veo 3.1 Fast", "creator": "Google" } ] } ``` --- ### GET /v1/models/schema — Get model input schema Returns an OpenAPI-style JSON schema describing a model's input parameters. Use this to dynamically build UIs or validate inputs. **Request:** ```http GET /v1/models/schema?model=bfl/flux-2 Authorization: Bearer ``` **Response:** ```json { "model": "bfl/flux-2", "name": "FLUX.2", "creator": "Black Forest Labs", "type": "image", "input_schema": { "type": "object", "required": ["prompt"], "properties": { "prompt": { "type": "string", "title": "Prompt", "x-imgly-builder": { "component": "TextArea" } }, "format": { "enum": ["1:1", "1:1_HD", "4:3", "16:9", "3:4", "9:16"], "type": "string", "title": "Format", "default": "4:3", "x-imgly-enum-labels": { "1:1": "Square", "4:3": "Landscape 4:3", "16:9": "Widescreen 16:9" } } }, "x-property-order": ["prompt", "format"] } } ``` Custom extension fields: - `x-property-order` — Suggested field ordering for UIs - `x-imgly-enum-labels` — Human-readable labels for enum values - `x-imgly-enum-icons` — Icon identifiers for enum values - `x-imgly-builder` — UI component hints (e.g., `{ "component": "TextArea" }`) - `x-imgly-step` — Step size for numeric inputs For models supporting custom dimensions, the schema uses `anyOf` with a `$ref` to `#/components/schemas/CustomSize`: ```json { "format": { "anyOf": [ { "enum": ["1:1", "4:3", "16:9"], "type": "string" }, { "$ref": "#/components/schemas/CustomSize" } ] }, "components": { "schemas": { "CustomSize": { "type": "object", "properties": { "width": { "type": "integer", "minimum": 1, "maximum": 2048, "default": 1024 }, "height": { "type": "integer", "minimum": 1, "maximum": 2048, "default": 768 } } } } } } ``` --- ### POST /v1/responses — Generate content The main generation endpoint. Accepts a model ID and input parameters, returns an SSE stream with progress updates and the final result. **Request (media model — image/video/audio):** ```http POST /v1/responses Authorization: Bearer Content-Type: application/json { "model": "bfl/flux-2", "prompt": "A serene mountain landscape at sunset", "format": "16:9" } ``` **Request (text model — chat completions):** ```http POST /v1/responses Authorization: Bearer Content-Type: application/json { "model": "anthropic/claude-sonnet-4.6", "messages": [ { "role": "user", "content": "Explain quantum computing in simple terms" } ] } ``` **Response:** SSE stream with `Content-Type: text/event-stream` The response includes an `x-gateway-request-id` header (format: `gw_`). #### SSE Events **Status updates** (during queuing and processing): ``` event: generation.status data: {"status":"queued","request_id":"gw_abc123"} event: generation.status data: {"status":"in_progress","request_id":"gw_abc123","progress":0.5} ``` **Text deltas** (streaming text for LLM models): ``` event: generation.delta data: {"contentType":"text","data":"Hello","request_id":"gw_abc123"} event: generation.delta data: {"contentType":"text","data":" world","request_id":"gw_abc123"} ``` **Completed** (final result): ``` event: generation.completed data: {"request_id":"gw_abc123","output":[{"type":"image_url","url":"https://gateway.img.ly/v1/assets/ephemeral/550e8400-...","content_type":"image/png"}]} ``` For text models, the completed event includes token usage: ``` event: generation.completed data: {"request_id":"gw_abc123","usage":{"input_tokens":25,"output_tokens":150,"total_tokens":175}} ``` **Failed:** ``` event: generation.failed data: {"error":"Generation timed out","request_id":"gw_abc123"} ``` #### Media model input fields | Field | Type | Required | Description | |---|---|---|---| | `model` | string | Yes | Model ID (e.g., `bfl/flux-2`) | | `prompt` | string | Yes | Generation prompt | | `image_urls` | string[] | Depends on model | Input image URLs (for image2image, image2video models) | | `format` | string or object | No | Output format — preset key (e.g., `"16:9"`) or `{ "width": 1024, "height": 768 }` | | `duration` | integer | No | Video duration in seconds (for video models) | | `resolution` | string | No | Resolution preset (e.g., `"720p"`, `"1080p"`, `"4k"`) | | *model-specific* | varies | No | Additional properties defined in the model schema | #### Text model input fields | Field | Type | Required | Description | |---|---|---|---| | `model` | string | Yes | Model ID (e.g., `anthropic/claude-sonnet-4.6`) | | `messages` | array | Yes | Chat messages in OpenAI format | | `messages[].role` | string | Yes | `"user"`, `"assistant"`, or `"system"` | | `messages[].content` | string or array | Yes | Message content | --- ### POST /v1/uploads — Upload assets Request a presigned URL for direct client-to-storage upload. Use this to upload input images before referencing them in generation requests. **Request:** ```http POST /v1/uploads Authorization: Bearer Content-Type: application/json { "content_type": "image/png" } ``` Allowed content types: `image/png`, `image/jpeg`, `image/webp`, `image/gif` **Response (201):** ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "upload_url": "https://presigned-upload-url...", "asset_url": "https://gateway.img.ly/v1/assets/ephemeral/550e8400-...", "expires_in": 3600 } ``` **Then upload the file directly to the presigned URL:** ```http PUT Content-Type: image/png ``` After upload, use the `asset_url` value in the `image_urls` field of generation requests. --- ### GET /v1/assets/:bucket/:id — Retrieve assets Redirects to a presigned download URL. Treat asset URLs as opaque tokens — anyone holding the URL can fetch the asset, so do not log them in shared systems or expose them publicly. **Response:** 302 redirect to a presigned URL (valid for 1 hour). Output URLs in `generation.completed` events already point to this endpoint. --- ## Model ID Convention Format: `creator/model` Examples: - `bfl/flux-2` — FLUX.2 by Black Forest Labs - `google/veo-3.1-fast` — Veo 3.1 Fast by Google - `anthropic/claude-sonnet-4.6` — Claude Sonnet 4.6 by Anthropic Use the model ID exactly as returned by `GET /v1/models`. ### Naming patterns Image models: - `creator/model` — text-to-image (default) - `creator/model-edit` — image-to-image editing Video models: - `creator/model` — text-to-video (default) - `creator/model-i2v` — image-to-video --- ## Available Models The model catalog is dynamic and changes over time. Always fetch the current list with `GET /v1/models` rather than hardcoding model IDs. Capabilities currently supported: `text2image`, `image2image`, `text2video`, `image2video`, `text2speech`, `text2text`. Filter the catalog with `?capability=` or group with `?groupBy=capability`. --- ## Error Handling All errors return structured JSON: ```json { "error": { "code": "error_code", "message": "Human-readable description" } } ``` | Code | HTTP Status | Meaning | |---|---|---| | `unauthorized` | 401 | Missing or invalid credentials | | `invalid_request` | 400 | Malformed request body or parameters | | `invalid_model_id` | 400 | Model ID syntax error | | `model_not_found` | 404 | Model not in catalog | | `provider_not_authorized` | 403 | Token does not have permission to call this model | | `provider_unavailable` | 503 | Model is temporarily unavailable | | `provider_not_supported` | 400 | Requested model variant is unavailable | | `validation_error` | 400 | Input validation failed (details in message) | | `insufficient_credits` | 402 | Not enough credits to run this generation | | `service_error` | 502 | Gateway encountered a temporary failure | | `not_found` | 404 | Asset not found | --- ## Complete Integration Examples ### Example 1: Text-to-Image (Server-Side with API Key) ```typescript const GATEWAY_URL = "https://gateway.img.ly"; const API_KEY = "sk_live_..."; // Generate an image const response = await fetch(`${GATEWAY_URL}/v1/responses`, { method: "POST", headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ model: "bfl/flux-2", prompt: "A serene mountain landscape at sunset, photorealistic", format: "16:9", }), }); // Parse the SSE stream const reader = response.body!.getReader(); const decoder = new TextDecoder(); let buffer = ""; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const frames = buffer.split("\n\n"); buffer = frames.pop() ?? ""; for (const frame of frames) { if (!frame.trim() || frame.startsWith(":")) continue; let eventType = ""; const dataLines: string[] = []; for (const line of frame.split("\n")) { if (line.startsWith("event:")) eventType = line.slice(6).trim(); if (line.startsWith("data:")) dataLines.push(line.slice(5).trim()); } if (dataLines.length === 0) continue; const data = JSON.parse(dataLines.join("\n")); if (eventType === "generation.completed") { const imageUrl = data.output[0].url; console.log("Image ready:", imageUrl); } if (eventType === "generation.failed") { console.error("Generation failed:", data.error); } } } ``` ### Example 2: Client-Side with JWT Token Flow ```typescript const GATEWAY_URL = "https://gateway.img.ly"; // Step 1: Your backend mints a JWT (keep API key server-side) async function getToken(): Promise { const res = await fetch("/api/gateway-token", { method: "POST" }); const { token } = await res.json(); return token; } // Your backend endpoint: // app.post("/api/gateway-token", async (req, res) => { // const response = await fetch("https://gateway.img.ly/v1/tokens", { // method: "POST", // headers: { // Authorization: `Bearer ${process.env.IMGLY_API_KEY}`, // "Content-Type": "application/json", // }, // body: JSON.stringify({ expires_in: 900 }), // }); // const data = await response.json(); // res.json({ token: data.token }); // }); // Step 2: Use the JWT for generation const token = await getToken(); const response = await fetch(`${GATEWAY_URL}/v1/responses`, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ model: "bfl/flux-2", prompt: "A cute robot painting", format: "1:1", }), }); // Step 3: Read the SSE stream (same pattern as Example 1) ``` ### Example 3: Image-to-Image Editing with Upload ```typescript const GATEWAY_URL = "https://gateway.img.ly"; const token = await getToken(); // Step 1: Get a presigned upload URL const uploadRes = await fetch(`${GATEWAY_URL}/v1/uploads`, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ content_type: "image/png" }), }); const { upload_url, asset_url } = await uploadRes.json(); // Step 2: Upload the image directly to storage await fetch(upload_url, { method: "PUT", headers: { "Content-Type": "image/png" }, body: imageFile, // File, Blob, or ArrayBuffer }); // Step 3: Use the uploaded image in a generation request const response = await fetch(`${GATEWAY_URL}/v1/responses`, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ model: "bfl/flux-2-edit", prompt: "Change the sky to a dramatic sunset", image_urls: [asset_url], format: "auto", }), }); // Read SSE stream for result... ``` ### Example 4: Video Generation ```typescript const response = await fetch(`${GATEWAY_URL}/v1/responses`, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ model: "google/veo-3.1-fast", prompt: "A timelapse of a flower blooming", format: "16:9", duration: 8, resolution: "1080p", generate_audio: true, }), }); // Video generation takes longer — expect multiple status events before completion // SSE events: generation.status (queued) → generation.status (in_progress) → generation.completed ``` ### Example 5: Text Generation (Streaming) ```typescript const response = await fetch(`${GATEWAY_URL}/v1/responses`, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ model: "anthropic/claude-sonnet-4.6", messages: [ { role: "system", content: "You are a helpful assistant." }, { role: "user", content: "Write a haiku about programming." }, ], }), }); // Text models stream deltas — accumulate them for the full response let fullText = ""; // ... parse SSE stream ... // On generation.delta: fullText += data.data; // On generation.completed: done, fullText has the complete response ``` ### Example 6: Schema-Driven Dynamic UI ```typescript // Fetch the model's input schema to build a form dynamically const schemaRes = await fetch( `${GATEWAY_URL}/v1/models/schema?model=bfl/flux-2`, { headers: { Authorization: `Bearer ${token}` } }, ); const schema = await schemaRes.json(); // schema.input_schema.properties describes each field: // - type, enum values, defaults, min/max constraints // - x-imgly-enum-labels for human-readable option names // - x-property-order for recommended field order // Build form fields from schema.input_schema.properties for (const key of schema.input_schema["x-property-order"] ?? Object.keys(schema.input_schema.properties)) { const prop = schema.input_schema.properties[key]; // Create appropriate UI control based on prop.type, prop.enum, etc. } ``` --- ## SSE Stream Parsing Helper Reusable function for reading SSE streams from the gateway: ```typescript type SSEHandler = (event: string, data: any) => void; async function readGatewaySSE(response: Response, onEvent: SSEHandler): Promise { const reader = response.body!.getReader(); const decoder = new TextDecoder(); let buffer = ""; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const frames = buffer.split("\n\n"); buffer = frames.pop() ?? ""; for (const frame of frames) { if (!frame.trim() || frame.startsWith(":")) continue; let eventType = ""; const dataLines: string[] = []; for (const line of frame.split("\n")) { if (line.startsWith("event:")) eventType = line.slice(6).trim(); else if (line.startsWith("data:")) dataLines.push(line.slice(5).trim()); } if (dataLines.length > 0) { try { onEvent(eventType, JSON.parse(dataLines.join("\n"))); } catch { onEvent(eventType, dataLines.join("\n")); } } } } } // Usage: await readGatewaySSE(response, (event, data) => { switch (event) { case "generation.status": console.log(`Status: ${data.status}`, data.progress ?? ""); break; case "generation.delta": process.stdout.write(data.data); // streaming text break; case "generation.completed": console.log("Output:", data.output ?? data.usage); break; case "generation.failed": console.error("Error:", data.error); break; } }); ``` --- ## Authorization Each API key (and every JWT minted from it) is permitted to call a specific set of models. By default keys can call any model in the catalog; narrower permissions are configured at key-issuance time. If a token cannot call a given model, the gateway returns `provider_not_authorized` (403). Use `GET /v1/models` to discover which models the current token can call — the catalog returned is already filtered to the token's permissions. --- ## Credit System Generation requests consume credits from the account associated with the API key. If credits are insufficient, the gateway returns `insufficient_credits` (402). Failed generations do not consume credits. Credit accounting is handled entirely by the gateway — integrators just need to handle the 402 error gracefully. --- ## Key Integration Notes 1. **All generation responses are SSE streams.** Even for fast models, always parse as SSE. 2. **Output URLs are gateway-hosted.** The gateway re-uploads provider outputs to its own storage. URLs in `generation.completed` point to `/v1/assets/...` and are accessible without auth. 3. **Use the schema endpoint** to discover model parameters at runtime. Don't hardcode input shapes — they vary by model and may change. 4. **Tokens expire quickly** (5-15 min). Cache them but refresh before expiry. 5. **Image-to-image models require `image_urls`**. Upload via `/v1/uploads` first if you have local files. 6. **Video models are slower** — expect 30s-5min of status events before completion. 7. **Text models stream deltas** — accumulate `generation.delta` events for the full response. 8. **The model list is dynamic.** Always fetch `/v1/models` at runtime rather than hardcoding model IDs.