Async jobs
The unified Job object, status values, polling, and the canonical agent loop.
No expensive call is synchronous. Uploading + parsing a plan, rendering a branded PDF, generating a visualization, and generating a tour all return a Job handle immediately — you then poll the job or receive a webhook. (A Marble tour can take ~10 minutes; nothing blocks on it.)
The Job object
{
"object": "job",
"id": "job_gj_1270…",
"type": "plan.parse | plan.render | visualization | tour | panorama | object_mesh",
"status": "queued | running | succeeded | failed | canceled",
"progress": 0.42,
"result": {
"assets": [
{ "id": "asset_a1c2…", "kind": "jpg", "url": "https://…r2…", "expires_at": "2026-06-18T10:05:00Z" }
]
},
"cost_usd_cents": 51,
"provider": "gemini",
"seed": "1270677270",
"error": null,
"metadata": { "agent_run": "abc" },
"created_at": "…", "updated_at": "…",
"urls": { "self": "/api/v1/jobs/job_gj_1270…", "cancel": null }
}statusis a canonical string — pattern-match it, don't infer from HTTP codes.progressis an optional float 0..1, best-effort, nullable (null where unknown, e.g. plan parse).resultis null until the job succeeds; thenresult.assets[]holds the signed, expiring asset URLs. Everyexpires_athas a hard TTL (≤ 300s) — if a URL is past it, re-sign viaGET /api/v1/assets/{id}/url.cost_usd_cents,provider, andseedare surfaced on the job (null where not applicable);erroris null unless the job failed, then{ code, message }.urls{}is self-describing — never template URLs by hand.cancelis reserved (null today; no cancel path yet).
Status mapping
| Internal source | Public status |
|---|---|
queued | queued |
running / parsing | running |
done / ready | succeeded |
failed | failed |
Two completion mechanisms
- Polling —
GET /api/v1/jobs/{id}. Optional fast-path headerPrefer: wait=<seconds≤30>holds the connection until the job is terminal or the budget elapses, then returns the current state — saves a tight poll loop for short jobs. - Signed webhooks — subscribe to
job.succeeded/job.failed(see webhooks).
The canonical agent loop
1. POST the work with an Idempotency-Key -> 202 { Job: status=queued, id }
2. EITHER poll GET /api/v1/jobs/{id} (respect Retry-After / pollInterval)
OR await webhook job.succeeded|job.failed for {id}
3. on succeeded -> read Job.result.assets[].url
(re-fetch if past expires_at via GET /api/v1/assets/{id}/url)
on failed -> read Job.error.{code,message}; act unaidedSee the example agent flow for this loop end to end in REST and MCP.