f. frame / docs

IMAGE API / VERSION 1 / PRIVATE PILOT

Build with the Image API.

Every production request uses https://img-analyse.vercel.app. This guide covers the public /v1 API, with copyable examples, exact routes and the details an automated client needs to recover safely.

01

Start here

Use a workspace key in Authorization: Bearer <key> on every /v1 request. Operators issue keys; there is no public signup. Keep the key on the server or in a local process environment. The playground accepts it in browser memory for manual use.

Two request styles

Most operations use the durable asset → job flow below. /v1/image-attributes and /v1/images/upscale accept raw image bytes directly and respond within one HTTP request. Provider work may still continue after a client timeout.

shell · authenticated discovery
export IMAGE_API_URL="https://img-analyse.vercel.app"
# Set IMAGE_API_KEY privately in your environment.
curl -sS "$IMAGE_API_URL/v1/capabilities"   -H "Authorization: Bearer $IMAGE_API_KEY"

For local development, point IMAGE_API_URL at http://127.0.0.1:4114. JSON endpoints return {"error":{"code":"…","message":"…"}} on failure. Production returns cloud signed asset URLs that do not take the bearer key; local transfer URLs require it.

02

Complete asset → job workflow

Reserve the input, PUT its exact bytes to the returned transfer URL, complete the asset, submit a job, receive its signed completion webhook (or poll Location for recovery), then fetch an output asset and its fresh download URL. Save each idempotency key before sending its request; reuse that key after a lost response.

JavaScript · upload, extract, download
// Node 20+; save as example.mjs and run with IMAGE_API_KEY in the environment.
import { readFile, writeFile } from "node:fs/promises";
import { createHash, randomUUID } from "node:crypto";

const base = process.env.IMAGE_API_URL ?? "https://img-analyse.vercel.app";
const key = process.env.IMAGE_API_KEY;
if (!key) throw new Error("Set IMAGE_API_KEY to a workspace bearer key");

async function api(path, init = {}) {
  const response = await fetch(new URL(path, base), {
    ...init,
    headers: { ...init.headers, Authorization: `Bearer ${key}` },
  });
  const data = await response.json();
  if (!response.ok) throw new Error(`${response.status}: ${JSON.stringify(data.error)}`);
  return { data, response };
}

function transferHeaders(url, headers = {}) {
  const target = new URL(url);
  // Local /v1/local-objects URLs need the bearer key; cloud signed URLs do not.
  return target.origin === new URL(base).origin && target.pathname.startsWith("/v1/local-objects/")
    ? { ...headers, Authorization: `Bearer ${key}` } : headers;
}

const bytes = await readFile("input.jpg");
const sha256 = createHash("sha256").update(bytes).digest("hex");
// Persist each key with your request state before sending in a real integration.
const uploadKey = randomUUID();
const { data: reserved } = await api("/v1/assets", {
  method: "POST",
  headers: { "Content-Type": "application/json", "Idempotency-Key": uploadKey },
  body: JSON.stringify({ contentType: "image/jpeg", bytes: bytes.length, sha256 }),
});

// Send the returned headers to the transfer URL. Only local transfer URLs need the key.
const put = await fetch(reserved.upload.url, {
  method: reserved.upload.method,
  headers: transferHeaders(reserved.upload.url, reserved.upload.headers), body: bytes,
});
if (!put.ok) throw new Error(`Upload failed: ${put.status}`);
const { data: asset } = await api(`/v1/assets/${reserved.asset.id}/complete`, { method: "POST" });

const jobKey = randomUUID();
const { data: accepted, response } = await api("/v1/jobs", {
  method: "POST",
  headers: { "Content-Type": "application/json", "Idempotency-Key": jobKey },
  body: JSON.stringify({ operation: "subject.extract", input: { assetId: asset.id }, options: {} }),
});
console.log("Poll", response.headers.get("Location"), "job", accepted.id);

let job;
do {
  await new Promise(resolve => setTimeout(resolve, 3000));
  ({ data: job } = await api(`/v1/jobs/${accepted.id}`));
} while (!["succeeded", "failed", "cancelled"].includes(job.status));
if (job.status !== "succeeded") throw new Error(JSON.stringify(job.error ?? job.status));

const cutout = job.outputs.find(output => output.role === "cutout");
if (!cutout) throw new Error("No cutout output");
const { data: outputAsset } = await api(`/v1/assets/${cutout.assetId}`);
const download = await fetch(outputAsset.downloadUrl, {
  headers: transferHeaders(outputAsset.downloadUrl),
});
if (!download.ok) throw new Error(`Download failed: ${download.status}`);
await writeFile("cutout.webp", Buffer.from(await download.arrayBuffer()));

The reserve response contains { asset, upload: { url, method, headers, expiresAt } }. The PUT must use the returned headers, including the cloud checksum header when present. Completion returns a ready asset. Do not put an external URL in a job body; jobs refer to workspace-owned asset IDs.

Job stage and completion webhooks

Configure a tenant webhook in /dashboard for default callbacks. For subject.extract and instagram.processForWeb, a top-level webhook overrides the tenant setting. Subject extraction returns HTTP 202; the Instagram route returns HTTP 200 as soon as its queued job is saved. Save that job ID and the idempotency key before waiting for callbacks.

JSON · add to either submission
{
  "webhook": {
    "url": "https://consumer.example.com/image-events",
    "secret": "a-shared-secret-of-at-least-32-characters"
  }
}

The receiver gets image.job.stage with stage, previousStage, sequence and a job snapshot, followed by image.job.completed with final status, errors and output assets, including dimensions and fresh download URLs. Check X-ImgAnalyse-Timestamp freshness and verify X-ImgAnalyse-Signature as HMAC-SHA256 of timestamp + period + the raw body using the shared secret. Deduplicate X-ImgAnalyse-Event-Id and ignore stage events with older sequence numbers. Persist receipt before acknowledging with 2xx. Failed deliveries retry with exponential backoff up to 12 attempts; delivery is at least once and may arrive out of order.

Use public HTTPS, or loopback HTTP in local storage mode. Download URLs expire within five minutes; use GET /v1/assets/{assetId} to refresh them. If callbacks stop, recover with GET /v1/jobs/{jobId}; reuse the identical submission and idempotency key after a lost response. New Instagram and subject-extraction playground runs receive callbacks through a server receiver and browser event stream. Older saved runs and other operations still poll. The upload example below demonstrates the polling recovery path. When rotating tenant secrets, accept both secrets until older jobs finish.

03

Route reference

