# Engines

> Manage private inference engines that run workers on your hardware. Use these endpoints to list engines, inspect status, and perform safe restarts without dropping in-flight tasks.

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

---

Manage private inference engines that run workers on your hardware. Use these endpoints to list engines, inspect status, and perform safe restarts without dropping in-flight tasks.

Requires API key scopes `engines:read` (list/get) and `engines:write` (drain, update, stop, restart).

→ [Workers](/docs/concepts/workers) · [Installing the engine](/docs/private/installing) · [Configuration](/docs/private/config)

---

## Register engine

`POST /engines/register`

Called by the **`inferencesh-engine`** binary on startup (after registration). Creates a new engine record for your workspace or re-registers an existing one, allocates workers from detected hardware, and returns worker IDs the engine uses for task routing.

Requires **`engines:write`**. You normally do not call this manually — install the engine and run `engine serve` instead.

### Request

| Field | Type | Description |
|-------|------|-------------|
| `local_engine_state` | object | Engine ID, name, config, and public key from local state |
| `api_url` | string | API base URL the engine connects to |
| `system_info` | object | Host OS, architecture, and detected hardware |
| `worker_config` | object | Worker allocation preferences from `engine.yml` |

### Response

| Field | Type | Description |
|-------|------|-------------|
| `engine_id` | string | Assigned or confirmed engine ID |
| `workers` | array | Allocated workers with `id`, `gpus`, `cpus`, and `rams` |

On first run, the API creates a new engine with status `pending`. On subsequent starts, the engine sends its saved ID; the API updates hardware info and worker allocation, sets status to `pending`, and clears any prior stop reason (starting the process is an explicit start, including after you **Stop** an engine in the workspace).

Register does **not** set `running`. The engine opens its WebSocket after register; that connection moves status to `running`. Between register and connect you may briefly see `pending` in list or get responses — nothing in the API waits on `running` during that gap.

After connect, the engine maintains the WebSocket for task dispatch, drain/update signals, and heartbeats. The API treats that connection as the source of truth for whether the engine is online. See [Installing the engine](/docs/private/installing) for setup steps.

---

## List engines

`GET /engines` or `POST /engines/list`

Cursor-paginated list of engines for your workspace.

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

---

## Get engine

`GET /engines/{id}`

Returns an engine DTO. List responses (`POST /engines/list`) use the same shape per item.

| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Engine ID |
| `name` | string | Display name |
| `status` | string | Lifecycle state — see table below |
| `engine_version` | string | Installed `inferencesh-engine` binary version (e.g. `v1.10.15`). Set from engine telemetry heartbeats; empty until the first telemetry sync after connect. Also shown in the web app's [Engines](https://app.inference.sh/engines) detail view |
| `api_url` | string | API base URL the engine connects to |
| `workers` | array | Worker DTOs with status, GPU/CPU allocation, and current `task_id` when busy |
| `system_info` | object | Optional host telemetry (OS, GPUs, RAM). Also includes `engine_version`, mirrored from the top-level field |
| `instance` | object | Cloud instance metadata when provisioned through inference.sh (includes `rental_type`: `on_demand` or `spot`) |

| Status | Meaning |
|--------|---------|
| `running` | Connected over WebSocket and accepting tasks |
| `pending` | Register completed; WebSocket not connected yet |
| `draining` | Connected; finishing in-flight tasks and rejecting new work |
| `restarting` | Connected; draining before a process restart (for example after **Update**) |
| `disconnected` | WebSocket closed; reversible when the engine reconnects |
| `stopping` | Shutting down |
| `stopped` | Fully stopped |

While connected, heartbeats report the finer states above (`running`, `draining`, or `restarting`). When the WebSocket drops, status moves to `disconnected` even if the host process is still running. A new connection moves the engine back to `running` from `disconnected`, `pending`, or `restarting`, and can revive a `stopped` engine after a disconnect-only stop. A **Stop** you issued or a completed **drain** is final for reconnect alone — run the engine again (`engine serve`) so **register** clears the stop and sets `pending`, then connect sets `running`.

