Docs
APIAPI referenceJobs

GET /v1/jobs/:jobId

Poll the unified envelope for any long-running operation - content generation, ingest, clones, influencer creation.

View as Markdown
GET/v1/jobs/{jobId}
Phase 1stable
Auth
Bearer
Scope
jobs:read

Every async endpoint on the API integration returns a jobId and hands you off to this one to watch the work finish. One shape, one poll loop, one error taxonomy. Learn it once; every async call looks the same.

Responses are ETag-aware. Send If-None-Match with the last etag you saw and we return 304 when nothing has changed. That keeps you safe to poll tightly without paying for a re-serialize on every tick.

See the Jobs concept for the 202-then-poll pattern.

Path
  • jobId
    string (job_<ULID>)required
    The jobId returned from the originating 202 response.
Headers
  • If-None-Match
    stringoptional
    ETag from a previous response. Returns 304 if the job has not advanced.

Example request

curl https://api.layers.com/v1/jobs/job_01HXA1NHKJZXPV8R7Q6WSM5BCD \
  -H "Authorization: Bearer lp_..."
poll-job.ts
export async function pollJob(jobId: string, apiKey: string) {
  let delayMs = 2_000;
  let etag: string | undefined;

  for (;;) {
    const res = await fetch(`https://api.layers.com/v1/jobs/${jobId}`, {
      headers: {
        Authorization: `Bearer ${apiKey}`,
        ...(etag ? { "If-None-Match": etag } : {}),
      },
    });

    if (res.status === 304) {
      await sleep(delayMs);
      delayMs = Math.min(delayMs * 1.3, 10_000);
      continue;
    }

    etag = res.headers.get("etag") ?? undefined;
    const job = await res.json();
    if (job.status !== "running") return job;

    await waitBeforeNextPoll();
  }
}
import httpx

def poll_job(job_id: str, api_key: str):
    etag = None
    with httpx.Client() as client:
        while True:
            headers = {"Authorization": f"Bearer {api_key}"}
            if etag:
                headers["If-None-Match"] = etag
            r = client.get(
                f"https://api.layers.com/v1/jobs/{job_id}",
                headers=headers,
            )
            if r.status_code == 304:
                wait_before_next_poll()
                continue
            etag = r.headers.get("etag")
            job = r.json()
            if job["status"] != "running":
                return job
            wait_before_next_poll()

Response

200Running - partial state with advisory progress and stage.
{
  "jobId": "job_01HXA1NHKJZXPV8R7Q6WSM5BCD",
  "kind": "content_generate",
  "status": "running",
  "stage": "generating_visuals",
  "progress": 0.42,
  "startedAt": "2026-04-18T19:20:11Z"
}
200Completed - result shape depends on kind. `stage` and `progress` are always emitted.
{
  "jobId": "job_01HXA1NHKJZXPV8R7Q6WSM5BCD",
  "kind": "content_generate",
  "status": "completed",
  "stage": "finalizing",
  "progress": 1.0,
  "startedAt": "2026-04-18T19:20:11Z",
  "finishedAt": "2026-04-18T19:23:47Z",
  "result": {
    "contentContainerIds": ["cnt_7d18b9a1..."],
    "mediaAssets": [
      { "assetId": "asset_01HXAJ...", "kind": "video", "durationMs": 14800 }
    ]
  }
}
200Failed - error.code is stable; error.data is stable per code. `stage`/`progress` reflect the last reached checkpoint.
{
  "jobId": "job_01HXA1NHKJZXPV8R7Q6WSM5BCD",
  "kind": "content_generate",
  "status": "failed",
  "stage": "generating_visuals",
  "progress": 0.42,
  "startedAt": "2026-04-18T19:20:11Z",
  "finishedAt": "2026-04-18T19:22:02Z",
  "error": {
    "code": "MODERATION_BLOCKED",
    "message": "Safety check rejected the generated caption.",
    "data": { "flag": "violence", "retryAfterMs": null }
  }
}
304No change since If-None-Match. Keep polling.
404Job does not exist in your organization.
{ "error": { "code": "NOT_FOUND", "message": "Unknown jobId." } }

Status values

status
  • running
    stateoptional
    Work in progress. Keep polling.
  • completed
    terminaloptional
    Success. result is populated. Sticky.
  • failed
    terminaloptional
    Fatal error. error is populated with a stable code. Sticky.
  • canceled
    terminaloptional
    You called cancel and it took effect. Sticky.

Terminal states never flip back to running. Once you see one, stop polling.

Stages by kind

stage is an advisory, human-readable label safe to display in a UI. The vocab is frozen per kind - renaming a stage is a breaking change.

KindStages (in order)
project_ingest_githubcloning → analyzing → generating_sdk_patch → opening_pr → finalizing
project_ingest_websitefetching → extracting → persisting
content_generateplanning → generating_visuals → assembling → finalizing
content_regenerateplanning → generating_visuals → assembling → finalizing
content_clone_from_postanalyzing_source → planning → generating_visuals → assembling → finalizing
influencer_creategenerating_identity → rendering_reference → persisting
appstore_ingestscraping → summarizing → persisting
marketing_bootstrapproject_create → ingest_website → sdk_app_create → layer_provision → influencer_create → first_content → finalizing
ad_optimizer_runfetching_metrics → scoring → planning_actions → applying_actions → finalizing
project_keywords_refreshextracting_keywords → expanding_candidates → scoring → finalizing

Polling pattern

Poll with jitter and back off while a job remains running. Paired with If-None-Match, 304 responses are effectively free.

See also

On this page