All paths below are relative to the base URL and require the bearer key. Braced IDs are opaque, workspace-owned strings such as asset_… and job_….

Discovery and accounting

MethodPathWhat it does
GET/v1/capabilitiesOperations, feature availability, limits and recipe revision.
GET/v1/openapi.jsonAuthenticated OpenAPI 3.1 document and input schemas.
GET/v1/usageWorkspace attempt, concurrency and storage counters.
GET/v1/runs?limit=50&cursor=…Newest-first run history and estimated costs; paginate until nextCursor is null.

Assets

MethodPathWhat it does
POST/v1/assetsReserve an upload; JSON {contentType, bytes, sha256} and Idempotency-Key. Returns signed PUT instructions (201).
POST/v1/assets/{assetId}/completeValidate uploaded bytes and create an oriented, normalized WebP asset.
GET/v1/assets/{assetId}Asset metadata and a fresh, short-lived downloadUrl.
DELETE/v1/assets/{assetId}Delete an asset when no unfinished job references it.

Asynchronous jobs

MethodPathWhat it does
POST/v1/jobsSubmit any job operation with JSON and Idempotency-Key; returns 202 and Location.
GET/v1/jobs/{jobId}Durable status, output asset IDs, geometry, findings, warnings and usage.
POST/v1/jobs/{jobId}/cancelRequest best-effort cancellation; continue polling to a terminal state.
POST/v1/jobs/{jobId}/retry-sceneExplicitly retry a failed scene foreground/background stage.
POST/v1/jobs/{jobId}/retry-backgroundAlias of retry-scene for existing clients.
POST/v1/jobs/{jobId}/retry-outpaintExplicitly retry failed automatic subject edge repair.
POST/v1/jobs/{jobId}/retry-compositionExplicitly retry the failed composition background stage.
POST/v1/jobs/{jobId}/retry-expansionExplicitly retry the failed background expansion stage.

Operation-specific job routes

MethodPathWhat it does
POST/v1/images/extendSubmit image.extend with {input, options}; operation is added by the route.
POST/v1/images/brand-colorsSubmit image.brand with an uploaded asset and neutral, primary and highlight hex colours.
POST/v1/images/layout-variantsSubmit image.layouts; standard formats return three or nine placements, while 9:16 mobile returns one or three.
POST/v1/images/process-for-webSubmit image.processForWeb; returns one finished image or two positioned layers per ready placement.
POST/v1/images/instagram-process-for-webScreen for text overlaid on the photo, remove it by default, optionally extract the words, then return reusable image layers and placement guides.
POST/v1/background/expandSubmit background.expand with {input, options}; operation is added by the route.

Synchronous image routes

MethodPathWhat it does
POST/v1/images/brand-analysisAnalyze raw image bytes for primary, secondary and highlight colours plus visible font type.
GET/v1/images/brand-analysisRecover the saved brand analysis using its Idempotency-Key.
POST/v1/image-attributesAnalyze raw JPEG, PNG or WebP bytes (4 MB max); JSON findings and usage.
GET/v1/image-attributesRecover a saved attribute result with the original Idempotency-Key.
POST/v1/images/upscale?factor=2Upscale raw image bytes 2× or 4×; returns WebP bytes and result headers.

Local development transfer only

MethodPathWhat it does
PUT/v1/local-objects/{key}Upload bytes to the local store using the URL returned by POST /v1/assets; requires the bearer key.
GET/v1/local-objects/{key}Download a local ready asset using its returned URL; requires the bearer key. Not available in production.
04

Asynchronous job operations

POST /v1/jobs accepts {operation, input, options}. The operation-specific routes accept {input, options} and set operation themselves. Every submission needs a unique Idempotency-Key for that logical request. The examples below use illustrative IDs; replace them with IDs returned by your workspace.

subject.extract

POST /v1/jobs

Cut out the subject as a transparent WebP and alpha mask. The default is a full-frame result; trim returns placement bounds. outpaint runs automatic minor-cutoff repair after extraction and requires trim: false.

JSON request body
{
  "operation": "subject.extract",
  "input": {
    "assetId": "asset_0123456789abcdef0123456789abcdef"
  },
  "options": {
    "trim": false,
    "removeTextOverlays": false,
    "outpaint": false
  }
}

Results: cutout, mask; optional originalCutout and originalMask after repair.

scene.separate

POST /v1/jobs

Extract the subject and reconstruct the background in the original frame. includeForeground selects physical supports; completeForeground adds a completion stage and requires includeForeground. Optional outpaint repairs detected minor cutoffs.

JSON request body
{
  "operation": "scene.separate",
  "input": {
    "assetId": "asset_0123456789abcdef0123456789abcdef"
  },
  "options": {
    "includeForeground": true,
    "completeForeground": false,
    "outpaint": false
  }
}

Results: cutout, mask, backgroundWithHole, background; optional foreground and subjectWithForeground.

scene.compose

POST /v1/jobs

Fit extracted layers inside a subject polygon on a new canvas, then fill background gaps. Use either an assetId (extracts in this job) or a completed scene.separate sceneJobId (reuses its layers). Polygon coordinates are normalized to the output canvas, not the input image.

JSON request body
{
  "operation": "scene.compose",
  "input": {
    "sceneJobId": "job_0123456789abcdef0123456789abcdef"
  },
  "options": {
    "background": {
      "width": 1200,
      "height": 800
    },
    "subjectPolygon": [
      {
        "x": 0.4,
        "y": 0.1
      },
      {
        "x": 0.85,
        "y": 0.1
      },
      {
        "x": 0.85,
        "y": 0.9
      },
      {
        "x": 0.4,
        "y": 0.9
      }
    ],
    "includeForeground": true
  }
}

Results: image, cutout, mask, background, backgroundWithHole; optional foreground and subjectWithForeground.

background.expand

POST /v1/background/expand or /v1/jobs

Expand the intact scene to a rectangle, then remove people and foreground supports. The anchor (0–1) places the original within the new canvas; default is centre. Strict local alignment checks can reject a drifting edit before the next paid stage.

JSON request body
{
  "input": {
    "assetId": "asset_0123456789abcdef0123456789abcdef"
  },
  "options": {
    "background": {
      "width": 1200,
      "height": 800
    },
    "anchor": {
      "x": 0.5,
      "y": 1
    }
  }
}

Results: expandedScene (intermediate), background (final clean scene).

image.extend

POST /v1/images/extend or /v1/jobs

