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.

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_githubcloninganalyzinggenerating_sdk_patchopening_prfinalizing
project_ingest_websitefetchingextractingpersisting
content_generateplanninggenerating_visualsassemblingfinalizing
content_regenerateplanninggenerating_visualsassemblingfinalizing
content_clone_from_postanalyzing_sourceplanninggenerating_visualsassemblingfinalizing
influencer_creategenerating_identityrendering_referencepersisting
appstore_ingestscrapingsummarizingpersisting
marketing_bootstrapproject_createingest_websitesdk_app_createlayer_provisioninfluencer_createfirst_contentfinalizing
ad_optimizer_runfetching_metricsscoringplanning_actionsapplying_actionsfinalizing
project_keywords_refreshextracting_keywordsexpanding_candidatesscoringfinalizing

There is no tiktok_lease job kind. Lease requests use their own status endpoint instead of the jobs API. See Request leased accounts.

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