One API — Developer Guide (v3.0)

Contents 0. Overview 1. Quickstart 2. Authentication & keys 3. Models plane 4. Streaming 5. OpenRouter compatibility 6. Tools plane 7. Connectors & connected accounts 8. Discovery 9. Agents plane 10. Workflows plane 11. MCP plane 12. Traces & observability 13. Errors, limits & headers 14. Skills, plugins & capability injection 15. SDKs & clients

The single front door to the Ocean Platform: https://api.plungeai.com. One endpoint for models, tools, agents, workflows, MCP, connectors, discovery, and traces. Machine-readable contract: GET /v1/openapi.json. Every # verify-marked example in this guide was run against the live platform on the build date (2026-09-18). Previous version: ONE-API-DEVELOPER-GUIDE-2.0.md (v2.0, kept intact).

Contents


0. Overview

The Ocean One API is a single HTTPS front door to the Ocean platform. One base URL, one router, and every capability the platform exposes reachable from it:

https://api.plungeai.com

Everything in this guide is a real route on that host. The router is a thin Hono worker (orchestration/api-gateway/api-gateway.ts) that fronts the registry, the CNL engine, the MCP gateway, the MCP executor, and the inference gateway over internal service bindings — you never address those services directly.

The seven planes

The router groups routes into planes. Six are the execution planes and take an ozk_ platform key; the models plane is OpenAI-compatible and takes an sk-ocean- key.

Plane What it does Key routes Key
Models OpenAI-compatible chat, embeddings, model catalog — a transparent proxy to the inference gateway with ordered fallback, sort, @preset/<slug>, guardrails, and an opt-in response cache POST /v1/chat/completions · POST /v1/embeddings · GET /v1/models sk-ocean-
Tools List tool agents, read a tool's invocation contract, execute one operation behind a trust fence GET /v1/tools · GET /v1/tools/{id} · POST /v1/tools/{id}/execute ozk_
Agents Execute a registry agent from a prompt — sync, async pointer, or OpenAI-shaped SSE GET /v1/agents · POST /v1/agents/{id}/execute · GET /v1/agents/results/{workflowId}/{taskId} ozk_
Workflows Run a multi-agent CNL workflow, inline or saved, with optional SSE POST /v1/workflows/execute · POST /v1/workflows/execute-stream · POST /v1/workflows/{id}/execute ozk_
MCP The platform as an MCP server (inbound Streamable HTTP), plus outbound MCP runs against catalog servers POST /v1/mcp · GET /v1/mcp/tools · POST /v1/mcp/runs ozk_
Discovery Search and recommend across the capability registry (agents, skills, connectors, models, MCP servers, …) GET /v1/discovery/search · POST /v1/discovery/recommend · GET /v1/discovery/cards/{type}/{id} ozk_
Traces Read the persisted execution trace (spans + gateway request log) for one trace id GET /v1/traces/{id} ozk_

The full machine-readable route list and schemas are served at GET /v1/openapi.json (source: orchestration/api-gateway/openapi.ts). There is also a signed, internal-only capability plane (/v1/capability, OceanCode RemotePacks) that customer keys cannot open — it is not one of the seven and is not documented here.

Positioning

Think of the One API as an OpenRouter-compatible models plane plus everything a model router cannot do. The models plane speaks the OpenAI wire format, so an existing OpenAI or OpenRouter integration points at it with a base-URL swap (see OpenRouter compatibility). The execution planes are the part a router has no answer for: real tools with contracts and a trust fence, registry agents, multi-agent workflows, discovery, MCP, and a persisted trace for every run.

Two kinds of key

