> ## Documentation Index
> Fetch the complete documentation index at: https://www.plungeai.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication & keys

> Every route except GET /health and GET /v1/openapi.json needs a key. One ozk_ platform key opens every plane on https://api.plungeai.com (models, tools, agents, workflows, MCP, discovery, traces and the connector proxy) and the MCP server on https://mcp.plungeai.com. Two older prefixes, …


Every route except `GET /health` and `GET /v1/openapi.json` needs a key. One `ozk_` platform
key opens every plane on `https://api.plungeai.com` (models, tools, agents, workflows, MCP,
discovery, traces and the connector proxy) and the MCP server on `https://mcp.plungeai.com`.
Two older prefixes, `sk-ocean-` and `sk-conn-`, are still accepted (legacy) on their own plane while
integrations move over; new integrations use `ozk_`.

## The one key

| Prefix | Name | Opens | Auth checked by |
|---|---|---|---|
| `ozk_` | Ocean account key | Every plane: models (`/v1/chat/completions`, `/v1/embeddings`, `/v1/models`), tools, agents, workflows, MCP, discovery, traces, and the connector proxy (`/v1/connect/:provider/proxy/*`) | `core/core-api-keys` — the execution planes through `orchestration/api-gateway/lib/auth.ts`, the models plane and the connector proxy through `inference/gateway/src/auth/api-key.ts` |

In this guide: `$PLUNGE_API_KEY` = `ozk_`.

Billing follows the account: a key carries your account id, so every metered call on it —
models, embeddings, the connector proxy — is charged to your account's one wallet, whichever
of your `ozk_` keys made it.

Legacy keys: `sk-ocean-` (models plane only) and `sk-conn-` (connector proxy only) keep working
on their own plane, are refused on the execution planes, and can no longer be created in the
Dashboard. Replace them with an `ozk_` key.

## Which routes accept which key

The router (`api-gateway.ts`) gates `/v1/discovery`, `/v1/tools`, `/v1/agents`,
`/v1/workflows`, `/v1/mcp`, and `/v1/traces` with `requireAuth`, which accepts **only** a
bearer that starts with `ozk_` (or a signed internal identity). The models plane and the
connector proxy are mounted **without** `requireAuth` — the inference gateway resolves the key
downstream, and it accepts the same `ozk_` key. A legacy `sk-ocean-` or `sk-conn-` key is
accepted on its own plane only, so the one mistake left is a legacy key on an execution plane,
which returns `401`.

An `ozk_` key on the models plane works (live-verified):

```bash
curl -s https://api.plungeai.com/v1/models \
  -H "Authorization: Bearer $PLUNGE_API_KEY"
```

A legacy model-key shape on an execution plane → `401` (the router rejects any non-`ozk_`
bearer before dispatch; live-verified):

```bash
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://api.plungeai.com/v1/agents/one-agent/execute \
  -H "Authorization: Bearer sk-ocean-EXAMPLE" \  # legacy model key
  -H "Content-Type: application/json" \
  -d '{"prompt":"hi"}'
```

The `401` body is the standard error envelope with `X-Error-Code: unauthorized`
(live-verified):

```bash
curl -s https://api.plungeai.com/v1/tools
```

## Header forms

Both header forms work on the execution planes; the models plane (`/v1/chat/completions`,
`/v1/embeddings`, `/v1/models`) takes `Authorization: Bearer` only — `X-API-Key` there is a
`401`, by design.

| Header | Planes | Example |
|---|---|---|
| `Authorization: Bearer <key>` | all | `-H "Authorization: Bearer $PLUNGE_API_KEY"` |
| `X-API-Key: <key>` | execution planes (`ozk_`); **not** the models plane | `-H "X-API-Key: $PLUNGE_API_KEY"` |

`X-API-Key` with an `ozk_` key is live-verified:

```bash
curl -s "https://api.plungeai.com/v1/tools?limit=1" \
  -H "X-API-Key: $PLUNGE_API_KEY"
```

The same header on the models plane → `401` (live-verified):

```bash
curl -s -o /dev/null -w '%{http_code}\n' https://api.plungeai.com/v1/models \
  -H "X-API-Key: $PLUNGE_API_KEY"
```

## How to obtain each key

| Key | Where | Notes |
|---|---|---|
| `ozk_` | Dashboard → **One API → Keys** (`https://dashboard.plungeai.com`) | Self-service; session-authenticated (`apps/ocean-dashboard/src/worker/routes/platform-keys.routes.ts`, `POST /api/platform-keys`). Name the key, pick an expiry (never / 7–365 days), copy it once — it is shown exactly once and stored hashed. Bound to your account and its tier; revoke any time from the same list. |

The tab shows your **Ocean account key** (prefix and last four characters; **Rotate** replaces
it) — the key the Playground, `ocean-cli` and OceanCode use — above any extra named keys you
create there.

The command-line clients get the account key through a browser sign-in, with no key to paste:
`ocean-cli login` and `oceancode login --provider ocean` listen on `127.0.0.1:<port>` and open
`https://dashboard.plungeai.com/api/cli/authorize?port=<port>&state=<state>`. Once you are
signed in (any sign-in method), the Dashboard redirects a one-time code (valid 120 s) to
`http://127.0.0.1:<port>/callback`, and the client exchanges it with
`POST https://dashboard.plungeai.com/api/cli/token` for the key. The key never appears in a
browser URL.

