# Cerver - Quick Reference for Agents ## What Cerver Is Cerver has two detached layers: - `session layer`: the app-facing layer for sessions, input, runs, routing, and metrics - `compute layer`: the provider-facing layer for local computers, remote sandboxes, streams, and workspaces Apps should usually use the session layer. Providers should usually implement the compute layer. ## Primitives At A Glance Everything hangs off a **project** (addressed by id `proj_…` or slug): - **Sessions** — one interaction (a chat turn / one-shot); always has a transcript. `GET|POST /v2/sessions`. - **Runs** — a multi-step job (steps, cost, status). Two kinds: **workflow runs** (you authored the steps) and **agent runs** (orchestrate — the LLM planned them). `GET /v2/runs`, `GET /v2/runs/:id`. - **Workflows** — an authored, deterministic step sequence you define; cerver runs it top to bottom, chaining outputs. `/v2/projects/:id/workflows`. - **Crons** — run an agent / workflow / webhook on a schedule (5-field UTC). `/v2/projects/:id/crons`. - **Agents** — a reusable worker: an AGENTS.md + config (model, tools). Sessions, crons, and workflows can run *as* one. `GET|POST /v2/agents`. - **Computes** — where work runs: your machine via the relay, or a cloud sandbox (Vercel, e2b). `GET /v2/computes`. - **Environments** — a per-project runtime context / stage (prod, staging) binding repos + a secrets vault; a session picks one at create time. `/v2/projects/:id/environments`. - **Vaults** — your secrets backend (Infisical or an encrypted cerver vault), injected at run time and never printed. `/v2/account/infisical`. - **UIs** — an embeddable chat you drop on your site, scoped to a project via a publishable key. Detail on each below. --- ## Session Model A session has three independent axes: - **transcript** — always on. lives in cerver. the durable thing. - **harness** — which LLM/CLI: `claude` | `codex` | `grok` | `gemma` | `glm` | `inkling` | `anthropic` | `openai` | `xai` | `google` | `zhipu` | `ollama` - **inference host** — *who runs* the model. Only meaningful for open-weights models, where the model and its host are different parties: `inkling` runs on Baseten (default), Fireworks, or your own vLLM box. For vendor models there is exactly one possible host, so it's implicit (claude → Anthropic, always). - **compute** — where *your code* runs (or none): `e2b` | `vercel` | `cloudflare` | `` | `none`. Distinct from the inference host: compute runs your code, the host runs the model. Set the inference host per account or per project, alongside the key it belongs to (the host and its key are one unit — each host names Inkling differently, and a Baseten key is meaningless at Fireworks' URL): ```bash curl -X POST https://gateway.cerver.ai/v2/account/providers \ -H "Authorization: Bearer $CERVER_API_TOKEN" \ -d '{"provider": "inkling", "credentials": { "api_key": "...", "base_url": "https://api.fireworks.ai/inference/v1", "model": "accounts/fireworks/models/inkling" }}' ``` Omit `base_url`/`model` to use Baseten's serverless API (`thinkingmachines/inkling`). There is no "type of session" — every session has all three axes; you just pick values. `compute = none` (or `session_type:"transcript"`) means cerver is purely a transcript inbox — the caller drives the LLM themselves and POSTs each message to `/v2/sessions/:id/transcript`. --- ## Base URL `https://gateway.cerver.ai` --- ## Agent Contract If you are an agent, do this by default: 1. Check memory first: `GET /v2/sessions?limit=20`. 2. Create with `POST /v2/sessions`. 3. Name the session with a short, task-specific `session_name`. 4. Pick the axes explicitly: - `harness`: `claude` | `codex` | `grok` | `anthropic` | `openai` | `xai` - `compute`: `{ "provider": "vercel" }`, `{ "provider": "e2b" }`, `{ "compute_id": "comp_..." }`, or `null` 5. Run shell/code with `POST /v2/sessions/:id/run`. 6. Run model calls with `POST /v2/sessions/:id/run-llm`. 7. Change compute with `POST /v2/sessions/:id/compute`; do not create a new session just to move compute. 8. Read `GET /v2/sessions/:id/metrics` before making cost, latency, or savings claims. 9. End or pause sessions deliberately when finished. Do not invent new endpoint shapes when the v2 session API fits. --- ## Compute Setup (Prerequisite) Sessions run on computes. Before you create your first session, your account needs at least one compute attached. Two ways: ### Option A — Local relay (your laptop, mac mini, server) One command, on the machine you want to use: ```bash curl -fsSL https://kompany.dev/install-cerver.sh | bash ``` Installs `uv`, runs `branch-monkey-relay --cerver-only --cerver-url https://gateway.cerver.ai`, opens a browser to log you in, then registers the host as a **private** compute on your account. Once running, the host shows up under `GET /v2/computes` with `provider: "cerver_local_provider"`. The relay self-updates (polls GitHub for new commits, replaces itself in place), so leaving it running on a always-on machine is the steady state. ### Option B — BYO cloud provider (Vercel, e2b) Enable a provider with your own credentials via the dashboard at `cerver.ai/dashboard/providers`, or: ```bash curl -X POST https://gateway.cerver.ai/v2/account/providers \ -H "Authorization: Bearer $CERVER_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"provider": "vercel", "credentials": {"vercel_token": "..."}}' ``` After enabling, sessions can target the provider via `policy.allowed_providers` or `target_compute_id` (use `provider_vercel`, `provider_e2b` for shared pool). ### Verifying ```bash curl https://gateway.cerver.ai/v2/computes \ -H "Authorization: Bearer $CERVER_API_TOKEN" ``` You should see at least one compute. If empty, a `POST /v2/sessions` without a `compute` field returns `400` with `code:"no_compute_attached"` and a `choices` object: `your_computer` (the relay installer) and `shared_providers` (any provider the gateway has creds for). Render the choice to the user, then re-send with the chosen `compute`. --- ## Normal App Flow 1. Create a session 2. Optionally append input/messages 3. Run code, stream output, or run an LLM call 4. Read metrics 5. Close, pause, or keep the session idle ### Minimal session flow ```bash curl -X POST https://gateway.cerver.ai/v2/sessions \ -H "Authorization: Bearer $CERVER_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "session_name": "hello-cerver", "compute": { "provider": "vercel" }, "harness": "openai" }' curl -X POST https://gateway.cerver.ai/v2/sessions/SESSION_ID/run \ -H "Authorization: Bearer $CERVER_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "code": "echo hello from cerver" }' curl https://gateway.cerver.ai/v2/sessions/SESSION_ID/metrics \ -H "Authorization: Bearer $CERVER_API_TOKEN" curl -X DELETE https://gateway.cerver.ai/v2/sessions/SESSION_ID \ -H "Authorization: Bearer $CERVER_API_TOKEN" ``` --- ## Cost Routing Cerver lets you change `harness` (model) and `compute` (sandbox) per call within the same session. The transcript persists across swaps; `/metrics` reports per-call cost. Pick whatever model and sandbox fit the work in front of you — cerver does not classify or route for you. Use `POST /gateway/recommend` before creating or moving compute when you want Cerver to score provider fit: ```bash curl -X POST https://gateway.cerver.ai/gateway/recommend \ -H "Content-Type: application/json" \ -d '{ "task": "Classify 500 support tickets", "workload": "general", "requirements": { "runtime": "node", "timeout_minutes": 5 }, "policy": { "mode": "cheapest" } }' ``` Use `POST /v2/sessions/:id/compute` to move an existing session to another compute: ```bash curl -X POST https://gateway.cerver.ai/v2/sessions/SESSION_ID/compute \ -H "Authorization: Bearer $CERVER_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "compute": { "provider": "e2b" } }' ``` Use `GET /v2/sessions/:id/metrics` to report actual session cost estimates. Do not claim savings without reading metrics. --- ## Session Layer The session layer is the primary product surface. ### Main endpoints - `GET /gateway/providers` - `POST /gateway/recommend` - `POST /gateway/sessions` - `GET /gateway/sessions/:id` - `POST /gateway/sessions/:id/input` - `POST /gateway/sessions/:id/run` - `POST /gateway/sessions/:id/run/stream` - `GET /gateway/sessions/:id/metrics` - `DELETE /gateway/sessions/:id` ### Session request shape ```json { "task": "Boot a preview environment for a Next.js repo", "workload": "preview", "requirements": { "runtime": "node", "package_install": true, "public_preview": true, "persistence_level": "medium", "timeout_minutes": 20 }, "policy": { "mode": "balanced", "allowed_providers": ["vercel", "e2b"], "max_startup_ms": 2000 }, "session_name": "preview-session" } ``` ### Session response shape ```json { "session_id": "sess_123", "session_name": "preview-session", "status": "ready", "provider": "vercel", "compute_id": "cmp_123", "sandbox_id": "sbx_local_123", "routing": { "recommended_provider": "vercel", "confidence": "high", "fallback_order": ["e2b"], "canary_run": false }, "metrics": { "provision_time_ms": 812, "time_to_first_exec_ms": null, "last_exec_latency_ms": null, "average_stream_open_latency_ms": null, "cost_estimate_usd": 0.01, "uptime_percent": 99.3, "engagement_label": "warming" } } ``` ### Important session fields - `requirements`: what the work needs - `policy`: how Cerver should choose compute - `provider`: the chosen compute provider - `compute_id`: the compute-layer binding for this session - `sandbox_id`: compatibility field for lower-level compute records --- ## Automation: Crons, Workflows, Runs Three concepts sit on top of sessions. All are project-scoped; the path segment accepts the project **id** (`proj_…`) or its slug. **Crons** — run something on a schedule (5-field cron, UTC). Cerver's Worker fires due crons every minute. A cron runs one of: - an **agent/prompt** (`agent_id` / `prompt`) → creates a session, - a **workflow** (`workflow_id`) → runs that workflow, - a **webhook** (`url`) → an https POST on schedule (no agent). ``` POST /v2/projects/:id/crons { "schedule": "0 9 * * *", "workflow_id": "wf_…" } # or prompt / agent_id / url POST /v2/projects/:id/crons/:cronId/run # fire now (ignores schedule) ``` **Workflows** — an authored, deterministic multi-step job you define (vs orchestrate, where the LLM plans the steps). A workflow is a list of steps run top to bottom; each names a primitive and its params. Step outputs chain via `{{steps.NAME.output}}`. Step kinds (`uses`): `llm` (run a model turn), `http.request`, `webhook`, `log`, `approval` (pauses the run for a human). ``` POST /v2/projects/:id/workflows { "name": "digest", "steps": [ { "name": "fetch", "uses": "http.request", "with": { "url": "https://…", "headers": {…} } }, { "name": "summarize", "uses": "llm", "with": { "prompt": "Summarize: {{steps.fetch.output}}" } }, { "name": "notify", "uses": "log", "with": { "message": "{{steps.summarize.output}}" } } ] } POST /v2/projects/:id/workflows/:wfId/run → { "runId": "run_…", "status": "done" | "failed" | "needs_resolution" } ``` **Runs** — a Run is any multi-step job with per-step logs, cost, and status (a plain chat/one-shot stays a *session*, not a run). Two kinds: - **workflow runs** — from workflows you authored (task = `workflow: …`), - **agent runs** — from orchestrate (the LLM planned the steps). Both persist in `cerver_runs` / `cerver_run_steps`. ``` GET /v2/runs # list (both kinds) GET /v2/runs/:id # run + its steps (each step has input/output/status) POST /v2/runs/:id/cancel ``` --- ## Compute Layer The compute layer is the lower-level provider surface. Apps usually do not need it directly. Provider adapters do. ### Compute endpoints These paths still use `/sandbox` for compatibility. - `POST /sandbox` - `GET /sandbox/:id` - `POST /sandbox/:id/run` - `POST /sandbox/:id/run/stream` - `POST /sandbox/:id/install` - `POST /sandbox/:id/files` - `GET /sandbox/:id/files` - `GET /sandbox/:id/state` - `POST /sandbox/:id/state` - `DELETE /sandbox/:id` Use these for: - provider-level testing - adapter verification - direct compute debugging --- ## Comparing Harnesses (Claude vs Codex vs Grok) When the caller (a coding agent, a CLI, a UI) wants to compare what different model providers would do with the same intent, run parallel sessions — one per harness — and diff the streamed outputs. Cerver handles the API key per provider; the caller never touches Anthropic / OpenAI / xAI directly. `harness:"claude"` aliases `harness:"anthropic"` (same Anthropic API adapter). `harness:"codex"` aliases `harness:"openai"`. `harness:"grok"` aliases `harness:"xai"`. No CLI binaries are spawned — these are server-side API calls keyed by env.ANTHROPIC_API_KEY / OPENAI_API_KEY / XAI_API_KEY on the gateway. The caller does NOT need their own Anthropic / OpenAI keys for these calls. ### Minimal comparison flow Create one session per harness, run the same input through each, read the transcript or metrics. Both sessions can target `compute:{"provider":"online"}` since the harness is the only moving piece you're comparing. ```bash INTENT="A teammate switched auth from cookies to short-lived JWTs. \ In one paragraph, is this safe to merge and what's the biggest risk?" # Session A — Claude / Anthropic API SID_A=$(curl -s -X POST $GATEWAY/v2/sessions \ -H "Authorization: Bearer $CERVER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"harness":"claude","compute":null,"session_name":"compare-claude"}' \ | python3 -c "import json,sys;print(json.load(sys.stdin)['session_id'])") # Session B — Codex / OpenAI API SID_B=$(curl -s -X POST $GATEWAY/v2/sessions \ -H "Authorization: Bearer $CERVER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"harness":"codex","compute":null,"session_name":"compare-codex"}' \ | python3 -c "import json,sys;print(json.load(sys.stdin)['session_id'])") # Fire both calls in parallel, stream into separate logs. curl -N -X POST $GATEWAY/v2/sessions/$SID_A/run-llm \ -H "Authorization: Bearer $CERVER_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"input\":\"$INTENT\"}" >a.log 2>&1 & curl -N -X POST $GATEWAY/v2/sessions/$SID_B/run-llm \ -H "Authorization: Bearer $CERVER_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"input\":\"$INTENT\"}" >b.log 2>&1 & wait # Pull the final transcripts + cost/latency to compare. curl -s "$GATEWAY/v2/sessions/$SID_A?tail=50" -H "Authorization: Bearer $CERVER_API_KEY" curl -s "$GATEWAY/v2/sessions/$SID_B?tail=50" -H "Authorization: Bearer $CERVER_API_KEY" curl -s $GATEWAY/v2/sessions/$SID_A/metrics -H "Authorization: Bearer $CERVER_API_KEY" curl -s $GATEWAY/v2/sessions/$SID_B/metrics -H "Authorization: Bearer $CERVER_API_KEY" ``` ### What to surface to the human For each harness, report: verdict in one line, best supporting point, elapsed seconds, and cost (from `usage.input_tokens` + `output_tokens` × the model's price). A 2-row table beats prose: | Harness | Verdict | Best point | Time | Cost | |---|---|---|---:|---:| | claude | … | … | … | … | | codex | … | … | … | … | Then call the winner (or "consensus") in one sentence. Do NOT invent token counts or cost — if `usage` is absent from the response, say so literally. ### Error modes - **`code:"missing_credentials"`** on a `/run-llm` response → the gateway lacks the API key for that harness's underlying provider. Keep the row in the table and mark it `unavailable`; don't hide it. - **`code:"unsupported_model"`** → the caller pinned a `model:` that the harness doesn't recognize. Drop the override or pick a model from the harness's catalog. - **One side hangs** → cap each `curl -N` with `--max-time 60` so a stalled provider doesn't block the whole comparison. ### Adding a third harness Add a third session with `harness:"grok"` (xAI). Same shape, same endpoint. The diff just gets a third row. ### Why this matters for agents This is the canonical "let me get a second opinion" loop. A coding agent that runs `/v2/sessions` with two harnesses on a risky decision turns model choice into an experiment — cheap, side-by-side, with the costs visible — instead of a guess locked in at app-init time. --- ## Secrets Cerver intentionally does NOT store user app-secrets (Buffer, Slack, OpenAI keys, etc.) — those belong in a tool built for secrets. Use Infisical, 1Password, AWS Secrets Manager, your shell env, whatever fits. The `cerver-mcp` package ships a `secret_fetch(name)` tool that gives the agent one uniform interface regardless of backend. ### Default: env backend Set the secret in the shell that runs the agent (or the relay process): ```bash export BUFFER_API_KEY=... uvx cerver-mcp ``` The agent calls `secret_fetch("BUFFER_API_KEY")` and gets the value from env. ### Production: Infisical backend ```jsonc { "mcpServers": { "cerver": { "command": "uvx", "args": ["cerver-mcp"], "env": { "CERVER_API_TOKEN": "ck_...", "CERVER_SECRETS_BACKEND": "infisical", "INFISICAL_TOKEN": "st....", "INFISICAL_PROJECT_ID": "...", "INFISICAL_ENVIRONMENT": "prod" } } } } ``` The agent calls `secret_fetch("BUFFER_API_KEY")` → cerver-mcp hits Infisical's `/api/v3/secrets/raw/BUFFER_API_KEY` → returns the value. Audited, rotateable, never stored on the relay disk. --- ## Cross-Agent Memory Every cerver session is also a transcript. The `transcript` field on a session record holds every entry the agent produced — user messages, assistant replies, tool calls, tool results — in the order they happened. This means cerver is implicitly a shared memory layer: any agent on the same account can read what any other agent did, just by listing sessions and reading transcripts. There is no separate "memory" service to wire up. ### From plain HTTP ```bash # Discover what conversations exist on this account curl https://gateway.cerver.ai/v2/sessions?limit=20 \ -H "Authorization: Bearer $CERVER_API_TOKEN" # Read the most recent N entries of one curl "https://gateway.cerver.ai/v2/sessions/SESSION_ID?tail=50" \ -H "Authorization: Bearer $CERVER_API_TOKEN" # → summary + last 50 transcript entries. Use ?full=1 only for an # intentional full transcript download. ``` ### From an MCP-aware agent (Claude Code, codex, etc.) Install the MCP server once, drop the API key in: ```jsonc { "mcpServers": { "cerver": { "command": "uvx", "args": ["cerver-mcp"], "env": { "CERVER_API_TOKEN": "ck_..." } } } } ``` The agent then has three tools available: - `cerver_session_list({ status?, limit? })` — discover sessions on the account - `cerver_session_peek({ session_id, last_n })` — read the most recent entries - `cerver_session_export({ session_id })` — pull the full transcript as text ### First-time vs N-th-time agent A first-time agent on a new account: `cerver_session_list` returns `[]`, nothing to recall, just create a session and start working. An agent on an account with prior runs: `cerver_session_list` returns sibling agents' work. The agent can `peek` into relevant ones to inherit context — what the cron decided yesterday, what the code reviewer flagged last week — and act on it without you having to wire up retrieval. --- ## Tool Providers (Server-Side Tool Loop) By default, when the cerver harness emits a `tool_use` event during a `POST /v2/sessions/:id/run-llm` call, the *caller* is responsible for executing the tool and POSTing the result back via `priorToolResults` to advance the conversation. That works for sandboxed coding agents (the relay executes the tools), but in-app assistants whose tools read/write the caller's database end up running a streaming consumption loop on their own backend — fragile, easy to leave the cerver-side transcript poisoned with an orphan `tool_use` if any round aborts. Pass a `toolProvider` block to make cerver run the loop server-side. Cerver POSTs each `tool_use` to your endpoint, takes the result back, appends it to the transcript, and re-invokes the harness — until the model stops emitting tool_use or a round cap is reached. The caller just keeps the SSE connection open and renders events. ### Request ```bash curl -N -X POST https://gateway.cerver.ai/v2/sessions/$SESSION_ID/run-llm \ -H "Authorization: Bearer $CERVER_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "harness": "claude", "input": "Why dont I see more decisions?", "tools": [ { "name": "get_decisions", "description": "Fetch current decisions in the project.", "input_schema": { "type": "object", "properties": { "status": { "type": "string" } } } } ], "toolProvider": { "url": "https://your-app.example.com/api/cerver/tools/exec", "secret": "$CERVER_TOOL_SECRET", "timeoutMs": 30000, "maxRounds": 8 } }' ``` `toolProvider.url` MUST be `https://`. Cerver fast-fails the request otherwise — your tool calls and their inputs are sensitive. ### What cerver POSTs to your endpoint, per tool_use ```json { "session_id": "sess_abc", "tool_id": "toolu_01...", "tool_name": "get_decisions", "tool_input": { "status": "pending" } } ``` Header: `X-Cerver-Tool-Secret: `. Verify it before running the tool. ### What you return ```json { "content": "...", "is_error": false } ``` `content` is whatever string the model should see as the tool result — usually JSON-stringified data, sometimes a plain message. Set `is_error: true` when the tool failed, so the model can recover. ### What the caller sees on the SSE stream ``` event: text_delta data: { content: "I'll check..." } event: tool_use data: { tool_id, tool_name, tool_input } event: tool_result data: { tool_id, content, is_error } ← new event: text_delta data: { content: "There are 3 pending decisions..." } event: usage data: { ... } event: done data: { ok: true } ``` The new `tool_result` event lets the UI render "ran tool X with result Y" alongside the tool_use that triggered it. ### Limits and recovery - Cap on rounds: `toolProvider.maxRounds` (default 8, max 16). If exhausted with the model still emitting tool_use, cerver stops and the session ends in a clean state — next user input runs against a valid transcript. - Per-call timeout: `toolProvider.timeoutMs` (default 30000). On timeout cerver synthesizes a tool_result with `is_error: true` so the transcript stays valid and the model can decide what to do. - Provider returns non-200: same — synthetic error tool_result, loop continues. Your endpoint never poisons the cerver-side transcript. ### Storing the shared secret Cerver intentionally does NOT store the secret for you (same policy as the rest of the secrets section above). Recommended setup: 1. Generate the secret once: ```bash openssl rand -hex 32 ``` 2. Store it in **Infisical** under a path your app reads at startup (e.g. `/CERVER_TOOL_SECRET`). Example using the Infisical CLI: ```bash infisical secrets set CERVER_TOOL_SECRET=$(openssl rand -hex 32) --env prod ``` 3. Your app reads it at boot via the Infisical SDK or `secret_fetch("CERVER_TOOL_SECRET")` from `cerver-mcp`, then includes it in `toolProvider.secret` on every `run-llm` call. 4. Your tool endpoint validates `X-Cerver-Tool-Secret` against the same Infisical-resolved value before running anything. This keeps the secret rotateable without a deploy, audited, and out of process env files. --- ## Metrics And Streaming Session metrics can include: - `provision_time_ms` - `time_to_first_exec_ms` - `last_exec_latency_ms` - `average_stream_open_latency_ms` - `cost_estimate_usd` - `uptime_percent` - `engagement_score` - `engagement_label` Streaming responses can include: - `X-Cerver-Session-Id` - `X-Cerver-Provider` - `X-Cerver-Stream-Latency-Ms` --- ## Stress Tests Use stress tests to compare providers before placing real traffic. ```bash curl -X POST https://your-cerver.example.com/gateway/stress-tests \ -H "Content-Type: application/json" \ -d '{ "task": "Compare preview launch backends", "kind": "preview_launch", "workload": "preview", "requirements": { "runtime": "node", "public_preview": true, "package_install": true, "timeout_minutes": 20 }, "providers": ["vercel", "e2b"], "sample_size": 5 }' ``` Today these reports may be simulated from provider profiles unless live canary execution is enabled in the deployment. --- ## Current Provider Picture - `vercel`: working - `e2b`: working with bring-your-own credentials - `p69`: working as a local computer provider when the local bridge is running - `cloudflare`: partial in the current codebase - `daytona`: planned, not yet live --- ## If You Are Adding A Provider A provider appears in Cerver by implementing the compute adapter contract and registering it. ```ts export interface CerverInterface { readonly providerName: "cloudflare" | "vercel" | "e2b" | "p69"; createSandbox(request, env): Promise; runSandbox(record, request, env): Promise; runSandboxStream(record, request, env): Promise; installPackage(record, request, env): Promise; writeFile(record, request, env): Promise; readFile(record, path, encoding, env): Promise; getState(record, env): Promise; setState(record, state, env): Promise; deleteSandbox(record, env): Promise; } ``` Then: 1. Register it in the provider registry 2. Add it to the provider catalog 3. Make it executable with the right BYO credentials Apps should not call this interface directly. They should use the session endpoints.