Prefix Opens How you get it
ozk_ The six execution planes (tools, agents, workflows, MCP, discovery, traces) Self-service at Dashboard → API Keys (https://dashboard.plungeai.com); the key is bound to your account and tier
sk-ocean- The models plane only (/v1/chat/completions, /v1/embeddings, /v1/models) Self-service at Dashboard → Gateway → Inference → Keys (https://dashboard.plungeai.com)

A third prefix, sk-conn-, authenticates the connector proxy (a separate host, gateway.plungeai.com/connect/...) and is covered in Connectors. Sending the wrong prefix to a plane is a 401 — see Authentication & keys. Full key mechanics are in that chapter; this guide writes keys as the shell variables $PLUNGE_API_KEY (ozk_), $PLUNGE_MODEL_KEY (sk-ocean-), and $PLUNGE_CONNECTOR_KEY (sk-conn-).

How a request flows

An execution-plane call is not a thin proxy to a model. The path is router → CNL engine → agent:

  1. The router authenticates the ozk_ key, mints a server-side x-request-id, and either adopts your UUID x-trace-id or generates the trace id (api-gateway.ts).
  2. It dispatches over an internal service binding to the CNL engine, which loads the agent card from the registry, refuses anything not status:active before dispatch, recalls memory, and runs the agent.
  3. The agent's result is stored in SharedMemory and returned; the whole run is persisted as trace spans.

The engine hop exists on purpose: it gives every call the same history, outcome vocabulary (ok | needs_input | needs_connection | needs_api_key | needs_approval | unavailable | error), and trust fence, whether the caller is a workflow, Ocean Studio, or you over HTTP. The rationale is recorded in orchestration/api-gateway/log/API-SURFACE-DECISION-AND-PLAN.md (everything routes through the engine so there is one execution path, not two).

What "live-verified" means in this guide

Every ozk_-plane example in this guide was run against the deployed API on 2026-09-18 and its output checked. Those examples carry a hidden # verify / # expect: marker that the docs build strips but a verifier (orchestration/api-gateway/docs/guide-3.0/verify-examples.mjs) re-runs. Models-plane (sk-ocean-) examples are verified against code and the live-verified 2.0 guide, not re-run live in this pass (a models-plane key was unavailable 2026-09-18) — that is stated again in the Quickstart.

60-second tour

Set your key once, then hit three planes. These three are live-verified:

curl -s https://api.plungeai.com/health
curl -s "https://api.plungeai.com/v1/discovery/search?q=web+search&kind=agents&limit=3" \
  -H "Authorization: Bearer $PLUNGE_API_KEY"
curl -s "https://api.plungeai.com/v1/agents?limit=3" \
  -H "Authorization: Bearer $PLUNGE_API_KEY"

For a first tool call, agent call, workflow, and model completion, go straight to the Quickstart.

What's new in 3.0

Where to get help


1. Quickstart

Five first calls, one per plane you will use most. The ozk_ examples here are live-verified (2026-09-18). The models-plane (sk-ocean-) examples are verified against code and the live-verified 2.0 guide, not re-run live in this pass — a models-plane key was unavailable on 2026-09-18. Get an sk-ocean- key from Dashboard → Gateway → Inference → Keys and an ozk_ key from Dashboard → API Keys (see Authentication & keys).

Throughout, $PLUNGE_API_KEY is your ozk_ key and $PLUNGE_MODEL_KEY is your sk-ocean- key.

(a) A model completion with the OpenAI SDK

The models plane is OpenAI-compatible. Point the official OpenAI SDK at https://api.plungeai.com/v1 and pass a provider/model slug. Valid slugs come from GET /v1/models and the router's alias table (inference/gateway/src/routing/model-router.ts); anthropic/claude-sonnet-4-6 is a stable one.

Python:

from openai import OpenAI

client = OpenAI(
    api_key="<your sk-ocean- key>",
    base_url="https://api.plungeai.com/v1",
)

resp = client.chat.completions.create(
    model="anthropic/claude-sonnet-4-6",
    messages=[{"role": "user", "content": "Reply with exactly the word: pong"}],
    max_tokens=16,
)
print(resp.choices[0].message.content)

TypeScript:

import OpenAI from 'openai'

const client = new OpenAI({
  apiKey: process.env.PLUNGE_MODEL_KEY,
  baseURL: 'https://api.plungeai.com/v1',
})

const resp = await client.chat.completions.create({
  model: 'anthropic/claude-sonnet-4-6',
  messages: [{ role: 'user', content: 'Reply with exactly the word: pong' }],
  max_tokens: 16,
})
console.log(resp.choices[0].message.content)

curl:

curl -s https://api.plungeai.com/v1/chat/completions \
  -H "Authorization: Bearer $PLUNGE_MODEL_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"anthropic/claude-sonnet-4-6","messages":[{"role":"user","content":"Reply with exactly the word: pong"}],"max_tokens":16}'

The response model field is the slug that actually served the request — after a failover it can differ from what you asked for. Add "stream": true for an SSE response. Ordered fallback (models[]), sort, @preset/<slug>, guardrails, and the response cache are covered in Models plane.

(b) A tool call — read the contract first, then execute

A tool agent has an invocation contract: operations, params, gates, examples. Read it before you call. This is live-verified:

curl -s https://api.plungeai.com/v1/tools/brave-agent \
  -H "Authorization: Bearer $PLUNGE_API_KEY"

Then execute an operation. brave-agent takes a free-text prompt (a search query, ≤ 400 chars). Live-verified:

curl -s -X POST https://api.plungeai.com/v1/tools/brave-agent/execute \
  -H "Authorization: Bearer $PLUNGE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"prompt":"cloudflare workers pricing"}'

The result carries ok, content, and an outcome. A 403 refused or 409 approval_required is the trust fence on a gated or money verb; a 422 invalid_params echoes the contract back with what was missing. See Tools plane.

(c) An agent call — sync, then streamed

POST /v1/agents/{id}/execute runs a registry agent from a prompt. Default is synchronous (the resolved result comes back on the same response). llm-agent is a general LLM agent. Live-verified:

curl -s -X POST https://api.plungeai.com/v1/agents/llm-agent/execute \
  -H "Authorization: Bearer $PLUNGE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"prompt":"Reply with exactly the word: pong","sync":true,"max_tokens":32}'

Add "stream": true for an OpenAI-shaped chat.completion.chunk SSE — a primer chunk, one delta per token, a final finish_reason chunk, then data: [DONE]. Live-verified:

curl -s -N -X POST https://api.plungeai.com/v1/agents/llm-agent/execute \
  -H "Authorization: Bearer $PLUNGE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"prompt":"Reply with exactly the word: pong","stream":true,"max_tokens":32}'

Set "sync": false for a 202 pointer you redeem at GET /v1/agents/results/{workflowId}/{taskId}. Full field list, tiers, and error codes are in Agents plane; the SSE vocabulary is in Streaming.

(d) A workflow — inline CNL YAML

POST /v1/workflows/execute runs a multi-agent workflow. Send the CNL YAML as a workflow string (or POST raw YAML with Content-Type: text/yaml). The response is an ack with a workflow_id; redeem each task's result from SharedMemory at GET /v1/workflows/results/{workflowId}/{taskId}. This fence executes and then polls the result — live-verified:

ack=$(curl -s -X POST https://api.plungeai.com/v1/workflows/execute \
  -H "Authorization: Bearer $PLUNGE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"workflow":"workflow:\n  name: quickstart\n  tasks:\n    - id: t1\n      agent: llm-agent\n      prompt: \"Reply with exactly the word: pong\"\n      max_tokens: 32\n"}')
echo "$ack"
wf=$(printf '%s' "$ack" | sed -n 's/.*"workflow_id":"\([^"]*\)".*/\1/p')
out=""
for i in $(seq 1 10); do
  out=$(curl -s "https://api.plungeai.com/v1/workflows/results/$wf/t1" \
    -H "Authorization: Bearer $PLUNGE_API_KEY")
  printf '%s' "$out" | grep -q '"content"' && break
done
echo "$out"

Use POST /v1/workflows/execute-stream for live SSE events instead of polling. See Workflows plane and the CNL primer there.

(e) Discover a capability

GET /v1/discovery/search is a hybrid semantic + keyword search over the registry. Describe the capability in natural language; filter by kind. Live-verified:

curl -s "https://api.plungeai.com/v1/discovery/search?q=convert+pdf+to+markdown&kind=agents&limit=3" \
  -H "Authorization: Bearer $PLUNGE_API_KEY"

Read a single card's markdown at GET /v1/discovery/cards/{type}/{id}. See Discovery.

Request ids and traces

Every response echoes a server-minted x-request-id. To correlate several calls under one trace, send your own x-trace-id — it must be a UUID or it is ignored. Read the persisted trace at GET /v1/traces/{id} (two sentences here; full detail in Traces & observability):

curl -s "https://api.plungeai.com/v1/traces/$(uuidgen)" \
  -H "Authorization: Bearer $PLUNGE_API_KEY"

2. Authentication & keys

Every route except GET /health and GET /v1/openapi.json needs a key. There are three key prefixes, and each opens a different set of planes. Sending the wrong prefix to a plane is a 401, not a silent fallback.

The three prefixes

Prefix Name Opens Auth checked by
ozk_ Ocean Zero Key The six execution planes: tools, agents, workflows, MCP, discovery, traces orchestration/api-gateway/lib/auth.ts (router)
sk-ocean- Inference key The models plane only: /v1/chat/completions, /v1/embeddings, /v1/models inference/gateway/src/auth/api-key.ts (downstream)
sk-conn- Connector key The connector proxy on gateway.plungeai.com (/connect/:provider/proxy/*) inference/gateway/src/routes/connector-proxy.ts

In this guide: $PLUNGE_API_KEY = ozk_, $PLUNGE_MODEL_KEY = sk-ocean-, $PLUNGE_CONNECTOR_KEY = sk-conn-.

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 money plane is mounted without requireAuth — the inference gateway owns sk-ocean- auth downstream. So the two obvious mistakes both return 401:

An ozk_ key on the models plane → 401 (live-verified):

curl -s -o /dev/null -w '%{http_code}\n' -X POST https://api.plungeai.com/v1/chat/completions \
  -H "Authorization: Bearer $PLUNGE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"anthropic/claude-haiku-4-5","messages":[{"role":"user","content":"hi"}],"max_tokens":1}'

A models-plane 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/llm-agent/execute \
  -H "Authorization: Bearer sk-ocean-EXAMPLE" \
  -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/tools

Header forms

Both header forms work on the execution planes; the models plane takes Authorization only.

Header Planes Example
Authorization: Bearer <key> all -H "Authorization: Bearer $PLUNGE_API_KEY"
X-API-Key: <key> execution planes (ozk_) -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"

How to obtain each key

Key Where Notes
ozk_ Dashboard → 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.
sk-ocean- Dashboard → Gateway → Inference → Keys (https://dashboard.plungeai.com) Self-service; session-authenticated (apps/ocean-dashboard/src/worker/routes/inference.routes.ts, POST /api/inference/keys)
sk-conn- Dashboard → Gateway → Connector → Keys Self-service connector key for the connector proxy; see Connectors

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) orchestration/mcp-gateway/auth-middleware.ts (ipAllowed)
allowed_models sk-ocean- 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- 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_ keys are stored hashed only (key:{sha256} in KV) and shown once at mint time — the plaintext is never at rest (orchestration/mcp-gateway/auth-middleware.ts).

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 (default) 100 10,000
enterprise 300 100,000

An unset tier defaults to pro. A per-key rate_limit override replaces the per-minute cap.

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

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

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 enables permissive CORS (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 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.


3. Models plane

The models plane is the OpenAI-compatible surface: POST /v1/chat/completions, POST /v1/embeddings, GET /v1/models. Base URL https://api.plungeai.com. Auth is an sk-ocean-… model key in Authorization: Bearer — see Authentication & keys. api.plungeai.com/v1/* is a transparent proxy (orchestration/api-gateway/routes/models-proxy.ts) to the inference gateway (inference/gateway/src/); the proxy adds no auth of its own — the inference gateway owns the key, wallet, and metering. gateway.plungeai.com/v1/* reaches the same code directly.

Authenticated models-plane examples in this chapter are verified against the gateway source (inference/gateway/src), not re-run live on 2026-09-18 (no sk-ocean- key was mintable this session). The negative auth checks below carry # verify and were run live.

3.1 Endpoints

Method Path Body Notes
POST /v1/chat/completions JSON Sync or stream:true SSE. Full field reference in 3.2. Handler routes/chat-completions.ts.
POST /v1/embeddings JSON OpenAI provider only in v1 (3.9). Handler routes/embeddings.ts.
GET /v1/models Aggregate catalog, OpenAI shape (3.8). Requires a key. Handler routes/models.ts.

3.2 Request fields

The support words below are: supported (mapped and sent), partial (only part works — the cell says which), ignored (accepted, no effect), rejected (errors), Ocean-only (a field OpenAI does not define).

Every request routes to exactly one provider shape. For openai-shape providers the gateway forwards your body verbatim (callUpstream in routes/chat-completions.ts spreads {...body}, deleting only models and sort) — the upstream provider, not Ocean, interprets each field, so anything the OpenAI/OpenRouter wire accepts passes through. openai-shape providers: openai, groq, mistral, deepseek, xai, together, fireworks, cerebras, perplexity, openrouter, moonshot (routing/model-router.ts PROVIDERS). For anthropic, google, and cohere the gateway builds a fresh provider body (toAnthropicBody / toGoogleBody / toCohereBody in streaming/sse-translate.ts); a field that is not explicitly mapped there is dropped (ignored).

Core fields

Field openai-shape anthropic google cohere Source / note
model supported supported supported supported provider/model slug or bare alias (3.7). Required unless models.
models (array) Ocean-only Ocean-only Ocean-only Ocean-only Ordered fallback list; stripped before an openai-shape upstream. routing/select.ts resolveCandidates (3.3).
sort Ocean-only Ocean-only Ocean-only Ocean-only price | latency | throughput; stripped before upstream. routing/select.ts sortCandidates.
messages supported supported supported supported Required; empty → 400 invalid_request_error "messages required".
stream supported supported supported supported SSE (Streaming).
max_tokens supported supported supported supported Defaults to 4096 on every shape when both token caps are omitted (DEFAULT_MAX_OUTPUT_TOKENS_ESTIMATE). For reasoning models (o1..o9, gpt-5*) it is folded into max_completion_tokens.
max_completion_tokens supported supported supported supported Preferred for reasoning models; on translated shapes it feeds the same output cap.
temperature supported supported supported supported Mapped on every shape.
top_p supported supported (top_p) supported (topP) supported (p)
stop supported supported (stop_sequences) supported (stopSequences) supported (stop_sequences) String or array.
n supported (passthrough) ignored ignored ignored Reserve is sized for n completions; only openai-shape returns multiple choices.
stream_options supported (passthrough) ignored ignored ignored Controls include_usage for openai-shape; translated shapes always emit a final usage (4).
user supported (passthrough) ignored ignored ignored

Messages & content parts

Field openai-shape anthropic google cohere Source / note
content part text supported supported supported supported
content part image_url supported (passthrough) partial ignored ignored anthropic forwards the raw OpenAI part unconverted (toAnthropicBody passes non-text parts as-is) — real Anthropic expects {type:"image",source:…} and may 400. google/cohere extract text parts only.
content part file / input_audio supported (passthrough) partial ignored ignored Same as image_url: forwarded raw to anthropic, dropped for google/cohere.
name (on a message) supported (passthrough) ignored ignored ignored Dropped by every translator.
role:"tool" message supported (passthrough) ignored ignored ignored Dropped for anthropic (if (m.role==='tool') continue); text-only extraction elsewhere.
tool_call_id supported (passthrough) ignored ignored ignored Only meaningful with a passed-through tool loop.
tool_calls (assistant) supported (passthrough) ignored ignored ignored Translators map assistant text, not the tool_calls array.

Tools & structured output

Field openai-shape anthropic google cohere Source / note
tools supported (passthrough) supported (mapped) supported (mapped) ignored anthropic → {name,description,input_schema}; google → functionDeclarations; cohere drops tools (toCohereBody emits none).
tool_choice supported (passthrough) ignored ignored ignored Not mapped by any translator.
parallel_tool_calls supported (passthrough) ignored ignored ignored
response_format supported (passthrough) ignored ignored ignored Not mapped for anthropic/google/cohere — JSON-mode / json-schema works on openai-shape only.

Sampling & OpenRouter extensions

Field openai-shape anthropic google cohere Source / note
top_k supported (passthrough) ignored ignored ignored Not mapped by translators (even though anthropic/google support it natively).
presence_penalty, frequency_penalty, repetition_penalty supported (passthrough) ignored ignored ignored
seed, logit_bias, logprobs, top_logprobs, min_p, top_a supported (passthrough) ignored ignored ignored Forwarded verbatim on openai-shape; the upstream decides.
prediction, modalities, verbosity, web_search_options supported (passthrough) ignored ignored ignored openai-shape passthrough only.
reasoning, reasoning_effort supported (passthrough) ignored ignored ignored Forwarded to reasoning-capable openai-shape providers. On the Agents plane reasoning_effort is a first-class field; here it is passthrough only.
usage (request-level) supported (passthrough) ignored ignored ignored OpenRouter's usage-accounting toggle. Ocean always meters; the field itself just rides through.
provider partial ignored ignored ignored OpenRouter provider-routing object. Ocean does not strip it — it is forwarded verbatim, so real OpenAI/most providers 400 on the unknown key; only openrouter/* accepts it. Use Ocean models/sort instead.
route partial ignored ignored ignored OpenRouter route:"fallback". Forwarded verbatim to openai-shape (most providers 400). Ocean's equivalent is the models array.
transforms, plugins partial ignored ignored ignored OpenRouter-only. Forwarded verbatim to openai-shape; no Ocean handling; most non-OpenRouter upstreams 400.
prompt (legacy) rejected rejected rejected rejected Chat endpoint requires messages — a body without them is 400 "messages required".

3.3 Routing: models, sort, failover

routing/select.ts and the runAttempts loop in routes/chat-completions.ts.

3.4 Presets

Set model to @preset/<slug> (policy/presets.ts). The gateway resolves a stored preset (org-specific row wins over a platform-global one) and merges it before candidate resolution. Precedence: your explicit request fields always win — preset params_json fills defaults, preset routing_json.models/sort apply only when you did not send models/sort; the decision log records @preset/<slug> as the requested model. Unknown or inactive slug → 404 preset_not_found. Presets are provisioned by an operator (signed /admin/presets); there is no public create route.

3.5 Response object

Non-stream body is OpenAI chat.completion; stream chunks are chat.completion.chunk (4).

Field Value
id chatcmpl-… on every shape (openai passthrough keeps the upstream id; translators mint one).
object chat.completion | chat.completion.chunk.
created Unix seconds (upstream's for openai passthrough; Date.now() for translated shapes).
model The served slug (see honest-model note in 3.3).
choices[].message / .delta Standard OpenAI shape.
choices[].finish_reason See vocabulary below.
usage prompt_tokens, completion_tokens, total_tokens, plus prompt_tokens_details.cached_tokens and Ocean's extension prompt_tokens_details.cache_write_tokens when a cache read/write occurred.

finish_reason per shape (mapAnthropicStop / mapGoogleStop, streaming/sse-translate.ts):

Upstream openai anthropic google cohere
normal end stop end_turn,stop_sequencestop STOPstop always stop
length cap length max_tokenslength MAX_TOKENSlength stop
tool call tool_calls tool_usetool_calls (n/a) stop
content filter content_filter (mapped to stop) SAFETY,RECITATIONcontent_filter stop

Usage & cache tokens — Anthropic's input_tokens (which excludes cache) is normalized to OpenAI semantics: prompt_tokens includes the cached portion, and prompt_tokens_details breaks it out. Cached tokens bill at cache rates and are subtracted from full-rate input, never double-counted (metering/pricing.ts computeCost).

3.6 Response headers

Header When Meaning
x-request-id always Server-minted request id (correlation/trace). The client's own x-request-id is never trusted as an id.
x-correlation-id when you send x-request-id Echoes your value.
x-cache cache-eligible + org cache on hit (served from cache, $0) or miss (stored for next time) (3.11).
content-type always application/json or text/event-stream.

The money-plane error body carries request_id inside the JSON envelope; note it does not set X-Error-Code (that header is execution-plane only — see Errors, limits & headers).

3.7 Providers & slugs

Address a model as provider/model (e.g. anthropic/claude-sonnet-4-6). Bare aliases in routing/model-router.ts ALIASES auto-prefix: gpt-4o, gpt-4o-mini, gpt-4.1, gpt-4.1-mini, o1, o1-mini, o3-mini, claude-opus-4-7, claude-sonnet-4-6, claude-haiku-4-5, gemini-2.5-pro, gemini-2.5-flash. Any other bare name (no /) → 404 model_not_found.

Provider id Shape Upstream
openai openai api.openai.com
anthropic anthropic api.anthropic.com/v1/messages
google google generativelanguage.googleapis.com
groq, mistral, deepseek, xai, together, fireworks, cerebras, perplexity, moonshot openai each provider's OpenAI-compatible endpoint
openrouter openai openrouter.ai/api/v1
cohere cohere api.cohere.com/v2/chat

Discover the live catalog with GET /v1/models (3.8) or the discovery plane GET /v1/discovery/search?kind=models (Discovery). Not every provider slug in the map is priced/active — the catalog is the source of truth.

3.8 GET /v1/models

Requires a key. Returns { "object":"list", "data":[ … ] } built from the rate-card snapshot (routes/models.ts, metering/pricing.ts listCatalog). Each entry:

{
  "id": "anthropic/claude-sonnet-4-6",
  "object": "model",
  "created": 0,
  "owned_by": "anthropic",
  "context_length": 200000,
  "max_output": 8192,
  "pricing": { "in": 3.0, "out": 15.0 },
  "capabilities": { "streaming": true, "tools": true, "vision": true, "embedding": false, "fim": false }
}

pricing.in/.out are USD per 1M tokens. vision:false means "not verified", not "verified absent"; max_output:null means unknown; fim is a name heuristic (codestral). This shape differs from OpenRouter's /models — see OpenRouter compatibility.

3.9 Embeddings

POST /v1/embeddings is OpenAI-only in v1 (routes/embeddings.ts): a non-openai/* model → 400 invalid_request_error "Only OpenAI embeddings supported in v1". Body is standard OpenAI (model, input string or array, optional encoding_format); the response is passed through verbatim, including the upstream status on failure. BYOK, guardrails, reserve/settle metering, and trial exemption apply exactly as for chat.

3.10 Guardrails

Org- and key-scoped policies (policy/guardrails.ts), merged strictest-wins and enforced before any spend or upstream call. What a developer sees:

Situation Status Code
Model/provider not on the allow-list (or key allowed_models) 403 model_not_allowed
Prompt matches a blocked-content regex 403 content_blocked
Provider requires your own key on this plan and none is connected 402 byok_required
Org day/week/month spend cap reached 429 spend_cap_exceeded

Spend caps count inference and connector spend plus outstanding wallet holds. On a models[] request the allow-list/require_byok filters narrow the candidate list per entry; only if none survive do you get the error above.

3.11 Response cache

Exact-match, per-org opt-in, default off (metering/response-cache.ts). Eligibility: stream:false and an explicit temperature:0 (an absent temperature defaults to 1 and is not eligible). The key is a SHA-256 of the entire request body scoped by org, so any differing field (tools, response_format, seed, …) is a different entry. A hit returns the stored body with x-cache: hit, bills $0 (no wallet touch, no upstream call); a miss on an eligible request sets x-cache: miss and stores the response (TTL clamped 60s–24h). A hit can never cross an org boundary.

3.12 BYOK-or-billed

Connect your own provider key (BYOK) in the dashboard; it is stored AES-256-GCM per org (routing/byok.ts, table inference_byok_keys on ocean-billing). When a BYOK key exists for the winning candidate's provider, the gateway uses it and bills only the org's flat byok_routing_fee (cost=0, price=byok_fee). A single-candidate BYOK request with a zero routing fee skips the wallet entirely (no reserve/settle/ledger row); a multi-candidate request always reserves (the winner may not be the zero-fee one). Guardrail require_byok can force BYOK for named providers → 402 byok_required if unmet.

3.13 Billing & metering

From inference/CHARGING-README.md. Every request is metered; only active accounts ever pay — an unknown/missing/stale status resolves to trial (metered, never debited).

Status Calls Wallet Monthly cap
trial yes never debited exempt; a per-org $10/mo platform-cost allowance applies (TRIAL_MONTHLY_CAP_USD), then 429 insufficient_quota
active yes reserve→settle at true price enforced
suspended / closed 401 at key resolution

3.14 Not supported today

curl -s "$API_BASE/v1/chat/completions" \
  -H "Authorization: Bearer $PLUNGE_API_KEY" \
  -H "content-type: application/json" \
  -d '{"model":"openai/gpt-4o-mini","messages":[{"role":"user","content":"hi"}],"max_tokens":8}'

The models plane requires an sk-ocean- key; the ozk_ execution-plane key above is rejected with invalid_api_key. GET /v1/models enforces the same:

curl -s "$API_BASE/v1/models" -H "Authorization: Bearer $PLUNGE_API_KEY"

4. Streaming

Three surfaces stream over Server-Sent Events (SSE): the models plane (/v1/chat/completions with stream:true), the workflows plane (/v1/workflows/execute-stream), and the agents plane (/v1/agents/:id/execute with stream:true). The models plane speaks the OpenAI delta wire; the workflows plane speaks the CNL engine's named-event wire; the agents plane re-shapes an engine run into the OpenAI delta wire. All three send Content-Type: text/event-stream and frame events as …\n\n.

4.1 Models-plane SSE

Code: routes/chat-completions.ts (stream branch) + streaming/sse-translate.ts + streaming/usage-meter.ts.

Wire format. Each event is one data: <json>\n\n line whose payload is a chat.completion.chunk, terminated by a literal data: [DONE]\n\n. The gateway injects no comment lines on this plane.

Role primer. Differs by provider shape:

Shape First chunk How
openai-shape (openai, groq, mistral, deepseek, xai, together, fireworks, cerebras, perplexity, openrouter, moonshot) whatever the upstream sends (typically delta:{role:"assistant"}) raw upstream SSE bytes are piped verbatim — Ocean does not add or rewrite chunks.
anthropic delta:{role:"assistant"} injected by Ocean translateAnthropic emits a primer in start().
google, cohere delta:{role:"assistant"} injected by Ocean emitted on the first upstream frame.

Final usage chunk.

Shape Final usage present?
anthropic, google, cohere always — the translator emits a final chunk carrying finish_reason + usage (anthropic adds prompt_tokens_details when cached).
openai-shape only when you send stream_options:{"include_usage":true} — Ocean forwards it verbatim and relays whatever the upstream emits.

Ocean meters tokens regardless (a passive usage-meter observes the passing stream for billing); the client-visible usage chunk on openai-shape still depends on stream_options.

model on stream chunks. On the openai-shape passthrough the per-chunk model is left exactly as the upstream reports it — it is not rewritten to the honest served slug (unlike the non-stream body). Translated shapes stamp the served slug on every chunk.

Response headers: content-type: text/event-stream, cache-control: no-cache, no-transform, x-accel-buffering: no, x-request-id.

Failover vs mid-stream failure. Failover between candidates is decided on the response status, before any SSE byte is read (runAttempts). Once bytes are flowing there is no re-failover: an openai-shape upstream error mid-stream is passed through as the upstream sent it; a translated shape simply stops emitting deltas.

Cancellation / abort. The client's AbortSignal is forwarded to the upstream fetch, so hanging up propagates to the provider. On a mid-stream disconnect the billing flush() never runs (TransformStream cancellation aborts the writable side without flushing) — the reserved hold simply stands until the W3 sweeper reclaims it; flush() (and settle) fire only on a fully-drained normal completion. There is no gateway-imposed stream idle timeout on this plane beyond the Workers runtime and the upstream's own.

4.2 Workflows-plane SSE

Code: routes/workflows.ts + lib/stream-relay.ts (workflowsEncoder) + engine orchestration/cnl-engine/context.ts.

Routes: POST /v1/workflows/execute-stream (inline YAML/JSON body), POST /v1/workflows/:id/execute-stream (saved workflow), and the legacy alias POST /v1/cnl/execute-stream. All require an ozk_ key (Authentication & keys). The relay also drains every provider sub-stream the run advertises and closes the execution row under waitUntil, so a client disconnect never aborts the run.

Wire format. Engine frames pass through verbatim as event: <name>\ndata: <json>\n\n. Provider token deltas are injected as event: token\ndata: {"task_id":"…","delta":"…"}\n\n. Token frames for a task are flushed before that run's terminal frame (ordering guarantee).

Engine event vocabulary (CNLEventType, orchestration/cnl-engine/context.ts):

request_received, workflow_loaded, workflow_started, task_dispatched, task_completed, task_error, task_skipped, streaming_started, streaming_completed, output_delivered, parallel_start, parallel_complete, sequential_start, sequential_complete, dynamic_start, dynamic_generated, dynamic_complete, batch_start, batch_item_start, batch_item_complete, batch_progress, batch_complete, debate_start, debate_round_complete, debate_judgment, debate_complete, condition_evaluated, workflow_yielded, workflow_result, workflow_completed, workflow_error, heartbeat.

Each data payload carries workflow_id, execution_id, timestamp, type, a nested data object, and elapsed_ms. The run ends on workflow_completed (carries final_task_id, execution_summary) or workflow_error (carries error).

Keepalive. The engine emits its own heartbeat events; the relay forwards them verbatim and additionally touches the execution row's updated_at every ~60s so a long run is not swept mid-flight. This plane does not inject comment-line keepalives (that is the agents plane, 4.3).

Reading the final result. After workflow_completed, fetch the stored output with GET /v1/workflows/results/:workflowId/:taskId (use the final_task_id) — see Workflows. While a run is in flight the results route returns 404 not_ready.

4.3 Agents-plane OpenAI-shaped stream

Code: routes/agents.ts (stream branch) + lib/stream-relay.ts (openaiEncoder). Requesting the stream: set "stream": true in the JSON body. This runs the same one-task engine envelope as the sync path but with force_streaming, and the openaiEncoder re-shapes the run into an OpenAI chat.completion.chunk stream — so an OpenAI SDK's streaming client works against an Ocean agent. (For the CNL-native event stream of the same agent, use the workflows plane instead.)

What the client sees — only data: lines and comments, never engine event: lines (they are dropped by the encoder):

Error after the first byte. Once SSE headers are committed, a run error is emitted as data: {"error":{"message":…,"type":"server_error","code":"engine_error"}} and the stream closes without [DONE] (matching OpenRouter's mid-stream-error behaviour). A rejection before the first byte is a normal JSON 502 engine_error instead.

Headers: content-type: text/event-stream; charset=utf-8, cache-control: no-cache, x-accel-buffering: no, x-request-id, and X-Execution-Id (the run id — reuse it to read the result later at GET /v1/agents/results/:workflowId/:taskId, Agents).

4.4 Verified examples

An agent OpenAI-shaped stream — the primer, content deltas, a final usage chunk, and [DONE]:

curl -s -N "$API_BASE/v1/agents/llm-agent/execute" \
  -H "Authorization: Bearer $PLUNGE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"prompt":"say hi in one word","stream":true,"max_tokens":16}'

A workflow event stream — engine frames pass through verbatim as named event: lines:

curl -s -N "$API_BASE/v1/workflows/execute-stream" \
  -H "Authorization: Bearer $PLUNGE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"workflow":"workflow:\n  name: doc-stream\n  tasks:\n    - type: task\n      id: t1\n      agent: llm-agent\n      prompt: say hi"}'

5. OpenRouter compatibility

Point your OpenAI or OpenRouter client at https://api.plungeai.com/v1, swap the key for an sk-ocean-… model key, and POST /v1/chat/completions works with the same provider/model slugs and the same models[] fallback array — Ocean is an OpenAI-compatible router with OpenRouter-style multi-provider addressing. What it is not is a drop-in for OpenRouter's cost-accounting fields, its error shape, or its provider-routing request object. This chapter is the developer-facing matrix; the exhaustive live/static diff (every field probed, request ids logged) is the full comparison log at orchestration/api-gateway/live-test-reports/2026-09-18-openrouter-compat/OPENROUTER-COMPAT-LOG.md.

5.1 Migration from OpenRouter

Concern OpenRouter Ocean Change needed
Base URL https://openrouter.ai/api/v1 https://api.plungeai.com/v1 change base URL
Key sk-or-… sk-ocean-… new key (Authentication & keys)
Auth header Authorization: Bearer Authorization: Bearer none
Model slug provider/model provider/model none (bare aliases also resolve, 3.7)
Fallback list models: [...] models: [...] none — same field, same semantics (3.3)
App attribution HTTP-Referer, X-Title headers ignored (no effect, no error) drop them or leave them — they do nothing
Cost in response usage.cost, cost_details absent — read traces remove reads of usage.cost
Provider routing provider:{...} request object not interpreted (see below) use sort / models / presets

5.2 Endpoints

OpenRouter path Ocean Notes
POST /chat/completions same field caveats in 5.3
GET /models differs different JSON shape (5.6)
POST /embeddings differs OpenAI models only in v1 (3.9)
POST /completions (legacy text) missing chat endpoint only
GET /generation (cost lookup) missing use GET /v1/traces/:id (Traces)
GET /credits, /auth/keys, /key missing balance/keys live in the dashboard
/images, /audio/* missing not offered
/v1/agents, /v1/workflows, /v1/tools, /v1/mcp, /v1/discovery, /v1/traces Ocean-only the execution planes (5.9)

5.3 Request fields

Full per-provider-shape table is in 3.2. Relative to OpenRouter:

Field OpenRouter Ocean
model, models, messages, stream, stream_options, temperature, top_p, max_tokens, stop, tools, tool_choice, response_format, seed, penalties, logit_bias, logprobs, top_k, min_p, top_a, reasoning, reasoning_effort supported openai-shape: forwarded verbatim (same behaviour). On anthropic/google/cohere many are dropped in translation — see 3.2.
sort not a request field Ocean-onlyprice/latency/throughput
provider (routing object) supported partial/rejected — not interpreted; forwarded verbatim, so non-openrouter upstreams 400 on it
route:"fallback" supported partial — not read; use models[]
transforms, plugins supported not interpreted — ride through to openai-shape upstreams (usually 400)
prompt_cache_key, prompt_cache_options, cache_control supported openai-shape passthrough; anthropic honours a cache_control marker on a system text part (3.5), otherwise ignored
usage:{include:true} opt-in cost accounting ignored — Ocean always meters; no cost returned

5.4 Response fields

Field OpenRouter Ocean
id gen-… chatcmpl-…
object, created, model, choices, usage present present (model is the honest served slug, 3.3)
provider (top-level) present absent
openrouter_metadata, system_fingerprint, service_tier present absent (system_fingerprint only if the upstream sent it via passthrough)
choices[].native_finish_reason present absent — only the normalized finish_reason
usage.cost, usage.cost_details, usage.is_byok present absent — cost lives in traces
usage.prompt_tokens_details.cached_tokens present present; Ocean also adds cache_write_tokens (Ocean-only)
error-in-choice (mid-stream) present matched on the agent plane (error frame, no [DONE])

5.5 Streaming

Aspect OpenRouter Ocean
Wire chat.completion.chunk + [DONE] same (4)
Keepalive comment : OPENROUTER PROCESSING none on the money plane (raw passthrough); the agents plane sends : OCEAN PROCESSING
Final usage chunk included (with cost) anthropic/google/cohere always; openai-shape only with stream_options.include_usage; never carries cost
Mid-stream error error object, close without [DONE] matched on the agents plane; money-plane openai passthrough relays the upstream's own bytes

5.6 Models list

OpenRouter GET /models returns data[].{id, canonical_slug, name, created, description, context_length, architecture, pricing:{prompt,completion}, top_provider, supported_parameters, …} where pricing is USD-per-token strings. Ocean returns data[].{id, object, created, owned_by, context_length, max_output, pricing:{in,out}, capabilities:{streaming,tools,vision,embedding,fim}} where pricing.in/.out are USD-per-1M numbers (3.8). A client that reads architecture, supported_parameters, or per-token pricing.prompt will find none of them.

5.7 Errors

OpenRouter's envelope is {"error":{"message":"…","code":401,"metadata":{…}}} — a numeric code, an optional metadata, no type. Ocean's money-plane envelope is {"error":{"message":"…","type":"authentication_error","code":"invalid_api_key","request_id":"…"}} — a string code, a type, a request_id, no metadata. The execution planes use a third shape ({"error":{"code":"…","message":"…"}} + X-Error-Code header). Full tables: Errors, limits & headers.

curl -s "$API_BASE/v1/chat/completions" -H "content-type: application/json" \
  -d '{"model":"openai/gpt-4o-mini","messages":[{"role":"user","content":"hi"}],"max_tokens":8}'

The "code" above is the string invalid_api_key, not the integer 401 an OpenRouter SDK expects.

5.8 What breaks an OpenRouter SDK user today

5.9 Ocean-only capabilities you gain


6. Tools plane

A tool agent is any active registry agent with a typed invocation contract. The tools plane is the shortest path from "I want Brave search" to a result: three routes, one security check, one execution path. Every call becomes a one-task workflow on the engine, so it lands in your execution history and carries an x-request-id you can trace (Traces).

Discover  ──►  GET  /v1/tools                 who is callable (the active catalog)
Inspect   ──►  GET  /v1/tools/:id             the contract: operations, params, examples
Execute   ──►  POST /v1/tools/:id/execute     contract check → trust fence → engine → result

Auth: Authorization: Bearer $PLUNGE_API_KEY (an ozk_ key — see Authentication & keys).

6.1 List the catalog — GET /v1/tools

Returns the active registry agents as summary cards. Paging: limit (default 50, max 100) and offset. The response count is the size of the page you received, not the catalog total — page until a short page comes back.

curl -s "https://api.plungeai.com/v1/tools?limit=100" \
  -H "Authorization: Bearer $PLUNGE_API_KEY"

Response shape: { "tools": [ { "id", "type": "agent", "name", "description", "category", "tags": [...], "status": "active", ... } ], "count" }. Tags worth reading: gated:<operation> marks operations the trust fence will refuse unattended (see 6.4), status:active is the only status this route lists.

On 2026-09-18 the catalog had 92 active tool agents in 14 categories (GET /v1/agents/categories gives the live breakdown; the same query backs GET /v1/agents, so the two lists are identical — the difference is how you call them).

6.2 Inspect the contract — GET /v1/tools/:id

curl -s "https://api.plungeai.com/v1/tools/brave-agent" \
  -H "Authorization: Bearer $PLUNGE_API_KEY"
Field Meaning
agent_id, name, description the card identity
operations[] { name, description?, required_params? } — the verbs the agent accepts as operation; an agent with no operations takes a prompt (or its soleRequiredParam)
inputSchema JSON Schema (type: object, properties, required, additionalProperties) for params; per-operation requireds live on operations[].required_params
examples[] { title, cnl } — copy-paste CNL task snippets from the card's demonstrations
output_type prose description of what comes back
soleRequiredParam present when exactly one string param is required — a bare prompt is mapped onto it
credentials filled by surfaces that know the user's connections; absent here

Unknown id → 404 unknown_tool (also in the X-Error-Code header):

curl -s -o /dev/null -w '%{http_code} %header{x-error-code}\n' \
  "https://api.plungeai.com/v1/tools/no-such-agent" \
  -H "Authorization: Bearer $PLUNGE_API_KEY"

6.3 Execute — POST /v1/tools/:id/execute

Body (JSON by default; the same fields as YAML with Content-Type: text/yaml):

Field Type Notes
operation string one of operations[].name; omit for prompt-only agents
params object validated against the contract; names that collide with task-envelope fields (agent, id, type, …) are reserved and rejected
prompt string free-text input; satisfies missing requireds on prompt-capable agents
format json · yaml · markdown · text response negotiation; Accept and the request Content-Type also count (default JSON, YAML in → YAML out)

Order of evaluation, from orchestration/api-gateway/routes/tools.ts:

  1. Contract validation. reserved names, or missing requireds on a prompt-less call → 422 invalid_params with { missing, reserved, warnings, contract } — the contract is echoed so you can fix the call without a second request.
  2. The trust fence (checkAgentCall, mode unattended) → 403 refused or 409 approval_required — see 6.4. Nothing is executed and no execution row is written.
  3. Execution. A workflow_executions row opens under your user (execution_id = the request's trace id), the engine runs the one-task wrapper, the row closes with the result.

Success (200):

{ "ok": true, "content": "…markdown or text…", "outcome": "ok", "execution_id": "…", "request_id": "…" }
curl -s -X POST "https://api.plungeai.com/v1/tools/brave-agent/execute" \
  -H "Authorization: Bearer $PLUNGE_API_KEY" -H "Content-Type: application/json" \
  -d '{"prompt":"cloudflare workers pricing","params":{"count":3}}'

Operation form (the weather agent takes location; here the contract says what is missing):

curl -s -w '\n%{http_code}\n' -X POST "https://api.plungeai.com/v1/tools/weather-agent/execute" \
  -H "Authorization: Bearer $PLUNGE_API_KEY" -H "Content-Type: application/json" \
  -d '{"operation":"current","params":{}}'

6.4 Outcomes and the trust fence

An agent that cannot do the job answers with a structured outcome, not prose (core/core-base/agent-outcome.ts). The tools plane maps outcomes onto HTTP (routes/_util.ts outcomeStatus):

outcome HTTP error.code What to do
ok 200 read content
needs_input 422 invalid_params fix paramserror.detail and the echoed contract say how
needs_connection 424 connection_required the user behind the key has not connected this provider — see Connectors
needs_api_key 424 credential_required the agent needs a BYO key the user has not stored
unavailable 503 agent_unavailable upstream/provider down — retry later or pick another agent
error 502 execution_failed the agent failed; error.message carries the agent's own text

The error body is { "error": { "code", "message", "outcome", "detail"?, "content"?, "contract"? } } plus an X-Error-Code header.

The trust fence (core/core-base/call-agent.ts) runs before every execution and cannot be turned off from the API:

curl -s -w '\n%{http_code}\n' -X POST "https://api.plungeai.com/v1/tools/resend/execute" \
  -H "Authorization: Bearer $PLUNGE_API_KEY" -H "Content-Type: application/json" \
  -d '{"operation":"send","params":{"from":"hello@example.com","to":"nobody@example.com","subject":"x","text":"x"}}'

A refusal writes no execution row — a denial is not a run.

6.5 The catalog by category (live 2026-09-18)

Ids are exact; GET /v1/tools/<id> is the source of truth for operations. Account = needs a connected account or BYO key for the user behind the key (see chapter 7); everything else runs on platform keys.

Category Agents Account
search (14) brave-agent, exa-agent, firecrawl-agent, tavily-agent, serper-agent, serpapi-agent, crawl4ai-agent, browserbase, arxiv-search, wikipedia-search, newsapi-get-articles, nyt-api, product-hunt-agent, branding-agent no
tools (23) calculator-tool-agent, markitdown, weather-agent, wolfram-alpha-full-results, data-analyst, code-reviewer, technical-writer, research-analyst, research-team, learn, pdf-worker, pptx-worker, xlsx-worker, ms-office-worker, report-worker, render-worker, cloud-browser-agent, gemini-file-search, google-docs-export-worker, n8n-converter, concierge, agentic-agent, universal-agent no (google-docs-export-worker: Google)
ai (3) llm-agent, vision-agent, skill-agent no
google-workspace (25) google-gmail, google-calendar, google-drive, google-docs, google-sheets, google-slides, google-forms, google-tasks, google-contacts, google-chat, google-meet, google-keep, google-classroom, google-admin, google-vault, google-analytics, google-search-console, google-ads, google-merchant-center, google-business-profile, google-books, google-youtube, google-translate, cloud-natural-language, google-workspace-assistant Google (OAuth)
microsoft-365 (12) microsoft-outlook-mail, microsoft-outlook-calendar, microsoft-outlook-contacts, microsoft-onedrive, microsoft-sharepoint, microsoft-teams, microsoft-todo, microsoft-planner, microsoft-onenote, microsoft-excel, microsoft-word, microsoft-powerpoint Microsoft 365 (OAuth)
coding-agents (1) gitlab-agent GitLab (OAuth or PAT)
communication (2) resend, cloudflare-email-agent no — send/send_batch gated
media (2) image-gen-agent, video-gen-agent (generate_video gated) no
financials (1) sec-agent no
entertainment (2) sports-schedule-agent, tv-listings-agent no
ecommerce-agents (2) amazon-shopping-agent, shopping-zinc (buy gated) Amazon retailer login
payment (2) x402-wallet-agent (fetch_paid gated), crossmint-agent wallet — guarded category
blockchain (2) circle-agent, circle-wallet-agent Circle wallet — guarded category
shopping (1) x402-broker (execute gated) wallet

6.6 Using tools from your own LLM loop

The tools plane is designed to sit behind any function-calling model — no SDK required:

  1. GET /v1/tools/:id for the agents you want; turn each contract into a function definition (name = agent id, parameters = inputSchema, one function per operations[].name if you want the model to pick operations).
  2. Send the definitions to your model as tools (any provider — the models plane works too).
  3. When the model returns a tool call, POST /v1/tools/<id>/execute with { "operation", "params" }.
  4. Feed content back as the tool result. On 422 feed back error.message + error.detail — the model fixes its own params. On 403 refused tell the user a human has to approve that action.

Every hop carries x-request-id; keep it in your logs and GET /v1/traces/:id shows the run. For a server-side loop that already does all of this (discover → inspect → execute with an approval gate), call the concierge agent on the agents plane.


7. Connectors & connected accounts

A connector is a third-party integration a user connects once — Gmail, Microsoft 365, GitHub, Adobe, Slack, Stripe, Notion, … A connected account is that user's live credential row. Tool agents then act as that user: google-gmail reads the mailbox of whoever owns the ozk_ key that made the call. Nothing about the credential ever appears in the API.

Three things are easy to confuse:

Thing What it is Where
Tool agent (google-gmail) code that calls a provider's API on behalf of a user tools / agents planes
Connector (google, github, …) the OAuth app or API-key slot a user connects Studio → Integrations
Connector proxy provider (brave-search, firecrawl, …) a metered pass-through to a provider's REST API with your own or the platform's key gateway.plungeai.com/connect/… (7.5)

7.1 Two credential tiers

Tier How the user connects Examples Stored as
OAuth clicks Continue with in Studio, approves at the provider, lands back connected Google Workspace, Microsoft 365, Adobe, GitHub, GitLab, Slack, Salesforce, HubSpot, Bitbucket, Shopify encrypted access + refresh token; refreshed server-side when tokensExpire
API key / PAT pastes a key in the connector dialog Anthropic, OpenAI, Stripe, Notion (internal token), GitLab PAT, Linear, Jira, Datadog, … encrypted key; verified live before storage (core/core-secrets/api-key-verify.ts probes the provider — a rejected key is never saved)

Both land in user_credentials on the compliance database, AES-256-GCM encrypted with per-scope keys; every read is audited (core/core-secrets/CLAUDE.md). Agents never receive raw tokens from callers — the platform injects them at run time, which is why params names such as api_key or token are stripped by contract validation.

7.2 How a connected account flows into an API call

your app ──ozk_ key──► One API ──user_id──► engine ──► google-gmail agent
                                                          │ resolveApiKey(google, user)
                                                          ▼
                                            user_credentials row?  yes → call Gmail as the user
                                                                   no  → outcome: needs_connection

Without a connection the agent answers with a structured refusal (success: false, outcome: needs_connection) and the tools plane returns 424 connection_required:

{ "error": { "code": "connection_required", "message": "Connect your Google account in Studio → Integrations to use google-gmail", "outcome": "needs_connection" } }

The same run with a connection returns 200 { ok: true, content } — live example from 2026-09-18 with a Microsoft 365 connection on the calling account (microsoft-todo, operation list_lists, request id 1ee507f1-a5b6-48c8-a963-51b6fbd0f904).

Which agents need what: the Account column in the tool catalog. The rule of thumb: google-* needs the Google connector, microsoft-* needs Microsoft 365, adobe-* Adobe, gitlab-agent GitLab; search/scrape/document tools run on platform keys.

7.3 How your testers connect an account

Connecting is a browser step in Ocean Studio (https://studio.plungeai.com), because OAuth needs a redirect back to a page the user is logged into:

  1. Studio → IntegrationsBrowse connectors → pick the provider.
  2. OAuth connector: Continue with → approve → back in Studio as Connected. API-key connector: paste the key → it is probed live → Connected (or an inline "rejected" error).
  3. Verify with the API: run the agent with the same user's ozk_ key — the needs_connection outcome disappears.

The Studio endpoints behind those buttons (session cookie, not API key — listed so you know what happens, not for you to call): GET /api/connections (yours), GET /api/connections/providers (public: every OAuth provider and its scopes), GET /api/auth/oauth/:provider/start, POST /api/connections/api-key (400 when the live probe rejects the key), DELETE /api/connections/:provider. Special flows exist for the Circle agent wallet (/api/connections/circle-agent-wallet/init|verify), retailer logins such as Amazon (/api/connections/retailer/connect) and QR/browser logins such as WhatsApp (/api/connections/browser-session/*).

curl -s "https://studio.plungeai.com/api/connections/providers"

Redirect URIs and OAuth app registration are platform-side — testers never configure anything at the provider.

7.4 The connector catalog

The registry lists 300 connector cards (GET /v1/discovery/search?kind=connectors&limit=100, page with offset; live 2026-09-18). Each card carries connector.auth:

connector.auth Meaning Count
oauth_static a platform-registered OAuth app; user clicks Continue ~45
api_key user pastes a key/PAT ~90
oauth_dcr an MCP server the user connects with Dynamic Client Registration (see MCP plane) ~110
none / absent platform-key or public provider — nothing to connect ~55
curl -s "https://api.plungeai.com/v1/discovery/search?kind=connectors&limit=3" \
  -H "Authorization: Bearer $PLUNGE_API_KEY"
curl -s "https://api.plungeai.com/v1/discovery/cards/connector/slack" \
  -H "Authorization: Bearer $PLUNGE_API_KEY"

OAuth apps registered on the platform today (client id wired in the Studio worker, wrangler-templates/template.wrangler.ocean.jsonccore/core-secrets/oauth-providers.ts): google (Workspace: Drive, Gmail, Calendar, Contacts, Tasks, Docs, Sheets, Slides, Analytics, Ads, Search Console, YouTube, …), azure (Microsoft 365: OneDrive, Outlook, Calendar, Teams, SharePoint, To Do, Planner, OneNote, Office files), adobe (Creative Cloud, Express, Photoshop, Lightroom, Stock, Experience Platform, Journey Optimizer), github, gitlab, bitbucket, slack, salesforce, hubspot, shopify. Provider entries also exist for sonos, spotify, tesla, facebook, linkedin, twitter, reddit, tiktok, pinterest, snapchat, intuit, square, zoom, sentry, notion — their cards show in Studio, but the OAuth app is not registered yet, so Continue with … bounces with oauth_not_configured; connect those with an API key where the card offers one.

By category (ids as the registry spells them):

Group Connectors
AI & ML openai, anthropic, huggingface, ibm
Google google (Workspace umbrella), google-drive, google-gmail, google-calendar, google-contacts, google-tasks, google-youtube
Microsoft azure (Microsoft 365 umbrella), microsoft-onedrive, microsoft-outlook-mail, microsoft-outlook-calendar, microsoft-teams, microsoft-sharepoint, microsoft-365
Adobe adobe, adobe-express, adobe-photoshop, adobe-lightroom, adobe-stock, adobe-cc-files, adobe-aep, adobe-ajo, adobe-workfront
Developer & cloud github, gitlab, bitbucket, postman, aws, cloudflare, vercel, netlify, supabase, databricks, snowflake
Observability sentry, datadog, grafana-mcp-server, honeycomb, incident-io, logrocket-mcp, jam
Communication slack, discord, telegram, twilio, sendgrid, mailchimp, resend, whatsapp, whatsapp-personal
Productivity & PM notion, airtable, asana, jira, linear, monday, clickup, trello, basecamp, teamwork, craft, mem, roam-research, ticktick, … (28 productivity + 19 project-management cards)
CRM, sales & marketing salesforce, hubspot, pipedrive, zoho, close, helpscout, intercom, attio, apollo-io, klaviyo, lemlist, outreach, salesloft, zoominfo, … (49 marketing-and-sales cards)
Payments & finance stripe, paypal, square, plaid, xero, circle, circle-agent-wallet, x402-wallet, intuit, intuit-quickbooks, intuit-payments, intuit-payroll, intuit-time-tracking, airwallex
Data & analytics amplitude, mixpanel, segment, looker, alpha-vantage
Search & scraping (platform-key providers) brave-search, exa, firecrawl, crawl4ai, browserbase, scrapingbee, serpapi, serper, perplexity-search, news-api, nyt, arxiv, wikipedia, product-hunt, weather, wolfram-alpha
Social facebook, instagram, threads, linkedin, twitter, reddit, pinterest, snapchat, tiktok, youtube, bereal, clubhouse
Commerce & travel shopify, amazon, target-app, shop, instacart, expedia, booking, tripadvisor, uber, viator, kiwi-com, amadeus, …
Legal, security, research, lifestyle 13 legal, 15 security, 8 research and 11 lifestyle MCP-server connectors (oauth_dcr / none)

7.5 The connector proxy — gateway.plungeai.com/connect/:provider/proxy/*

For providers you would otherwise call directly, the inference gateway offers a metered pass-through (inference/gateway/src/routes/connector-proxy.ts): one sk-conn- key, one bill, transparent bytes in both directions (streaming included, no body transformation).

Key sk-conn-… — Dashboard → Connectors → API keys (https://dashboard.plungeai.com); per-org, optional allowed_providers and monthly_limit_usd
Route ANY https://gateway.plungeai.com/connect/<provider_id>/proxy/<upstream path> — the path after /proxy/ is appended to the provider's base_url
Mode (from the connector_providers rate card) byok — your own upstream token connected for the org is used, you pay byok_routing_fee; platform — the platform's key is used, you pay price_per_call; both — your token if connected, else the platform's
No connection, provider is BYOK-only 402 "connection required"
Unknown or inactive provider 404 model_not_found / 400 provider_unavailable
Limits per-provider rate_limit_rpm; org spend caps and guardrails apply exactly as on the models plane
Auth failures 401 invalid_api_key; a suspended or closed account is 401 Account suspended

Example (verified against the source, not run live — no sk-conn- key was available on 2026-09-18):

curl "https://gateway.plungeai.com/connect/brave-search/proxy/res/v1/web/search?q=cloudflare+workers" \
  -H "Authorization: Bearer $PLUNGE_CONNECTOR_KEY"

Each call is metered as a connector_requests row and appears in the Dashboard next to your inference spend. Prefer the tools plane when you want the agent's judgement (result formatting, pagination, outcome vocabulary); use the proxy when you want the provider's raw API with one key and one bill.


8. Discovery

Discovery is the read-only catalog behind every other plane: agents, skills, plugins, MCP servers, connectors, models, providers and (once you have saved some) workflows. Three routes, all ozk_-authenticated, all backed by the registry service (orchestration/api-gateway/routes/discovery.tsorchestration/registry/).

8.1 Search — GET /v1/discovery/search

Param Values Notes
q text natural-language or keyword query; with q the registry runs hybrid search (semantic + keyword), without it a plain listing
kind agents · skills · plugins · mcp-servers · connectors · models · providers · workflows · experts · backgrounds · personas (= digital-twins) the plural catalog groups (registry.ts KIND_GROUPS)
type singular storage type (agent, skill, mcp_server, …) alternative to kind
category e.g. search, google-workspace exact card category
status active (default) the public catalog shows callable capabilities only; other statuses need an explicit value
tier card tier tag e.g. verified
mode hybrid (default with q) · keyword · vector force one search leg
fields summary · full full adds parameters, demonstrations and prompt_format
limit, offset ints limit is capped by the registry; page with offset
include quality attaches a quality object per agent card — { score, success_rate, runs } or null when the evaluation service has no data (it was null for every card on 2026-09-18)

Response: { "cards": [...], "count": <cards in this page>, "searchMethod": "text" | "hybrid" | "vector", "query": { …the normalised query… } }. count is the page size, not the catalog total.

curl -s "https://api.plungeai.com/v1/discovery/search?q=web+search&kind=agents&limit=3" \
  -H "Authorization: Bearer $PLUNGE_API_KEY"
curl -s "https://api.plungeai.com/v1/discovery/search?q=llm&kind=agents&limit=2&include=quality" \
  -H "Authorization: Bearer $PLUNGE_API_KEY"

Catalog sizes on 2026-09-18 (limit=100, first page): agents 92, skills 12, plugins 5, providers 11, mcp-servers 100+, models 100+, connectors 300 (3 pages), workflows 0 for a fresh key (saved workflows are per user), experts / backgrounds / personas 0 (capability cards are created by users and missions — see Skills, plugins & capability injection).

8.2 The card object

Summary cards (fields=summary, the default for the agents/tools lists) carry: id, type, name, version, description, category, best_for[], not_for[], tags[], status, icon, color, created_at, updated_at. Full cards add parameters[], demonstrations[], prompt_format, output_type, output. Connector cards add a connector object (auth, provider_key, key_placeholder, help_text); model cards add pricing and capability columns from the rate card.

Tags encode the operational facts the planes act on:

Tag Meaning
status:active callable — the only status the tools/agents planes accept
gated:<op> the trust fence refuses <op> unattended (403) — Tools plane
tier:verified reviewed card
auth:oauth_static · auth:api_key · auth:oauth_dcr how a connector is connected — Connectors
serves:agent a connector some tool agent uses

8.3 Recommend — POST /v1/discovery/recommend

Body { "type": "agent" | "skill" | … (plural kinds accepted as aliases), "task": "<what you want done>" } — the registry ranks cards for the task. Status 2026-09-18: this route returned 502 upstream_error for every request (the registry's /api/cards/recommend failed upstream; the router replaces raw upstream 5xx with a clean envelope). Until it is fixed, use GET /v1/discovery/search?q=<task>&kind=agents — the same hybrid ranking, one card list. The defect is logged in orchestration/api-gateway/live-test-reports/2026-09-18-openrouter-compat/OPENROUTER-COMPAT-LOG.md (follow-ups) and in log/2026-09-18-one-api-docs-3.0.md.

8.4 One card — GET /v1/discovery/cards/:type/:id

Returns the full card as markdown with YAML frontmatter (Content-Type: text/markdown) — the same document the platform's own agents read before calling a tool. :type is the singular storage type (agent, skill, plugin, mcp_server, connector, model, provider, workflow, expert, background, digital_twin); the plural kind spellings (agents, mcp-servers, …) are accepted as aliases.

curl -s "https://api.plungeai.com/v1/discovery/cards/agent/brave-agent" \
  -H "Authorization: Bearer $PLUNGE_API_KEY"

Unknown card → 404 not_found. A 4xx from the registry is normalised into the One API error envelope; a registry 5xx never reaches you raw (502 upstream_error).

8.5 From an id to a call

Card type Use it as
agent GET /v1/tools/<id> + POST /v1/tools/<id>/execute (tools); POST /v1/agents/<id>/execute (agents); agent: <id> in a CNL task (workflows)
model "model": "<id>" on POST /v1/chat/completions — ids are provider/model slugs (models); also model: + provider: on an agent-plane call
provider the provider/ prefix of model slugs; provider: on agent-plane calls
skill · plugin · expert · background · digital_twin skills: [<id>], plugins: [<id>], experts: [<id>], backgrounds: [<id>], persona: <id> on a type: harness task (capability injection)
mcp_server POST /v1/mcp/runs with the server id (MCP plane); mcp: [<id>] on a harness mission
connector connect it in Studio → Integrations; then the agents that serves:agent it stop returning needs_connection (connectors)
workflow POST /v1/workflows/<id>/execute (workflows)

Search results are cached briefly on the platform side (KV + LRU, ~200 ms per uncached call); agents that discover at run time — concierge, the harness loop — hit this same route, so what you see is what they see.


9. Agents plane

The agents plane runs one registry agent per request and hands back its result. It is the execution plane you reach with an ozk_ key (see Authentication) at POST /v1/agents/:id/execute. Every call wraps exactly one minimal CNL task in the engine envelope, so the agents plane is the single-task front door; multi-task orchestration lives on the Workflows plane and bounded autonomous missions on a type: harness task there (see capability injection).

Base URL is always https://api.plungeai.com. All examples below use $PLUNGE_API_KEY for an ozk_… key. Source: orchestration/api-gateway/routes/agents.ts.

9.1 Agent tiers

An agent is a registry card whose id equals its Worker service name. There are ~90 active agents (GET /v1/agents returned count: 92, live-verified 2026-09-18). They fall into these tiers. Every id below is status:active in the registry unless the row says otherwise (agents/agents/README.md, agents/agents/CORE-AGENTS-DECISION.md).

Tier id(s) What it is Use when Call how
Tool agent ~90 ids: serper-agent, exa-agent, tavily-agent, sec-agent, serpapi-agent, markitdown, weather-agent, … a registry agent with a typed invocation contract (parameters/operations) you need one bounded capability (search, documents, finance, a connector) POST /v1/agents/:id/execute with a prompt; or the Tools plane POST /v1/tools/:id/execute with typed params; or an agent: task in a workflow
llm-agent llm-agent thin single-shot LLM call — YAML in → provider RPC → result plain generation with a pinned model/provider agents-plane execute; model/provider/system/messages
skill-agent skill-agent single-shot LLM with persona/expert/skill prompt injection generation shaped by a persona or skill agents-plane execute with persona; skills via a workflow (chapter 14)
speed-agent speed-agent ultra-low-latency stateless pass-through to the provider (the internal perf baseline) latency-critical simple calls not status:active on the ozk plane today — live-verified 2026-09-18 it returns 403 agent_not_active; used inside workflows run by the engine
Harness loop universal-agent, agentic-agent a bounded ReAct mission runtime: tool fence, iteration cap, long-term memory, capability injection multi-step autonomous work that calls tools/sub-agents a type: harness task on the Workflows plane (recommended) — the agents plane runs these single-shot only (no mission fields, see 9.3)
concierge concierge interactive Discover→Inspect→Execute front-door over any agent, with a payment/approval gate "do X" where the platform must find and call the right agent, with a money gate its own approval contract (9.7) and the MCP plane plungeai_continue; the raw agents plane forwards only generation knobs
claude-managed-agent deployed, not in the active list Anthropic hosted Managed Agents runtime for autonomous cloud code execution autonomous cloud code runs (higher infra overhead) not exposed on the ozk agents plane today — live-verified 2026-09-18 it is absent from GET /v1/agents, so /v1/agents/claude-managed-agent/execute returns 404 unknown_agent
Campaign kind Studio kind: campaign (no campaign-agent Worker) a scheduled, ledger-driven workflow that works a task list in short runs recurring bounded batch work via the scheduler / MCP plungeai_schedule / Studio, not a callable agent id (agents/agents/campaign-agent/README.md)

The bare loop-runtime binding harness-agent is not a callable registry id on the planes (live-verified: 404 unknown_agent), and speed-agent is not status:active there (403 agent_not_active). Call the harness loop through universal-agent / agentic-agent, or through a type: harness task on the workflows plane; llm-agent / skill-agent are the directly-callable single-shot ids (both live-verified).

9.2 List agents & categories

curl -s "https://api.plungeai.com/v1/agents?limit=5" \
  -H "Authorization: Bearer $PLUNGE_API_KEY"

9.3 Execute — body reference

POST /v1/agents/:id/execute. JSON body (default) or an Ocean-format text/yaml body with the same fields. Every accepted field, from routes/agents.ts:

Field Type Status Meaning
prompt string supported, required the per-run input. Missing/blank → 400 missing_prompt
input string supported alias for prompt
sync boolean supported (default true) false202 pointer, poll the result route (9.5)
stream boolean supported (default false) true → an OpenAI-shaped SSE stream (9.6)
messages array partial forwarded to the agent as messages; prompt is still required even when present (live-verified: messages alone → 400 missing_prompt). Multi-turn threading depends on the target agent — for reliable conversation use the MCP plane (session_id, plungeai_continue)
system string supported system-prompt frame forwarded to the agent
model string supported pin a model; validated against the catalog → 422 unknown_model if unknown/inactive
provider string supported pin a provider; normalised (googlegemini). Must travel with model — a pinned model without its provider routes to the agent default
temperature number supported sampling temperature
top_p number supported nucleus sampling
max_tokens / maxTokens number supported output-token budget (must be > 0)
reasoning_effort string supported reasoning-effort hint forwarded to the model
thinking_level string supported thinking-level hint forwarded to the model
persona string supported the only capability field forwarded on this plane
format json|yaml|markdown|text supported response format (also ?format= or Accept). Unknown value → 400 invalid_format (lib/format.ts)

Fields not in this table (e.g. goal, mission, allowed_tools, skills, experts, mcp, approved, action_token) are ignored on the agents plane — run those through a type: harness task on the Workflows plane.

Sync execute (default)

curl -s "https://api.plungeai.com/v1/agents/llm-agent/execute" \
  -H "Authorization: Bearer $PLUNGE_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"prompt":"Reply with only the word: pong","max_tokens":24}'

Success (200): { "content": "...", "workflow_id": "...", "task_id": "t1", "request_id": "..." }. request_id equals the trace id (see Traces).

Model / provider pinning

Pin a model and provider together; discover models with GET /v1/models (Models plane) or GET /v1/discovery/search?kind=models (Discovery).

curl -s "https://api.plungeai.com/v1/agents/llm-agent/execute" \
  -H "Authorization: Bearer $PLUNGE_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"prompt":"Reply with only: pong","provider":"openai","model":"gpt-4o-mini","max_tokens":16}'

9.4 Async execution (202 + poll)

Send "sync": false. The response is 202 with a pointer { workflow_id, task_id, request_id }; read the result from the result route once it is ready.

RESP=$(curl -s "https://api.plungeai.com/v1/agents/llm-agent/execute" \
  -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \
  -d '{"prompt":"Reply with only the word: pong","max_tokens":24,"sync":false}')
echo "$RESP"
WF=$(printf '%s' "$RESP" | sed -n 's/.*"workflow_id":"\([^"]*\)".*/\1/p')
TASK=$(printf '%s' "$RESP" | sed -n 's/.*"task_id":"\([^"]*\)".*/\1/p')
for i in 1 2 3 4 5 6 7 8; do
  R=$(curl -s "https://api.plungeai.com/v1/agents/results/$WF/$TASK" \
    -H "Authorization: Bearer $PLUNGE_API_KEY")
  case "$R" in *'"content"'*) echo "$R"; break;; *) sleep 2;; esac
done

9.5 Results route

GET /v1/agents/results/:workflowId/:taskId{ content, content_type, workflow_id, task_id }. It reconciles the run's terminal state (lib/execution-row.ts):

Situation Status Code
result stored 200
run still in flight 404 not_ready
run failed 409 execution_failed
run completed but produced no visible content 422 empty_completion (raise max_tokens ≥ 64)

9.6 Streaming

Send "stream": true on POST /v1/agents/:id/execute. The agents plane emits an OpenAI-shaped chunk stream (the only stream shape on this plane) — Content-Type: text/event-stream, data: lines only (no named event: lines), each a chat.completion.chunk, terminated by data: [DONE]. This mirrors the models plane; see Streaming. Relay + encoder: lib/stream-relay.ts (relayEngineStream, openaiEncoder).

Exact request shape:

curl -sN "https://api.plungeai.com/v1/agents/llm-agent/execute" \
  -H "Authorization: Bearer $PLUNGE_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"prompt":"Reply with only the word: pong","max_tokens":24,"stream":true}'

Observed wire (live-verified 2026-09-18):

data: {"id":"chatcmpl-…","object":"chat.completion.chunk","created":…,"model":"gemini-3.1-flash-lite","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}

data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"pong"},"finish_reason":null}]}

