# Tasks

> Run apps and manage tasks.

**URL:** https://inference.sh/docs/api/rest/tasks
**Last updated:** 2026-10-18

---

Run apps and manage tasks.

---

## Run Task

`POST /run`

Run an app and optionally wait for completion.

### Request

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `app` | string | Yes | App identifier (`namespace/name@version`) |
| `input` | object | Yes | Input data matching app schema |
| `setup` | object | No | Setup parameters |
| `infra` | string | No | `"cloud"`, `"private"`, or `"private_first"` (default `"cloud"` when omitted) |
| `workers` | string[] | No | Specific worker IDs |
| `webhook` | string | No | URL to receive a POST when the task completes |
| `session` | string | No | `"new"` to start a session, or existing session ID |
| `session_timeout` | number | No | Session idle timeout in seconds (1-3600, only with `session: "new"`) |

**Example:**
```json
{
  "app": "infsh/flux",
  "input": {
    "prompt": "A sunset over mountains"
  },
  "setup": {
    "model": "schnell"
  }
}
```

### Response

| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Task ID |
| `status` | number | Status code |
| `input` | object | Input data |
| `output` | object | Output data (when complete). Internal pricing field `output_meta` is stripped — app authors still set it in app code; see [Output metadata](/docs/extend/output-meta). |
| `error` | string | Error message (if failed) |
| `session_id` | string | Session ID (if using sessions) |
| `created_at` | string | ISO timestamp |

**Example:**
```json
{
  "id": "task_abc123",
  "status": 10,
  "input": { "prompt": "A sunset..." },
  "output": {
    "image": { "uri": "https://..." }
  },
  "created_at": "2024-01-15T10:30:00Z"
}
```

### Errors

These apply to both `POST /run` and `POST /apps/run` (same handler).

