# API Keys

> Create, list, and revoke API keys programmatically. Use the scope catalog to build least-privilege keys for CI, engines, and automation.

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

---

Create, list, and revoke API keys programmatically. Use the scope catalog to build least-privilege keys for CI, engines, and automation.

→ [API keys](/docs/org/api-keys) — personal and workspace keys, who manages them, offboarding  
→ [Authentication](/docs/api/authentication) — how keys are sent on requests  
→ [Device authorization](/docs/api/rest/device-auth) — CLI sign-in sessions (alternative to long-lived keys)  
→ [Entitlements](/docs/api/rest/entitlements) — plan limits on API key count

---

## Personal and workspace keys

A key's `scope` field says who it acts as. It is set at creation and never changes.

| `scope` | Acts as | `user_id` | Created, listed and revoked by |
|---------|---------|-----------|--------------------------------|
| `user` (default) | The person who created it | That person | Its creator, and workspace owners and admins |
| `workspace` | The workspace's service account | The service account | Workspace owners and admins only |

`created_by` is always the person who created the key. A personal key stops working, and is revoked, when its creator leaves the workspace; a workspace key keeps working. Workspace keys are not available on personal workspaces.

A workspace key never holds the `user` or `apikeys` scopes, whatever it was created with, and it cannot sign in, approve devices or OAuth apps, manage the workspace's members, vault or billing, or create keys. See [API keys](/docs/org/api-keys#what-a-workspace-key-can-do).

All four `/apikeys` routes also require the **`create_keys`** workspace capability, which every member who is a person holds and service accounts do not. A credential without it gets **403** `team_role_required` even when it carries `apikeys:read` or `apikeys:write`. **`manage_keys`** (workspace owners and admins) adds workspace keys and every member's keys.

---

## Permission scopes

API keys carry a list of **scopes**. Every operation the API performs declares the scope it takes (for example reading an agent takes `agents:read`, storing a secret `secrets:write`), and the scope is checked on whatever reaches the operation: a REST route, an MCP tool, an agent's tool call, a flow step or a trigger. Each REST page documents the scopes its operations take.

What a credential without the scope gets:

- **A read of a resource that can be public** (agents, apps, conversations, files, flows, engines, credentials, knowledge, artifacts): the public rows, as a signed-out caller would. A private row answers `404`, a list shows only public rows.
- **A sign-in-only scope** (below): `403 requires_sign_in`.
- **Any other scope**: `403 insufficient_scope`. The detail names the scope.

### Runs act with the scopes that started them

An agent run, a flow run, a task or a trigger records the scopes of the credential that started or set it up, and everything it does later acts with those scopes: its tool calls, a step that runs after an approval, a cron or webhook fire. A run started with a key that holds only `apps:execute` cannot read your knowledge or use a secret's value, whoever approves its tool calls. Using a secret's value in a tool takes `secrets:reveal`. A trigger edited by a narrower key takes that key's scopes.

### Hierarchy

- **Resource-level** scopes (for example `agents`, `apps`) grant every action on that resource (`agents:read`, `agents:write`, `agents:execute`).
- **Action-level** scopes grant a single capability (for example `flows:execute`).
- **`*`** grants full access.
- **Empty `scopes`** on a key means every scope a key can hold (backwards compatibility for keys created before scoped permissions). It never includes a sign-in-only scope.
- A `belt login` or web sign-in narrowed when it was approved holds exactly the scopes it was approved with, sign-in-only ones included only when listed.

Keys cannot grant scopes the creator does not hold. If you authenticate with a scoped credential (a limited API key, or a `belt login` session), `POST /apikeys` rejects any scope outside your own grant set, and a request with no `scopes` creates a key with **your** scopes, not full access. Only an unrestricted creator (a dashboard session, or a key with empty scopes) can create a full-access key.

### Common scope strings