Extend the intact photo around its subjects. Omitted provider chooses flux-high; explicit openai or flux-fast can be used for comparison. A non-empty backgroundHint selects flux-fill-pro regardless of provider and costs more. Alignment is report-only, so inspect the result.

JSON request body
{
  "input": {
    "assetId": "asset_0123456789abcdef0123456789abcdef"
  },
  "options": {
    "width": 1200,
    "height": 800,
    "anchor": {
      "x": 0.5,
      "y": 0.5
    },
    "provider": "flux-high"
  }
}

Results: image; imageExtension.sourcePlacement and alignment report.

image.brand

POST /v1/images/brand-colors or /v1/jobs

Apply a brand palette in one OpenAI image edit. Neutrals cover broad editable areas (roughly 60%), primary marks a prominent existing element (roughly 30%), and highlight accents a small existing detail (roughly 10%). These are visual targets, not measured pixel ratios. The model is asked to preserve people, products, text and logos, but review the generated result. Source limit: 4 megapixels and aspect 1:3 to 3:1. Existing keys with image.extend permission can call this route; newly issued keys include image.brand.

JSON request body
{
  "input": {
    "assetId": "asset_0123456789abcdef0123456789abcdef"
  },
  "options": {
    "neutrals": [
      "#F3F1EB",
      "#23272B"
    ],
    "primary": "#1A365D",
    "highlight": "#00C9B7"
  }
}

Results: image (branded WebP resampled to the original dimensions); brandColors palette, model, rule and review warnings.

image.layouts

POST /v1/images/layout-variants or /v1/jobs

Expand one master image, extract its subject, then create left/centre/right crops at the bottom for up to four formats. A clear, complete-touching or minor-cutoff bottom edge enables six more centre/top variants per standard format, with bottom expansion in the same generation. Mobile 9:16 centres the subject in the lower half; a complete bottom edge adds mobile centre and top placements. Major or uncertain cutoffs keep the original bottom placements. A non-empty backgroundHint selects the higher-cost Fill Pro provider.

JSON request body
{
  "input": {
    "assetId": "asset_0123456789abcdef0123456789abcdef"
  },
  "options": {
    "formats": [
      {
        "id": "landscape",
        "width": 1200,
        "height": 675
      },
      {
        "id": "square",
        "width": 900,
        "height": 900
      },
      {
        "id": "mobile",
        "width": 540,
        "height": 960
      }
    ]
  }
}

Results: expandedScene, cutout, mask, backgroundWithHole, crop assets with variantId and verticalPlacement.

image.processForWeb

POST /v1/images/process-for-web or /v1/jobs

Expand one master, evaluate the extracted subject, then choose layered or finished-image layouts. A clean subject yields an opaque background and tightly trimmed transparent subject for each ready horizontal/vertical placement; subjectPlacement positions it in layout pixels. Otherwise each layout is a finished crop. Eligible bottom edges enable nine standard-format placements; major or uncertain cutoffs retain three. Mobile has one lower-half placement plus centre and top when the bottom edge is complete.

JSON request body
{
  "input": {
    "assetId": "asset_0123456789abcdef0123456789abcdef"
  },
  "options": {
    "formats": [
      {
        "id": "landscape",
        "width": 1200,
        "height": 675
      },
      {
        "id": "square",
        "width": 900,
        "height": 900
      },
      {
        "id": "mobile",
        "width": 540,
        "height": 960
      }
    ]
  }
}

Results: expandedScene plus, per ready variantId, background+cutout with subjectPlacement (mode=layers) or crop (mode=flat).

instagram.processForWeb

POST /v1/images/instagram-process-for-web or /v1/jobs

Screen an Instagram image for added text, then create reusable image layers. Mobile-only processing is the default: one 9:24 background plus one transparent subject placed at native scale so the visible subject fills about 90% of the background width. The title baseline sits 3% of the visible subject height below its top, behind the cutout. Face and held-object areas of interest are separate labeled rectangles with 15% padding on each side, without a subject-height cap; its top/bottom lines are returned in crop pixels. A detected floor edge is an optional polyline that may slope. Set mobileOnly:false to request the older desktop and mobile layouts with optional support-surface splits. Images without overlay text skip cleanup; extractText defaults to false and uses removal-only cleanup when needed. extractShadow defaults to false; true adds a local soft contact shadow to a grounded subject without another provider call. Lettering photographed on tattoos or signs does not count as overlay text.

JSON request body
{
  "input": {
    "assetId": "asset_0123456789abcdef0123456789abcdef"
  },
  "options": {
    "mobileOnly": true,
    "extractText": false,
    "extractShadow": false
  }
}

Results: two assets by default: mobile background and transparent cutout; processForWeb.responsiveLayers.mobile has subjectPlacement, titleGuide, dontOverlap and optional floor boundaryPoints.

image.analyze

POST /v1/jobs

Run OCR and/or person framing checks. An unavailable check is reported as unavailable, not as a clean result. OCR regions can feed text.remove.

JSON request body
{
  "operation": "image.analyze",
  "input": {
    "assetId": "asset_0123456789abcdef0123456789abcdef"
  },
  "options": {
    "checks": [
      "ocr",
      "framing"
    ]
  }
}

Results: checks; no generated image required.

text.remove

POST /v1/jobs

Remove caller-supplied, validated OCR text regions. Coordinates are normalized to the oriented input. The repair mask is a preview and may have different dimensions from the input.

JSON request body
{
  "operation": "text.remove",
  "input": {
    "assetId": "asset_0123456789abcdef0123456789abcdef"
  },
  "options": {
    "strategy": "lama",
    "regions": [
      {
        "x": 0.1,
        "y": 0.2,
        "width": 0.3,
        "height": 0.08,
        "text": "Example",
        "confidence": 0.98
      }
    ]
  }
}

Results: image (cleaned WebP), mask preview.

Shape rules

Asset images: JPEG, PNG or WebP, 16 × 16 pixels minimum, 20 MB maximum, 32 megapixels maximum. Image extension: 64–4096 px per side and 4,194,304 pixels total; FLUX has a tighter 2560 px side limit. Background rectangle: 16–4096 px per side and 16 megapixels total. Layout formats: 1–4 unique IDs, 128–2048 px per side, aspect 1:2 to 3:1; the mobile ID requires 9:16. A composition polygon needs 3–32 non-crossing points in the 0–1 output-canvas coordinate space.

Brand colours: apply a 60–30–10 palette