Existing `sk-ocean-` and `sk-conn-` keys are listed read-only in the same tab under **Legacy
keys**; each can be revoked there, and no new ones are created.

## Key fences (scoping)

Keys can be scoped so a leak is bounded.

| Fence | Key kind | Effect | Source |
|---|---|---|---|
| `allowed_tools` | `ozk_` | Restrict the key to a named set of tools | `orchestration/mcp-gateway/auth-middleware.ts` |
| `allowed_ips` | `ozk_` | Accept the key only from listed IPv4 addresses / CIDR blocks or exact IPv6 addresses; a call from elsewhere is a definitive `401` (no fallback) | `core/core-api-keys/index.ts` (`ipAllowed`, `verifyApiKeyRecord`) |
| `allowed_models` | `sk-ocean-` (legacy) | Restrict the key to a model allow-list; an off-list model is `403 model_not_allowed` | `inference/gateway/src/auth/api-key.ts` (`assertModelAllowed`) |
| `monthly_limit_usd` | `sk-ocean-` / `sk-conn-` (legacy) | Per-key monthly spend cap; exceeding it is `429 insufficient_quota` (inference) or `402 insufficient_balance` (connector) | `inference/gateway/src/auth/rate-limit.ts` |

`ozk_` key records are stored hashed only (`key:{sha256}` in KV, `core/core-api-keys`); a named
key is shown once at mint time. The account key's plaintext is also kept encrypted
(`ocean-compliance.user_credentials`) so the Dashboard can hand it to your signed-in clients.

## Tiers and rate limits

Execution-plane rate limiting is per-key (per user uuid), enforced by a Durable Object on
`POST` executions only — catalog `GET`s are free (`api-gateway.ts` calls `checkRateLimit`
for `POST` only). The limits below are the code values in
`orchestration/mcp-gateway/rate-limiter.ts`:

| Tier | Requests / minute | Requests / day |
|---|---|---|
| free | 30 | 1,000 |
| pro | 100 | 10,000 |
| enterprise | 300 | 100,000 |

Reaching the day limit pauses the key for **ten minutes** (`Retry-After` ≤ 600), after which its day
count starts over — a key is never shut out longer than that (2026-10-05); the minute limit waits out
the minute. A key with no tier is treated as `free`: the key resolver defaults it before the limiter sees it
(`orchestration/mcp-gateway/auth-middleware.ts` `validateApiKey`). A per-key `rate_limit` override
is **ignored** on these planes (`orchestration/api-gateway/lib/auth.ts` does not pass it to
`checkRateLimit`); it applies only on `https://mcp.plungeai.com/v1`.

The numbers above are the defaults. PlungeAI sets the per-tier numbers, or switches the limits off, in the operator console
(One API › Keys › Rate limits; KV `rate-limits:config`, read by the limiter within a minute); while they are off no call is
counted and none returns `429`, and the failed-authentication meter in 13.4 stays on.

The models plane has its own limiter — a default of **600 req/min per key**
(`RATE_LIMIT_RPM_DEFAULT`, `inference/gateway/src/routes/chat-completions.ts`), also
overridable per key.

A separate **failed-auth limiter** meters bad credentials at **20 attempts / minute per
client IP** (`auth-fail` tier in `rate-limiter.ts`; wired in `auth.ts`) so key enumeration
is bounded.

When you are limited, the response is `429` with a `Retry-After` header (seconds) and a
`rate_limited` (execution planes) or `rate_limit_exceeded` (models plane) error code.

## Request ids and trace correlation

- **`x-request-id`** is minted server-side on every request and echoed on the response
  (`api-gateway.ts`). It is never trusted from the client. Include it when reporting a
  problem. Live-verified — the header is present:

```bash
curl -sD - -o /dev/null https://api.plungeai.com/health | grep -i '^x-request-id'
```

- **`x-trace-id`** is yours to set for correlating several calls into one trace. It is
  honoured **only if it is a UUID**; otherwise the router falls back to the request id. On an
  execution route the trace id doubles as the execution/workflow primary key, so replaying a
  UUID that was already used returns `409 duplicate_execution_id` — send a fresh UUID or omit
  the header.

Responses also set `X-Content-Type-Options: nosniff`. Error responses additionally set
`X-Error-Code` (equal to `error.code`) so a client can branch on the header regardless of the
negotiated body format.

## CORS

The router answers CORS for any origin and exposes the response headers a browser client needs (`x-request-id`, `X-Error-Code`, `Retry-After`, …; `cors()` in `api-gateway.ts`), and the models plane sets
`access-control-allow-origin: *` with `authorization, content-type, x-request-id` allowed and
`x-request-id, x-correlation-id, x-ocean-cost-usd, x-ocean-dropped-params, x-generation-id` exposed
(`inference/gateway/src/index.ts`). Browser SDKs
work, but do not ship a secret key in client-side code.

## Internal HMAC identity (not for customers)

Service-to-service calls inside the platform authenticate with a signed internal identity
(`X-Internal-Signature`, HMAC over the request) instead of a key, verified before the `ozk_`
path in `auth.ts`. This is the mechanism Ocean Studio and the engine use to call each other;
it is not available to external developers and needs no action from you.
