# Jobs (/docs/api/concepts/jobs)



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 [#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.

```http
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:

```http
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:

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

## State machine [#state-machine]

```text
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 [#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 [#job-kinds]

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

| `kind`                    | Triggered by                                    | Notable stages                                                             |
| ------------------------- | ----------------------------------------------- | -------------------------------------------------------------------------- |
| `project_ingest_github`   | `POST /v1/projects/:id/ingest/github`           | `cloning`, `analyzing`, `generating_sdk_patch`, `opening_pr`, `finalizing` |
| `content_generate`        | `POST /v1/projects/:id/content`                 | `planning`, `generating_visuals`, `assembling`, `rendering`                |
| `content_regenerate`      | `POST /v1/content/:id/regenerate`               | Same vocab as `content_generate`.                                          |
| `content_clone_from_post` | `POST /v1/projects/:id/content/clone-from-post` | `fetching_source`, `planning`, `rendering`                                 |
| `influencer_create`       | `POST /v1/projects/:id/influencers`             | `generating`, `rendering_references`                                       |
| `appstore_ingest`         | `POST /v1/projects/:id/ingest/appstore`         | `fetching`, `parsing`, `merging_context`                                   |

<Callout type="warn">
  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](/docs/api/concepts/social-accounts#leased-accounts), 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.
</Callout>

## Canceling a job [#canceling-a-job]

```http
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](/docs/api/operational/errors) 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`](/docs/api/reference/jobs/cancel-job).

## Error shape [#error-shape]

A failed job carries a structured error:

```json
{
  "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](/docs/api/operational/errors). `data` is structured — always check `details.platform` and `details.platformCode` before parsing `message`, which is human-friendly and subject to change.

## Idempotency [#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 [#webhooks]

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