Skip to main content

Jobs

Every request that returns a job_id — companion chat, retarget, media tagging, and generation — records a job. These endpoints are how you look those jobs back up, whichever API produced them.

  • Auth: Bearer key with the job:read scope. This scope is granted to every API key by default, so no extra setup is needed.

Each job carries the endpoint it came from in endpoint_key, so a single list shows chat turns and custom GPU workflow runs side by side:

typeendpoint_keyProduced by
companion_chatchat.companionPOST /companion/chat
companion_retargetchat.companion_retargetPOST /companion/retarget
companion_media_tagmedia.companion_tagPOST /companion/media-tag
generategenerate.<workflow-slug>POST /generate/:slug

List jobs

GET /api/v1/jobs

Query parameters

ParameterTypeNotes
limitinteger1–100. Default 50.
offsetintegerDefault 0.
typestringOne of companion_chat, companion_retarget, companion_media_tag, generate.
decisionstringpass or reject. Only companion jobs carry a verdict; generation jobs never match.
statusstringComma-separated: queued, processing, completed, failed.
api_key_idstringOnly jobs submitted with this API key. Use it to split usage per key.
searchstringPrefix match on job_id, up to 64 characters.
fromstringISO 8601. Inclusive lower bound on created_at.
tostringISO 8601. Exclusive upper bound on created_at.

Example

curl "https://api.phantomrouter.ai/api/v1/jobs?status=queued,processing&limit=20" \
-H "Authorization: Bearer $PHANTOM_KEY"

Response

{
"jobs": [
{
"job_id": "9f1c2e4a-7b3d-4c8e-9a1f-2d5b6c7e8f90",
"type": "generate",
"endpoint_key": "generate.portrait",
"status": "processing",
"api_key_id": "vJ8kQ2mXpL4nR7tY",
"api_key_name": "production",
"created_at": "2026-07-28T10:15:00.000Z",
"completed_at": null
},
{
"job_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"type": "companion_chat",
"endpoint_key": "chat.companion",
"status": "completed",
"is_prohibited": false,
"api_key_id": "vJ8kQ2mXpL4nR7tY",
"api_key_name": "production",
"created_at": "2026-07-28T10:14:12.000Z",
"completed_at": "2026-07-28T10:14:14.310Z"
}
],
"total": 2,
"limit": 20,
"offset": 0
}

is_prohibited is absent on generate jobs — generation never produces a moderation verdict — and null while a moderated job is still pending. An absent field means "not applicable"; a null one means "not decided yet".

api_key_id names the key the job was submitted with, and api_key_name is the name you gave that key. Issue one key per environment or per customer, and you can attribute every job — and every charge on the matching ledger row — to the caller that made it. Both fields are null for a job started from the dashboard, and api_key_name alone is null once the key has been deleted.

:::tip Reconciling with webhooks Webhooks carry the fast path — see Webhooks for how to verify one. For reconciliation, poll ?status=queued,processing once on a timer rather than polling each job individually — one request covers everything still in flight. :::

Get a job

GET /api/v1/jobs/{job_id}

The result object is shaped by the job type.

Companion chat / retarget

{
"job_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"type": "companion_chat",
"endpoint_key": "chat.companion",
"status": "completed",
"created_at": "2026-07-28T10:14:12.000Z",
"completed_at": "2026-07-28T10:14:14.310Z",
"result": {
"is_prohibited": false,
"chat_id": "chat-42"
}
}

Media tagging

{
"job_id": "5d6e7f80-1a2b-4c3d-9e8f-7a6b5c4d3e2f",
"type": "companion_media_tag",
"endpoint_key": "media.companion_tag",
"status": "completed",
"created_at": "2026-07-28T09:02:00.000Z",
"completed_at": "2026-07-28T09:02:03.120Z",
"result": {
"is_prohibited": false,
"media_url": "https://example.com/photo.jpg",
"media_kind": "image"
}
}

