Layers
Partner APIConcepts

Jobs

The unified envelope for every long-running operation. One pattern you learn once.

View as Markdown

A lot of what the Partner API does takes longer than a sensible HTTP request. Ingesting a GitHub repo and opening a PR. Generating a video. Creating an influencer. Cloning a top-performing post into a new creative. Rather than make you learn a different pattern per endpoint, every long-running operation returns the same envelope — a job — and you poll one endpoint to watch it finish.

Learn this pattern once and every async call works the same way.

The 202 + poll pattern

Any endpoint that kicks off work returns 202 Accepted immediately with a job envelope — jobId, kind, status, stage, plus a kind-specific set of pointers to the resource being produced (e.g. projectId, containerId) and a locationUrl you should poll.

POST /v1/projects/254a4ce1-f4ca-42b1-9e36-17ca45ef3d39/content
→ 202 Accepted
{
  "jobId": "job_01HX9Y6K7EJ4T2ABCDEF01234",
  "kind": "content_generate",
  "status": "running",
  "stage": "queued",
  "projectId": "254a4ce1-f4ca-42b1-9e36-17ca45ef3d39",
  "containerId": "cnt_01HX9Y6K7EJ4T2ABCDEF",
  "locationUrl": "/v1/jobs/job_01HX9Y6K7EJ4T2ABCDEF01234",
  "startedAt": "2026-04-18T12:04:11.000Z"
}

Then you poll:

GET /v1/jobs/job_01HX9Y6K7EJ4T2ABCDEF01234
→ 200 OK
{
  "jobId": "job_01HX9Y6K7EJ4T2ABCDEF01234",
  "kind": "content_generate",
  "status": "running",
  "progress": 0.42,
  "stage": "generating_visuals",
  "startedAt": "2026-04-18T12:04:11.000Z"
}

When it finishes, the same endpoint returns a terminal shape:

{
  "jobId": "job_01HX9Y6K7EJ4T2ABCDEF01234",
  "kind": "content_generate",
  "status": "completed",
  "finishedAt": "2026-04-18T12:07:33.000Z",
  "result": {
    "containerId": "cnt_01HX9Y6K7EJ4T2ABCDEF",
    "assets": [/* ... */]
  }
}

State machine

running ──┬── completed
          ├── failed
          └── canceled
  • running is the only non-terminal state.
  • completed, failed, and canceled are sticky. Once a job lands in one of them it stays there forever. You can safely cache terminal responses.
  • progress is a float in [0, 1]. It moves forward and never snaps back.
  • stage is a job-kind-specific string (for example cloning, analyzing, generating_visuals). The stages for each kind are documented on the endpoint that starts them.

How to poll

Poll every 5 to 30 seconds. Start at 5s, grow to 30s as the job ages. Stop polling the moment you see a terminal status. If you hit 429 during polling, honor the Retry-After — don't treat rate limits as a job failure.

Job kinds

Six kinds ship today. Each wraps a specific workflow and has its own stage vocabulary.

kindTriggered byNotable stages
project_ingest_githubPOST /v1/projects/:id/ingest/githubcloning, analyzing, generating_sdk_patch, opening_pr, finalizing
content_generatePOST /v1/projects/:id/contentplanning, generating_visuals, assembling, rendering
content_regeneratePOST /v1/content/:id/regenerateSame vocab as content_generate.
content_clone_from_postPOST /v1/projects/:id/content/clone-from-postfetching_source, planning, rendering
influencer_createPOST /v1/projects/:id/influencersgenerating, rendering_references
appstore_ingestPOST /v1/projects/:id/ingest/appstorefetching, parsing, merging_context

There is no tiktok_lease job kind. Leased TikTok account provisioning is a manual admin function at Layers — permanently. You submit a lease request, you poll the lease-request status endpoint, and assigned accounts appear in your social-accounts list once Layers fulfills them. It isn't plumbed through the jobs envelope because it isn't a workflow.

Canceling a job

POST /v1/jobs/job_01HX9Y6K7EJ4T2ABCDEF01234.../cancel

Cancel is best-effort. The status code tells you what happened:

  • 202{ "jobId": "...", "accepted": true } — cancel signal delivered, worker will wind down at its next checkpoint.
  • 200{ "jobId": "...", "accepted": false, "reason": "ALREADY_COMPLETED" | "ALREADY_FAILED" | "ALREADY_CANCELED", "stage"?: "..." } — job is already in a terminal state, so there's nothing to cancel.
  • 409 — canonical error envelope with code: "CONFLICT" and details.subcode: "JOB_CANCEL_UNAVAILABLE" — the current stage refuses cancellation (rolling it back would leave orphaned state). Wait for the next stage and retry, or let the job finish.

We don't roll back side effects of a completed stage. If you need to undo something a completed job did, look up the resource it produced and act on it directly.

See the endpoint reference for the full response contract: POST /v1/jobs/:jobId/cancel.

Error shape

A failed job carries a structured error:

{
  "jobId": "job_01HX9Y6K7EJ4T2ABCDEF01234...",
  "status": "failed",
  "finishedAt": "2026-04-18T12:09:02Z",
  "error": {
    "code": "PLATFORM_ERROR",
    "message": "Meta rejected the ad creative: aspect ratio not supported",
    "details": {
      "platform": "meta",
      "platformCode": "1487194",
      "platformMessage": "Video must be at least 4:5",
      "retryAfterMs": null
    }
  }
}

code is drawn from the stable error set. data is structured — always check details.platform and details.platformCode before parsing message, which is human-friendly and subject to change.

Idempotency

Any POST that starts a job accepts Idempotency-Key: <uuid>. If the same key arrives twice within 24 hours:

  • Same body: the original 202 { jobId, ... } is replayed. Safe to retry on network errors.
  • Different body: 409 IDEMPOTENCY_CONFLICT.

Use an idempotency key on every job-starting POST you make. Networks are messy; duplicate content_generate jobs cost credits.

Webhooks

Polling is the zero-setup path. If you'd rather get a push — job.completed, job.failed, job.canceledregister a webhook endpoint and subscribe to the job events you care about. The delivery contract (HMAC-SHA256 signatures, retry ladder, dedupe, replay) is documented in Webhooks. Polling and webhooks coexist — webhooks don't replace the jobs envelope, they let you avoid polling it.

On this page