PlanCover Docs
API

Admin portal

Where PlanCover staff manage API keys and the model, read usage, cost and the audit log, and, when enabled, conversations.

The admin portal is PlanCover's own web app for running the chat API. It is for PlanCover staff, not for integrators: if you call the API, ask the PlanCover team for a key and skip this page.

It holds no chat data of its own. Every page reads or changes chat-api through the admin API, with one admin key that only the portal holds.

PageWhat it doesRole
KeysCreate, edit and revoke API keysadmin
ModelChoose the models every turn usesadmin
Usage & costRequests, tokens and cost per key per dayviewer, admin
AuditEvery tool call and confirmationviewer, admin
ConversationsRead a conversation's transcript, when enabledadmin

Accounts and sign-in

There is no sign-up. An operator creates each account from the shell, and the command prints a one-time password once:

node scripts/create-user.ts --email <email> --name <name> --role viewer
node scripts/reset-password.ts --email <email>

--role is viewer or admin.

On the first sign-in the portal asks for a new password (at least 12 characters), then for an authenticator app. TOTP is mandatory: until an account has enrolled, it can open none of the pages below. Sign-in is the password, then the authenticator's code or one of the backup codes shown at enrolment. Each backup code works once.

reset-password prints a new one-time password and ends every session of that account. The authenticator stays enrolled unless you pass --reset-totp, for a lost phone.

A session ends after 12 hours idle, and after 7 days in any case.

Roles

RoleSees
viewerUsage & cost and Audit
adminEvery page

The sidebar shows only the pages a role may open, and any other page answers "not found". The portal is the only place the two roles differ: chat-api trusts whoever holds the admin key, so it cannot tell a viewer from an admin.

Keys

Lists every key with its display form (pcai_live_…a1B2), role, state, limit, expiry and when it was last used.

  • Create. Give a name, a role (client for an integration), and optionally a per-minute limit and an expiry. The new key is shown once, with a copy button. Copy it before you press "Done": the server keeps only its hash, so a lost key can only be replaced.
  • Edit a key's limit and expiry. An empty limit means the deployment's default (API_KEY_RATE_LIMIT_PER_MINUTE, 120 a minute unless set); 0 means unlimited. An empty expiry means never.
  • Revoke by typing the key's name. It takes effect on the next request, which gets 401 invalid_api_key. A revoked key stays in the list.

Expiry times are in UTC. Authentication covers what a key may call and how its limit works.

Model

Three OpenRouter model ids, free text, such as openai/gpt-4o-mini:

FieldUsed for
Gather modelRoutes the turn and calls the tools
Answer modelWrites the reply the member reads
Fallback modelTakes over when a primary model is unavailable

A change applies from the next turn, with no deploy. An empty field falls back to the environment's value, which the form shows in its placeholder. The page also shows the OpenRouter provider order, whether other providers are allowed, and who changed the settings last.

The answer model configured for a turn is message.started.model on the native stream. /v1/chat/completions echoes the model your request sent instead, so the admin-set model never changes what an integrator sees there.

Usage & cost

One row per key per UTC day: requests, errors, prompt and completion tokens, and cost in US dollars. A key's name links to that key alone.

  • A request is one turn, on any chat route: /v1/chat/completions, /chat/stream, /chat/confirm or /chat/resume. A turn that failed counts as an error; one the client hung up on counts as a request but not an error.
  • Tokens add up every model call in the turn, the router and fallbacks included, but not the reranker. They match the usage the turn reported to the caller.
  • Cost is OpenRouter's own figure. It is blank for a day with no reported cost. A turn cut short can cost slightly more than recorded, because a model call still in flight is never counted.

Usage rows are kept for 400 days.

Audit

Every tool call and every confirmation, newest first, filtered by date range, tool name and outcome:

ColumnHolds
Kindtool or confirmation
Tool, RiskThe tool and its risk level
OutcomeFor a tool, ok or the failure code. For a confirmation, its status: pending, approved, rejected, superseded or expired
DurationHow long the tool ran
Conversation, MemberThe conversation id, and the member's telemetry id

The log is metadata only. Tool arguments, previews and confirmation summaries can hold member data, so they are never shown. A member is their telemetry id, the same id as in Langfuse and the logs, never their login. It is blank when chat-api runs without ENCRYPTION_KEY.

Conversations

A list of conversations whose last message falls in a date range, and each one's transcript.

Off outside dev

The conversation viewer is behind ADMIN_CONVERSATIONS_ENABLED. Unset, it is on in dev and off in staging and prod. While it is off, the page is not in the sidebar and chat-api answers 404 to both routes. Turning it on in prod waits on the data-protection sign-off.

A transcript shows the member's and the assistant's text only, with secrets redacted. Tool calls, tool results and internal messages are left out. Every transcript you open is recorded in chat-api's admin log under your email, before the transcript is read.

Reports

Usage & cost, Audit and Conversations share one form:

  • From and To are whole UTC days, both included. The default is the last seven days.
  • Results come 100 rows a page. "Next page" goes forward; "First page" starts over.

Every change is logged

Each key created, edited or revoked, each model change and each transcript read writes a row to chat-api's admin_events table, in the same transaction as the change, with the staff member's email. A change whose log row fails is not made. Saving the model form with nothing changed writes nothing.

The admin API

The portal is the admin API's only client. It is documented here so the boundary is clear, not for integrations.

RouteDoes
GET /admin/keysLists keys, never the key itself or its hash
POST /admin/keysCreates a key; the 201 response holds key, once
PATCH /admin/keys/{id}Changes name, rate_limit_per_minute or expires_at
POST /admin/keys/{id}/revokeRevokes a key. Idempotent
GET /admin/settings, PATCH /admin/settingsReads or sets the three models; null reverts one to env
GET /admin/usageUsage per key per UTC day; optional key_id
GET /admin/auditThe audit log; optional tool and outcome
GET /admin/conversationsConversations by last message; optional key_id
GET /admin/conversations/{id}One redacted transcript
  • Every call needs an admin key and X-Admin-Actor: <staff email>. A client key gets 403 permission_denied, and a missing or malformed actor 400 invalid_request. An admin key is refused on every other route.
  • from and to are required on the reports and need a UTC offset (Z or +05:30); a bare time is a 422. to is exclusive.
  • Lists return { "data": [...], "next_cursor": "..." }. Pass next_cursor back as ?cursor= until it is null. limit is 1 to 100, default 50.
  • No response carries a member's user_ref, only their telemetry_id.
  • Errors use the native envelope.

On this page