# REST API

> Direct HTTP access to inference shell.

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

---

Direct HTTP access to inference shell.

---

## Base URL

```
https://api.inference.sh
```

---

## Path prefix

There is none. Routes live at the root of the API host:

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

---

## Authentication

All requests require an API key in the `Authorization` header:

```
Authorization: Bearer inf_your_api_key
```

---

## Content Type

```
Content-Type: application/json
```

---

## Response format

Every JSON response is an envelope: the resource under `data`, and `messages` for any notices the
server attached (`level`, `code`, `message`, optional `meta`). Errors are
[RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) `application/problem+json`. Streams (SSE, NDJSON)
carry bare DTOs per event. The official SDKs unwrap `data` and surface `messages` through a callback.

**Success example** (task from `POST /run`):

```json
{
  "data": {
    "id": "task_abc123",
    "status": 10,
    "output": { "image": { "uri": "https://..." } }
  },
  "messages": []
}
```

**Error example** (401):

```json
{
  "type": "https://api.inference.sh/errors/unauthorized",
  "title": "Unauthorized",
  "status": 401,
  "detail": "Invalid or missing API key"
}
```

Older examples on other pages show the resource without the `data` wrapper; read it from `data`.

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

---

## Error Responses

Errors use `Content-Type: application/problem+json` with the `type`, `title`, `status`, and `detail` fields shown above. The `type` URI ends with the error code (for example `.../errors/not_found`).

