POST /v1/jobs/:jobId/cancel
Best-effort cancel on a running job. Some stages refuse cancellation; the response tells you which.
/v1/jobs/{jobId}/cancel- Auth
- Bearer
- Scope
- jobs:cancel
Requests cancellation for a running job. Cancellation is cooperative, not forceful: an in-flight platform API call may complete first, and some stages refuse cancellation because rolling them back would leave partial external state, such as a half-uploaded asset or a PR already 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 until status flips to canceled or a terminal state.
jobIdstring (job_<ULID>)requiredThe jobId returned from the originating 202 response.
Example request
curl -X POST https://api.layers.com/v1/jobs/job_01HXA1NHKJZXPV8R7Q6WSM5BCD/cancel \
-H "Authorization: Bearer lp_..."const res = await fetch(
`https://api.layers.com/v1/jobs/${jobId}/cancel`,
{ method: "POST", headers: { Authorization: `Bearer ${apiKey}` } },
);
const { accepted, reason } = await res.json();import httpx
r = httpx.post(
f"https://api.layers.com/v1/jobs/{job_id}/cancel",
headers={"Authorization": f"Bearer {api_key}"},
)
payload = r.json()Response
{
"jobId": "job_01HXA1NHKJZXPV8R7Q6WSM5BCD",
"accepted": true
}reason is only present on the 200 "already terminal" shape below. On a fresh 202 accept, it's omitted.
{
"jobId": "job_01HXA1NHKJZXPV8R7Q6WSM5BCD",
"accepted": false,
"reason": "ALREADY_COMPLETED"
}{
"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.
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 |
project_ingest_website | persisting |
content_generate | finalizing |
content_regenerate | finalizing |
content_clone_from_post | finalizing |
influencer_create | persisting |
appstore_ingest | persisting |
marketing_bootstrap | project_create, ingest_website, sdk_app_create, layer_provision, influencer_create, first_content, finalizing |
ad_optimizer_run | applying_actions, finalizing |
project_keywords_refresh | finalizing |
marketing_bootstrap currently refuses cancel at every stage. Bootstrap is a fan-out orchestrator and cancellation mid-flight could leak half-built projects (project row exists, no influencer, etc.); this is a v1 limitation while bootstrap stages don't yet have rollback semantics. Partners should treat bootstrap as an atomic operation — if it fails, retry with the same idempotency key (which short-circuits to the prior bootstrapJobId) once the underlying issue is fixed.
Everything else is fair game. If the job is still in an early cancellable stage, the cancel request is usually accepted.
Notes
accepted: truemeans the cancel signal was delivered, not that the job is already canceled. PollGET /v1/jobs/:jobIdfor 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_generatejob beforefinalizingreverses any reserved credits. Canceling after does not - the content exists.
See also
GET /v1/jobs/:jobId- poll for terminal state- Jobs - the 202 → poll pattern