data: {"id":"chatcmpl-…","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":72,"completion_tokens":1,"total_tokens":73}}

data: [DONE]

Notes: model is the pinned model, else the provider's default (never the agent id); a final usage is included when the provider reported the token split. On an error after the first byte the stream emits data: {"error":{…}} and closes without [DONE] (OpenRouter behaviour); a pre-dispatch rejection is a JSON error (see 9.8), not a stream.

9.7 The concierge approval flow

concierge runs a bounded Discover→Inspect→Execute loop with three tools — find_agent (discover), agent_card (inspect), run_agent (execute) — and a payment gate (agents/agents/concierge-agent/README.md). Money operations (buy, purchase, send, transfer, pay, withdraw, shop, …) and any card gated:<op> are held for approval:

  1. A gated action returns { "needs_approval": true, "pending_action": {…}, "action_token": "<10-char>", "messages": [...] }.
  2. Re-invoke with the same messages, plus "approved": true and the action_token, to execute and continue. The token is held server-side for 1 hour. ask_user questions work the same way (append the user's answer to messages).

On the One API this handshake is surfaced through the MCP plane: the pause arrives as a ⏸ continuation, and you approve with plungeai_continue { approve: true } (never approve on the user's behalf). The raw agents plane forwards only generation knobs + messages + persona — it does not forward approved/action_token, so drive the multi-turn approval loop over MCP, Studio, or the concierge agent's own endpoint. (Not live-verified here: an approval run can spend money.)

9.8 Errors

Pre-dispatch and post-dispatch errors from routes/agents.ts / routes/_util.ts. Full envelope and header reference: Errors, limits & headers.

Status Code When
400 missing_prompt no prompt/input string
400 invalid_format format is not json/yaml/markdown/text
404 unknown_agent no such agent id (fenced before dispatch)
403 agent_not_active the card is not status:active
422 unknown_model pinned model absent/inactive in the catalog
422 empty_completion run completed but produced no visible content
422 invalid_params agent outcome needs_input
424 connection_required outcome needs_connection (connect an account — chapter 7)
424 credential_required outcome needs_api_key
503 provider_unconfigured the provider has no API key configured on the platform
503 agent_unavailable outcome unavailable
502 engine_error / execution_failed the engine/agent failed
502 result_unavailable run finished but the result could not be retrieved
409 duplicate_execution_id a replayed x-trace-id UUID
413 payload_too_large body over the cap (default 1 MiB)
curl -s "https://api.plungeai.com/v1/agents/no-such-agent-xyz/execute" \
  -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \
  -d '{"prompt":"hi","max_tokens":8}'

10. Workflows plane

The workflows plane runs a CNL workflow — one or many agent tasks orchestrated by the engine — and hands back a pointer to the result. Reach it with an ozk_ key (Authentication) at POST /v1/workflows/execute (inline) or POST /v1/workflows/:id/execute (saved). Base URL is always https://api.plungeai.com. Source: orchestration/api-gateway/routes/workflows.ts, engine orchestration/cnl-engine/.

Where the Agents plane runs one task, the workflows plane runs a whole DAG: parallel fan-out, sequential stages, data flow between tasks, and bounded autonomous type: harness missions.

10.1 CNL in one page

CNL (the YAML workflow language) has a workflow with a name and a tasks[] array. Each task is either a leaf task or a compound block; a top-level input: (sibling of workflow:) seeds the run. Field names are from orchestration/cnl-engine/context.ts, schema-types.ts and the real files in workflows/*.yaml.

Field On Meaning
name workflow workflow name
tasks[] workflow ordered list of tasks/blocks
type task task | parallel | sequential | harness | dynamic | batch | debate (the TaskType enum also has the legacy agent_loop and internal output / validate)
id task task id — the key its output is stored under
agent leaf task the agent id to run (must be status:active, or the run is fenced 404/403)
prompt leaf task the task input; search agents take the literal query
operation leaf task a named operation for a structured tool-agent (e.g. serpapi-agent operation: amazon)
(agent params) leaf task any extra card parameters as task fields (e.g. num: 15, searchType: shopping)
subtasks[] compound block children of a parallel / sequential / … block

Data flow. There is no depends_on — a reference is the dependency:

A real 3-task workflow — a parallel search fan-out feeding one LLM synthesis (adapted from workflows/amazon-price-comparison.yaml):

workflow:
  name: price-check
  tasks:
    - type: parallel
      id: search_phase
      subtasks:
        - type: task
          id: amazon_search
          agent: serpapi-agent
          operation: amazon
          prompt: "{input}"
          num: 15
        - type: task
          id: shopping_search
          agent: serper-agent
          searchType: shopping
          prompt: "{input}"
    - type: task
      id: recommendation
      agent: llm-agent
      prompt: |
        For the search "{input}", pick the top 3 products.
        Amazon results: {data:amazon_search}
        Shopping results: {data:shopping_search}
        Use only links that appear verbatim in the data.
input: "wireless headphones"

10.2 Inline execute

POST /v1/workflows/execute accepts both body shapes (routes/workflows.ts):

Success is a pointer, not the content: { success: true, workflow_id, final_task_id, request_id }. Read the content from the result route (10.5).

curl -s "https://api.plungeai.com/v1/workflows/execute" \
  -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \
  -d '{"workflow":{"name":"doc-demo","tasks":[{"type":"task","id":"t1","agent":"llm-agent","prompt":"Reply with only the word: pong"}]},"input":"go"}'

The same run as a raw YAML body (the response mirrors the request Content-Type → YAML, see 10.6):

printf 'workflow:\n  name: doc-demo-yaml\n  tasks:\n    - type: task\n      id: t1\n      agent: llm-agent\n      prompt: "Reply with only the word: pong"\n' \
| curl -s "https://api.plungeai.com/v1/workflows/execute" \
  -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: text/yaml' --data-binary @-

10.3 Streaming — execute-stream

POST /v1/workflows/execute-stream (same body shapes) forces the engine to stream and relays every engine SSE frame verbatim, plus a relay-injected event: token per provider delta (lib/stream-relay.ts workflowsEncoder). This is the named-event stream (distinct from the OpenAI-shaped agent stream in Streaming). Event vocabulary (context.ts CNLEventType):

Event When Payload (data:)
request_received run accepted request metadata
workflow_loaded workflow parsed workflow name/id
workflow_started execution begins workflow id
task_dispatched a task is sent to its agent {task_id, agent, streaming?}
streaming_started a task's provider stream opened {task_id, stream_url}
token a provider delta (relay-injected) {task_id, delta}
streaming_completed a task's provider stream closed {task_id}
task_completed a task finished {task_id, …}
task_error a task failed {task_id, error}
output_delivered a task output was stored {task_id}
parallel_start / parallel_complete a parallel block boundary block id
sequential_start / sequential_complete a sequential block boundary block id
batch_* / debate_* / dynamic_* block-specific progress block payload
condition_evaluated a task condition was resolved {result}
heartbeat idle keepalive
workflow_result final result frame {final_task_id, …}
workflow_completed terminal (success) {final_task_id, execution_summary}
workflow_error terminal (failure) {error}

The terminal event is workflow_completed (or workflow_error). Live-verified 2026-09-18 the stream carried: request_received, workflow_loaded, workflow_started, task_dispatched, streaming_started, token, streaming_completed, task_completed, workflow_result, workflow_completed.

printf 'workflow:\n  name: stream-demo\n  tasks:\n    - type: task\n      id: t1\n      agent: llm-agent\n      prompt: "Reply with only the word: pong"\n' \
| curl -sN "https://api.plungeai.com/v1/workflows/execute-stream" \
  -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: text/yaml' --data-binary @-

10.4 Saved workflows

A saved workflow has an id in the engine's KV (workflow:{user}:{id}). Create one in Studio or over the MCP plane (plungeai_build_workflow from a goal, or plungeai_workflow action: create). Then run it by id:

# not run here — requires a saved workflow id you own
curl -s "https://api.plungeai.com/v1/workflows/<workflow_id>/execute" \
  -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \
  -d '{"input":"quarterly numbers"}'

Body: { "input"?: any, "inputs"?: any }. An unknown id → 404 workflow_not_found. A streaming variant POST /v1/workflows/:id/execute-stream exists with the same body and the 10.3 event vocabulary.

10.5 Results route

GET /v1/workflows/results/:workflowId/:taskId{ content, content_type, workflow_id, task_id }, with the same reconciliation as the agents plane: 404 not_ready (in flight), 409 execution_failed, 422 empty_completion.

RESP=$(curl -s "https://api.plungeai.com/v1/workflows/execute" \
  -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \
  -d '{"workflow":{"name":"res-demo","tasks":[{"type":"task","id":"t1","agent":"llm-agent","prompt":"Reply with only the word: pong"}]}}')
WF=$(printf '%s' "$RESP" | sed -n 's/.*"workflow_id":"\([^"]*\)".*/\1/p')
FT=$(printf '%s' "$RESP" | sed -n 's/.*"final_task_id":"\([^"]*\)".*/\1/p')
for i in 1 2 3 4 5 6 7 8; do
  R=$(curl -s "https://api.plungeai.com/v1/workflows/results/$WF/$FT" \
    -H "Authorization: Bearer $PLUNGE_API_KEY")
  case "$R" in *'"content"'*) echo "$R"; break;; *) sleep 2;; esac
