Norano API

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 }
}
  • status is a canonical string — pattern-match it, don't infer from HTTP codes.
  • progress is an optional float 0..1, best-effort, nullable (null where unknown, e.g. plan parse).
  • result is null until the job succeeds; then result.assets[] holds the signed, expiring asset URLs. Every expires_at has a hard TTL (≤ 300s) — if a URL is past it, re-sign via GET /api/v1/assets/{id}/url.
  • cost_usd_cents, provider, and seed are surfaced on the job (null where not applicable); error is null unless the job failed, then { code, message }.
  • urls{} is self-describing — never template URLs by hand. cancel is reserved (null today; no cancel path yet).

Status mapping

Internal sourcePublic status
queuedqueued
running / parsingrunning
done / readysucceeded
failedfailed

Two completion mechanisms

  1. PollingGET /api/v1/jobs/{id}. Optional fast-path header Prefer: 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.
  2. 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 unaided

See the example agent flow for this loop end to end in REST and MCP.