POST /v1/images/brand-colors creates an asynchronous image.brand job from an uploaded asset. Supply one to four six-digit hex neutrals, one primary and one highlight. The worker makes one OpenAI image edit and returns one WebP asset resampled to the source dimensions. The 60–30–10 proportions are prompt guidance over suitable editable surfaces, not a measured or guaranteed pixel distribution. Skin, faces, logos, text and product identity may still change; inspect the result before use.

Images are limited to 4 megapixels and aspect ratio 1:3 to 3:1. Save the Idempotency-Key before submitting. An uncertain accepted edit is not automatically sent again, and a failed job records any returned model usage. Existing keys with image.extend permission can use the route; new keys include image.brand. Check job.brandColors, job.warnings and /v1/runs. Locally, run npm run image-api:worker to process queued jobs.

shell · submit a brand edit
# Upload and complete input.jpg using section 02, then set its ready asset ID.
export IMAGE_API_ASSET_ID="asset_0123456789abcdef0123456789abcdef"
BRAND_KEY="brand-$(uuidgen)" # Save this key with the job ID.
curl --fail-with-body -sS "$IMAGE_API_URL/v1/images/brand-colors" \
  -X POST -H "Authorization: Bearer $IMAGE_API_KEY" \
  -H "Content-Type: application/json" -H "Idempotency-Key: $BRAND_KEY" \
  -d '{"input":{"assetId":"'"$IMAGE_API_ASSET_ID"'"},"options":{"neutrals":["#F3F1EB","#23272B"],"primary":"#1A365D","highlight":"#00C9B7"}}' \
  -o brand-job.json

# Poll GET /v1/jobs/{jobId} with the same bearer key until status=succeeded.
# Find outputs[].assetId where role=image, GET /v1/assets/{assetId},
# then fetch its downloadUrl. Local transfer URLs require the bearer key.
# Reuse BRAND_KEY after a lost submit response; a new key can incur a new edit.

Process for web: assemble responsive layouts

POST /v1/images/process-for-web creates one asynchronous image.processForWeb job from an uploaded asset. Supply 1–4 formats or omit options for landscape (1200 × 675), square (900 × 900), portrait (720 × 900) and mobile (540 × 960). Standard formats offer three bottom-aligned horizontal placements and, for a clear, complete-touching or minor-cutoff bottom edge, six centre/top placements. Mobile has one horizontally centred placement in the lower half, plus centre and top when the bottom edge is complete. Its subject occupies at most about 30% of frame height, with padding around it. An optional nonempty backgroundHint selects FLUX Fill Pro for the shared expansion; otherwise FLUX High is used.

The worker checks the original subject bottom edge, expands one master image, extracts a candidate subject, then evaluates whether that extraction is clean. Read processForWeb.mode after the job succeeds. A layers result provides an opaque background and a tightly trimmed transparent cutout for each ready variant. Place the cutout at subjectPlacement.x and subjectPlacement.y on the background at its native width and height; all four values are integer pixels in that variant’s canvas. A flat result provides one finished crop image per ready variant.

Success always includes an expandedScene asset. Iterate processForWeb.variants, use placement and verticalPlacement to select a position, skip entries with status: "unavailable", and match job.outputs by both role and variantId. Variant asset IDs also appear directly as backgroundAssetId, subjectAssetId or imageAssetId. Inspect evaluation.verdict and evaluation.reason when deciding whether to publish; generated backgrounds and anatomy still need visual review. The taller shared master used for mobile framing can raise the expansion cost.

JavaScript · submit, poll and download web layouts
// Node 20+; first upload an image as shown in section 02.
// Set IMAGE_API_ASSET_ID to the ready asset ID. Persist IMAGE_API_JOB_KEY
// before the first POST, and reuse it after a lost response.
import { mkdir, writeFile } from "node:fs/promises";

const base = new URL(process.env.IMAGE_API_URL ?? "https://img-analyse.vercel.app");
const key = process.env.IMAGE_API_KEY;
const assetId = process.env.IMAGE_API_ASSET_ID;
const jobKey = process.env.IMAGE_API_JOB_KEY;
if (!key || !assetId || !jobKey) throw new Error("Set IMAGE_API_KEY, IMAGE_API_ASSET_ID and IMAGE_API_JOB_KEY");
if (!/^[A-Za-z0-9:._-]{8,128}$/.test(jobKey)) throw new Error("IMAGE_API_JOB_KEY must be 8–128 URL-safe characters");

async function api(path, init = {}) {
  const response = await fetch(new URL(path, base), {
    ...init, headers: { ...init.headers, Authorization: "Bearer " + key },
  });
  const data = await response.json();
  if (!response.ok) throw new Error(response.status + ": " + JSON.stringify(data.error));
  return data;
}

const accepted = await api("/v1/images/process-for-web", {
  method: "POST",
  headers: { "Content-Type": "application/json", "Idempotency-Key": jobKey },
  body: JSON.stringify({ input: { assetId }, options: {
    formats: [{ id: "landscape", width: 1200, height: 675 },
              { id: "square", width: 900, height: 900 },
              { id: "mobile", width: 540, height: 960 }],
  } }),
});
console.log("Job ID (save this):", accepted.id);

let job = accepted;
while (!["succeeded", "failed", "cancelled", "reconciling"].includes(job.status)) {
  await new Promise(resolve => setTimeout(resolve, 3000));
  job = await api("/v1/jobs/" + accepted.id);
}
if (job.status !== "succeeded") throw new Error("Job " + job.status + ": " + JSON.stringify(job.error));

async function saveAsset(id, filename) {
  const asset = await api("/v1/assets/" + id);
  if (!asset.downloadUrl) throw new Error("Asset is not ready: " + id);
  const url = new URL(asset.downloadUrl);
  // Local transfer routes need the bearer key. Do not send it to cloud signed URLs.
  const local = url.origin === base.origin && url.pathname.startsWith("/v1/local-objects/");
  const response = await fetch(url, { headers: local ? { Authorization: "Bearer " + key } : {} });
  if (!response.ok) throw new Error("Download failed: " + id + " (" + response.status + ")");
  await writeFile(filename, Buffer.from(await response.arrayBuffer()));
}

