> ## 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 MCP call needs an ozk_ key. The key is your identity: it resolves to one user account, and every tool reads and writes only what that account owns. There is no OAuth sign-in on the MCP server today; bearer keys are the only way in. This chapter uses https://mcp.plungeai.com/v1; …


Source: `orchestration/mcp-gateway/auth-middleware.ts`, `index.ts`, `rate-limiter.ts`, `core/core-api-keys/rate-limit-config.ts`, `extras.ts` (`plungeaiWhoami`), `core/core-api-keys/index.ts`, `apps/ocean-dashboard/src/worker/routes/platform-keys.routes.ts`, `orchestration/api-gateway/lib/auth.ts`, `orchestration/api-gateway/routes/mcp.ts`. Unauthenticated `401` responses live-verified 2026-09-24.

Every MCP call needs an `ozk_` key. The key **is** your identity: it resolves to one user account, and every tool reads and writes only what that account owns. There is no OAuth sign-in on the MCP server today; bearer keys are the only way in. This chapter uses `https://mcp.plungeai.com/v1`; `https://api.plungeai.com/v1/mcp` accepts the same key, with the differences noted in each section.

## 2.1 The `ozk_` key

| Property | Value |
|---|---|
| Format | `ozk_live_` + 48 hex characters |
| Where | Dashboard → **One API → Keys** (`https://dashboard.plungeai.com`), self-service while signed in |
| Name | required, 1–64 characters |
| Expiry | never, or 7 / 30 / 90 / 180 / 365 days, chosen when you create it |
| Shown | exactly once, at creation. It is stored only as a SHA-256 hash; nobody can show it to you again |
| Per account | up to 25 active keys; the 26th is refused until you revoke one |
| Tier | copied from your account's tier when the key is created (`free`, `pro`, `enterprise`; an unknown account tier gives `free`) |
| Works on | the MCP server and every One API plane, models included (`https://api.plungeai.com/docs#2-authentication--keys`) |

The Keys list shows each key's name, redacted prefix and last four characters, created and last-used dates, expiry (highlighted when it is less than a week away) and status (`active`, `revoked`, `expired`), with a revoke action on active keys.

