For AI agents: this page is also available as Markdown at https://docs.refabric.com/task-apis/calling-tasks/asynchronous-jobs.md, and the index of every page is https://docs.refabric.com/llms.txt.
Task APIs › Calling tasks
Jobs
Submit a task as a job, follow its state, read its result, and cancel or resume it.
Submitting a task creates a job. A job runs in the background; you learn its outcome by polling, by waiting on the submit call, or by a webhook.
Submit
curl -s -X POST "$REFABRIC_API/tasks/image.generate" \
-H "Authorization: Key $REFABRIC_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"prompt": "a navy jacket in this fabric", "references": [{"image": "art:x1", "use_case": "fabric"}]}'HTTP 202
X-Refabric-Credits: <credits held>
X-Refabric-Credit-Type: <credit type>
{ "job_id": "j_…", "lifecycle": "queued",
"status_url": "https://api.refabric.com/v1/jobs/j_…",
"result_url": "https://api.refabric.com/v1/jobs/j_…/result",
"cancel_url": "https://api.refabric.com/v1/jobs/j_…/cancel" }The three URLs are absolute. Use them as given rather than building them yourself.
Request headers
| Header | Meaning |
|---|---|
Idempotency-Key | makes retries safe (Idempotency) |
Prefer: wait=N | wait up to N seconds (capped at limits.prefer_wait.max_seconds of GET /v1/meta) for the result (Sync mode) |
X-Refabric-Start-Timeout: <seconds> | deadline for the job to start (Start deadline) |
X-Request-ID | your own correlation id; echoed back and logged with ours |
Refabric-Version | pin a dated API version (Versioning) |
Query parameter webhook_url=https://… asks for a callback when the job ends (Webhooks).
Credits
Submitting holds the estimated cost: X-Refabric-Credits is the amount held and
X-Refabric-Credit-Type its credit type (values at GET /v1/vocab/credit_type). When the job
ends the hold is settled against what was actually delivered; a failed or cancelled job releases what
it did not use. Check the
cost first with POST /v1/tasks/{name}/estimate, which takes the same body and runs nothing.
Every task's price per option is on Pricing; what you spent is Usage.
States
queued ──▶ running ──▶ succeeded
│ ├──────▶ failed
└──────────┴──────▶ cancelledA job has two fields, not one: lifecycle says where it is, outcome says how it ended
and is null until then.
lifecycle | outcome | Meaning |
|---|---|---|
queued | null | accepted, waiting to start |
running | null | working |
terminal | succeeded | finished; the result is ready |
terminal | failed | ended with an error (Errors) |
terminal | cancelled | cancelled by you (or by us, with a reason) |
New non-terminal lifecycles may be added; treat anything but terminal as "not finished yet". The
submit answer's lifecycle is the job's lifecycle at that moment (queued, or running after a
Prefer: wait that ran out). Live list: GET /v1/vocab/lifecycle.
Read a job
curl -s "$REFABRIC_API/jobs/j_…" -H "Authorization: Key $REFABRIC_API_KEY"{ "job_id": "j_…", "task": "image.generate", "lifecycle": "running", "outcome": null,
"progress": { "done": 1, "total": 4, "phase": "" },
"metrics": { "queued_at": "…", "started_at": "…", "duration_ms": null }, "error": null,
"api_key_id": "key_8f3a…",
"charge": { "credits": <credits>, "credit_type": "<credit type>", "state": "reserved" } }api_key_id is the key that started the job (null for work started in the Refabric app).
charge is what the job costs: while it runs the credits are reserved (the most it can cost);
once it has ended they are charged (what it cost) or refunded (it cost nothing) (live list: GET /v1/vocab/charge_state). charge is null
for a task that holds no credits.
progress counts outputs: total is how many the job planned (null until known), done how
many it has delivered so far; a succeeded job's done is what it delivered. phase is a short label of the job's progress.
A failed job's error is the same error object every error answer uses
(Errors):
{ "job_id": "j_…", "task": "image.generate", "lifecycle": "terminal", "outcome": "failed",
"error": { "code": "processing_failed", "type": "processing_failed",
"message": "The job ran and failed.", "field": null, "retryable": true,
"request_id": null } }A cancelled job's error is {"code": "cancelled", "type": "conflict", "retryable": false, …}
— never null.
(Full field list: the OpenAPI spec.) Poll every few seconds at most; for jobs longer than a minute prefer a webhook.
Event log (?logs=1)
curl -s "$REFABRIC_API/jobs/j_…?logs=1" -H "Authorization: Key $REFABRIC_API_KEY"adds events to the same answer — what happened to the job, in order:
{ "job_id": "j_…", "lifecycle": "terminal", "outcome": "succeeded", …,
"events": [
{ "at": "2026-10-01T10:00:00Z", "phase": "queued", "message": "Accepted and queued." },
{ "at": "2026-10-01T10:00:02Z", "phase": "running", "message": "Started." },
{ "at": "2026-10-01T10:00:09Z", "phase": "delivered", "message": "Delivered a file.", "file": "art:…" },
{ "at": "2026-10-01T10:00:12Z", "phase": "warning", "message": "…", "field": "back" },
{ "at": "2026-10-01T10:00:12Z", "phase": "succeeded", "message": "Finished." } ] }| Field | Meaning |
|---|---|
at | when (ISO-8601 UTC) |
phase | queued · running · delivered · warning · succeeded · failed · cancelled (new values may appear — Conventions) (live list: GET /v1/vocab/lifecycle) (live list: GET /v1/vocab/outcome) |
message | one sentence; a failed event carries the job's error.message, a warning its warning's |
file | on delivered: the file, as in the result (art:…, or the record a record-making task made) |
field | on warning / failed: the input or output the event is about, when there is one |
Deliveries are listed in the result's order, not by arrival. While a job runs it lists the files delivered so far; a task that makes a record (a moodboard, a shoot) lists its record once the job succeeded.
Result
curl -s "$REFABRIC_API/jobs/j_…/result?limit=100" -H "Authorization: Key $REFABRIC_API_KEY"{ "job_id": "j_…",
"files": [
{ "file": "art:x2", "url": "https://…", "media_type": "image/png",
"task": "image.rotate_views", "job_id": "j_…", "created_at": "2026-09-28T10:01:10Z" },
{ "file": "art:x3", "url": "https://…", "media_type": "image/png",
"task": "image.rotate_views", "job_id": "j_…", "created_at": "2026-09-28T10:01:08Z" } ],
"has_more": false,
"summary": { "requested": 3, "delivered": 2,
"warnings": [ { "code": "output_not_produced", "field": "right",
"message": "One output of this job could not be produced; `field` names which." } ] } }files— what the job produced, each file once, in the one file shape (Files). Only THIS job's files.- Documented order. Files come in the order the task documents for its outputs (for example
image.rotate_views: back · left · right;image.repose: the order ofposes.items), not the order they finish.created_atmay therefore go backwards between two files. - Paged.
limit's default and maximum are in the OpenAPI spec. When there are more,has_moreistrueandnext_cursoris a string: pass it as?cursor=for the next page. The last page hashas_more: falseand nonext_cursor. A cursor this listing did not give answers422 invalid_requestwithfield: "cursor". - A task that makes a record (a moodboard, a fabric, a range plan, a brand kit) lists ONLY
that record's file — the same object
GET /v1/files/{ref}returns; its images aredata.items[].url, never separate files. Its result is always one page.
- Documented order. Files come in the order the task documents for its outputs (for example
summary— present only when the task has counts or warnings to say; a single-file task's result has nosummarykey. The same words for every task, each present only when the task has it:requested(outputs asked for),delivered(outputs produced) andwarnings[]({code, field, message}, codes from the error catalogue with typewarning— Errors). A missing output is a warningoutput_not_producedwhosefieldnames it, and is not charged; a job that produced none failed instead. The task'soutputSchemadocuments which words it uses.- Before the job finishes:
409 conflict,code: result_not_ready. - A job that failed:
409 conflict,code: job_failed— "This job failed; its reason is the job read'serror." ReadGET /v1/jobs/{id}. A cancelled job:409 conflict,code: cancelled. - The webhook payload and a
Prefer: waitanswer are this same object, first page: when theirhas_moreistrue, read the rest here with?cursor=<next_cursor>.
Start deadline
X-Refabric-Start-Timeout: 30If the job has not started within that many seconds of submission (for example because the
job queue is busy), it fails with code: start_timeout, releases its credit
hold, and the response for it carries X-Refabric-Start-Timeout-Type: user (the deadline was
yours, not a server fault). The deadline covers only the wait before running, not the run.
Cancel
curl -s -X PUT "$REFABRIC_API/jobs/j_…/cancel" -H "Authorization: Key $REFABRIC_API_KEY"HTTP 202
{ "job_id": "j_…", "lifecycle": "running" }Answers 202: cancellation is requested and the job moves to outcome: cancelled shortly (its
error then has code: cancelled). lifecycle is where the job is at that moment. Cancelling a
job that already ended also answers 202 (lifecycle: terminal) and changes nothing — read the
job to see how it ended. Work already delivered before the cancel may still be charged. 404 if
no job of yours has that id. Rate limited like every public endpoint.
Resume
A job that waits for your decision is resumed with
POST /v1/jobs/{id}/resume and {node_id, action, params}:
HTTP 202
{ "job_id": "j_…", "node_id": "shoot", "action": "select", "lifecycle": "running" }202: the resume is sent and the job continues shortly. lifecycle is where the job is
at that moment — the same word the cancel answer uses, never status. A job that already ended
answers 400.
List jobs
curl -s "$REFABRIC_API/jobs?task=image.generate&lifecycle=terminal&limit=50" \
-H "Authorization: Key $REFABRIC_API_KEY"{ "items": [ { "job_id": "j_…", "task": "image.generate", "lifecycle": "terminal",
"outcome": "succeeded", "queued_at": "…" } ],
"has_more": true, "next_cursor": "…" }Newest first.
Filters: task (a task name from GET /v1/tasks; an unknown name answers
422 invalid_option), lifecycle (queued · running · terminal; live list: GET /v1/vocab/lifecycle), api_key_id (only the jobs
started with this key — its id key_… from API keys, never the secret; a key that
is not one of yours answers 404 not_found). limit (default and maximum in the OpenAPI spec), cursor = the previous
page's next_cursor. Pagination: Conventions.
Python: submit and wait
import time
def run(session, api, task, body, key, wait=30, poll=5):
r = session.post(f"{api}/tasks/{task}", json=body, timeout=wait + 45,
headers={"Idempotency-Key": key, "Prefer": f"wait={wait}"})
r.raise_for_status()
if r.status_code == 200:
return r.json()
handle = r.json()
while True:
job = session.get(handle["status_url"]).json()
if job["lifecycle"] == "terminal":
break
time.sleep(poll)
if job["outcome"] != "succeeded":
raise RuntimeError(job.get("error"))
return session.get(handle["result_url"]).json()