await mkdir("web-layouts", { recursive: true });
const manifest = { jobId: job.id, mode: job.processForWeb.mode, evaluation: job.processForWeb.evaluation, sourceAnalysis: job.sourceAnalysis ?? null, variants: [] };
for (const variant of job.processForWeb.variants) {
  if (variant.status !== "ready") {
    manifest.variants.push({ id: variant.id, formatId: variant.formatId, placement: variant.placement, verticalPlacement: variant.verticalPlacement, status: variant.status, reason: variant.reason });
    continue;
  }
  if (job.processForWeb.mode === "layers") {
    if (!variant.backgroundAssetId || !variant.subjectAssetId || !variant.subjectPlacement) throw new Error("Missing layer metadata: " + variant.id);
    const backgroundFile = "web-layouts/" + variant.id + "-background.webp";
    const subjectFile = "web-layouts/" + variant.id + "-subject.webp";
    await saveAsset(variant.backgroundAssetId, backgroundFile);
    await saveAsset(variant.subjectAssetId, subjectFile);
    manifest.variants.push({ id: variant.id, formatId: variant.formatId, placement: variant.placement, verticalPlacement: variant.verticalPlacement,
      width: variant.width, height: variant.height, status: "ready", subjectPlacement: variant.subjectPlacement,
      backgroundAssetId: variant.backgroundAssetId, subjectAssetId: variant.subjectAssetId, backgroundFile, subjectFile });
  } else {
    if (!variant.imageAssetId) throw new Error("Missing finished image: " + variant.id);
    const imageFile = "web-layouts/" + variant.id + ".webp";
    await saveAsset(variant.imageAssetId, imageFile);
    manifest.variants.push({ id: variant.id, formatId: variant.formatId, placement: variant.placement, verticalPlacement: variant.verticalPlacement,
      width: variant.width, height: variant.height, status: "ready", imageAssetId: variant.imageAssetId, imageFile });
  }
}
await writeFile("web-layouts/manifest.json", JSON.stringify(manifest, null, 2));
Local worker and recovery

Locally, keep npm run image-api:worker running. A queued job remains durable until the worker starts. If polling stops, GET the saved job ID or resubmit the same body with the same idempotency key; do not create a fresh paid job. A provider attempt that becomes uncertain is not repeated automatically. Existing keys may call this operation when they allow both image.layouts and scene.separate; newly issued keys include image.processForWeb.

Video: select usable stills locally

POST /v1/videos/process queues video.frames and returns HTTP 202 before decoding or AI work. Upload first through POST /v1/video-assets, PUT the returned bytes, then complete via /v1/assets/{assetId}/complete. SDK: client.uploadVideo(bytes, contentType, uploadKey), then client.processVideo(assetId, options, jobKey, signal, webhook).

JSON request
{
  "input": {
    "assetId": "asset_0123456789abcdef0123456789abcdef"
  },
  "options": {
    "maxFrames": 10,
    "reviewLimit": 30,
    "sampleFps": 2,
    "minSharpness": 35
  }
}

The local FFmpeg worker samples low-resolution previews, discards blank/poor exposure, blur and nearly identical frames, and chooses sharp candidates spread across time. AI reviews only that shortlist in batches of five for sharp subject detail, visibility, pose and framing. Major or uncertain top/side cutoffs are rejected; bottom cutoffs are allowed. Up to ten distinct approved frames are extracted at original dimensions as WebP images. If fewer pass, fewer are returned; no weak frames are added to fill the list.

Limits: 200 MiB, three minutes, 4K, square pixels and 240 fps. Sampling allows 0.5–4 fps; AI review is capped at 40 candidates, default 30. The sharpness threshold is a heuristic and can be adjusted for low-texture footage. This currently requires local storage and FFmpeg/FFprobe; hosted mode is unavailable. No fal generation, cleanup or RunPod inference is used. Video text remains in the stills.

Use tenant/request webhooks for validating, sampling, filtering, reviewing and extracting progress and completion. videoProcessing reports sample/discard/review counts; its final frames include timestamps, asset IDs, dimensions and per-frame decisions. Outputs use role frame. Existing image-analysis keys work. An uncertain AI batch is not automatically repeated; GET the saved job for recovery.

Process IG post: extract a subject or return the original with background guidance

POST /v1/images/process-ig-post queues instagram.processPost and returns HTTP 202 before provider work. Upload and complete an asset first. Use tenant or request webhooks for stage/completion events, or recover the saved job through GET. SDK: client.processIgPost(assetId, options, key, signal, webhook).

Original screening checks extraction suitability, overlay text, subject bounds, cutoff edges and background composition. A distinct subject or coherent interacting group can proceed. Posters, collages, unusable scenes, distributed flat-lays without a standout subject, or unknown extraction composition return the original. A dominant single dish or product can qualify even in an overhead photo.

Extraction-ineligible is a successful result: status: succeeded, processIgPost.outcome: original_returned, suitability.eligible: false, an explanation and only the uploaded image asset. Inspect suitability.extractionComposition, reasons, summary and screeningFeedback. No cleanup, repair, extraction, cutout/mask or final cutout evaluation runs. Existing text and graphics remain. A failed screening request is a separate error, not this successful branch.

Suitable originals use conditional overlay cleanup and cleaned-source feedback, optional minor-cutoff repair, one full-frame RunPod extraction, and advisory quality evaluation on light/dark mattes. Repair defaults to OpenAI; FLUX is optional. Complete/touching subjects skip repair; major/unknown edges skip speculative repair with a warning. Processed success returns aligned image, cutout and mask. The image retains its original background. Evaluation pass/fail/unknown does not discard available outputs; actual processing failures can fail the job.

JSON request
{
  "input": {
    "assetId": "asset_0123456789abcdef0123456789abcdef"
  },
  "options": {
    "extractText": false,
    "expansionProvider": "openai",
    "expansionFraction": 0.12,
    "mobileViewport": {
      "width": 360,
      "height": 640
    }
  }
}

Choose whether to extract a subject

options.extractSubject defaults to true. Set false for original screening and background guidance only: the endpoint returns the original image with interests, primary selection and crop suggestions, skipping text cleanup, repair, extraction and final cutout evaluation. Existing overlay text stays; text-extraction and repair options have no effect. The test UI has an Extract subject when eligible checkbox.

processIgPost.extractionRequested records the choice. Original-returned results use originalReturnReason: extraction_not_requested for consumer opt-out, or unsuitable_source for rejection by the extraction gate. Eligibility remains a separate image assessment and can be true when extraction was disabled. Inspect outcome and actual output roles before expecting cutout/mask assets. Explicit true and omitted are equivalent for replay; a mode change needs a new idempotency key.

Use an extraction-ineligible image as a background

A multi-dish restaurant scene can be unsuitable for isolation but useful intact on a website. The same initial screening returns processIgPost.backgroundUse independently of extraction eligibility: background suitability/summary/confidence, broad areasOfInterest, dontCropAreas, dontOverlapAreas, textSafeAreas, focalPoint and primaryAreaOfInterest. This works even when sourceAnalysis is absent. The older source-analysis interests are limited to faces and held objects.

