One API
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, 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):
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):
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):
curl -s https://api.plungeai.com/v1/toolsHeader 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:
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):
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 GETs 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-idis 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:
curl -sD - -o /dev/null https://api.plungeai.com/health | grep -i '^x-request-id'x-trace-idis 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 returns409 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.