> ## Documentation Index
> Fetch the complete documentation index at: https://docs.runflow.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Cancel a run

> There is no public cancel endpoint. The admin-only POST /v1/admin/runs/{run_id}/cancellation transitions a non-terminal run to cancelled. When to use it, when not to.

<Warning>
  **Admin-only endpoint.** `POST /v1/admin/runs/{run_id}/cancellation` requires a service key (`rf_svc_*`) with admin scope. Inference keys (`rf_live_*`) get `401`. There is intentionally no public per-run cancel.
</Warning>

## Why there's no public cancel

Almost every Runflow model completes in under 60 seconds. By the time a caller decides to cancel, the run is usually already finishing or about to bill anyway. Adding a public cancel surfaces race conditions (cancel-after-success) and partial-charge ambiguity without saving meaningful time or money. So the API ships without one.

The admin endpoint exists for **operational override**: shutting down a stuck run, responding to an abuse report, or honoring a customer ticket that asks for one specific run to be killed.

## When to use it

* **A run is stuck in `queued` or `running` longer than the model's typical envelope** and the customer needs the slot freed for retries.
* **You are an org admin processing a support ticket** asking to cancel a specific run.
* **You are responding to an abuse pattern** and want to terminate the offending run while you investigate the key.

If you're a normal API caller and just want to stop polling, drop the run; the in-flight inference will complete on its own and any callback will silently fail at your end. Cancel adds no value in that case.

## Behavior

`POST /v1/admin/runs/{run_id}/cancellation` atomically transitions a non-terminal run to `cancelled` and queues a `run.cancelled` audit side effect.

* **Idempotent on terminal runs.** Hitting it on an already-`succeeded` / `failed` / `cancelled` run returns the run unchanged with `200 OK`. No audit log entry, no callback re-fire.
* **`201 Created`** on a real cancellation transition. The status-code difference lets audit-tailers distinguish "I caused this" from "someone got there first."
* **Does not abort the in-flight inference.** The compute job keeps running on the provider side. Late callbacks land on the now-cancelled run and are silently dropped by the terminal-state guard.
* **Billing follows the underlying job.** Cancellation does not auto-refund; the run bills whatever the provider charged for the work performed up to the cancel.

## Request

```
curl -X POST "https://api.runflow.io/v1/admin/runs/{run_id}/cancellation" \
  -H "Authorization: Bearer $RUNFLOW_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "customer ticket #4821 - duplicate submission" }'
```

Body is optional; the only field is `reason`:

| Field    | Type           | Required | Notes                                                                                                                                       |
| -------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `reason` | string \| null | no       | 3-500 chars. Stored in `audit_log.meta.reason`. Strongly encouraged for cross-org admin overrides so the audit trail tells the story later. |

## Response

Both `200` and `201` return the post-cancel `Run` object (same shape as `GET /v1/runs/{id}`). Inspect `status_code` to confirm:

```json theme={"dark"}
{
  "id": "01J0...",
  "status_code": "canceled",
  "model": "runflow/background-removal",
  "completed_at": "2026-05-23T10:42:11.000+00:00"
}
```

Note the spelling: run records use US `canceled`; webhook event names use UK `cancelled`. Both literals are stable.

## Errors

| Code  | Cause                                                         | Action                                                                        |
| ----- | ------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| `401` | Missing or non-admin key.                                     | Use a service key (`rf_svc_*`) with admin scope. Inference keys are rejected. |
| `403` | `PERMISSION_DENIED` or `EMAIL_NOT_VERIFIED`.                  | Check the `code` field in the response body.                                  |
| `404` | Run id not found in your reachable orgs.                      | Confirm the id and the org-scope of your admin key.                           |
| `409` | State invariant violated (e.g. `self_action_forbidden`).      | Inspect `errors[0].type`.                                                     |
| `422` | `reason` failed validation (under 3 chars or all whitespace). | Send a longer reason or omit the field.                                       |

## Related

<CardGroup cols={2}>
  <Card title="Runs" icon="play" href="/concepts/runs">Lifecycle, statuses, polling.</Card>
  <Card title="Callbacks" icon="webhook" href="/concepts/callbacks">Note: late callbacks on cancelled runs are dropped.</Card>
  <Card title="Authentication" icon="key" href="/concepts/authentication">Service vs inference keys.</Card>
  <Card title="Errors" icon="triangle-exclamation" href="/concepts/errors">Status codes and error envelope.</Card>
</CardGroup>
