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



Long-running operations return a **job**: GitHub ingest, content generation, influencer creation, and top-performer cloning all use the same `202 Accepted` response shape and the same polling endpoint.

## 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/prj_254a4ce1-f4ca-42b1-9e36-17ca45ef3d39/content
→ 202 Accepted
{
  "jobId": "job_01HX9Y6K7EJ4T2ABCDEF01234",
  "kind": "content_generate",
  "status": "running",
  "stage": null,
  "projectId": "prj_254a4ce1-f4ca-42b1-9e36-17ca45ef3d39",
  "containerId": "cnt_7d18b9a1-8b2c-4f3e-a4d5-6e7f8a9b0c1d",
  "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_7d18b9a1-8b2c-4f3e-a4d5-6e7f8a9b0c1d",
    "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 until you see a terminal status. If you hit `429` during polling, honor the `Retry-After` header - don't treat rate limits as a job failure.

## Job kinds [#job-kinds]

Five kinds ship today. Each 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` |
| `project_ingest_website` | `POST /v1/projects/:id/ingest/website`  | `fetching`, `extracting`, `persisting`                                     |
| `appstore_ingest`        | `POST /v1/projects/:id/ingest/appstore` | `scraping`, `summarizing`, `persisting`                                    |
| `content_generate`       | `POST /v1/projects/:id/content`         | `planning`, `generating_visuals`, `assembling`, `finalizing`               |
| `influencer_create`      | `POST /v1/projects/:id/influencers`     | `generating_identity`, `rendering_reference`, `persisting`                 |

<Callout type="warn">
  There is no `tiktok_lease` job kind. Lease requests have their own status endpoint. Submit a request, poll the [lease-request status endpoint](/docs/api/concepts/social-accounts#leased-accounts), and assigned accounts appear in your social-accounts list once fulfilled.
</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 }` - cancellation accepted. The job exits at the next cancellable 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",
    "data": {
      "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 `data.platform` and `data.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 inside the replay window:

* 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]

If you prefer push notifications, [register a webhook endpoint](/docs/api/reference/webhooks/create-endpoint) and subscribe to `job.completed`, `job.failed`, and `job.canceled`. Polling and webhooks coexist; webhooks let you avoid polling while preserving the same job contract.