primaryAreaOfInterest identifies an area index, kind, label, normalized bounds, confidence and rationale. Selection weighs central/rule-of-thirds placement, prominence, sharpness, contrast, colour, lighting and context. It is null when no priority is defensible. A primary area for cropping does not make the scene eligible for extraction.

Crop positioning keeps the primary region whole when possible, then favours complete neighbours. A wide crop can move upward to include a primary soup bowl and trim a secondary lower plate. It is not a fixed centre band. primaryAreaPreserved reports full retention in the safe crop, or the cover alternative when the safe crop is null.

  • Mobile 100% height: 9:16.
  • Mobile 50% height: 9:8.
  • Mobile 30% height: 9:4.8, equivalent to 15:8.
  • Square 1:1 and desktop 16:9 / 21:9 remain available.

The default viewport is 360 × 640 CSS pixels; options.mobileViewport changes the three mobile ratios. Each crop has an ID, aspect ratio, status, normalized crop, CSS object-position percentages and reason. suggested preserves primary and don’t-crop regions. review_needed means uncertain framing. contain_recommended has null crop/objectPosition; show the whole image or explicitly review coverCrop, which may clip secondary protected content.

All background boxes and crop coordinates refer to the original oriented input with sourceSize. They are not output-pixel guides. Apply them to the original image, remap overlay regions through the chosen crop, and avoid don’t-overlap areas. Empty text-safe areas mean no reliable quiet space was found. Text-tone and scrim advice does not establish accessible contrast.

The test UI draws interests in orange and the primary in blue, with confidence and selection rationale. Don’t-crop and don’t-overlap toggles use teal and purple. Background crops shows selectable thumbnails and a larger view; 30% height defaults to the wide cover alternative with a warning when needed. Switching crops makes no AI calls and does not create image assets. Background guidance shares original screening; only geometry and previews are computed locally.

For processed outputs, sourcePlacement maps pre-repair source regions into output pixels. processIgPost.guides adds visible subject bounds, a title baseline 3% below the subject top and padded face/held-object regions. Repair allows 2–20%, up to 4096 px per side and 4 megapixels; FLUX also limits sides to 2560 px. There is no automatic support recovery or background reconstruction.

Older results may omit newer metadata. Reloading or reusing an idempotency key retrieves that saved outcome and viewport; use a new key for an intentional new screening attempt. Claude malformed JSON/schema responses get one persisted retry per screening phase. Extra background object properties are discarded; required findings and coordinates remain validated. Exhausted screening failures retain usage and need a new run rather than a saved-result reload.

Instagram post to responsive web layers

POST /v1/images/instagram-process-for-web takes any uploaded image accepted by Process for web. An initial OpenAI vision screening checks for text added over the photo, scene quality and image attributes in one call when no overlay text is found. Lettering photographed on tattoos, billboards, or street signs does not count as an overlay. instagramPost.textDetection.status is present, absent or uncertain. Only absent skips text cleanup; instagramPost.layerization.status is then skipped. options.extractText defaults to false: present or uncertain uses fal text removal ($0.04/image), leaves extractedText empty, and screens the cleaned image again. Set extractText: true to use Ideogram V3 Layerize Text ($0.09/image) and return raw words in instagramPost.layerization.extractedText. Removal-only can also erase photographed writing such as tattoos and signs, so inspect the cleaned image. The cleaned-image screening returns image attributes when text was removed. The worker reuses screened attributes without a separate OpenAI attribute call; older saved jobs fall back to that call. The text-free image is an internal processing input. Screening rejects with HTTP 422 INSTAGRAM_POST_UNSUITABLE for genuine inset photos or collage, unusable visual quality, or damage to the subject. The amount of text is not a rejection reason. A carousel arrow is not an inset image.

options.extractShadow defaults to false, leaving the generated soft contact shadow out of the subject cutout. Set it to true to add that shadow when surface contact is detected; responsiveLayers.subject.contactShadow reports none or soft. This is local processing and adds no provider call. Existing haze in the model cutout may remain. options.mobileOnly defaults to true, including when options is omitted. A successful default job returns exactly two WebP assets: one 9:24 mobile background and one transparent cutout. The visible subject, excluding any soft contact shadow, fills about 90% of the mobile background width; draw the cutout at responsiveLayers.mobile.subjectPlacement without resizing. The output crop and subject retain the expanded master’s native pixel scale. responsiveLayers.mobile.titleGuide.lineY is a title baseline 3% of the visible subject height below its top; draw the title on this line behind the cutout. responsiveLayers.mobile.areasOfInterest.regions maps individual sourceAnalysis.areasOfInterest face and held-object boxes into crop-local pixels with 15% padding per side. Held phones, trophies, signs, cups and instruments are included when visible; unrelated background objects are excluded. The test UI draws separate orange rectangles. Detection reuses the existing vision call. Older saved analysis falls back to its combined protected focus. For compatibility, responsiveLayers.mobile.dontOverlap maps the protected sourceAnalysis.attributes.neverCropArea from the oriented input into crop pixels, then adds 15% of the rectangle’s width and height on every side. Its topY and bottomY mark the padded bounds, without a subject-height cap; it reports unavailable when no protected area was identified. When a reliable floor/wall edge is detected, responsiveLayers.mobile.boundaryPoints gives its crop-local polyline, including any incline. The floor line is optional. These guides add no image-generation call. Set options.mobileOnly: false to request desktop and 9:16 mobile backgrounds with one shared cutout; reliable support boundaries may also produce farBackground and supportSurface split assets in that mode. The mobile-only default omits all split assets. When extractShadow is true and the subject rests on a surface, the subject cutout can include an approximate soft contact shadow. The cleaned image and expanded master are processing intermediates. A subject that cannot be separated cleanly fails with INSTAGRAM_SUBJECT_UNLAYERABLE.

Preview markerResponse fieldMeaning
Red title linemobile.titleGuide.lineYText baseline 3% of visible subject height below its top. Draw the title first, then the cutout, so the title sits on the line behind the subject.
Orange rectanglesmobile.areasOfInterest.regionsIndividual face and held-object rectangles with labels and 15% padding per side, clipped only to the crop. Older saved analysis falls back to its combined protected focus.
Blue floor edgemobile.boundaryPointsOptional detected floor or support edge; the points can form an incline.

The marker coordinates are pixels in the mobile background. They are preview guides, not pixels drawn into either output asset. Areas of interest can be unavailable if image attributes find no usable focus; the floor edge can be absent. The local test preview shows the title and padded rectangles by default.