| Status | Code | Description |
|--------|------|-------------|
| 404 | `not_found` | App ref does not exist |
| 404 | `SESSION_NOT_FOUND` | Session ID does not exist (when reusing `session`) |
| 403 | `forbidden` | No permission to run the app |
| 403 | `blocked_by_usage_policy` | The app (or its publisher) is outside your workspace [usage policy](/docs/org/usage-policy). Checked **before** balance — a blocked app returns this even when credits are zero, not `payment_required`. |
| 402 | `payment_required` | Insufficient balance for a run the usage policy already allows, or the workspace's spending is locked, on hold, or suspended (free runs included) |
| 402 | `payment_method_required` | A run certain to cost $0 when the balance would refuse it and the workspace has no saved payment method. `meta.billing_page` names the page that adds one. See [Billing: free runs](/docs/api/rest/billing#free-runs) |
| 409 | `APP_MISMATCH` | Session is bound to a different app |
| 409 | `VERSION_MISMATCH` | Session is bound to a different app version |
| 410 | `SESSION_EXPIRED` | Session timed out (when reusing an existing session ID) |
| 410 | `SESSION_ENDED` | Session was explicitly ended |
| 410 | `app_retired` | App has been retired — `error.message` includes the app's `status_message` |
| 412 | `precondition_failed` | Missing setup requirements — see [Credentials](/docs/api/rest/credentials#check-requirements-pre-flight) |
| 422 | `validation_error` | Invalid input — for example an unknown `function` on a multi-function app. The `data` array lists field errors; unknown-function errors include available function names in the message. |
| 503 | `app_maintenance` | App is temporarily unavailable — `error.message` includes the app's `status_message`; response includes `Retry-After: 3600` |

When the `app` ref is wrong, the response is `not_found` without suggestions; use [Search](/docs/api/rest/search) or [Store apps](/docs/api/rest/store) to discover apps.

---

## Get Task

`GET /tasks/:id`

Get task status and output.

### Response

Same as Run Task response.

**Example:**
```bash
curl https://api.inference.sh/tasks/task_abc123 \
  -H "Authorization: Bearer inf_your_key"
```

---

## List Tasks

`POST /tasks/list`

List your tasks with cursor-based pagination. Requires the `apps:read` scope.

### Request

| Field | Type | Description |
|-------|------|-------------|
| `limit` | number | Max results per page (default 10) |
| `cursor` | string | Pagination cursor from a previous response |
| `direction` | string | `"next"` or `"prev"` (default `"next"`) |
| `filters` | array | Filter objects (`field`, `operator`, `value`) |
| `sort` | array | Sort objects (`field`, `dir`) |

**Filter by status** — use an integer code or a status name from [Task status codes](#task-status-codes) (`received`, `queued`, `dispatched`, `preparing`, `serving`, `setting_up`, `running`, `cancelling`, `uploading`, `completed`, `failed`, `cancelled`, `rejected`):

```json
{
  "limit": 20,
  "filters": [
    { "field": "status", "operator": "eq", "value": 10 }
  ]
}
```

String names are accepted and converted server-side (for example `"completed"` is equivalent to `10`):

```json
{
  "limit": 20,
  "filters": [
    { "field": "status", "operator": "eq", "value": "completed" }
  ]
}
```

For `in` / `not_in` operators, each array element may be an integer or a recognized status name. Unrecognized status strings return **400** with code `invalid_filter`.

**Filter by app:**

```json
{
  "limit": 20,
  "filters": [
    { "field": "app_id", "operator": "eq", "value": "app_xyz789" }
  ]
}
```

### Response

| Field | Type | Description |
|-------|------|-------------|
| `items` | array | Task objects (same shape as Get Task) |
| `next_cursor` | string | Cursor for the next page |
| `prev_cursor` | string | Cursor for the previous page |
| `has_next` | boolean | More results available |
| `has_previous` | boolean | Previous page available |
| `total_items` | number | Total matching tasks |

**Example:**

```bash
curl -X POST https://api.inference.sh/tasks/list \
  -H "Authorization: Bearer inf_your_key" \
  -H "Content-Type: application/json" \
  -d '{"limit": 10}'
```

From the CLI: `belt task list` (alias `belt task ls`). See [CLI setup](/docs/extend/cli-setup#tasks-cli).

---

## Queue stats

`GET /tasks/queue-stats`

Returns queue depth for your workspace: how many tasks are waiting for workers, scheduled for later, or actively running. Requires the `apps:read` scope. Counts are scoped to tasks you can see (same visibility rules as [List Tasks](#list-tasks)).

Use this to monitor backlog before scaling workers or alerting on capacity. See also [Queued longer than expected?](/docs/concepts/tasks#queued-longer-than-expected).

### Response

| Field | Type | Description |
|-------|------|-------------|
| `total_queued` | number | Tasks in **Queued** status |
| `with_run_at` | number | Queued tasks with `run_at` in the future (scheduled) |
| `ready_to_run` | number | Queued tasks ready to dispatch now |
| `in_progress` | number | Tasks in active execution statuses (dispatched through uploading) |
| `by_app` | array | Breakdown by app |
| `by_app_function` | array | Breakdown by app, version, and function |

Each breakdown entry:

| Field | Type | Description |
|-------|------|-------------|
| `app_id` | string | App ID |
| `app_name` | string | `namespace/name` when available |
| `version_id` | string | App version ID |
| `function` | string | Function name (when set) |
| `count` | number | Queued tasks in this group |
| `with_run_at` | number | Subset with future `run_at` |
| `in_progress` | number | Subset actively running |

**Example:**

```bash
curl https://api.inference.sh/tasks/queue-stats \
  -H "Authorization: Bearer inf_your_key"
```

```json
{
  "total_queued": 12,
  "with_run_at": 2,
  "ready_to_run": 10,
  "in_progress": 4,
  "by_app": [
    {
      "app_id": "app_xyz789",
      "app_name": "myteam/image-gen",
      "count": 8,
      "with_run_at": 0,
      "in_progress": 3
    }
  ],
  "by_app_function": []
}
```

---

## Get Task Status

`GET /tasks/:id/status`

Lightweight status poll without fetching full task input, output, or logs. Useful when you only need to know whether a task is still running.

### Response

| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Task ID |
| `status` | number | Status code |
| `updated_at` | string | ISO timestamp of last update |

**Example:**

```bash
curl https://api.inference.sh/tasks/task_abc123/status \
  -H "Authorization: Bearer inf_your_key"
```

---

## Get Task Logs

`GET /tasks/:id/logs`

Returns execution logs and status events for a task without the full task payload.

### Response

| Field | Type | Description |
|-------|------|-------------|
| `task_id` | string | Task ID |
| `status` | number | Current status code |
| `events` | array | Status transition events |
| `logs` | array | Log entries (`log_type`, `content`) |

### Log types

| `log_type` | Name | Typical content |
|------------|------|-----------------|
| `0` | build | Container image build output |
| `1` | run | App runtime stdout/stderr |
| `2` | serve | Model serving logs |
| `3` | setup | Setup-phase initialization |
| `4` | task | Task-level messages |

**Example:**

```bash
curl https://api.inference.sh/tasks/task_abc123/logs \
  -H "Authorization: Bearer inf_your_key"
```

From the CLI: `belt task get <id> -f` streams status until complete; `belt task logs <id> -f` tails live logs (filter with `--type setup`). See [CLI setup](/docs/extend/cli-setup#tasks-cli).

**JavaScript SDK:** `client.tasks.getLogs('task_abc123')`

---

## Get Task Timings

`GET /tasks/:id/timings`

Returns a timing breakdown derived from status events — total duration, phase groups, and per-transition timings.

### Response

| Field | Type | Description |
|-------|------|-------------|
| `task_id` | string | Task ID |
| `status` | number | Current status code |
| `total_duration` | string | Human-readable total duration |
| `total_duration_ms` | number | Total duration in milliseconds |
| `groups` | array | High-level phases (`label`, `start_at`, `end_at`, `duration`, `duration_ms`) |
| `events` | array | Per-status transitions with durations |

**Example:**

```bash
curl https://api.inference.sh/tasks/task_abc123/timings \
  -H "Authorization: Bearer inf_your_key"
```

From the CLI: `belt task timings <id>`. See [CLI setup](/docs/extend/cli-setup#tasks-cli).

**JavaScript SDK:** `client.tasks.getTimings('task_abc123')`

---

## Get Task Telemetry

`GET /tasks/:id/telemetry`

Returns time-series resource samples collected while the task ran on a worker (CPU, RAM, GPU, and volume usage). Samples are recorded by the engine during execution and stored in TimescaleDB.

Requires the same access as [Get Task](#get-task) (`apps:read` scope). Long runs may return up to **100** downsampled points so responses stay bounded.

### Response

Array of telemetry records under `data`:

| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Sample record ID |
| `timestamp` | string | ISO timestamp when the sample was taken |
| `task_id` | string | Task ID |
| `task_seq` | number | Monotonic sequence within the task |
| `app_id` | string | App that ran |
| `app_version_id` | string | App version |
| `engine_id` | string | Engine that hosted the worker |
| `worker_id` | string | Worker that executed the task |
| `system_info` | object | Worker CPU, RAM, GPU, and volume metrics at this sample |
| `engine_resources` | object | Engine-level CPU, RAM, GPU, and volume metrics at this sample |

Each `system_info` / `engine_resources` object includes:

| Nested field | Description |
|--------------|-------------|
| `cpus` | Per-CPU usage (`usage`, `normalized_usage`, core metadata) |
| `ram` | Memory totals and usage percentages |
| `gpus` | GPU memory, temperature, and driver metadata (when present) |
| `volumes` | Disk volume size and usage |

**Example:**

```bash
curl https://api.inference.sh/tasks/task_abc123/telemetry \
  -H "Authorization: Bearer inf_your_key"
```

**JavaScript SDK:**

```typescript
const { data: samples } = await client.tasks.getTelemetry('task_abc123');
```

Telemetry is most useful for debugging resource-heavy apps (large models, GPU runs, or disk-heavy pipelines). For phase durations without resource metrics, use [Get Task Timings](#get-task-timings).

→ [Observability](/docs/runtime/observability) · [SDK overview — inspect and observability](/docs/api/sdk/overview#inspect-and-observability-javascript)

---

## Cancel Task

`POST /tasks/:id/cancel`

Cancel a running task. By default the platform requests a graceful stop (apps with an `on_cancel` hook can clean up first).

### Request body (optional)

| Field | Type | Description |
|-------|------|-------------|
| `force` | boolean | If `true`, skip the graceful stop and end the run immediately |
| `timeout` | number | Milliseconds the app gets to stop on its own before the run is ended for it (default `10000`) |

Either way the worker is free for the next task when the call returns. An app that stops on its own keeps its loaded state; one that is ended for it is restarted, so the next task on that worker runs `setup()` again. See [Graceful cancellation](/docs/extend/cancellation).

**Example (graceful):**
```bash
curl -X POST https://api.inference.sh/tasks/task_abc123/cancel \
  -H "Authorization: Bearer inf_your_key"
```

**Example (force kill):**
```bash
curl -X POST https://api.inference.sh/tasks/task_abc123/cancel \
  -H "Authorization: Bearer inf_your_key" \
  -H "Content-Type: application/json" \
  -d '{"force": true}'
```

### Response

```json
{
  "id": "task_abc123",
  "status": 12,
  "cancelled": true
}
```

---

## Delete Task

`DELETE /tasks/:id`

Delete a finished task. Requires the `apps:execute` scope.

A task that is still queued or running answers `409 conflict`. [Cancel it](#cancel-task) first, or wait for it to finish.

### Query parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `files` | boolean | If `true`, also delete the task's input and output files. Default `false`: the files stay in your workspace |

With `files=true` the files are deleted even when another task, flow or chat still uses them. Their URLs stop resolving. Files you cannot delete are skipped, as in [Delete Task Files](#delete-task-files); they do not stop the task from being deleted.

**Example (task only):**
```bash
curl -X DELETE https://api.inference.sh/tasks/task_abc123 \
  -H "Authorization: Bearer inf_your_key"
```

**Example (task and its files):**
```bash
curl -X DELETE "https://api.inference.sh/tasks/task_abc123?files=true" \
  -H "Authorization: Bearer inf_your_key"
```

CLI equivalent: `belt task delete <task-id>` (add `--files` to delete its files too, `--yes` to skip the confirmation prompt).

---

## Attach Task Files

`POST /tasks/:id/files`

Create platform file records for a task and link them to it as **output** files. Requires the `apps:execute` scope and write access to the task.

Workers call this while a task runs to register uploads; the caller then `PUT`s the bytes to each `upload_url`, as with [Upload File](/docs/api/rest/files#upload-file). Files attached here appear in [List Task Files](#list-task-files) even when the task's `output` never names their URLs.

The body is the same as `POST /files`: a `files` array of partial file descriptors.

```bash
curl -X POST https://api.inference.sh/tasks/task_abc123/files \
  -H "Authorization: Bearer inf_your_key" \
  -H "Content-Type: application/json" \
  -d '{"files":[{"uri":"","filename":"image.png","content_type":"image/png","size":102400}]}'
```

**Response:** `FileDTO[]`, the same shape as [Upload File](/docs/api/rest/files#1-create-the-record).

| Status | Description |
|--------|-------------|
| 400 | Invalid JSON body |
| 403 | No write access to the task |
| 500 | Any other failure, including a task that does not exist |

There is no SDK helper for this endpoint.

---

## List Task Files

`GET /tasks/:id/files`

List the files a task consumed and produced. Requires the `apps:read` scope.

Use this to get the file IDs behind the URLs in a task's `input` and `output`, for example to delete one file with [`DELETE /files/:id`](/docs/api/rest/files#delete-file).

Files are found from the platform URLs in the task's `input` and `output` and from the files linked to the task, including uploads attached with [`POST /tasks/:id/files`](#attach-task-files) that `output` never names. Files you cannot read (for example another workspace's file named in an input by its public URL) are left out.

### Query parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `role` | string | `input` or `output` to list one side only. Omit for both. Any other value answers `400 validation_error` |

### Response

```json
[
  {
    "id": "8k2m4n6p8q0r2s4t6v8w0x2y4z",
    "created_at": "2026-10-07T08:31:12Z",
    "role": "input",
    "uri": "https://cloud.inference.sh/t/1a2b3c4d/5e6f7g8h.png",
    "filename": "photo.png",
    "content_type": "image/png",
    "size": 102400
  },
  {
    "id": "9l3n5p7q9r1s3t5v7w9x1y3z5a",
    "created_at": "2026-10-07T08:31:15Z",
    "role": "output",
    "uri": "https://cloud.inference.sh/t/1a2b3c4d/9i0j1k2l.png",
    "filename": "result.png",
    "content_type": "image/png",
    "size": 245760
  }
]
```

| Field | Description |
|-------|-------------|
| `id` | File ID, as [`DELETE /files/:id`](/docs/api/rest/files#delete-file) takes it |
| `role` | `input` if the task consumed the file, `output` if it produced it. A file the app passes through from input to output is an `input` |
| `uri` | The URL that appears in the task's `input` or `output` |

Only files stored on inference.sh are listed. An input you passed as a URL on your own storage is not a file here and does not appear.

CLI equivalent: `belt task files <task-id>` (add `--role input` or `--role output`, `--json` for the raw payload).

---

## Delete Task Files

`DELETE /tasks/:id/files`

Delete a finished task's files and keep the task. Requires the `apps:execute` scope.

The task record stays, with its `input` and `output` unchanged; the file URLs in them stop resolving. A task that is still queued or running answers `409 conflict`.

A file is deleted even when another task, flow or chat still uses it. If you reuse one uploaded input across several tasks, pass `role=output` to keep it.

### Query parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `role` | string | `input` or `output` to delete one side only. Omit for both. Any other value answers `400 validation_error` |

**Example:**
```bash
curl -X DELETE https://api.inference.sh/tasks/task_abc123/files \
  -H "Authorization: Bearer inf_your_key"
```

### Response

```json
{
  "deleted": ["8k2m4n6p8q0r2s4t6v8w0x2y4z", "9l3n5p7q9r1s3t5v7w9x1y3z5a"],
  "skipped": []
}
```

| Field | Description |
|-------|-------------|
| `deleted` | IDs of the files that were deleted |
| `skipped` | Files left in place, each with an `id` and a `reason`: `forbidden` (the file belongs to another workspace) or `failed` (the delete did not go through; call again to retry) |

The call is safe to repeat: files already deleted are not listed again.

CLI equivalent: `belt task delete-files <task-id>` (add `--role output` to keep the inputs, `--yes` to skip the confirmation prompt).

---

## Infrastructure routing

The `infra` field controls which workers the scheduler considers:

| Value | Behavior |
|-------|----------|
| `cloud` | Cloud workers only (default when `infra` is omitted on `POST /run`) |
| `private` | Your workspace's private workers only |
| `private_first` | Try private workers first; fall back to cloud if none are available |

`private_first` is useful when you prefer your own hardware but still want runs to succeed when private capacity is offline or busy. The scheduler ranks private workers ahead of cloud workers for matching tasks.

Apps may restrict which infra types they allow (for example cloud-only store apps). If the chosen infra is not permitted, the task fails immediately. If no worker is available yet, the task stays **Queued** until capacity appears or the queue expires.

→ [Workers](/docs/concepts/workers) · [Using private workers](/docs/private/using)

---

## Stream Task Updates

`GET /tasks/:id/stream`

Receive task status, logs, and output over Server-Sent Events (SSE) instead of polling `GET /tasks/:id`.

→ [Streaming API](/docs/api/rest/streaming#stream-task-updates)

Use polling (`GET /tasks/:id` in a loop) when your environment cannot hold long-lived connections. The Python and JavaScript SDKs also support `stream: false` — see [SDK polling](/docs/api/sdk/polling).

---

## Get Task Cost

`GET /usage/tasks/:id/cost`

Get the cost breakdown for a completed task.

### Response

| Field | Type | Description |
|-------|------|-------------|
| `total` | number | Total cost in microcents (1 microcent = $0.00000001) |
| `discount` | number | Total discount applied in microcents (omitted if zero) |
| `charged` | number | Amount actually charged in microcents |
| `refunded` | number | Amount refunded in microcents (omitted if zero) |

**Example:**
```bash
curl https://api.inference.sh/usage/tasks/task_abc123/cost \
  -H "Authorization: Bearer inf_your_key"
```

**Response:**

```json
{
  "total": 50000,
  "charged": 50000
}
```

See [REST overview — response format](/docs/api/rest/overview#response-format).

> **Note:** All values are in microcents. To convert to dollars: `dollars = microcents / 100000000`. For example, `50000` microcents = `$0.0005`.

### Errors

| Status | Code | Description |
|--------|------|-------------|
| 403 | `forbidden` | You don't have permission to access this task |
| 404 | `not_found` | Cost not found for this task |

---

## Task Status Codes

| Status | Code | Description |
|--------|------|-------------|
| Received | 1 | Task received |
| Queued | 2 | Waiting for worker |
| Dispatched | 3 | Assigned to worker |
| Preparing | 4 | Setting up environment |
| Serving | 5 | Loading model |
| Setting Up | 6 | Task initialization |
| Running | 7 | Executing |
| Cancelling | 8 | Cancel requested; worker shutting down |
| Uploading | 9 | Uploading results |
| Completed | 10 | Done |
| Failed | 11 | Error occurred |
| Cancelled | 12 | Cancelled |
| Rejected | 13 | Refused before running: the workspace has no balance or no payment method. The input is kept; the run can be retried. |

### Task failure messages

When `status` is `11` (**Failed**), the `error` string describes the failure. App errors pass through from your code unchanged. Platform failures return short, retry-friendly messages without internal scheduler or worker details.

| `error` value | Meaning |
|---------------|---------|
| `task interrupted, please retry` | Transient infrastructure issue (worker disconnect, engine reconnect, orphaned task) |
| `task could not be dispatched, please retry` | Could not assign a worker when the task was submitted |
| `task could not be processed after multiple attempts, please retry` | Exceeded dispatch retry limit while stuck in **Dispatched** |
| `task expired while waiting for available capacity, please retry` | Queued ~24 hours with no available worker |
| `no active workers with matching resources` | No worker matches GPU/VRAM/infra (~10 minutes) |
| `no available workers — capacity reserved for higher-priority apps` | Matching cloud workers exist but are reserved by another app's `min_concurrency` floor (~10 minutes) |
| `failed to prepare secrets for this app, please check your secret configuration` | Missing or invalid workspace secret |
| `session worker busy too long` | Session worker busy for ~30 seconds |
| `session worker offline` | Session's worker is no longer available |
| `session expired or not found` | Invalid session ID or idle timeout |

→ [Failed tasks and error messages](/docs/concepts/tasks#failed-tasks-and-error-messages) · [Troubleshooting](/docs/extend/troubleshooting#platform-task-failures)

---

## cURL Examples

### Run and wait

```bash
curl -X POST https://api.inference.sh/run \
  -H "Authorization: Bearer inf_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "app": "infsh/flux",
    "input": {"prompt": "A sunset"}
  }'
```

### Run without waiting

```bash
curl -X POST "https://api.inference.sh/run?wait=false" \
  -H "Authorization: Bearer inf_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "app": "infsh/flux",
    "input": {"prompt": "A sunset"}
  }'
```

### Run with session

```bash
# Start a new session
curl -X POST https://api.inference.sh/run \
  -H "Authorization: Bearer inf_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "app": "my-app",
    "input": {"action": "init"},
    "session": "new"
  }'
# Response includes session_id: "sess_abc123"

# Continue session
curl -X POST https://api.inference.sh/run \
  -H "Authorization: Bearer inf_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "app": "my-app",
    "input": {"action": "process"},
    "session": "sess_abc123"
  }'
```

### Run with custom session timeout

```bash
# Start session with 5-minute idle timeout
curl -X POST https://api.inference.sh/run \
  -H "Authorization: Bearer inf_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "app": "my-app",
    "input": {"action": "init"},
    "session": "new",
    "session_timeout": 300
  }'
```

See [Sessions Developer Guide](/docs/extend/sessions) for full documentation.

### Session errors on run

When you pass an existing `session` ID to `POST /run` or `POST /apps/run` and the session has timed out, the API returns **410 Gone** with code `SESSION_EXPIRED`. Start a new session with `"session": "new"`.

Other session errors on run (`SESSION_NOT_FOUND`, `SESSION_ENDED`, `APP_MISMATCH`, `VERSION_MISMATCH`) are listed in [Run Task errors](#errors) above. `WORKER_LEASED` and other session-management errors are documented on the [Sessions API](/docs/api/rest/sessions#errors). See also [Sessions developer guide — Error handling](/docs/extend/sessions#error-handling).

---

## Webhooks

Get notified when a task completes by providing a `webhook` URL. When the task reaches a terminal state, a POST request is sent to your URL.

### Run with webhook

```bash
curl -X POST https://api.inference.sh/run \
  -H "Authorization: Bearer inf_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "app": "infsh/flux",
    "input": {"prompt": "A sunset"},
    "webhook": "https://your-server.com/webhook"
  }'
```

### Webhook payload

Your endpoint receives a JSON POST with the task result:

```json
{
  "event": "task.completed",
  "timestamp": "2024-01-15T10:30:05Z",
  "data": {
    "id": "task_abc123",
    "short_id": "abc123",
    "status": 10,
    "status_text": "completed",
    "output": { "image": { "uri": "https://..." } },
    "created_at": "2024-01-15T10:30:00Z",
    "updated_at": "2024-01-15T10:30:05Z"
  }
}
```

| Field | Type | Description |
|-------|------|-------------|
| `event` | string | `task.completed`, `task.failed`, or `task.cancelled` |
| `timestamp` | string | When the webhook was sent (ISO 8601, UTC) |
| `data` | object | The task result, fields below |

`data` fields:

| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Task ID |
| `short_id` | string | Short task ID |
| `status` | number | Terminal status code (10=completed, 11=failed, 12=cancelled) |
| `status_text` | string | Status name (`completed`, `failed`, `cancelled`) |
| `output` | object | Task output (when completed; `null` when there is none) |
| `error` | string | Error message. Present only when the task has one |
| `session_id` | string | Session ID. Present only when the task ran in a session |
| `created_at` | string | ISO timestamp |
| `updated_at` | string | ISO timestamp |
| `run_at` | string | Scheduled start time (UTC). Present only for scheduled tasks |

The webhook is sent when the task reaches a terminal status. Delivery is not retried: a failed request or a 4xx/5xx response is logged and dropped. The request times out after 10 seconds, and only public addresses are called.
