Appearance
Jobs
Submit, poll, and list uniquify jobs. All routes require authentication and are owner-scoped.
Create a job
POST /jobsSubmit one source with your settings; get back a jobId to poll. Accepts multipart/form-data (with a file upload) or application/json (with a pre-uploaded inputKey).
Body
You must provide a source (one of image or inputKey) and settings.
Settings resolve through a precedence chain — the first of these that is present wins:
recipeConfigorrecipeIdon the request (sending both is a400)presetIdon the request- the default preset bound to your API key (set when the key was created)
If none of the three resolves, you get a 400. So a key with a default preset can post nothing but the image; a key without one must name settings on every call.
copies, strength and options are not part of that chain — they are per-call knobs folded on top of whichever base was resolved, and they override the recipe's own values. That is deliberate: it lets one saved preset serve callers who each want a different fan-out or intensity. See Configuring your own settings.
| Field | Type | Required | Notes |
|---|---|---|---|
image | file | one of | Multipart file upload of the source (≤ 100 MB). |
inputKey | string | one of | Key of a pre-uploaded source under the temp/ prefix. |
recipeId | string | see above | A bundled starter preset (see Options). |
recipeConfig | object | see above | A full recipe graph. See Configuring your own settings. |
presetId | string | see above | A saved preset — system or your own. See Presets. |
copies | integer ≥ 1 | no | Number of variants. Asking for more than the limit (100000) is a 400, not a clamp. |
strength | integer 0–100 | no | Default transform intensity. Clamped into range. |
delivery | drive | ephemeral | no | Where results go. Defaults to drive. See Delivery modes. |
options | object | no | Merged onto the recipe's global_settings (e.g. ultraSafe, noise, carousel). ultraSafe needs an Agency plan or higher — see Ultra Safe. |
Response — 202 Accepted
json
{ "jobId": "8f3c1a90-…", "statusUrl": "/jobs/8f3c1a90-…" }statusUrl is relative to the API base — poll it (see Get a job).
Errors
| Status | error | When |
|---|---|---|
400 | bad_request | No source; both recipeId and recipeConfig; no settings at all (no recipeId/recipeConfig/presetId and no default preset on the key); unknown recipeId or presetId; copies above the limit; unsupported file type; recipe failed validation (findings in details). |
402 | insufficient_balance | Your shared balance can't cover the job (details.balance, details.required). Nothing runs. |
403 | forbidden | The resolved recipe asks for a feature your plan doesn't include — currently Ultra Safe, which is Agency and higher. Refused before anything is uploaded or charged, rather than run without the feature. |
413 | payload_too_large | Upload exceeds 100 MB. |
429 | rate_limited | Too many job creations in the window (see Rate limits). |
Example
bash
curl -s -X POST "$API/jobs" \
-H "Authorization: Bearer $FLUIDGHOST_KEY" \
-F "[email protected]" \
-F "recipeId=full-refresh" \
-F "copies=3" \
-F "delivery=ephemeral" \
-F 'options={"ultraSafe": true, "noise": 35}'Get a job
GET /jobs/{id}Poll a job's status. Returns the Job status shape. Poll until status is terminal (completed, partial, failed).
Response — 200 OK
json
{
"jobId": "8f3c…",
"status": "processing",
"progress": { "done": 1, "total": 3 },
"variants": [
{ "index": 0, "status": "completed", "fileName": "IMG_4827.JPG", "downloadUrl": ".../jobs/8f3c…/download/0", "detectionRisk": 12, "riskBand": "low" },
{ "index": 1, "status": "processing" }
]
}Errors
| Status | error | When |
|---|---|---|
404 | not_found | No such job, or the job isn't yours (owner-scoped; the two are indistinguishable by design). |
List jobs
GET /jobsYour recent jobs, newest first (capped at 100). Each row is a Job status plus createdAt and copies.
Response — 200 OK
json
{
"jobs": [
{
"jobId": "8f3c…",
"status": "completed",
"progress": { "done": 3, "total": 3 },
"variants": [ … ],
"createdAt": "2026-07-03T14:00:00Z",
"copies": 3
}
]
}For ephemeral jobs, note that the bytes are gone once downloaded or after the 1h purge — the job row remains, but its variants' downloadUrls may already be spent.