The initial screening is an OpenAI vision call. Posts without overlay text avoid a fal cleanup charge. Posts with visible or uncertain overlays pay $0.04 for removal only, or $0.09 for extraction with Ideogram, plus a second OpenAI screening call. An early rejection avoids fal text cleanup. Its fal request is saved under the Idempotency-Key; retry the same request and key after a pending response to resume collection without another fal submission. A lost submit receipt is treated as uncertain and is not automatically resubmitted. Poll the accepted job ID, then fetch the base and any split asset IDs with GET /v1/assets/{assetId} and follow each downloadUrl. The cleaned image and expanded master are processing intermediates and are not returned as extra output images. Rejected screening appears in /v1/runs. Existing keys need image.layouts, scene.separate and text.remove permissions.

JavaScript · collect responsive image layers
// Node 20+; upload an image and persist IMAGE_API_JOB_KEY.
const base = process.env.IMAGE_API_URL ?? "http://127.0.0.1:4114";
const key = process.env.IMAGE_API_KEY;
const assetId = process.env.IMAGE_API_ASSET_ID;
const jobKey = process.env.IMAGE_API_JOB_KEY;
if (!key || !assetId || !jobKey) throw new Error("Set IMAGE_API_KEY, IMAGE_API_ASSET_ID and IMAGE_API_JOB_KEY");
const response = await fetch(new URL("/v1/images/instagram-process-for-web", base), {
  method: "POST",
  headers: { Authorization: "Bearer " + key, "Content-Type": "application/json", "Idempotency-Key": jobKey },
  body: JSON.stringify({ input: { assetId } }), // Mobile-only and removal-only are the defaults.
});
const body = await response.json();
if (response.status === 422) {
  console.log("Post rejected:", body.error.code, body.error.message);
} else if (!response.ok) {
  throw new Error(response.status + ": " + JSON.stringify(body.error));
} else {
  console.log("Accepted job:", body.id, "text detection:", body.instagramPost.textDetection?.status, "Text cleanup:", body.instagramPost.layerization.status, "extracted text:", body.instagramPost.layerization.extractedText);
  let job = body;
  while (!["succeeded", "failed", "cancelled"].includes(job.status)) {
    await new Promise(resolve => setTimeout(resolve, 3000));
    const polled = await fetch(new URL("/v1/jobs/" + body.id, base), { headers: { Authorization: "Bearer " + key } });
    job = await polled.json();
    if (!polled.ok) throw new Error(JSON.stringify(job.error));
  }
  if (job.status !== "succeeded") throw new Error(JSON.stringify(job.error));
  const layers = job.processForWeb.responsiveLayers;
  console.log("Mobile background:", layers.mobile.backgroundAssetId, layers.mobile.subjectPlacement);
  console.log("One subject layer:", layers.subject.assetId, layers.subject.contactShadow);
  console.log("Title line:", layers.mobile.titleGuide);
  console.log("Protected area lines:", layers.mobile.dontOverlap);
  console.log("Optional floor edge:", layers.mobile.boundaryPoints);
  // GET /v1/assets/{assetId} for each required ID, then fetch its downloadUrl.
  // Draw the subject at subjectPlacement over the background at native scale (1:1).
}
Original image analysis in the job response

image.processForWeb, instagram.processForWeb and image.layouts return job.sourceAnalysis once source analysis finishes. For the Instagram operation, analysis uses the cleaned result when overlay text was detected, or the original image when cleanup was skipped. When status is available, read sourceAnalysis.attributes.neverCropArea, all four edge conditions, suggestedCrop, subject and reasoning. The rectangles are normalized x/y/width/height on the oriented source image named by sourceAnalysis.sourceSize; they are not output-layout pixel coordinates. neverCropArea: null means the model could not identify a reliable protected focus. If analysis fails, status: "unavailable" includes a reason and the worker conservatively skips bottom expansion.

Keeping a mat or other support aligned

For Instagram web processing, processForWeb.evaluation.contactSupport describes a distinct mat, tabletop or seat found by the existing subject-quality vision call. Its bounds are normalized x/y/width/height on the full expanded master. When accepted, the worker locally preserves the visible support pixels around the subject in both backgrounds. evaluation.supportRestored: true confirms this happened; false means the generated background was used unchanged. Null or absent contactSupport means no eligible object was found. This adds no generation call. Review seams, shadows and contact before publishing; earlier saved jobs are unchanged. For a distinct movable support such as a log, bench or chair, the foreground should contain the person and that object together. If the first extraction fails quality review and a distinct support is detected, the worker requests separate person and support masks from fal SAM 3 (up to two $0.005 calls), unions them with the original cutout, and runs a second quality evaluation. The masks select original expanded-scene pixels; implausible masks are rejected before background fill. A successful repaired cutout reports evaluation.subjectIncludesSupport: true and the background fill removes the support too. If the second quality review fails, the job fails with its reason. An interrupted selection is not submitted again automatically.

05

Direct image routes

Brand analysis

POST /v1/images/brand-analysis accepts raw JPEG, PNG or WebP bytes up to 4 MB with Content-Type and Idempotency-Key. It returns approximate primary, secondary and highlight hex colours with confidence and evidence. basis distinguishes graphic design from colours inferred from a photograph. When readable text is visible, typography.fontType classifies it; likelyFontFamily is null unless a specific match is defensible. Missing colour roles and font details are null. It requires image.analyze permission, uses one pilot attempt, and needs no worker. GET /v1/images/brand-analysis with the same key retrieves the saved result without another model call. This route analyzes the source; POST /v1/images/brand-colors edits an image with a supplied palette.

Process IG post gates expensive processing on initial suitability: unsuitable posters, graphics and other concerns return the original image with outcome original_returned and suitability.eligible false, skipping text removal and extraction. Only final extraction quality is advisory: screeningFeedback includes acceptable/warning source assessments, and evaluation reports pass/fail/unknown. Its halo feedback describes fringe location, severity and colour, plus an optional suggested background hex colour and confidence to reduce contrast. This camouflages fringe rather than repairing it. Available outputs are returned even when extraction is not ideal, with warnings. Actual processing failures can still fail.

Remove text and banners

POST /v1/images/remove-overlays accepts a completed image asset and returns 202 with a job ID. One OpenAI image edit removes added text and entire banner panels; no manual regions, FAL or RunPod are used. Photographed signs and physical lettering are preserved by prompt. Receive signed stage/completion webhooks or recover via the job endpoint. Returns one cleaned image at source dimensions; inspect generative repairs. Accepts up to 4 megapixels and aspect ratios between 1:3 and 3:1. Requires overlay.remove or existing text.remove permission. Available in the test UI as Remove text & banners.

