GET /v1/jobs/:jobId
Poll the unified envelope for any long-running operation - content generation, ingest, clones, influencer creation.
/v1/jobs/{jobId}- 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.
jobIdstring (job_<ULID>)requiredThe jobId returned from the originating 202 response.
If-None-MatchstringoptionalETag 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_..."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
{
"jobId": "job_01HXA1NHKJZXPV8R7Q6WSM5BCD",
"kind": "content_generate",
"status": "running",
"stage": "generating_visuals",
"progress": 0.42,
"startedAt": "2026-04-18T19:20:11Z"
}{
"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 }
]
}
}{
"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 }
}
}{ "error": { "code": "NOT_FOUND", "message": "Unknown jobId." } }Status values
runningstateoptionalWork in progress. Keep polling.completedterminaloptionalSuccess. result is populated. Sticky.failedterminaloptionalFatal error. error is populated with a stable code. Sticky.canceledterminaloptionalYou 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.
| Kind | Stages (in order) |
|---|---|
project_ingest_github | cloning → analyzing → generating_sdk_patch → opening_pr → finalizing |
project_ingest_website | fetching → extracting → persisting |
content_generate | planning → generating_visuals → assembling → finalizing |
content_regenerate | planning → generating_visuals → assembling → finalizing |
content_clone_from_post | analyzing_source → planning → generating_visuals → assembling → finalizing |
influencer_create | generating_identity → rendering_reference → persisting |
appstore_ingest | scraping → summarizing → persisting |
marketing_bootstrap | project_create → ingest_website → sdk_app_create → layer_provision → influencer_create → first_content → finalizing |
ad_optimizer_run | fetching_metrics → scoring → planning_actions → applying_actions → finalizing |
project_keywords_refresh | extracting_keywords → expanding_candidates → scoring → finalizing |
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
- Jobs - the 202 → poll pattern
POST /v1/jobs/:jobId/cancel- best-effort cancel- Error codes - the full taxonomy
error.codedraws from