| Group | Scopes |
|-------|--------|
| Agents | `agents:read`, `agents:write`, `agents:execute` |
| Apps / tasks | `apps:read`, `apps:write`, `apps:execute` |
| Knowledge / skills | `knowledge:read`, `knowledge:write` |
| Flows | `flows:read`, `flows:write`, `flows:execute` |
| Secrets | `secrets:read`, `secrets:write`, `secrets:reveal` (a key with `secrets:read` holds it; an app's OAuth token never does) |
| Artifacts | `artifacts:read`, `artifacts:write` |
| Credentials | `credentials:read`, `credentials:write` |
| Workspaces | `teams:read`, `teams:write` |
| API keys | `apikeys:read`, `apikeys:write` (sign-in only) |
| Engines | `engines:read`, `engines:write` (admin catalog only) |
| Remotes | `remotes:read`, `remotes:write` |
| Billing | `billing:read`, `billing:write` (sign-in only) |
| Settings | `settings:read`, `settings:write` |
| User | `user:read`, `user:write` (sign-in only) |
| Approvals | `approvals:write` (sign-in only) |
| Admin | `admin:read`, `admin:write` (sign-in only) |
| Store | `store:pricing`, `store:submit` (the platform's workspace only) |

### Sign-in-only scopes

Some scopes are held only by your own sign-in: the web app, or the CLI after `belt login`. No API key (personal or workspace), OAuth app token or workspace frame session can be granted them, and a key that lists one, or a full-access key, is still refused with `403 requires_sign_in`:

- `approvals:write`: answering a tool approval, gate, widget or MCP input request, "always allow", and widening what runs without asking (turning on "allow every tool" or skipping hooks on a chat, adding an allow rule or lifting an ask/deny rule on a chat, agent or remote, turning on an agent's `permissions.allow_all_tools`)
- `user:read`, `user:write`: your profile, sessions, OAuth consents and connected apps, surveys, bounties, feedback
- `apikeys:read`, `apikeys:write`: listing, creating and revoking keys
- `billing:write`: subscriptions, checkout, payment methods, vouchers, invoices sent
- `admin:read`, `admin:write`: platform administration. Held only while the sign-in carries platform power: a staff member's own browser session proved by their authenticator app, or a CLI login they elevated. Without it the refusal is `admin_session_required`, `admin_authenticator_required` or `otp_required`, and a caller who is not staff gets `403 forbidden`. A sign-in approved with `admin:write` also holds the store scopes. A staff member who has platform power but whose login was approved without the scope an operation needs gets `403 insufficient_scope` naming that scope: run `belt login` again and approve it, no new elevation needed.

Staff viewing a workspace through an impersonation grant hold none of its sign-in-only scopes except `admin`.

`POST /apikeys` refuses a sign-in-only scope with `400`. `GET /scopes` leaves them out of `scopes` and lists them under `account_scopes` (with their groups in `account_groups`), for the [device approval page](/docs/api/rest/device-auth#web-app-approval-ui). The admin scopes appear there only for staff.

### Store scopes

The store's pricing and submissions are run from the platform's own workspace, not by platform administration:

- `store:pricing`: read and change a store version's pricing (draft, evaluate, publish, apply to the listing) and read the sample tasks pricing is checked against.
- `store:submit`: submit a resource version to the store on its publisher's behalf.

They are held, when the credential's scopes name them, only while it acts in the platform's workspace as a member: a member's sign-in (no elevation), their API keys, the workspace's own keys, and the agent runs, flows and triggers started there. A staff member with platform power holds them from any workspace. They end when the membership ends, for keys and runs already made too. No other workspace, and no app's OAuth token, ever holds them: naming one on a key elsewhere is refused with `403`, and a full-access key carries none. `GET /scopes` lists them only to callers who hold them.

`store:pricing` replaces `admin:pricing`. A key that named `admin:pricing` no longer holds pricing: create a new one with `store:pricing`.


Use `GET /scopes` for the full catalog with labels, descriptions, and preset bundles.

---

## Scope catalog

`GET /scopes`

Returns every scope a key can hold, UI group, and preset bundle, plus the sign-in-only scopes a login can be approved for (`account_scopes`, `account_groups`) and the preset the `belt login` approval page starts at (`login_preset`, with the scopes it grants in `grants`). Public: no credential needed. `public_rows` on a group says a credential without its read scope still reads the group's public rows. Non-admin callers do not see engine scopes or the `engine` preset, and only the platform's workspace sees the store scopes.

**Example:**

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

**Response:**

```json
{
  "scopes": [
    {
      "value": "agents:execute",
      "label": "execute",
      "description": "run agents and send messages",
      "group": "agents"
    }
  ],
  "groups": [
    {
      "id": "agents",
      "label": "agents",
      "description": "manage and run AI agents"
    }
  ],
  "presets": [
    {
      "id": "read",
      "label": "read-only",
      "description": "view resources without making changes",
      "summary": [
        "view your apps, agents, and flows",
        "view files, datastores, and projects",
        "view team information"
      ],
      "scopes": [
        "agents:read", "apps:read", "conversations:read", "files:read", "flows:read",
        "datastores:read", "projects:read", "teams:read", "credentials:read",
        "knowledge:read", "artifacts:read", "settings:read"
      ]
    }
  ]
}
```

### Preset bundles

| Preset ID | Label | Typical use |
|-----------|-------|-------------|
| `standard` | standard | Full developer access. The `belt login` approval page starts at `login_preset`: `standard` plus `approvals:write`. Every read, execute, and write scope a key can hold, except the engine scopes. Includes `billing:read`. |
| `run-only` | run only | A server that runs apps and agent chats for its users, such as the [server proxy](/docs/api/sdk/server-proxy): `apps:read`, `apps:execute`, `agents:read`, `agents:execute`, `conversations:read`, `conversations:write`, `files:write`. It cannot change apps, agents, secrets, keys or billing, or answer approvals. |
| `read` | read-only | Dashboards and monitoring. Read scopes only, without secrets, API keys, or billing. |
| `engine` | engine | Connect private engines: `engines:read`, `engines:write`, `apps:execute`. Returned to admins only. |

Presets are convenience lists — pass individual scope strings when creating a key.

A session or API key stores its scope list when it is created. Scopes added to a preset later are not added to existing credentials. Run `belt login` again to get a session with the current `standard` scopes.

---

## List API keys

`GET /apikeys` or `POST /apikeys/list`

Requires **`apikeys:read`** (sign-in only: an API key cannot list keys) and **`create_keys`**. Workspace owners and admins get every key of the workspace; everyone else gets their own personal keys.

Uses [cursor pagination](/docs/api/rest/skills#list-skills) (`cursor`, `limit`, filters). `POST /apikeys/list` accepts the same fields in the JSON body.

Listed keys are **masked** — only the last four characters of the key string are shown (`1nfsh-****wxyz`).

**Example:**

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

**Response item fields:**

| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Key ID |
| `name` | string | Display name |
| `key` | string | Masked key (`1nfsh-****wxyz`) |
| `scope` | string | `user` (personal key) or `workspace` |
| `user_id` | string | Who the key acts as: its creator, or the workspace's service account |
| `created_by` | string | The person who created the key |
| `creator` | object | That person's `id`, `name`, `full_name`, `email` and `avatar_url` |
| `scopes` | string[] | Granted permissions (empty = full access) |
| `last_used_at` | string | ISO timestamp of the last authenticated request. Omitted when the key has never been used. Updates are debounced (about five minutes), so the value can lag behind actual use. |
| `expires_at` | string | Expiration (optional) |
| `source` | string | Origin — empty for manual keys; `token` for [temporary keys](#temporary-keys); `device-auth` for legacy CLI keys; `engine:<id>` for cloud-engine credentials; `oauth:<client_id>` for OAuth-issued keys |

---

## Create API key

`POST /apikeys`

Requires **`apikeys:write`** (sign-in only) and **`create_keys`**; a `workspace` key also needs **`manage_keys`**. Subject to [API key entitlement limits](/docs/api/rest/entitlements) for the workspace.

### Request

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | Display name |
| `scope` | string | No | `user` (default) or `workspace`. A workspace key needs a workspace owner or admin, and fails with `403` otherwise |
| `scopes` | string[] | No | Permission list. Omit or `[]` to get the creator's own scopes (full access only if the creator is unrestricted) |
| `expires_at` | string | No | ISO timestamp; must be in the future. 24 hours or less makes it a [temporary key](#temporary-keys) |

`source`, `user_id` and `created_by` are set by the server; values in the request body are ignored. A workspace key cannot create keys.

A key created by another API key cannot outlive it: if the creating key expires, the new key's `expires_at` is capped at the creator's, and a missing `expires_at` is set to it.

```bash
curl -X POST https://api.inference.sh/apikeys \
  -H "Authorization: Bearer inf_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "ci-deploy",
    "scopes": ["apps:read", "apps:execute", "files:write"]
  }'
```

A workspace key for CI:

```bash
curl -X POST https://api.inference.sh/apikeys \
  -H "Authorization: Bearer inf_your_key" \
  -H "Content-Type: application/json" \
  -d '{"name": "ci", "scope": "workspace", "scopes": ["apps:execute"]}'
```

The **full key string** is returned only in the create response. Store it immediately — list responses never include the plaintext value.

**JavaScript SDK:**

```typescript
const { data: key } = await client.apiKeys.create({
  name: 'ci-deploy',
  scopes: ['apps:read', 'apps:execute'],
});
console.log(key.key); // only time the full value is available
```

---

## Temporary keys

A personal key created with `expires_at` 24 hours or less from now is a **temporary key**. The server marks it `source: "token"`. A short-lived workspace key is not a temporary key. It works like any other key (same scopes, same [workspace pinning](#workspace-pinning)) with these differences:

- It cannot create API keys (no key holds `apikeys:write`, which is sign-in only), so a leaked one cannot renew itself. `POST /apikeys` returns `403`.
- It sends no expiry reminder emails.
- It is listed while live, where it can be revoked, and deleted once expired.

```bash
curl -X POST https://api.inference.sh/apikeys \
  -H "Authorization: Bearer inf_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "nightly-report",
    "scopes": ["apps:read", "apps:execute"],
    "expires_at": "'"$(date -u -d '+15 minutes' +%Y-%m-%dT%H:%M:%SZ)"'"
  }'
```

This is what [`belt auth token`](/docs/extend/cli-setup#temporary-token-for-scripts) does: it mints a 15-minute key for the active workspace so scripts never see the stored login.

---

## Revoke API key

`DELETE /apikeys/{id}`

Requires **`apikeys:write`** (sign-in only) and **`create_keys`**. Members revoke their own personal keys; workspace owners and admins revoke any key of the workspace. Anyone else gets `403`. Returns the deleted key DTO.

```bash
curl -X DELETE https://api.inference.sh/apikeys/key_abc123 \
  -H "Authorization: Bearer inf_your_key"
```

Revocation is immediate — in-flight requests may still succeed briefly while caches expire.

---

## Workspace pinning

Keys created in [settings → workspace → api keys](https://app.inference.sh/settings/team/keys) or via `POST /apikeys` are **pinned to the workspace active when they were created**. They ignore `X-Team-ID`, so a leaked automation key cannot be retargeted at another workspace via a header.

This includes [temporary keys](#temporary-keys) from `belt auth token`: minted from a session that can switch workspaces, the key itself stays on the workspace that was active at mint time.

Device-auth and magic-link **session tokens** support switching workspaces. See [Authentication: workspace context](/docs/api/authentication#workspace-context).

---

## Next

- [Authentication](/docs/api/authentication): header format and workspace context
- [Device authorization](/docs/api/rest/device-auth) — CLI sessions and `auth/sessions` management
- [Secrets API](/docs/api/rest/secrets) — runtime credentials (distinct from API keys)