Image attributes

POST /v1/image-attributes takes raw JPEG, PNG or WebP bytes, up to 4 MB, with Content-Type and Idempotency-Key. It uses one shared pilot attempt and concurrency slot, and records a run. The response has edge conditions, neverCropArea, individual areasOfInterest, suggestedCrop, reasoning and usage for the oriented original. The same AI call returns photo: background description/elements, indoor/outdoor/mixed setting, brightness, lighting, dominant colour name and approximate hue in degrees, and colour temperature. Hue is null for achromatic or uncertain colours. Older saved results may omit photo details. Missing findings remain null or unknown. Save the key: GET /v1/image-attributes with that same header recovers the saved result without another inference call, while it is retained.

shell · analyze and recover
ATTR_KEY="attributes-$(uuidgen)"
curl -sS "$IMAGE_API_URL/v1/image-attributes"   -X POST -H "Authorization: Bearer $IMAGE_API_KEY"   -H "Idempotency-Key: $ATTR_KEY" -H "Content-Type: image/jpeg"   --data-binary @input.jpg -o attributes.json

# After a lost response, use the original key; no image bytes are needed.
curl -sS "$IMAGE_API_URL/v1/image-attributes"   -H "Authorization: Bearer $IMAGE_API_KEY" -H "Idempotency-Key: $ATTR_KEY"

Upscale

POST /v1/images/upscale?factor=2 takes raw image bytes (JPEG, PNG or WebP, 20 MB max); factor=4 is also supported. It returns WebP bytes directly, with X-Upscale-Width, X-Upscale-Height, X-Upscale-Factor, X-Upscale-Model and estimated X-Upscale-Cost-USD headers. It requires the image.analyze key permission. This route has no idempotency key, durable job ID or /v1/runs record: if a request times out after the provider accepts it, inspect provider history before sending it again.

shell · direct 2× WebP
curl --fail-with-body -sS "$IMAGE_API_URL/v1/images/upscale?factor=2"   -X POST -H "Authorization: Bearer $IMAGE_API_KEY"   -H "Content-Type: image/jpeg" --data-binary @input.jpg   -D upscale.headers -o upscaled.webp
06

Results and output assets

Job submission returns HTTP 202, a JSON job and Location: /v1/jobs/{jobId}. Poll the Location (respect Retry-After: 3) until succeeded, failed or cancelled. A job can pass through queued → submitting → running; reconciling means a provider submission is uncertain and requires operator investigation.

JSON · trimmed successful job
{
  "id": "job_0123456789abcdef0123456789abcdef",
  "operation": "subject.extract",
  "status": "succeeded",
  "outputs": [
    { "role": "cutout", "assetId": "asset_0123456789abcdef0123456789abcdef" },
    { "role": "mask", "assetId": "asset_fedcba9876543210fedcba9876543210" }
  ],
  "geometry": { "coordinateSpace": "input_asset", "sourceSize": { "width": 1200, "height": 800 } },
  "warnings": [],
  "usage": { "processingMs": 4200, "cached": false }
}

Additional job fields depend on the operation: scene, composition, backgroundExpansion, imageExtension, brandColors, layoutVariants, processForWeb, outpaint and checks. Match outputs by role, and by variantId for each layout asset; never rely on array position. GET /v1/assets/{assetId} returns metadata and a short-lived downloadUrl. Fetch that URL directly and refresh it when expired. Copy any result you need to keep into your own storage.

Coordinates for extraction, analysis and attributes refer to the oriented input image, usually normalized to 0–1. Composition polygon coordinates refer to the output canvas. processForWeb.variants[].subjectPlacement uses integer pixels in each output layout. Other pixel rectangles and dimensions are identified in their fields. Times are Unix milliseconds.

Consumer concurrency

A new workspace permits five active jobs or synchronous analysis requests across all of its API keys. Queued and unresolved jobs count too. A sixth new request receives HTTP 429 with JOB_LIMIT; reusing the same accepted idempotency key consumes no extra slot. Read /v1/usage for activeJobs, concurrentLimit and availableConcurrency. Slots are released when work reaches its terminal checkpoint, including cancellation.

This is an application admission guard for pilot cost, fairness and resource use. It does not increase the number of local worker phases or provider jobs that actually execute in parallel. Existing tenant limits are changed by an operator, not by ordinary API keys.

07

Reliability, retries and errors

Reuse keys after uncertainty

For upload reservation, job submission and attribute analysis, retry the same logical request with the same idempotency key (8–128 characters). A new key starts a new operation and may incur another charge. A key reused with a different body returns 409.

Retry failed stages deliberately

Use the matching /retry-… route only when the job reports that stage as retryable. Give each intentional retry a new key, then reuse that retry key if its response is lost. Completed stages are reused; failed or uncertain provider attempts remain in usage and cost history.

Handle cancellation and outages

Cancellation is best effort and does not erase already incurred usage. A 503 can be temporary; recover with the same key or poll an existing job. Do not turn a reconciling job into a second provider submission.

JSON · error envelope
{ "error": { "code": "INVALID_REQUEST", "message": "Request fields do not match the image API contract." } }

Common HTTP statuses: 400 invalid fields, 401 missing/invalid key, 403 denied operation, 404 absent or expired workspace resource, 409 idempotency or state conflict, 413 oversized body, 415 unsupported image type, 422 invalid image, 429 allowance/concurrency limit, and 502–504 provider or temporary service failure. Use the returned error.code for handling.

08

Limits, retention and schema

30 daysDefault key lifetime
100Accepted attempts per workspace
2Concurrent jobs
200 / 250 MBReserved assets / storage
7 daysAsset and job availability
10 / 5 minUpload / output URL lifetime

These are default private-pilot allowances, not purchased credits. Accepted attempts count even when a job later fails or is cancelled. GET /v1/usage returns mode, acceptedJobs, activeJobs, jobLimit, concurrentLimit, storedBytes and byteLimit. GET /v1/runs returns {runs, nextCursor}; follow nextCursor until null, even after an empty page. Its cost summaries outlive image assets and jobs; provider charges are estimates, and unknown costs remain null. Signed links expire sooner than the underlying assets.

For generated clients, request schemas and route descriptions are available from GET /v1/openapi.json with your bearer key. The OpenAPI document and this page cover the image API. The website builder and development /api routes are experiments, outside this public contract.