Generation

{
"job_id": "9f1c2e4a-7b3d-4c8e-9a1f-2d5b6c7e8f90",
"type": "generate",
"endpoint_key": "generate.portrait",
"status": "completed",
"created_at": "2026-07-28T10:15:00.000Z",
"completed_at": "2026-07-28T10:15:42.000Z",
"result": {
"created_at": "2026-07-28T10:15:00.000Z",
"completed_at": "2026-07-28T10:15:42.000Z",
"outputs": { "image": "https://…/out.png?X-Amz-Signature=…" },
"workflow_slug": "portrait"
}
}

Media URLs are signed fresh on every read and expire after 15 minutes. Fetch the artifact soon after reading, or re-request the job for a new link — don't cache the URL itself.

Failed jobs

A failed job carries error — one sentence saying what to do next — and error_code, which is what you branch on. The message is deliberately generic: it describes the class of failure, not the internals of whichever engine ran the job.

error_codeMeaningRetry?
PROVIDER_BUSYCapacity is throttled right now.Yes, after a short backoff.
PROVIDER_UNAVAILABLEThe rendering service is down or unreachable.Yes, after a short backoff.
JOB_TIMEOUTThe job ran past its time limit.Yes, or simplify the request.
JOB_CANCELLEDThe job was cancelled before it finished.No — resubmit if unintended.
RENDER_FAILEDThe engine rejected the request.No — fix the inputs first.
CHARGE_FAILEDThe job never started; the charge failed.No — top up the account first.
QUEUE_FAILEDThe job could not be queued.Yes.
INTERNAL_ERRORSomething else went wrong on our side.Yes, then contact support.

Quote the job_id if you contact support: we hold the underlying cause against that id.

Request inputs, webhook URLs, and internal provenance are never returned.

Export jobs as CSV

GET /api/v1/jobs/export

from and to are required and the window cannot exceed 366 days. to is exclusive — pass the start of the day after your last desired date to include that final day. The response is streamed, so a wide window starts returning rows immediately.

A window holding more than 500,000 jobs is rejected with a 400 before any data is written — narrow the range and export in several requests. The export is never silently truncated: a body that downloads to completion is the complete export for that window.

curl "https://api.phantomrouter.ai/api/v1/jobs/export?from=2026-07-01T00:00:00Z&to=2026-08-01T00:00:00Z" \
-H "Authorization: Bearer $PHANTOM_KEY" \
-o jobs.csv

Columns

ColumnNotes
job_id, type, endpoint_key, statusIdentity and routing.
is_prohibitedEmpty for generation jobs and for jobs still pending.
created_at, completed_at, duration_msduration_ms is empty until the job finishes.
reasonWhy a companion job was rejected.
chat_id, media_url, media_kindCompanion request context.
workflow_slug, provider_job_id, gpu_execution_time_msGeneration provenance.
error_code, errorFailure detail.
api_key_id, api_key_nameWhich API key submitted the job. Empty for dashboard jobs.

The file is UTF-8 with a byte-order mark so spreadsheet apps decode it correctly, and cells that would otherwise be interpreted as formulas are escaped.

Following a job to completion

There is no progress stream. A job's result reaches you two ways, and both report the same terminal state:

  1. Webhook — pass webhook_url when you submit. This is the fast path: the callback fires as soon as the job finishes, so nothing has to poll. Every callback is signed — verify the signature before you act on the payload.
  2. PollingGET /api/v1/jobs/{job_id} for one job, or GET /api/v1/jobs?status=queued,processing for every job still in flight. Prefer the list form for reconciliation: one request covers the whole backlog instead of one request per job.

Only generate jobs are ever non-terminal by the time you hold a job_id. The companion endpoints return their result in the HTTP response itself, so their jobs are already finished.

tip

Back your polling interval off as the job runs. A generation still running after two minutes will not finish in the next two seconds, and a fixed 1s poll only spends your rate limit.