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

HeaderMeaning
Idempotency-Keymakes retries safe (Idempotency)
Prefer: wait=Nwait 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-IDyour own correlation id; echoed back and logged with ours
Refabric-Versionpin 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
   └──────────┴──────▶ cancelled

A job has two fields, not one: lifecycle says where it is, outcome says how it ended and is null until then.

lifecycleoutcomeMeaning
queuednullaccepted, waiting to start
runningnullworking
terminalsucceededfinished; the result is ready
terminalfailedended with an error (Errors)
terminalcancelledcancelled 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." } ] }
FieldMeaning
atwhen (ISO-8601 UTC)
phasequeued · running · delivered · warning · succeeded · failed · cancelled (new values may appear — Conventions) (live list: GET /v1/vocab/lifecycle) (live list: GET /v1/vocab/outcome)
messageone sentence; a failed event carries the job's error.message, a warning its warning's
fileon delivered: the file, as in the result (art:…, or the record a record-making task made)
fieldon 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 of poses.items), not the order they finish. created_at may therefore go backwards between two files.
    • Paged. limit's default and maximum are in the OpenAPI spec. When there are more, has_more is true and next_cursor is a string: pass it as ?cursor= for the next page. The last page has has_more: false and no next_cursor. A cursor this listing did not give answers 422 invalid_request with field: "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 are data.items[].url, never separate files. Its result is always one page.
  • summary — present only when the task has counts or warnings to say; a single-file task's result has no summary key. The same words for every task, each present only when the task has it: requested (outputs asked for), delivered (outputs produced) and warnings[] ({code, field, message}, codes from the error catalogue with type warning — Errors). A missing output is a warning output_not_produced whose field names it, and is not charged; a job that produced none failed instead. The task's outputSchema documents 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's error." Read GET /v1/jobs/{id}. A cancelled job: 409 conflict, code: cancelled.
  • The webhook payload and a Prefer: wait answer are this same object, first page: when their has_more is true, read the rest here with ?cursor=<next_cursor>.

Start deadline

X-Refabric-Start-Timeout: 30

If 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()