Because the tier is copied at creation, a key keeps the tier it was created with. After your account changes tier, create a new key to get the new limits; `plungeai_whoami` shows the tier of the key you are using ([2.7](#27-checking-yourself--plungeai_whoami)).

A **shared team key** is simply a key created under someone else's account: whoever uses it acts as that account. Ask the account owner for one; you cannot create a key for another account yourself.

## 2.2 Sending the key

| Header | Example |
|---|---|
| `Authorization: Bearer <key>` | `-H "Authorization: Bearer $PLUNGE_API_KEY"` |
| `X-API-Key: <key>` | `-H "X-API-Key: $PLUNGE_API_KEY"` |

Both are **supported**. The server tries `X-API-Key` first, then `Authorization: Bearer`. A bearer value that does not start with `ozk_` is **rejected** (`401`): OAuth tokens and `sk-ocean-` model keys (legacy) are not credentials here. `X-API-Key` exists for integrations whose "API key" field does not send a bearer header:

```bash
curl -s https://mcp.plungeai.com/v1 \
  -H "X-API-Key: $PLUNGE_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

On `https://api.plungeai.com/v1/mcp` both headers work too, and the value must start with `ozk_`.

## 2.3 What an unauthenticated call returns

Any request to `/v1` without a valid key gets `401` with a JSON-RPC error body and a `WWW-Authenticate` challenge. No credentials at all gives a bare `Bearer` challenge (live-verified 2026-09-24):

```bash
curl -s -o /dev/null -w '%{http_code} %header{www-authenticate}\n' \
  -X POST https://mcp.plungeai.com/v1 \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

A credential that was sent but is not valid adds `error="invalid_token"` (live-verified 2026-09-24, with a made-up key):

```bash
curl -s -o /dev/null -w '%{http_code} %header{www-authenticate}\n' \
  -X POST https://mcp.plungeai.com/v1 \
  -H 'Authorization: Bearer ozk_live_not_a_real_key' \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

The body is the same in both cases:

```bash
curl -s -X POST https://mcp.plungeai.com/v1 \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

```json
{"jsonrpc":"2.0","error":{"code":-32001,"message":"No valid authentication provided. Use an X-API-Key header or \"Authorization: Bearer <api key>\"."},"id":null}
```

What each failure looks like:

| Cause | Status | `WWW-Authenticate` | `error.message` |
|---|---|---|---|
| no key | `401` | `Bearer` | `No valid authentication provided. …` |
| unknown, mistyped, revoked or expired key, or a non-`ozk_` bearer | `401` | `Bearer error="invalid_token"` | `No valid authentication provided. …` — the exact reason is logged server-side, not returned |
| valid key used from an address outside its `allowed_ips` fence | `401` | `Bearer error="invalid_token"` | `API key not permitted from <your IP>` |
| more than 20 failed attempts in a minute (or 500 in a day) from one IP | `429` + `Retry-After` | — | `Too many failed authentication attempts` |

The same request on `https://api.plungeai.com/v1/mcp` answers in the One API envelope instead (live-verified 2026-09-24):

```bash
curl -s -X POST https://api.plungeai.com/v1/mcp \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

There: no key or a non-`ozk_` value → `401 Bearer ozk_ key required`; an `ozk_` key that is invalid, revoked, expired or outside its IP fence → `401 Invalid or revoked API key`; too many failures → `429 rate_limited` (`too many failed authentications`). Every error carries `X-Error-Code` and `x-request-id` headers.

The server does not publish OAuth discovery metadata (`/.well-known/oauth-protected-resource`). Clients that can only connect through OAuth need a bridge — see [Connecting clients](/mcp/connecting-clients).

## 2.4 Expiry, revocation and rotation

- **Expiry** is checked on every request. From the moment a key expires it answers `401`.
- **Revoke** a key from the same Keys list. The record stays (so the list and audit trail keep it) and is marked revoked. Each server instance caches a successfully checked key for up to 60 seconds, and the key store's own edge cache can add up to another 60 seconds, so a revoked key can keep working for up to about two minutes (about 75 seconds measured), then answers `401`. If the key leaked, revoke it first and allow for that window before you treat it as dead.
- **Rotate** without downtime: create the new key, update every client config, confirm it with `plungeai_whoami` ([2.7](#27-checking-yourself--plungeai_whoami)), then revoke the old key.
- **Leaked or lost?** Create a replacement and revoke the old one yourself. A lost key cannot be recovered: only its hash is stored.

## 2.5 Key fences

A key can carry two optional fences. Keys you create yourself in the Dashboard have **no fences** (every tool, from any address). Fenced keys are issued by PlungeAI for you — ask the account owner who gave you the key, or [Requests](https://www.plungeai.com/request/new?kind=question).

| Fence | Effect on `https://mcp.plungeai.com/v1` |
|---|---|
| `allowed_ips` | The key authenticates only from the listed addresses: exact IPv4 or IPv6 addresses, or IPv4 CIDR blocks (IPv6 ranges are not supported — list exact IPv6 addresses). From anywhere else: `401 API key not permitted from <ip>`, counted as a failed attempt. |
| `allowed_tools` | Only the listed tools are registered for this key: the others do not appear in `tools/list`, and a `tools/call` to one returns JSON-RPC error `-32602` `Tool not permitted for this key: <name>`. |

If a tool this guide documents is missing from your `tools/list`, or a call is refused this way, the key is fenced: that is policy, not a fault.

## 2.6 Tiers and rate limits

Default limits per tier, from `tier-limits.ts` (the operator can change them, see the last point below):

| Tier | Tool-call requests per minute | Tool-call requests per day |
|---|---|---|
| free | 30 | 1,000 |
| pro | 100 | 10,000 |
| enterprise | 300 | 100,000 |

How they are counted:

- **Per account, not per key.** The counter is keyed by your user id, so every key on your account shares it. The tier and any per-key override of the key you are using decide the cap.
- **Only `tools/call` counts** on `https://mcp.plungeai.com/v1`: each HTTP request that carries one counts once (a JSON-RPC batch is one request). `initialize`, `tools/list`, `resources/*` and `prompts/*` are free. `plungeai_whoami` is a tool call and counts.
- On `https://api.plungeai.com/v1/mcp` the rule is the same: the One API does not count `POST /v1/mcp`, and this server counts each request that carries a `tools/call`, with your key's per-key override.
- **Fixed windows.** A minute window opens with your first counted call and resets 60 seconds later; a day window lasts 24 hours from its first call, not until midnight.
- A per-key `rate_limit` override, when PlungeAI sets one on a key, replaces the per-minute cap.
- **Set by the operator.** The numbers above are the defaults. PlungeAI sets the per-tier numbers, or switches the limits off, in Operator › One API › Keys › Rate limits (`GET`/`PUT /admin/rate-limits` for internal callers). While they are off, no call is counted and no tool call or One API `POST` returns `429`; the one limit left is the failed-authentication meter (20 invalid-key attempts a minute per client IP), which is always on. A change reaches the gateways within a minute. A per-key `rate_limit` still applies while limits are on.

When you are over the limit, the call is refused before anything runs:

```
HTTP/2 429
retry-after: 42

{"jsonrpc":"2.0","error":{"code":-32000,"message":"Rate limit exceeded: Rate limit: 100 requests/minute","data":{"retryAfter":42}},"id":3}
```

`Retry-After` (and `error.data.retryAfter`) is the number of seconds until the window resets. Wait that long, then continue; do not retry in a tight loop. On the One API host the body is `{"error":{"code":"rate_limited","message":"rate limit exceeded for tier pro","retry_after":42}}`. The full error catalogue is in [Errors, feedback & limits](/mcp/errors-limits).

## 2.7 Checking yourself — `plungeai_whoami`

`plungeai_whoami` shows who the server thinks you are and how much of your window you have used. It is read-only.

> Try asking: *"Who am I, what tier am I on, and how much of my rate limit have I used?"* — or, right after connecting a client, *"use plungeai_whoami to confirm my identity"*.

| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
| `user_request` | string (≤ 4,000 chars) | no | — | the user's own words |
| `format` | `markdown` · `json` | no | `markdown` | `json` returns the fields below as a JSON document plus `structuredContent` |

```bash
curl -s https://mcp.plungeai.com/v1 \
  -H "Authorization: Bearer $PLUNGE_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_whoami","arguments":{"user_request":"how much of my rate limit have I used?"}}}'
```

The Markdown answer:

```
# Who am I

- **User:** `<your user id>`
- **Auth:** api-key · tier **pro**
- **Key:** my-laptop (`<key id>`)
- **Rate window:** 3/min used · 41/day used
- **Server:** plungeai.com (mcp.plungeai.com)
```

With `format: "json"`:

```bash
curl -s https://mcp.plungeai.com/v1 \
  -H "Authorization: Bearer $PLUNGE_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"plungeai_whoami","arguments":{"user_request":"who am I, as JSON","format":"json"}}}'
```

```json
{
  "user": "<your user id>",
  "auth_type": "api-key",
  "tier": "pro",
  "key_label": "my-laptop",
  "key_id": "<key id>",
  "rate_window": { "minute_used": 3, "day_used": 41 },
  "server": "plungeai.com (mcp.plungeai.com)"
}
```

What to know:

- The counts include the `plungeai_whoami` call itself. Compare them with your tier's caps in [2.6](#26-tiers-and-rate-limits); the card shows usage, not the caps.
- The rate-window line is best-effort. If the counter cannot be read, the line is absent (`rate_window: null`); that alone is not an error.
- There is no permissions field. A tool fence shows up as fewer tools in `tools/list`, not on this card.
- Through `https://api.plungeai.com/v1/mcp` the card is the same: the One API forwards your key and the gateway reads its record.
- Without an identity the tool answers `Authentication required.` with `isError: true` — in practice you get the `401` from [2.3](#23-what-an-unauthenticated-call-returns) first.

## 2.8 Identity and scoping

- Everything is scoped to the account behind the key: executions, workflows, schedules, conversations, memory. You see what that account owns, the same view it has in Studio — never the whole system.
- Id-based lookups check ownership before reading anything. Someone else's id, or a mistyped one, answers "not found", never "forbidden" (`plungeai_get_result` answers with a `needs_input` outcome naming the id). An id that looks right but is "not found" usually belongs to another account or is mistyped.
- What you create through MCP carries your user id, so it shows up in Studio, the dashboard and the other PlungeAI apps under your account.
- Every authenticated request is written to your activity trail with its arguments and answer. You can read your own day at `https://mcp.plungeai.com/log?date=YYYY-MM-DD` with the same key ([Errors, feedback & limits](/mcp/errors-limits)). This is also why `user_request` matters: it is where your own sentence survives into support diagnostics.

## 2.9 Keeping the key safe

- Treat it like a password. Anyone holding it acts as your account.
- Keep the literal key in user-level config (`~/.claude.json`, `~/.cursor/mcp.json`, the client's secret store) or an environment variable. A config file inside a repo should only reference a variable ([Connecting clients](/mcp/connecting-clients) shows how per client).
- Before the first commit in a repo you just configured, both of these must print nothing:

```bash
git grep -n "ozk_"                                                 # tracked files
grep -rn "ozk_" . --exclude-dir=.git --exclude-dir=node_modules    # untracked files too
```

- Never put the key in browser code or a client bundle. Apps call PlungeAI from a server with the key in a server-side secret.
- If a key may have leaked, rotate it now ([2.4](#24-expiry-revocation-and-rotation)).