**Spot cloud engines:** when the cloud provider reclaims a spot instance (`instance.rental_type` is `spot`), the engine is finalized as `stopped` with `stop_reason: "preempted"` and does not come back on reconnect. Unused prepaid time is refunded as for a [stop](#stop-engine). Provision a new cloud engine (or switch to on-demand) to replace the capacity.

If an engine stays `disconnected` for about **five minutes**, the API finalizes it: workers are marked inactive, in-flight tasks fail, and cloud instances may be torn down. Brief outages (for example during an API deployment) normally recover when the engine reconnects before that window.

While the engine is still in that reconnect grace (`disconnected`, not yet finalized), worker liveness follows the **engine**, not a separate wall-clock on each worker heartbeat. A WebSocket drop does not, by itself, retire workers or fail their in-flight tasks — work can keep running on the host and resume dispatch when the connection returns. Workers are retired when the engine is finalized, when you stop or delete the engine, or when the engine stays connected but has not reported a given worker in its heartbeats for about **90 seconds** (for example, a crashed worker slot). API downtime does not count against workers either: engine heartbeats stop being recorded at the same time, so workers are not marked gone during a platform restart.

If the platform ends a task while your engine could not be reached (for example a **Dispatched** task that fails after about **30 seconds** without a connection), the task row is already **Failed** or **Cancelled** but a worker may still be running that work on the host. On the next **engine telemetry** report after reconnect (on connect and about every **30 seconds** while connected), the API compares what each worker reports against the task record and sends a **force cancel** to any worker still running a task the platform no longer owns on that engine. That stops stray side effects without re-queueing the same run on another engine. **Completed** tasks are never treated as stale — a worker may still be uploading results after the compute phase finishes.

JavaScript SDK: `EngineStatusDisconnected`, `EngineStatusRestarting`, and other status constants ship in **@inferencesh/sdk ≥ v0.6.12**. `EngineDTO` includes `engine_version`, matching the REST API response. Python SDK: `EngineDTO` in `inferencesh.types` includes the same field — use it when calling `GET /engines/{id}` directly.

```bash
curl "https://api.inference.sh/engines/eng_abc123" \
  -H "Authorization: Bearer inf_your_key"
```

---

## Match engines to resource requirements

`POST /engines/resources`

Check which workers can satisfy a given resource profile before you run an app with `infra: "private"` or choose between cloud and private capacity. The web app's run form uses this endpoint to show available private workers and cloud price ranges.

Authentication is optional (guest workspaces get public cloud availability only). With an API key, your workspace's private workers are included in the response.

### Request

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `resources` | object | Yes | Resource requirements to match |
| `resources.gpu.count` | integer | No | GPUs required (`0` for CPU-only) |
| `resources.gpu.vram` | integer | No | Minimum VRAM per GPU (bytes) |
| `resources.gpu.type` | string | No | `any`, `none`, `nvidia`, `amd`, `intel`, or `apple` |
| `resources.ram` | integer | No | Minimum system RAM (bytes) |

Pass the `resources` block from the app version you plan to run (see [App configuration](/docs/extend/configuration#resources)). Values are in bytes — the same internal representation stored on deployed app versions after CLI deploy.

```bash
curl -X POST https://api.inference.sh/engines/resources \
  -H "Authorization: Bearer inf_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "resources": {
      "gpu": { "count": 1, "vram": 24000000000, "type": "nvidia" },
      "ram": 16000000000
    }
  }'
```

### Response

| Field | Type | Description |
|-------|------|-------------|
| `private.workers` | array | Matching workers in your workspace |
| `cloud.can_run` | boolean | Whether shared cloud workers can satisfy the profile |
| `cloud.min_price` | integer | Lowest matching cloud worker price (cents per hour) |
| `cloud.max_price` | integer | Highest matching cloud worker price (cents per hour) |
| `error` | string | Present when matching failed |

Each worker summary includes `id`, `engine_id`, `engine_name`, `status`, GPU/CPU/RAM details, and the current `task_id` when busy.

Use this before `POST /run` when you need to confirm capacity or pick specific `workers` — see [Using private workers](/docs/private/using#specific-workers).

→ [Instance types](/docs/api/rest/instances) · [Workers concept](/docs/concepts/workers)

---

## List engine instance types

`GET /engines/types`

Authenticated catalog for provisioning **cloud** engines in the workspace. Requires `engines:read`.

Returns one row per GPU type, GPU count, and rental type (`on_demand` or `spot`): the cheapest available option for that tuple, or for on-demand the cheapest unavailable row when nothing is in stock. Spot offers are omitted when no region has spot stock.

Each row is an instance type object with `rental_type`, `region`, and `hourly_price` set for that offer (the raw public catalog leaves `rental_type` empty). Spot row IDs are suffixed with `.spot`.

Each offer also includes an `options` array: every **in-stock** cloud provider for that GPU type, GPU count, and rental type. Options are sorted by cheapest `hourly_price` first; the first entry always matches the top-level offer (`cloud`, `shade_instance_type`, `region`, and `hourly_price`). Providers with no in-stock regions for that rental type are omitted.

| Field (on each `options[]` item) | Description |
|-----------------------------------|-------------|
| `cloud` | Provider key |
| `cloud_logo_url` | Provider logo URL (same pattern as the public catalog) |
| `shade_instance_type` | Shadeform catalog name to pass when launching |
| `cloud_instance_type` | Provider-specific SKU |
| `hourly_price` | Cheapest in-stock region, in **cents** per hour |
| `configuration` | GPU and machine specs when present |
| `regions` | In-stock regions for this provider, each with `region` and `hourly_price` (cents). Sorted cheapest first. Spot prices can differ by region; on-demand regions share the on-demand price |

Use the parent row's `rental_type` when launching. Pick `cloud`, `shade_instance_type`, and `region` from whichever `options[]` entry the user selects (not only the top-level offer fields). In the web app, the cloud engine confirm step exposes the same choice as a provider list and region selector — see [Provision a cloud engine](/docs/private/managing-engines#provision-a-cloud-engine).

```bash
curl "https://api.inference.sh/engines/types" \
  -H "Authorization: Bearer inf_your_key"
```

Example fragment (one on-demand H100 offer with two providers):

```json
{
  "id": "runpod.NVIDIA H100 80GB HBM3",
  "cloud": "runpod",
  "region": "eu-north-1",
  "shade_instance_type": "NVIDIA H100 80GB HBM3",
  "rental_type": "on_demand",
  "hourly_price": 250,
  "options": [
    {
      "cloud": "runpod",
      "shade_instance_type": "NVIDIA H100 80GB HBM3",
      "hourly_price": 250,
      "regions": [{ "region": "eu-north-1", "hourly_price": 250 }]
    },
    {
      "cloud": "lambdalabs",
      "shade_instance_type": "gpu-h100-sxm-80gb",
      "hourly_price": 300,
      "regions": [
        { "region": "us-east-1", "hourly_price": 300 },
        { "region": "us-west-2", "hourly_price": 300 }
      ]
    }
  ]
}
```

→ [Instance types API](/docs/api/rest/instances) for the full public `GET /instances/types` catalog and filters.

---

## Create cloud engine

`POST /engines`

Provisions a new **cloud** engine on Shadeform. Requires `engines:write`. The rental **starts when the API receives the request** and runs until `end_time`, which is also the cloud instance **auto-delete** threshold. Your workspace balance is debited in **microcents** for that span (from now through `end_time`) using the **server-quoted** hourly rate — the `hourly_price` field on `instance_type` is ignored.

`end_time` must be at least **1 hour** and at most **30 days** ahead of the request. The API allows about one minute of slack on the minimum so a client-computed `end_time` is not rejected by network latency. Requests outside that window return **`400`** with code `invalid_request`.

There is no `start_time` field — billing always begins at request arrival. When you expose a duration picker in a UI, compute `end_time` at **submit** from the user's chosen span (not when they opened the form) so the prepaid window matches what they selected. The inference.sh workspace does this for [cloud engine launch](/docs/private/managing-engines#provision-a-cloud-engine).

### Request

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | Engine display name |
| `end_time` | string (RFC3339) | Yes | When the rental ends and the cloud instance auto-deletes |
| `instance_type` | object | Yes | Launch target from a row returned by `GET /engines/types` |
| `instance_type.cloud` | string | Yes | Provider key from the chosen `options[]` entry (or the top-level offer when you use the default) |
| `instance_type.region` | string | Yes | Region from that option's `regions` list |
| `instance_type.shade_instance_type` | string | Yes | `shade_instance_type` from the same `options[]` entry |
| `instance_type.rental_type` | string | No | `on_demand` (default) or `spot` |
| `instance_type.hourly_price` | integer | No | Ignored; pricing comes from the live Shadeform catalog |

```bash
curl -X POST https://api.inference.sh/engines \
  -H "Authorization: Bearer inf_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "h100-spot-1",
    "end_time": "2026-10-01T20:00:00Z",
    "instance_type": {
      "cloud": "runpod",
      "region": "us-east-1",
      "shade_instance_type": "NVIDIA H100 80GB HBM3",
      "rental_type": "spot"
    }
  }'
```

The response is the created engine record (including `instance` with `rental_type` and quoted `hourly_price`). The engine boots via a launch script and moves to `running` after register and WebSocket connect — same lifecycle as a self-hosted engine after that point.

Spot instances cost less but can be reclaimed by the provider at any time; see [Get engine](#get-engine) for behavior when that happens.

### Errors and billing

If Shadeform cannot quote the instance (for example the offer is unavailable), the request fails **before** your balance is debited. Failures other than an invalid `end_time` return **`500`** `internal_error` with a generic message.

After the reservation debit succeeds, the API provisions the cloud instance and creates the engine record. If any later launch step fails, the request returns an error and **no engine** is left in your workspace — the API deletes the engine row if one was created, reverses the reservation debit, removes any engine API key from the attempt, and tears down any cloud instance that was started. The unused-time credit from **Stop** only applies when a cloud engine finishes normally.

Check your workspace [billing history](https://app.inference.sh/settings/billing) if a launch failed but you expected a charge or refund line item.

**Balance charges:** Launch, [extend](#extend-cloud-engine-duration), and [stop](#stop-engine) refunds all move workspace balance in **microcents** (see [Billing API — Money units](/docs/api/rest/billing#money-units)). Catalog and instance `hourly_price` values are **cents per hour**; runtime cost prorates by elapsed seconds at that rate (for example, three hours at 138 cents/hour is 414 cents of usage, stored as microcents on the balance). Each **extend** call debits separately; when you **stop** a cloud engine early, the API credits unused prepaid time (launch plus extensions) minus elapsed usage since the engine was created. In billing history, look for references such as `cloud_engine:<name>` (launch debit), `cloud_engine_extend:<engine id>` (extension debits), and `engine:<engine id> stop refund` (early-stop credit).

---

## Drain engine

`POST /engines/{id}/drain`

Gracefully prepare an engine for shutdown:

1. Marks the engine as **draining** in the API.
2. Sends a drain signal over the engine WebSocket.
3. The engine **stops accepting new tasks** and waits for in-flight work to finish.
4. When all workers are idle, the engine shuts down.

The engine must be **connected** (online WebSocket). If it is offline, the API returns an error.

Use drain before maintenance or when you want a clean shutdown without cutting off running tasks.

```bash
curl -X POST "https://api.inference.sh/engines/eng_abc123/drain" \
  -H "Authorization: Bearer inf_your_key"
```

While draining, new tasks routed to that engine are rejected with reason `engine_draining`.

---

## Update engine binary

`POST /engines/{id}/update`

Triggers a **safe engine restart** on a connected private engine:

1. The API sends an update signal over the engine WebSocket.
2. The engine checks `https://dist.inference.sh/engine/manifest.json` for a newer binary (same OS/arch). If one exists, it downloads and swaps the binary before restarting.
3. The engine **drains** in-flight tasks and restarts the process (re-exec) after workers are idle — even when already on the latest version.

This is the API equivalent of the web app's **Update** action on [Engines](https://app.inference.sh/engines) or running `engine update` on the host. Use it for binary upgrades or a clean restart without killing running tasks.

```bash
curl -X POST "https://api.inference.sh/engines/eng_abc123/update" \
  -H "Authorization: Bearer inf_your_key"
```

The engine must be online. On hosts you manage directly, you can also update with the engine CLI — see [Installing the engine](/docs/private/installing).

---

## Extend cloud engine duration

`POST /engines/{id}/extend`

Adds runtime to a **cloud-provisioned** engine (Shadeform instance) before its auto-delete threshold. Requires `engines:write`. Your workspace balance is debited in **microcents** at `hourly_price × additional_hours` (with `hourly_price` in cents per hour).

### Request

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `additional_hours` | number | Yes | Hours to add (must be > 0) |

The engine must have an active instance with `auto_delete.date_threshold` and a positive `hourly_price` synced from the provider.

```bash
curl -X POST "https://api.inference.sh/engines/eng_abc123/extend" \
  -H "Authorization: Bearer inf_your_key" \
  -H "Content-Type: application/json" \
  -d '{"additional_hours": 3}'
```

Returns the updated engine DTO with a new instance `auto_delete` threshold. The new threshold cannot be more than **30 days** ahead of the request (same cap as [create](#create-cloud-engine)); otherwise the API returns **`400`** with code `extend_failed`.

Other errors use `400` with code `extend_failed` when the instance cannot be extended (for example, missing threshold or price).

Self-hosted engines on your own hardware do not use this endpoint — use [Update engine binary](#update-engine-binary) or restart the service on the host.

---

## Stop engine

`POST /engines/{id}/stop`

Stops a managed engine instance. If the engine is connected, it receives a graceful stop signal, finishes in-flight tasks, then terminates (including cloud instance teardown when applicable). If the engine is offline, the API finalizes the stop immediately.

For **cloud-provisioned** engines with a prepaid reservation, finalize also credits unused prepaid time minus elapsed usage (see [Create cloud engine — Balance charges](#errors-and-billing)). Self-hosted engines on your own hardware are not billed through this path.

---

## Restart engine

`POST /engines/{id}/restart`

Restarts the underlying **cloud instance** (Shadeform) for engines provisioned through inference.sh. For self-hosted engines on your own hardware, use **update** or restart the `inferencesh-engine` process on the host.

---

## JavaScript SDK

```typescript
import { inference } from '@inferencesh/sdk';

const client = inference({ apiKey: 'inf_your_key' });

await client.engines.drain('eng_abc123');
await client.engines.updateBinary('eng_abc123'); // POST /engines/{id}/update
await client.engines.stop('eng_abc123');
await client.engines.restart('eng_abc123');
// POST /engines/{id}/extend — pass { additional_hours } in the request body (REST or HttpClient)
```

`client.engines` also exposes `list`, `get`, `create`, `update`, `delete`, and `stream`.

---

## Related

| Guide | Topic |
|-------|--------|
| [Workers](/docs/concepts/workers) | Cloud vs private workers |
| [Installing the engine](/docs/private/installing) | Host setup and manual upgrades |
| [Using private workers](/docs/private/using) | Routing tasks with `infra: "private"` |
| [Instance types](/docs/api/rest/instances) | GPU catalog for cloud-provisioned engines |
