# POST /v1/jobs/:jobId/cancel (/docs/api/reference/jobs/cancel-job)



<Endpoint method="POST" path="/v1/jobs/{jobId}/cancel" auth="Bearer" scope="jobs:cancel" phase="1" />

Signals the workflow behind a `jobId` to cancel. This is cooperative, not forceful — the job checks at stage boundaries and exits cleanly. An in-flight platform API call will usually complete first, and some terminal stages refuse cancellation entirely because rolling them back would create orphaned state (a half-uploaded asset, a PR opened against your repo).

The response tells you whether the signal landed. It does **not** tell you the job is already canceled — keep polling [`GET /v1/jobs/:jobId`](/docs/api/reference/jobs/get-job) until `status` flips to `canceled` or a terminal state.

<Parameters
  title="Path"
  rows="[
  { name: 'jobId', type: 'string (job_<ULID>)', required: true, description: 'The jobId returned from the originating 202 response.' },
]"
/>

## Example request [#example-request]

<Tabs items="['curl', 'TypeScript', 'Python']">
  <Tab value="curl">
    ```bash
    curl -X POST https://api.layers.com/v1/jobs/job_01HXA1NHKJZXPV8R7Q6WSM5BCD/cancel \
      -H "Authorization: Bearer lp_live_01HX9Y6K7EJ4T2_4QZpN..."
    ```
  </Tab>

  <Tab value="TypeScript">
    ```ts
    const res = await fetch(
      `https://api.layers.com/v1/jobs/${jobId}/cancel`,
      { method: "POST", headers: { Authorization: `Bearer ${apiKey}` } },
    );
    const { accepted, reason } = await res.json();
    ```
  </Tab>

  <Tab value="Python">
    ```python
    import httpx

    r = httpx.post(
        f"https://api.layers.com/v1/jobs/{job_id}/cancel",
        headers={"Authorization": f"Bearer {api_key}"},
    )
    payload = r.json()
    ```
  </Tab>
</Tabs>

## Response [#response]

<Response status="202" description="Cancel signal accepted. Poll the job to see status flip to canceled.">
  ```json
  {
    "jobId": "job_01HXA1NHKJZXPV8R7Q6WSM5BCD",
    "accepted": true
  }
  ```

  `reason` is only present on the 200 "already terminal" shape below. On a fresh 202 accept, it's omitted.
</Response>

<Response status="200" description="Already terminal — nothing to cancel. Flat rejection shape with the terminal reason.">
  ```json
  {
    "jobId": "job_01HXA1NHKJZXPV8R7Q6WSM5BCD",
    "accepted": false,
    "reason": "ALREADY_COMPLETED"
  }
  ```
</Response>

<Response status="409" description="Current stage refuses cancellation. Canonical CONFLICT envelope with details.subcode = JOB_CANCEL_UNAVAILABLE.">
  ```json
  {
    "error": {
      "code": "CONFLICT",
      "message": "Job cannot be canceled during this stage.",
      "requestId": "req_01HXA1NHZ4KYE8GP9Q2WX3BCDE",
      "details": {
        "subcode": "JOB_CANCEL_UNAVAILABLE",
        "jobId": "job_01HXA1NHKJZXPV8R7Q6WSM5BCD",
        "stage": "opening_pr"
      }
    }
  }
  ```

  Branch on `error.details.subcode === "JOB_CANCEL_UNAVAILABLE"` and read `details.stage` for the current non-cancelable stage.
</Response>

<Response status="404" description="Unknown jobId in your organization." />

## Non-cancelable stages [#non-cancelable-stages]

Some stages would leave external state in an inconsistent place if interrupted. The API refuses cancellation during them and returns `STAGE_NOT_CANCELABLE` with the current `stage`.

| Kind                                                                  | Stages that refuse cancel  |
| --------------------------------------------------------------------- | -------------------------- |
| `project_ingest_github`                                               | `opening_pr`, `finalizing` |
| `content_generate` / `content_regenerate` / `content_clone_from_post` | `finalizing`               |
| `influencer_create`                                                   | `persisting`               |
| `appstore_ingest`                                                     | `persisting`               |

Everything else is fair game. If you caught the job early in `cloning`, `analyzing`, `planning`, or `generating_visuals`, cancel lands within a second or two.

## Notes [#notes]

* `accepted: true` means the cancel signal was delivered, not that the job is already canceled. Poll [`GET /v1/jobs/:jobId`](/docs/api/reference/jobs/get-job) for the terminal flip.
* Canceled jobs are sticky — once `status: "canceled"`, they never revert.
* Cancel is best-effort. If a platform API call is already in flight, it will finish before the job exits.
* Canceling a `content_generate` job before `finalizing` reverses any reserved credits. Canceling after does not — the content exists.

## See also [#see-also]

* [`GET /v1/jobs/:jobId`](/docs/api/reference/jobs/get-job) — poll for terminal state
* [Jobs](/docs/api/concepts/jobs) — the 202 → poll pattern
