Layers
Partner 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 Partner API 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 and recommended polling cadence.

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_live_01HX9Y6K7EJ4T2_4QZpN..."
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 sleep(delayMs + Math.random() * 500);
    delayMs = Math.min(delayMs * 1.3, 10_000);
  }
}

const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
import httpx, random, time

def poll_job(job_id: str, api_key: str):
    delay = 2.0
    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:
                time.sleep(delay)
                delay = min(delay * 1.3, 10.0)
                continue
            etag = r.headers.get("etag")
            job = r.json()
            if job["status"] != "running":
                return job
            time.sleep(delay + random.random() * 0.5)
            delay = min(delay * 1.3, 10.0)

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_01HXAJK..."],
    "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_githubcloninganalyzinggenerating_sdk_patchopening_prfinalizing
content_generateplanninggenerating_visualsassemblingfinalizing
content_regenerateplanninggenerating_visualsassemblingfinalizing
content_clone_from_postanalyzing_sourceplanninggenerating_visualsassemblingfinalizing
influencer_creategenerating_identityrendering_referencepersisting
appstore_ingestscrapingsummarizingpersisting

There is no tiktok_lease job kind. Leased account provisioning is a manual ops step at Layers, not an async workflow. See Request leased accounts.

Polling cadence

Two seconds with a bit of jitter is a sane starting interval so many clients do not hit the same second. Back off to ten seconds after the first thirty seconds of running. Paired with If-None-Match, 304 responses are effectively free.

Do not poll faster than once per second. You will not learn anything useful and you will eat into your reads budget.

See also

On this page