# Secrets API

> Manage workspace-scoped API keys and credentials used when apps run.

**URL:** https://inference.sh/docs/api/rest/secrets
**Last updated:** 2026-09-25

---

Manage workspace-scoped API keys and credentials used when apps run.

Secret **values are never included** in list or create/update responses — only `masked_value`. Use `GET /secrets/reveal/{key}` when you need the plaintext value (audit-logged).

→ [Secrets overview](/docs/secrets/overview) — security model and how apps declare requirements  
→ [Environment secrets](/docs/secrets/environment) — user-facing setup  
→ [Extend: declaring secrets](/docs/extend/secrets) — `inf.yml` schema for app authors  
→ [Vaults API](/docs/api/rest/vaults) — group secrets and credentials into named vaults

---

## Scopes

| Scope | Endpoints |
|-------|-----------|
| `secrets:read` | List, get |
| `secrets:read` + workspace admin | Reveal (`GET /secrets/reveal/{key}`) |
| `secrets:write` + workspace admin | Create, update, delete |

Create, update, delete, and reveal require the **workspace admin** role (or owner) in addition to the API key scope. Platform admins bypass the role check. List and get are open to any workspace member with `secrets:read`. A caller without the role gets `403` with code `team_role_required`.

Create API keys with these scopes at [settings → workspace → api keys](https://app.inference.sh/settings/team/keys). See [REST overview](/docs/api/rest/overview#authentication).

---

## List secrets

`GET /secrets` or `POST /secrets/list`

Returns **workspace-scoped** secrets for the API key's workspace. Credential-managed secrets (`scope: internal`) and system settings (`scope: system`) are omitted from list results. When the request has no `scope` filter, the API applies `scope=team`. An explicit `scope` filter overrides it.

### Query / body parameters

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

### Response

| Field | Type | Description |
|-------|------|-------------|
| `items` | array | Secret objects |
| `next_cursor` | string | Cursor for the next page |
| `has_next` | boolean | More results available |
| `total_items` | number | Total matching secrets |

Each item:

| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Secret ID |
| `key` | string | Environment variable name (e.g. `OPENAI_API_KEY`) |
| `masked_value` | string | Masked preview (never the full value) |
| `description` | string | User-visible description |
| `scope` | string | `team` for user-managed secrets |
| `credential_id` | string | The credential this secret belongs to. Absent on a plain secret. |
| `created_at` | string | ISO timestamp |

**Example:**

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

```json
{
  "items": [
    {
      "id": "sec_abc123",
      "key": "OPENAI_API_KEY",
      "masked_value": "sk-…xxxx",
      "description": "OpenAI API key for GPT models",
      "scope": "team",
      "created_at": "2026-01-15T10:30:00Z"
    }
  ],
  "next_cursor": "",
  "has_next": false,
  "has_previous": false,
  "items_per_page": 1,
  "total_items": 1
}
```

---

## Get secret

`GET /secrets/{key}`

Requires `secrets:read`. Returns one secret's metadata for your workspace, with the same fields as a list item, including `masked_value` but **not** the plaintext value. Use [Reveal secret](#reveal-secret) when you need the decrypted value (audit-logged).

Returns `404 not_found` if the key does not exist for your workspace.

**Example:**

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

```json
{
  "id": "sec_abc123",
  "key": "OPENAI_API_KEY",
  "masked_value": "sk-…xxxx",
  "description": "OpenAI API key for GPT models",
  "scope": "team",
  "created_at": "2026-01-15T10:30:00Z"
}
```

`belt secrets get` calls this endpoint. On deployments that predate the route, the CLI falls back to listing secrets and filtering locally.

---

## Create secret

`POST /secrets`

Requires `secrets:write`.

### Request body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `key` | string | Yes | Environment variable name |
| `value` | string | Yes | Secret value (stored encrypted) |
| `description` | string | No | Shown in the UI and list responses |
| `provider` | string | No | Slug of the provider this key belongs to (for example `openai`, `acme`). The secret becomes that provider's credential. The provider does not have to be one the platform lists. |
| `provider_name` | string | No | Display name of a provider the platform doesn't list. Ignored for a listed provider. |
| `provider_website` | string | No | Website of a provider the platform doesn't list (`acme.com`). Its logo is looked up from the domain. Ignored for a listed provider. |
| `connection_scope` | string | No | Who owns the credential this key activates: `user`, `team`, `org`, or `platform`. Empty uses the provider default. Requires the matching admin role. Same rules as [`connection_scope` on connect](/docs/api/rest/credentials#connection-scope). |

When `provider` is set, the API links the secret to that provider's credential, creating it if none exists. For a provider the platform doesn't list, the credential is an `api_key` one named `provider_name`. An `api_key` credential becomes `connected` once a key is saved.

When `provider` is omitted, the API still links the key to a provider's credential if its name is a listed provider's standard API key (for example `ARK_API_KEY` belongs to ByteDance). Saving such a key from the vault's secrets tab, `belt secrets set`, or an app's `secrets:` block makes it that provider's credential all the same. Names that match an OAuth provider's BYOK app fields (such as `X_CLIENT_ID`) are not inferred this way.

A `provider` that is not a valid slug (lowercase letters, digits and single hyphens) fails with `validation_error`, so a key is never saved as a plain secret by mistake. If `connection_scope` is set, a validation or permission error fails the whole request. If it is omitted and the credential lookup fails, the secret is still created without a link. Omit `provider` for a plain secret that belongs to no service.

New workspace-scoped secrets are linked to the workspace's default [vault](/docs/api/rest/vaults).

**Example:**

```bash
curl -X POST https://api.inference.sh/secrets \
  -H "Authorization: Bearer inf_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "key": "OPENAI_API_KEY",
    "value": "sk-your-key-here",
    "description": "OpenAI API key"
  }'
```

Returns the created `SecretDTO` with `masked_value` only.

### Reserved platform keys

You cannot create team secrets whose `key` matches an environment variable name reserved for platform settings (SSO, SMTP, object storage, billing, and other deployment configuration). The API returns `400` with code `validation_error` and a `detail` such as `'STRIPE_SECRET_KEY' is a reserved platform key`. Pick a different name for your app (for example `MY_STRIPE_SECRET_KEY`) and declare that same name in `inf.yml`.

Examples of reserved names include `STRIPE_SECRET_KEY`, `SMTP_PASSWORD`, `TURNSTILE_SECRET_KEY`, and `STORAGE_ACCESS_KEY_SECRET`. The full set matches the legacy environment variable names used for platform settings.

**BYOK example with connection scope:**

```bash
curl -X POST https://api.inference.sh/secrets \
  -H "Authorization: Bearer inf_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "key": "OPENAI_API_KEY",
    "value": "sk-your-key-here",
    "provider": "openai",
    "connection_scope": "team",
    "description": "Team OpenAI BYOK key"
  }'
```

**Key for a provider the platform doesn't list:**

```bash
curl -X POST https://api.inference.sh/secrets \
  -H "Authorization: Bearer inf_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "key": "ACME_API_KEY",
    "value": "your-acme-key",
    "provider": "acme",
    "provider_name": "Acme CRM",
    "provider_website": "acme.com",
    "connection_scope": "team"
  }'
```

**Errors:**

| Code | HTTP | When |
|------|------|------|
| `already_exists` | 409 | A secret with this `key` already exists for the workspace. To make an existing secret a provider's credential, [attach it](#attach-a-secret-to-a-provider). |
| `invalid_request` | 400 | Missing `key` or malformed body |
| `team_role_required` | 403 | Caller is not a workspace admin |
| `team_role_required` | 403 | `connection_scope: "org"` without org admin |
| `validation_error` | 400 | An unknown `connection_scope`, `org` when the workspace is not part of an organization, or `org` / `platform` requested from a workspace other than the organization or platform workspace. `detail` says which. |
| `forbidden` | 403 | `connection_scope: "platform"` without platform admin |

---

## Attach a secret to a provider

`PUT /secrets/{key}/provider`

Requires `secrets:write` and workspace admin.

Makes an existing secret a provider's credential, with the same effect as creating it with `provider`. The value and key name stay the same. An empty `provider` detaches the secret back to a plain secret. The credentials it joins and leaves have their status updated.

### Request body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `provider` | string | Yes | Provider slug. Empty string detaches. |
| `provider_name` | string | No | As on [create](#create-secret) |
| `provider_website` | string | No | As on [create](#create-secret) |
| `connection_scope` | string | No | As on [create](#create-secret) |

```bash
curl -X PUT https://api.inference.sh/secrets/ACME_API_KEY/provider \
  -H "Authorization: Bearer inf_your_key" \
  -H "Content-Type: application/json" \
  -d '{ "provider": "acme", "provider_name": "Acme CRM", "provider_website": "acme.com" }'
```

Returns the `SecretDTO`. `credential_id` is set while the secret is attached and absent on a plain secret.

| Code | HTTP | When |
|------|------|------|
| `not_found` | 404 | No workspace secret with this key |
| `validation_error` | 400 | An invalid `provider` slug, or a secret that is not workspace-scoped |

---

## Update secret

`PUT /secrets/{key}`

Requires `secrets:write`. Updates the value and/or description for an existing workspace secret.

### Request body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `value` | string | Yes | New secret value |
| `description` | string | No | New description (omit to leave unchanged) |

**Example:**

```bash
curl -X PUT https://api.inference.sh/secrets/OPENAI_API_KEY \
  -H "Authorization: Bearer inf_your_key" \
  -H "Content-Type: application/json" \
  -d '{"value": "sk-rotated-key"}'
```

Returns `404 not_found` if the key does not exist for your workspace.

---

## Reveal secret

`GET /secrets/reveal/{key}`

Requires `secrets:read`. Returns the decrypted value. Access is **audit-logged** (`secret.revealed`).

**Example:**

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

```json
{
  "key": "OPENAI_API_KEY",
  "value": "sk-your-key-here"
}
```

Use sparingly in production automations. Prefer injecting secrets at task runtime (the platform resolves requirements when you `POST /run`) rather than pulling values into your own systems.

---

## Delete secret

`DELETE /secrets/{key}`

Requires `secrets:write`.

**Example:**

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

```json
{
  "success": true
}
```

---

## JavaScript SDK

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

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

const { items } = await client.secrets.list({ limit: 50 });
await client.secrets.create({
  key: 'OPENAI_API_KEY',
  value: 'sk-...',
  description: 'OpenAI',
});
const revealed = await client.secrets.reveal('OPENAI_API_KEY');
await client.secrets.update('OPENAI_API_KEY', { value: 'sk-rotated' });
await client.secrets.delete('OPENAI_API_KEY');
```

`client.secrets.create` also accepts `provider`, `provider_name`, `provider_website` and `connection_scope`. `client.secrets.setProvider(key, { provider })` attaches an existing secret.

→ [JavaScript SDK](/docs/api/sdk-javascript) · [SDK overview](/docs/api/sdk/overview)

---

## Python SDK

The Python SDK has no `client.secrets` namespace. Call the REST endpoints directly. `inferencesh.types` has `SecretCreateRequest` and `CredentialScope` for typing the body:

```python
import httpx
from inferencesh.types import CredentialScope, SecretCreateRequest

body: SecretCreateRequest = {
    "key": "OPENAI_API_KEY",
    "value": "sk-...",
    "provider": "openai",
    "connection_scope": CredentialScope.TEAM,
    "description": "Team OpenAI BYOK key",
}
response = httpx.post(
    "https://api.inference.sh/secrets",
    headers={"Authorization": "Bearer inf_your_key"},
    json=body,
).json()
```

→ [Python SDK: generated types](/docs/api/sdk-python#generated-types-inferenceshtypes)

---

## Related

- [Credentials](/docs/credentials/overview): OAuth and managed service connections (separate from workspace secrets)
- [Extend: secrets](/docs/extend/secrets) — declare `secrets:` in `inf.yml`
- [Tasks API](/docs/api/rest/tasks) — run apps that consume injected secrets
- [Entitlements API](/docs/api/rest/entitlements) — plan limits (API keys, storage, and other resources)