done

10.6 Response formats

One negotiated payload, four wires (lib/format.ts): json (default), yaml, markdown, text. Precedence: body.format / ?format=Accept header → the request Content-Type mirror (text/yamlyaml, else json). An explicit unknown format value → 400 invalid_format; an unknown Accept type falls through to the mirror. markdown renders through the same renderer the MCP plane uses; text is the bare result content.

10.7 The type: harness task

A type: harness task binds an inline mission onto the harness loop runtime — a bounded ReAct agent with a tool fence, iteration cap, long-term memory, and capability injection. All fields and a full example are in Skills, plugins & capability injection; in short:

workflow:
  name: research-brief
  tasks:
    - type: harness
      id: brief
      agent: universal-agent
      goal: "One-paragraph brief on the latest Anthropic model; every claim a source URL."
      mission: "You are a research analyst. Produce sourced, factual briefs."
      allowed_tools: [web_search, web_fetch, task_complete]
      success_criteria:
        - "Every factual claim is followed by a source URL."
      max_iterations: 8
      skills: [research]

10.8 Legacy aliases, limits & headers

Deprecated aliases (same auth, same workflow-plane handlers; prefer /v1/workflows/*): POST /v1/execute, POST /v1/cnl/execute, POST /v1/cnl/execute-stream (api-gateway.ts).

Limits (see Errors, limits & headers for the full table):


11. MCP plane

The MCP plane is bidirectional: inbound, the platform is an MCP server you point a client at; outbound, you connect the platform to other MCP servers and call their tools during a run. Both use an ozk_ key (Authentication). Base URL is https://api.plungeai.com. Source: orchestration/api-gateway/routes/mcp.ts, orchestration/mcp-gateway/, orchestration/mcp-executor/.

11.1 Inbound — the platform as an MCP server

POST /v1/mcp is a Streamable-HTTP MCP endpoint that proxies (signed) to the mcp-gateway. It is stateless — no Mcp-Session-Id is required (send one and it is forwarded). Authenticate with an ozk_ bearer. It speaks plain JSON-RPC: initialize, then tools/list / tools/call. Protocol version 2025-06-18 (orchestration/mcp-gateway/USER-GUIDE.md).

https://mcp.plungeai.com/mcp is the same MCP server (the gateway's own hostname); https://api.plungeai.com/v1/mcp is the One API route to it with an ozk_ bearer. Use whichever your client makes easiest.

A convenience listing without JSON-RPC:

curl -s "https://api.plungeai.com/v1/mcp/tools" \
  -H "Authorization: Bearer $PLUNGE_API_KEY" \
  -H 'Accept: application/json'

The same over JSON-RPC (what an MCP client sends):

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

The 20 plungeai_* tools

tools/list returns 20 tools (live-verified 2026-09-18), in this order (orchestration/mcp-gateway/server.ts):

Tool One line
plungeai_execute_workflow run a CNL workflow (saved id or ad-hoc YAML); mode:"async" for long runs
plungeai_execute_agent run one prompt-driven agent (e.g. llm-agent, skill-agent), optionally with a session_id
plungeai_get_tool_contract the exact invocation contract for one agent (params schema, operations, live credential status)
plungeai_execute_tool run one structured tool-agent with typed {agent_id, operation?, params}
plungeai_run_mission run a bounded autonomous mission (goal + optional fence/iteration cap); async by default
plungeai_learn manage your private skill library — learn / list / forget
plungeai_list_agents discover platform building-block agents (the capability registry)
plungeai_list_workflows list your saved workflows (what users call "my agents")
plungeai_get_result fetch one execution's result (full conversation thread for chat runs)
plungeai_get_workflow_status check an execution's status; returns a continuation when paused
plungeai_workflow manage workflows — create/get/update/delete/versions
plungeai_executions browse execution history — list/get/output/conversation/delete
plungeai_followup ask a follow-up about a completed execution
plungeai_continue continue a paused run — answer (message) or approve (approve:true)
plungeai_chat persistent chat with the platform assistant — send/new/list_sessions/history
plungeai_build_workflow generate or refine a saved workflow from a goal
plungeai_memory long-term memory scoped to you — recall/remember/search_runs/get_run
plungeai_templates workflow templates — list/get/use
plungeai_schedule scheduled (cron) jobs — stats/list/get/create/update/pause/resume/delete/run_now/runs
plungeai_whoami the authenticated identity — user id, tier, key label, rate-limit window

Connecting a client

11.2 Outbound — MCP runs against other servers

The outbound surface connects the platform to registry MCP servers, lists their tools, calls them, and tears the run down. Secrets are resolved server-side in the mcp-executor (never in your request): ${VAR} → a platform secret; ${{ credentials.<provider> }} → the acting user's BYOK key / OAuth token, a miss becoming a structured needs_connection (orchestration/mcp-executor/CLAUDE.md). Each run is a per-tenant Durable Object keyed customerUuid:runId; background runs are held in resumable ~10-minute legs (the Ten-Minute Relay). Discover connectable servers with GET /v1/discovery/search?kind=mcp-servers (Discovery).

Route Body Returns
POST /v1/mcp/runs { "server_ids": ["<id>", …] } (non-empty string array) 201 { run_id, tools, connected, failed, warnings, skipped }
GET /v1/mcp/runs/:id/tools { tools, count }
POST /v1/mcp/runs/:id/call { "name": "mcp__<server>__<tool>", "args"?: {…}, "timeout_ms"?: n } { content }
DELETE /v1/mcp/runs/:id { success: true }

Notes:

A full cycle against the credential-free cloudflare-docs server — create, list tools, delete:

RID=$(curl -s "https://api.plungeai.com/v1/mcp/runs" \
  -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \
  -d '{"server_ids":["cloudflare-docs"]}' \
  | sed -n 's/.*"run_id":"\([^"]*\)".*/\1/p')
echo "run_id=$RID"
curl -s "https://api.plungeai.com/v1/mcp/runs/$RID/tools" \
  -H "Authorization: Bearer $PLUNGE_API_KEY"
echo
curl -s -X DELETE "https://api.plungeai.com/v1/mcp/runs/$RID" \
  -H "Authorization: Bearer $PLUNGE_API_KEY"

To call a tool on the run (not run here — it does real work):

# not run here
curl -s "https://api.plungeai.com/v1/mcp/runs/<run_id>/call" \
  -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \
  -d '{"name":"mcp__cloudflare-docs__search_cloudflare_documentation","args":{"query":"durable objects"}}'

11.3 MCP servers inside a harness mission

Outbound runs are the manual surface. Inside a bounded mission, list the servers on the task's mcp: field and the loop host connects them for the run (through the same mcp-executor), exposing their tools to the agent under the fence. See Skills, plugins & capability injection for the mcp: field and a full type: harness example, and the Workflows plane for running it. In-loop MCP is the inbound path for our agents; the POST /v1/mcp endpoint above is the outbound control plane for external clients — the two never cross.


12. Traces & observability

Every One API response is traceable. This chapter covers the trace id, the GET /v1/traces/:id read, execution history and cost, the response headers, and what is not observable through the API today. Base URL is https://api.plungeai.com; reads use an ozk_ key (Authentication). Source: orchestration/api-gateway/routes/traces.ts, orchestration/api-gateway/lib/execution-row.ts.

12.1 The trace id

Every response carries x-request-id — this is the trace id. On execution calls it also equals the run's workflow_id / execution_id, and it is echoed in the body as request_id (live-verified 2026-09-18: x-request-id, x-execution-id, workflow_id and request_id were the same UUID for one agent execute). So you can trace a run from any of them.

You may also supply the trace id: send x-trace-id: <uuid> and the run adopts it (a replayed UUID that already ran → 409 duplicate_execution_id, see Errors).

12.2 Response headers

Header On Meaning
x-request-id every response the trace id
x-execution-id execution responses the run id (= x-request-id for a run)
x-error-code error responses the machine error code (mirrors error.code)
server-timing every response total;dur=<ms>;desc="Total Response Time"
x-content-type-options every response nosniff

No x-ocean-* headers are emitted today. Rate-limit state is not returned as a header — read it with plungeai_whoami (the MCP plane) or see Authentication.

12.3 GET /v1/traces/:id

Reads one trace by id and joins two telemetry tables (both on ocean-telemetry via TELEMETRY_DB, per the root CLAUDE.md DB map):

{ "trace_id": "<id>", "spans": [ … ], "gateway_requests": [ … ] }

Because spans are queue-written, GET /v1/traces/:id for an unknown id and for a run whose spans have not landed yet both return 200 with empty arrays — the endpoint does not distinguish "no such trace" from "no telemetry yet" (there is no 404 on this route).

Execute, then read the trace by its request_id (the trace id round-trips):

RESP=$(curl -s "https://api.plungeai.com/v1/agents/llm-agent/execute" \
  -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \
  -d '{"prompt":"Reply with only: pong","max_tokens":24}')
RID=$(printf '%s' "$RESP" | sed -n 's/.*"request_id":"\([^"]*\)".*/\1/p')
echo "trace=$RID"
sleep 3
TRACE=$(curl -s "https://api.plungeai.com/v1/traces/$RID" \
  -H "Authorization: Bearer $PLUNGE_API_KEY")
echo "$TRACE"
echo "$TRACE" | grep -q "\"trace_id\":\"$RID\"" && echo TRACE_MATCH_OK

An unknown trace id — 200 with empty arrays, not a 404:

curl -s "https://api.plungeai.com/v1/traces/00000000-0000-4000-8000-000000000000" \
  -H "Authorization: Bearer $PLUNGE_API_KEY"

12.4 Execution history & cost

Every agent and workflow run opens a workflow_executions row on ocean-db (DB), source: 'api' (lib/execution-row.ts). The row carries status (runningcompleted / failed), input, workflow_name, duration, end_time, execution_summary (the per-task durations the pricer reads) and, once priced, the run cost. It is closed from the engine's response after the run; a run the platform cannot record does not run "for free".

Where a developer sees these rows:

Cost is computed asynchronously by the workflow-cost worker from execution_summary; a run whose summary was preserved is priced (an empty-completion or retrieval fault is flipped to failed without wiping the summary, so the reasoning tokens it spent are still metered).

12.5 What is not observable via the API today


13. Errors, limits & headers

The platform has two error envelopes. The execution planes (tools, agents, workflows, MCP, discovery, traces) use the router envelope; the models plane (/v1/chat/completions, /v1/embeddings, /v1/models) uses the OpenAI-shaped envelope from the inference gateway. Branch on the HTTP status and, on the execution planes, the X-Error-Code header — not on any field's type.

13.1 The two envelopes

Execution planes (orchestration/api-gateway/lib/respond.ts, lib/format.ts):

{ "error": { "code": "unknown_agent", "message": "No such agent: foo", "…": "extra fields" } }

Models plane (inference/gateway/src/errors.ts) — OpenAI shape, no X-Error-Code:

{ "error": { "message": "Invalid API key", "type": "authentication_error", "code": "invalid_api_key", "request_id": "…" } }

13.2 Execution-plane error codes

HTTP code When Do
400 invalid_json / invalid_body / invalid_request Body is not valid JSON / not an object Fix the body
400 invalid_format ?format=/body.format is not json|yaml|markdown|text Use a known format
400 missing_prompt Agent execute without a prompt/input string Send a prompt
400 missing_workflow Workflow execute without a workflow Send YAML/JSON workflow
400 invalid_workflow Engine rejected the workflow (structural) Fix the CNL
401 unauthorized Missing/invalid/revoked ozk_ key Send a valid key (Auth)
401 invalid_signature Internal HMAC identity failed Service-to-service only
403 refused Trust fence — agent/tool refused (13.5) Do not retry
404 not_found No route for method+path Check the path
404 unknown_agent No such (or inactive) agent id Discover a valid id (Discovery)
404 unknown_tool No such tool id Discover a valid id
404 workflow_not_found Saved workflow id not in the engine KV Check the id/owner
404 not_ready Result not stored yet (still running) Poll again
409 approval_required Trust fence — human approval pending (13.5) Approve, then retry
409 execution_failed Result route: the run failed Read the message
409 duplicate_execution_id Replayed x-trace-id UUID hit the run PK Send a fresh UUID or omit it
413 payload_too_large Body over the size cap (13.6) Shrink the body
422 invalid_params Outcome needs_input — agent needs different args Fix the params
422 unknown_model Pinned model absent/inactive in the catalog Discover a model
422 empty_completion Run completed but produced no visible content Raise max_tokens (≥64)
424 connection_required Outcome needs_connection — no connected account Connect the account (Connectors)
424 credential_required Outcome needs_api_key — no API key for a connector Add the key
429 rate_limited Tier RPM/RPD or failed-auth cap (13.4) Honour Retry-After
500 config_error Server misconfiguration Report with x-request-id
500 internal_error Unhandled router error Report with x-request-id
502 engine_error Engine failed (untagged) Retry; report if persistent
502 upstream_error Upstream service/storage error Retry with backoff
502 tool_call_failed Tool execution failed Read the message
502 no_servers_connected MCP: no server connected Connect a server
502 result_unavailable Run finished but the result could not be retrieved Retry the result route
503 agent_unavailable Outcome unavailable — agent temporarily down Retry later
503 provider_unconfigured Chosen provider has no key on this platform Choose another provider
503 money_plane_unavailable Model routes not staged on this tier Call gateway.plungeai.com directly

13.3 Outcome → HTTP mapping

Execute routes classify an agent/tool outcome (routes/_util.ts outcomeStatus). A success: false engine result tagged with an outcome remaps to a caller-actionable status; an untagged failure stays 502 engine_error.

Outcome HTTP code
needs_input 422 invalid_params
needs_connection 424 connection_required
needs_api_key 424 credential_required
unavailable 503 agent_unavailable
error (or untagged) 502 execution_failed / engine_error
needs_approval 409 approval_required (13.5)

13.4 Rate limits

Execution planes (orchestration/mcp-gateway/rate-limiter.ts, a Durable Object; enforced in lib/auth.ts). Counted on POST executions only — catalog GETs are free. Per tier:

Tier per minute per day
free 30 1,000
pro (default) 100 10,000
enterprise 300 100,000

A key may carry a per-key rate_limit override (raises the per-minute cap). Failed authentications are metered per client IP (auth-fail: 20/min, 500/day) to bound key brute-force. On a limit hit: 429 rate_limited with a Retry-After header (seconds).

Money plane (inference/gateway/src/auth/rate-limit.ts). Per key, atomic per-minute window (RateLimiterDO, KV soft-cap fallback): default 600 req/min (RATE_LIMIT_RPM_DEFAULT) → 429 rate_limit_exceeded. Separate from billing caps: 429 insufficient_quota (monthly limit / trial allowance), 402 insufficient_balance (empty wallet), 429 spend_cap_exceeded (guardrail) — see 3.10, 3.13.

13.5 Trust fence

Two statuses gate risky actions:

13.6 Limits: body caps, timeouts

13.7 Retry guidance

13.8 Response headers

Header Plane Meaning
x-request-id both Server-minted request id. Send your own x-request-id to have it echoed.
X-Error-Code execution (errors) The error code, for header-only branching.
X-Execution-Id execution The run id on results, streams, and async pointers.
x-correlation-id models Echo of your x-request-id when supplied.
x-cache models hit / miss on cache-eligible chat requests (3.11).
Server-Timing execution total;dur=<ms> timing.
X-Content-Type-Options execution nosniff on every router response.
Retry-After both Seconds to wait after a 429.
Access-Control-Allow-Origin (+ CORS) both *; both planes answer OPTIONS preflight.
Content-Type both application/json | text/event-stream | negotiated text/yaml|text/markdown|text/plain.

13.9 Verified examples

Unauthenticated execution-plane request → 401 unauthorized with the x-error-code header:

curl -is "$API_BASE/v1/agents"

Unknown route → 404 not_found, header x-error-code: not_found, plus x-request-id:

curl -is "$API_BASE/v1/nope"

Agent execute without a prompt → 400 missing_prompt:

curl -s "$API_BASE/v1/agents/llm-agent/execute" \
  -H "Authorization: Bearer $PLUNGE_API_KEY" \
  -H "Content-Type: application/json" -d '{}'

Unknown agent id → 404 unknown_agent (the trust/fence pre-dispatch check):

curl -s "$API_BASE/v1/agents/zzz-not-an-agent/execute" \
  -H "Authorization: Bearer $PLUNGE_API_KEY" \
  -H "Content-Type: application/json" -d '{"prompt":"hi"}'

14. Skills, plugins & capability injection

The platform does not ship one agent per behaviour. It ships one loop agent — the harness runtime (universal-agent / agentic-agent, see Agents plane) — into which capabilities are injected per run. Capability injection is the six kinds below: skills, experts, persona, backgrounds, plugins, and MCP servers. You declare them on a type: harness task and the loop host resolves them in one parallel pass before the run (core/core-base/pack-enrich.ts). Base URL is https://api.plungeai.com; runs use an ozk_ key (Authentication).

14.1 The six kinds

Kind What it injects Where it comes from Eager limit Field
backgrounds always-on ambient context, prepended first registry card prompt_format 3 backgrounds: [id, …]
persona identity / voice, prepended persona-memory 1 (one voice per run) persona: id (aliases digital_twin, personas[0])
experts labelled ## EXPERT: <id> sections expert-memory 3 experts: [id, …]
skills an ## ADDITIONAL INSTRUCTIONS body skill-memory, else the registry skill card 5 skills: [id, …]
plugins a bundled SKILL.md index + script index (never MCP ids) plugin-memory plugins: [id, …]
mcp remote MCP tools, connected for the run and exposed under the fence mcp-executor (secrets resolved server-side) mcp: [id, …]

Injection order into the system prompt: backgrounds → persona → experts, then the skill body under ## ADDITIONAL INSTRUCTIONS. Plugin scripts become available to the run_python tool; plugin skills are advertised by name + description and pulled on demand with load_skill. MCP ids are connected by the loop host through the mcp-executor (the same machinery as the MCP plane outbound runs) and their tools appear as mcp__<server>__<tool> under the mission's fence.

14.2 Limits & degradation

14.3 The skill catalog

Skill sources live in skills/<id>/SKILL.md; the runtime injects the published registry skill card. The live catalog is the source of truth — discover it with GET /v1/discovery/search?kind=skills (Discovery). The ids on disk today (skills/):

id one line
web-search search the web and cite sources
report structure findings into a clean report
refactor refactor code toward a smaller, clearer shape
code-review review a diff for correctness and simplification
debug systematic root-cause debugging
file-ops read/write/search files in the workspace
prd draft a product requirements document
qa test-and-verify discipline
thinking structured reasoning before acting
workflow author a CNL workflow
biotech biotech-domain research helper
curl -s "https://api.plungeai.com/v1/discovery/search?kind=skills&q=web-search&limit=5" \
  -H "Authorization: Bearer $PLUNGE_API_KEY"

Plugins are bundled SKILL.md indexes + runnable scripts (not MCP servers). Discover them with kind=plugins; MCP servers (the mcp: kind) with kind=mcp-servers:

curl -s "https://api.plungeai.com/v1/discovery/search?kind=mcp-servers&limit=5" \
  -H "Authorization: Bearer $PLUNGE_API_KEY"

The discovery kinds personas and experts exist but returned 0 cards live-verified 2026-09-18 — persona/expert text is resolved from the persona-memory / expert-memory services at run time, not from a discovery catalog.

14.4 Pre-built agent cards — mission_ref / pack

A pre-built agent is a registry agent card whose prompt_format holds one portable markdown document: YAML frontmatter (a typed capability pack) plus a body that is the system prompt, compiled on read by core/core-base/agent-markdown.ts. Reference it instead of writing the mission inline:

- type: harness
  mission_ref: research-analyst      # canonical; `pack:` is an alias; a single-token `mission:` also works

Frontmatter keys the compiler reads: name, description, model, effort, tools, skills, experts, persona, backgrounds, plugins, mcp, agents (named sub-agents), permissions, success_criteria — plus the body. Unknown keys are ignored with a warning (forward-compatible); ownership/visibility are stamped server-side, never read from frontmatter. Merge order (per key, last wins): pre-built card → workflow root → task; arrays replace. So a card can define the base mission and the task can override model or clear skills: [].

14.5 How to run it

Workflows plane (recommended) — a type: harness task carries the mission and its capability fields; run it inline or save it (see Workflows plane). A full example:

workflow:
  name: sourced-brief
  tasks:
    - type: harness
      id: brief
      agent: universal-agent
      goal: "Write a one-paragraph brief on the latest Anthropic model; every claim a source URL."
      mission: "You are a research analyst. Produce sourced, factual briefs."
      allowed_tools: [web_search, web_fetch, task_complete]
      success_criteria:
        - "Every factual claim is followed by a source URL."
      max_iterations: 8
      skills: [web-search, report]
      experts: [python-pro]
      persona: analyst
      backgrounds: [acme-corp]
      plugins: [finance-bundle]
      mcp: [brave-search]

allowed_tools is the fence (fail-closed — a tool not listed never executes); max_iterations (alias max_turns) is the loop cap; effort (quick/standard/deep) sets a default cap when no explicit number is given.

Agents planePOST /v1/agents/:id/execute forwards only persona among the capability fields (routes/agents.ts); skills, experts, backgrounds, plugins, mcp, goal, allowed_tools are ignored there. To inject skills/plugins/MCP, use the type: harness task on the workflows plane, or the MCP plane plungeai_run_mission.

A minimal harness run with a real skill injected, capped at 2 iterations:

curl -s "https://api.plungeai.com/v1/workflows/execute" \
  -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \
  -d '{"workflow":{"name":"cap-demo","tasks":[{"type":"harness","id":"h1","agent":"universal-agent","goal":"Reply with the single word pong, then call task_complete.","mission":"You are a terse assistant. Answer in one word.","allowed_tools":["task_complete"],"max_iterations":2,"skills":["report"]}]}}'

14.6 Seeing what was injected

The enrichment pass reports what it resolved and what degraded (pack-enrich.ts): capabilities that could not be resolved appear as warning lanes, and those dropped for the eager budget/cap are reported as deferred (loadable with load_skill). These surface in the run's result metadata and in the trace spans (Traces & observability); a missing skill shows up as a warning rather than silently absent behaviour. For the full runtime model — tool fence, memory, delegation, recursion guard — see orchestration/HARNESS-README.md.


15. SDKs & clients

You do not need an Ocean-specific SDK to use the One API. The models plane is OpenAI-wire, so every OpenAI-compatible client works with a base-URL swap; the execution planes are plain HTTP+JSON and have a small typed TypeScript client. This chapter lists the options, shortest path first.

Keys throughout: $PLUNGE_API_KEY = ozk_ (execution planes), $PLUNGE_MODEL_KEY = sk-ocean- (models plane). See Authentication & keys.

OpenAI SDK (models plane)

Point the official OpenAI SDK at https://api.plungeai.com/v1 with your sk-ocean- key. This covers /v1/chat/completions, /v1/embeddings, and /v1/models.

Python:

from openai import OpenAI

client = OpenAI(
    api_key="<your sk-ocean- key>",
    base_url="https://api.plungeai.com/v1",
    default_headers={"x-trace-id": "00000000-0000-4000-8000-000000000000"},  # optional UUID
)
resp = client.chat.completions.create(
    model="anthropic/claude-sonnet-4-6",
    messages=[{"role": "user", "content": "Reply with exactly the word: pong"}],
    max_tokens=16,
)

TypeScript:

import OpenAI from 'openai'

const client = new OpenAI({
  apiKey: process.env.PLUNGE_MODEL_KEY,
  baseURL: 'https://api.plungeai.com/v1',
  defaultHeaders: { 'x-trace-id': crypto.randomUUID() }, // optional
})

Ocean-only fields (models[], sort, @preset/<slug>) pass through as extra body params — use the SDK's extra_body (Python) or just add them to the request object (TypeScript). See Models plane.

@plungeai/one-api (execution planes, TypeScript)

A thin typed client generated from GET /v1/openapi.json. It wraps openapi-fetch; every method maps 1:1 to a route. Source: sdk/one-api-client/.

npm install @plungeai/one-api

The only entry point is createOneApi({ apiKey, baseUrl? }), which returns a client with .GET, .POST, and .DELETE. Three real calls (method names verified against sdk/one-api-client/src/index.ts and its README):

import { createOneApi } from '@plungeai/one-api'

const client = createOneApi({ apiKey: process.env.PLUNGE_API_KEY! })

// 1) GET /v1/tools — data.count and data.tools are typed
const { data, error } = await client.GET('/v1/tools')
if (data) console.log(data.count, data.tools.length)

// 2) POST /v1/agents/{id}/execute — path param and body both typed
const { data: exec } = await client.POST('/v1/agents/{id}/execute', {
  params: { path: { id: 'llm-agent' } },
  body: { prompt: 'Reply with exactly the word: pong', sync: true, max_tokens: 32 },
})
console.log(exec?.content) // "pong"

// 3) GET /v1/discovery/search — query params are typed
const { data: found } = await client.GET('/v1/discovery/search', {
  params: { query: { q: 'web search', kind: 'agents', limit: 3 } },
})

The paths and components types are exported for advanced use. Regenerate the types after any spec change with npm run regen — never hand-edit src/schema.d.ts. The package targets One API 2.1.0. (Note: as of 2026-09-18 the package is publish-ready but not yet on npm; until it ships, consume it from the in-repo path or npm pack.)

OpenAI-compatible frameworks (models plane)

Any framework that accepts an OpenAI base URL works. Set base URL https://api.plungeai.com/v1 and the sk-ocean- key.

LiteLLM:

import litellm
resp = litellm.completion(
    model="openai/anthropic/claude-sonnet-4-6",  # openai/ tells LiteLLM to use the OpenAI wire
    api_base="https://api.plungeai.com/v1",
    api_key="<your sk-ocean- key>",
    messages=[{"role": "user", "content": "pong?"}],
)

Vercel AI SDK:

import { createOpenAI } from '@ai-sdk/openai'

const ocean = createOpenAI({
  baseURL: 'https://api.plungeai.com/v1',
  apiKey: process.env.PLUNGE_MODEL_KEY,
})
const model = ocean('anthropic/claude-sonnet-4-6')

LangChain (Python):

from langchain_openai import ChatOpenAI

llm = ChatOpenAI(
    model="anthropic/claude-sonnet-4-6",
    base_url="https://api.plungeai.com/v1",
    api_key="<your sk-ocean- key>",
)

MCP clients (the platform as an MCP server)

The platform is itself an MCP server. Point any MCP client at the inbound Streamable-HTTP endpoint with your ozk_ key. There are two equivalent URLs (both live-verified, both return the same tool table): https://api.plungeai.com/v1/mcp (the One API's signed pass-through) and https://mcp.plungeai.com/mcp (the MCP gateway directly).

Claude Code:

claude mcp add --transport http plungeai https://api.plungeai.com/v1/mcp \
  --header "Authorization: Bearer $PLUNGE_API_KEY"

Claude Desktop / other clients that speak mcp-remote:

{
  "mcpServers": {
    "plungeai": {
      "command": "npx",
      "args": ["mcp-remote", "https://api.plungeai.com/v1/mcp",
               "--header", "Authorization: Bearer ${PLUNGE_API_KEY}"]
    }
  }
}

You can confirm the tool table over plain curl (JSON-RPC tools/list) — live-verified:

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

See MCP plane for inbound tools and outbound MCP runs.

Legacy SDKs

The @ocean-platform/external-sdk family predates the One API router. They still work (they target the legacy /v1/execute alias, which the router maps to the workflow plane) but are deprecated — prefer the OpenAI SDK (models) and @plungeai/one-api or plain HTTP (execution planes). All four use an ozk_ key and expose callAgent / executeWorkflow / executeYAML / executeStream.

Package Language Install Source
@ocean-platform/external-sdk TypeScript npm install @ocean-platform/external-sdk sdk/ocean-external-sdk/
ocean-external-sdk (Python) Python pip install ocean-external-sdk sdk/ocean-external-sdk-python/
ocean-external-sdk-go Go go get github.com/ocean-platform/ocean-external-sdk-go/ocean sdk/ocean-external-sdk-go/
ocean-external-sdk (Rust) Rust ocean-external-sdk = "1.0" sdk/ocean-external-sdk-rust/

curl conventions used in this guide

A plain execution-plane list call (the shape every ozk_ GET follows) is live-verified:

curl -s "https://api.plungeai.com/v1/agents?limit=3" \
  -H "Authorization: Bearer $PLUNGE_API_KEY"