Some errors include an optional `meta` extension member with structured context — for example entitlement errors (`limit_exceeded`, `feature_not_available`) attach `resource`, `limit`, `current`, `upgrade_available`, and optional addon plan fields. See [Entitlements API — Error metadata](/docs/api/rest/entitlements#error-metadata).

### Error Codes

| Code | HTTP | Description |
|------|------|-------------|
| `unauthorized` | 401 | Invalid or missing API key |
| `forbidden` | 403 | Insufficient permissions |
| `blocked_by_usage_policy` | 403 | Resource is outside the workspace [usage policy](/docs/org/usage-policy) reach (apps, agents, MCPs, knowledge, flows). Checked before prepaid balance on paid runs — not a credits problem. |
| `not_found` | 404 | Resource not found |
| `invalid_request` | 400 | Malformed request |
| `rate_limited` | 429 | Too many requests |
| `entitlement_unavailable` | 500 | The entitlement lookup failed, so plan limits could not be verified. Retriable. |
| `internal_error` | 500 | Server error |

### Pre-authentication access controls

Some requests are rejected before authentication. These responses do not use the problem+json format above:

| Condition | HTTP | Response |
|-----------|------|----------|
| IP blocked for abuse | **403** | `{ "message": "Access denied. This attempt has been logged. If you believe this is a mistake, please disconnect any VPN or proxy and try again, or contact security@inference.sh." }` |
| Region blocked (sanctions) | **451** | `service not available in your region` (plain text) |

Both checks apply to almost every route. Some health and status endpoints stay reachable. They are separate from the permission **403** errors (`forbidden`, `feature_not_available`) returned after authentication.

Regional blocking uses the request's country and, for some countries, its subdivision. Blocked countries are Cuba, Iran, North Korea, Russia, and Syria. Within Ukraine, Crimea, Sevastopol, Donetsk, and Luhansk are blocked. VPN or proxy exit nodes in a blocked region get the same response.

### CORS (browser clients)

Browser `fetch` from JavaScript needs [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) headers on the response. The API answers in two modes, chosen from the request's `Origin` header:

| Origin | `Access-Control-Allow-Origin` | Credentials |
|--------|-------------------------------|-------------|
| The deployment's own app (for example `https://app.inference.sh`) and any extra hosts configured for that deployment | The matching origin | Allowed (`Access-Control-Allow-Credentials: true`) |
| Any other origin, whatever the request's auth | Reflects the request origin | Not allowed: cookies are never honoured cross-site, so send an API key in `Authorization` |

Both modes allow methods `GET`, `POST`, `PUT`, `DELETE`, `OPTIONS`, and `HEAD`, request headers `Accept`, `Authorization`, `Content-Type`, `X-CSRF-Token`, `X-Team-ID`, `X-Client-Source`, and `X-API-Version`, and expose the response headers `Link` and `Retry-After` (see [Rate limits](#rate-limits) below).

**Refusals stay readable.** CORS runs before middleware that can end the request early: invalid or revoked bearer tokens, suspended accounts, denied impersonation, regional blocks, and IP redlist hits. Those answers carry CORS headers for every origin, so browser code sees the real HTTP status and body instead of an opaque network error.

### Signup email restrictions

New accounts cannot use temporary or disposable email domains. The check runs when a new user record would be created, so every sign-up path enforces it:

| Auth path | When the check runs | Error for new signups |
|-----------|---------------------|-----------------------|
| Magic link (`POST /auth/magic-link/send`) | Before the email is sent | `disposable_email` (400) |
| OAuth sign-in | After the provider redirects back | `disposable_email` (400) |
| SAML SSO | On first login for a new email | `user_creation_failed` (500). Use a permanent address and retry. |

Existing accounts are not affected. An account registered on such a domain before the restriction can still sign in.

**Stored form:** Email addresses on user accounts and pending team invites are stored trimmed and lowercased. Sign-in and account lookup match case-insensitively, so `You@Example.com` and `you@example.com` resolve to the same account when only one exists. API responses return the stored address.

For **Gmail** and **Googlemail**, team invites and membership checks also treat addresses that deliver to the same mailbox as one address (dots in the local part and `+` tags after the local part are ignored for that comparison). Magic link sign-in uses the same mailbox rules when plus-alias blocking is enabled.

**Plus addressing (`+` aliases) on magic link:** New sign-ups can currently use `+` in the local part (for example `you+tag@example.com`). The API may enable stricter checks later to block aliases that would open a separate account (trial-credit abuse). When that enforcement is on, `POST /auth/magic-link/send` returns `invalid_email` (400) for a new address whose provider treats `+` as a distinct mailbox. Gmail and Googlemail are always allowed: those providers fold `+` tags into one mailbox, and sign-in lands on the canonical account. An address that already has an account is never blocked by the alias rule. OAuth and SAML do not apply the magic-link alias rule.

---

## Billing and plan limits

These billing and plan errors can block API calls:

| Kind | Typical codes | What to check |
|------|---------------|---------------|
| **Plan limits** | `limit_exceeded` (402), `feature_not_available` (403) | Subscription tier caps — API keys, storage, concurrent tasks, feature flags |
| **Entitlement lookup failure** | `entitlement_unavailable` (500) | Transient server error while reading plan limits. Retry. Not a tier cap. |
| **Usage policy** | `blocked_by_usage_policy` (403) | Outside-resource reach and allow/block lists — see [Usage policy](/docs/org/usage-policy). On paid runs the API judges policy before balance, so a blocked app never returns `payment_required`. |
| **Prepaid balance** | `payment_required` (402) | Credits for task and agent runs the usage policy already allows |
| **Payment method** | `payment_method_required` (402) | A run certain to cost $0 needs a saved payment method when the balance would refuse it; `meta.billing_page` names the page that adds one. See [Billing: free runs](/docs/api/rest/billing#free-runs). |

Inspect current usage programmatically with `GET /entitlements/usage`. List entitlement rows (limits and sources) with `GET /entitlements`. See [Entitlements API](/docs/api/rest/entitlements) for response formats and error metadata.

### Web app modals

In the inference shell app, entitlement errors with `upgrade_available: true` open an **upgrade modal** instead of only surfacing a toast:

| API error | Modal |
|-----------|-------|
| `limit_exceeded` or `feature_not_available` (tier cap) | Upgrade modal — lists self-serve plans above your current tier |
| `feature_not_available` with `addon_plan_id` | Add-on purchase modal — shows addon name, price, and **subscribe - $X/mo** CTA that routes to the add-ons page |
| `payment_required` (402 without `upgrade_available`) | Top-up modal — add prepaid credits |

For `payment_required`, credits always go to whoever **pays for the selected workspace** (the team itself, or the organization workspace for an org member team). If you manage organization billing while switched into a member team, checkout runs against the organization balance and returns to that workspace's billing settings. If the organization pays and you cannot manage its billing, the modal shows the org notice instead of checkout — the same rule as inline **add credits** controls elsewhere in the app.

The upgrade modal shows the API `detail` message (or which resource requires a higher plan). For `limit_exceeded`, it also shows **current usage / plan cap** (for example `3 / 5`) when the cap is greater than zero. A `0` cap means the feature is disabled on your tier — the modal omits a usage line instead of showing `0 / 0`.

Each listed plan shows monthly price, included credits, and key limits. The **recommended** badge marks the cheapest plan that resolves the blocked resource (unlimited, a higher numeric cap, or an enabled boolean feature). Select a plan or **View all plans** to open [settings → workspace → subscription](https://app.inference.sh/settings/team/billing/subscription).

When the error includes `addon_plan_id`, the modal shows a single add-on card with a **subscribe - $X/mo** button that routes to [settings → workspace → add-ons](https://app.inference.sh/settings/team/billing/addons), the same purchase flow as the add-ons page (Stripe Checkout via `POST /subscription/addon`). Most add-ons do not require a base subscription; some set `required_plan_ids` on the plan.

Direct links: [Pricing](https://app.inference.sh/pricing) (browse plans) · [settings → workspace → billing](https://app.inference.sh/settings/team/billing) (credits) · [settings → workspace → subscription](https://app.inference.sh/settings/team/billing/subscription) (manage plan) · [settings → workspace → add-ons](https://app.inference.sh/settings/team/billing/addons) (purchase add-ons)

---

## Description length limits

Create and update endpoints enforce a maximum length on resource `description` fields. Limits count **Unicode characters** (runes), not bytes.

| Constant | Max length | Applies to |
|----------|------------|------------|
| `DescriptionLimitListing` | **200** | Apps, agent template versions, flows, workspace MCP servers, and knowledge entries whose `type` is not `skill` |
| `DescriptionLimitSkill` | **1024** | Skills (`type: skill` on knowledge, and skill create/update APIs) — matches the [Agent Skills](https://agentskills.io/specification) frontmatter limit |

When a new description exceeds the limit, the API returns **`validation_error`** (400) with `detail` such as `description must be at most N characters, got M`. If an update leaves the description **unchanged**, the limit is not re-checked — existing text longer than the cap can stay until you edit the field.

**Agent-facing reads:** Discovery and catalog surfaces clip descriptions to **200** characters for the model: whitespace collapses to a single space and overflow ends with `...`. This includes `/suggest` result lines and catalog tools such as `search_tools` / `list_tools`. Put longer guidance in the resource body (app README, skill instructions, knowledge content, system prompt).

Client-side validation: JavaScript (`@inferencesh/sdk` ≥ v0.20.0) exports `DescriptionLimitListing` and `DescriptionLimitSkill`; Python (`inferencesh` ≥ v0.19.0) exports `DescriptionLimit.LISTING` and `DescriptionLimit.SKILL` on the `DescriptionLimit` enum in `inferencesh.types`.

**Web app:** On [app.inference.sh](https://app.inference.sh), description fields for apps, agents, flows, skills, workspace MCP servers, and knowledge entries show a counter (`n/limit`) under the input and stop accepting characters at the same limits as the API. Knowledge uses the skill limit when `type` is `skill` or unset, and the listing limit for other types (same rule as [knowledge create/update](/docs/api/rest/knowledge#create-knowledge)). Text saved before limits existed may still be longer than the cap; the counter turns red until you shorten it, and you can save other fields without editing the description.

---

## Rate Limits

| Endpoint | Limit |
|----------|-------|
| Run task | 100/minute |
| Get task | 1000/minute |
| Upload file | 50/minute |

Response headers:
```
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1640000000
Retry-After: 60
```

Some endpoints (for example magic link send when throttled by IP) return HTTP **429** with a `Retry-After` header (whole seconds). Browser clients can read it — the API exposes `Retry-After` in CORS `Access-Control-Expose-Headers`.

---

## OpenAPI

Download the generated route catalog:

```
GET https://api.inference.sh/openapi.json
```

OpenAPI **3.1** document listing public paths, methods, and schemas. Useful for client generation and discovering routes not yet covered in these guides. No authentication required.

---

## Cursor pagination

Most list endpoints (`GET /tasks`, `GET /skills`, `POST /apps/list`, `GET /chats/:id/messages`, and others) use the same cursor-list shape. Send parameters as query strings on `GET` or in a JSON body on `POST …/list`.

| Field | Type | Description |
|-------|------|-------------|
| `limit` | integer | Page size (default varies by endpoint — often 10 or 50). Pass **`-1`** to return all matching items in one response without pagination. |
| `cursor` | string | Opaque cursor from `next_cursor` or `prev_cursor` (ignored when `limit=-1`) |
| `direction` | string | `forward` (default) or `backward` (ignored when `limit=-1`) |
| `sort` | array | Sort orders, for example `[{ "field": "created_at", "dir": "desc" }]` |
| `filters` | array | Filter expressions (`field`, `operator`, `value`) |
| `search` | object | Full-text search (`term`, `fields`, …) where supported |
| `include_others` | boolean | Include other users' items in the response, where the endpoint supports it |
| `include_private` | boolean | Workspace owners and admins only. Include every row the selected workspace owns, including members' `private` resources. Ignored for other roles. Each use is audited. See [Workspace admin visibility](#workspace-admin-visibility). |

### Workspace admin visibility

By default, list endpoints hide a member's `private` resources from everyone except the creator. Workspace owners and admins can widen a listing by passing `include_private: true` (`include_private=true` in the query string on `GET`, a JSON body field on `POST …/list`).

When set, the response includes every row owned by the workspace in the request context: your own `private` items, other members' `private` items, and `team`-visible items. The flag has no effect for the `member` role, which always gets the standard listing.

Use this when cleaning up a workspace after someone leaves, for example to transfer or delete their private apps, flows, or knowledge. Each `include_private` list request is audited as `team_admin.list_private`.

Workspace owners and admins can also read or write an individual member's `private` resource when the normal permission check would deny access, for example `GET /apps/{id}` on a departed member's private app. The bypass does not cover running the resource. Each access is audited as `team_admin.permission_bypass`. Org admins get the same bypass for resources in workspaces attached to their org. Person-owned identity data (exports, sessions, and similar) is never reachable this way.

### Response shape

| Field | Type | Description |
|-------|------|-------------|
| `items` | array | Resources for this page (or all matches when `limit=-1`) |
| `next_cursor` | string | Cursor for the next page |
| `prev_cursor` | string | Cursor for the previous page |
| `has_next` | boolean | More results after this page |
| `has_previous` | boolean | Results before this page |
| `items_per_page` | number | Items in this response |
| `total_items` | number | Total matching items |

Responses may include RFC 8288 `Link` headers (`rel="next"`, `rel="prev"`) with the same cursors.

### Full dumps (`limit=-1`)

Pass `limit=-1` on any cursor-paginated list endpoint to skip pagination and return matching rows in one response (capped at **10,000** rows). Sort and filters still apply; `cursor` and `direction` are ignored. `has_next` is `false` — treat `items` as the complete result set when the match count is under the cap.

Use this for exports, migrations, or debugging on smaller datasets. Prefer normal cursor pagination for UI scroll-back and tables that may exceed 10,000 rows.

```bash
curl "https://api.inference.sh/tasks?limit=-1&filters=%5B%7B%22field%22%3A%22status%22%2C%22operator%22%3A%22eq%22%2C%22value%22%3A10%7D%5D" \
  -H "Authorization: Bearer inf_your_key"
```

---

## Endpoints

- [Tasks](/docs/api/rest/tasks) — Run apps, status, logs, timings, telemetry, cancellation, webhooks, cost
- [Remotes](/docs/api/rest/remotes) — Register machines that connect as outbound daemons
- [Exec runs](/docs/api/rest/exec-runs) — Dispatch commands to remotes and stream output
- [Engines](/docs/api/rest/engines) — Private engine list, drain, update, stop
- [Files](/docs/api/rest/files) — Upload files
- [Agents](/docs/api/rest/agents) — Agent chat API
- [Publications](/docs/api/rest/publications) — Publish agents for embedding (`POST /publications`)
- [Embed](/docs/api/rest/embed) — Anonymous chat API for published agents
- [Triggers](/docs/api/rest/triggers) — Cron and webhook automation (run agents, apps, flows)
- [Skills](/docs/api/rest/skills) — Manage and access skills
- [Knowledge](/docs/api/rest/knowledge) — Manage knowledge entries
- [Artifacts](/docs/api/rest/artifacts) — Publish versioned HTML and Markdown pages to a shareable URL
- [Pages](/docs/api/rest/pages) — Read CMS pages (docs, blog, landing pages)
- [Search](/docs/api/rest/search) — Search apps, skills, knowledge, and pages
- [Entitlements](/docs/api/rest/entitlements) — Plan limits, usage, and feature flags
- [Streaming](/docs/api/rest/streaming) — SSE endpoints
- [Billing](/docs/api/rest/billing) · [Subscription](/docs/api/rest/subscription) — Credits and plan management
- [Magic link sign-in](/docs/api/rest/magic-link) · [Device authorization](/docs/api/rest/device-auth): web app and CLI login
