PlanCover Docs
API

Use with AI coding agents

Point Claude Code, Codex or another coding agent at these docs, with an AGENTS.md for your repository and every page as Markdown.

This site is built to be read by coding agents as well as people. Give your agent /AGENTS.md: a short integration guide that sets the rules an integration must follow and links to the pages behind each one.

URLWhat it is
/AGENTS.mdThe integration guide for coding agents, shown below
/llms.txtAn index of the docs pages, linking to their Markdown
/llms-full.txtEvery page in one Markdown file
/docs/<page>.mdOne page as Markdown, such as /docs/api/quickstart.md

A docs page URL sent with Accept: text/markdown also answers in Markdown. On each page, Copy Markdown copies it for a chat, and the menu beside it opens it in ChatGPT, Claude or Cursor.

The commands below read this site's address from DOCS_URL. This host is interim, like the API's, until PlanCover assigns a domain:

export DOCS_URL=https://docs.142.93.215.84.sslip.io

Codex

Codex reads AGENTS.md files from your repository, not from a URL. Save the guide at the root of your project:

curl -fsS -o AGENTS.md "$DOCS_URL/AGENTS.md"

If the project already has an AGENTS.md, save it under another name, such as docs/plancover-AGENTS.md, and point to it from yours.

Claude Code

Claude Code reads CLAUDE.md. Save the guide next to it, under a name that leaves any AGENTS.md of yours alone, and import it with a line of its own in CLAUDE.md:

curl -fsS -o plancover-AGENTS.md "$DOCS_URL/AGENTS.md"
echo '@plancover-AGENTS.md' >> CLAUDE.md

Any agent that can fetch a URL

For a one-off task, put the URL in the prompt instead of saving the file, with $DOCS_URL replaced by this site's address:

Read $DOCS_URL/AGENTS.md and follow it to integrate the PlanCover chat API
into this project. Fetch the pages it links to before writing the code they
cover.

The file

Download it rather than copying this block: the downloaded copy's links are absolute, so they work outside this site.

AGENTS.md
# PlanCover chat API: integration guide for coding agents

Instructions for an AI coding agent (Claude Code, Codex, Cursor and others)
building an integration with the PlanCover chat API. Follow them, and read the
linked pages before writing the code they cover. The full documentation is
indexed in [llms.txt](/llms.txt).

## What the API is

- A **server-to-server** HTTP API. The user's backend holds the API key and
  calls PlanCover; browsers and mobile apps call that backend, never PlanCover
  with a key.
- `POST /v1/chat/completions` speaks OpenAI's Chat Completions protocol, so use
  an OpenAI-style client. It is the only endpoint under `/v1`: the Responses API
  and `GET /v1/models` answer `404 unsupported_endpoint`.
- The assistant answers a PlanCover member's questions about their policies,
  claims and coverage, and acts for them only after they approve.

Pages: [API overview](/docs/api), [Quickstart](/docs/api/quickstart),
[Chat completions](/docs/api/chat-completions).

## Configuration

- **Base URL:** one per environment. Production is
  `https://chat.142.93.215.84.sslip.io`, staging is
  `https://chat-staging.142.93.215.84.sslip.io`; `dev` runs locally. These
  `sslip.io` hostnames are interim, until PlanCover assigns the API a domain.
  Read the base URL from an environment variable such as `BASE_URL`, never
  hard-code it, and point the client at `${BASE_URL}/v1`.
- **API key:** read it from a server environment variable such as
  `CHAT_API_KEY`. `pcai_test_…` keys work in `dev` and `staging`, `pcai_live_…`
  keys in `prod` only. Never commit a key, log it, or put it in a browser or
  app bundle. Never parse a key by splitting on `_`; match the prefix.

Page: [Authentication](/docs/api/authentication).

## Every call

1. Send the key as `Authorization: Bearer <key>`.
2. For a member, mint a session once with `POST /auth/session` (key only, no
   body). It answers `201` with `{"session": "<32 hex characters>"}`. Store it
   per member like a password and send it on every call as
   `X-PlanCover-Session: <session>`. Never send it in `Authorization`, a URL or
   a log line. Without a session a call is anonymous: knowledge questions only,
   and no conversation continuity. A session id does not expire, so keep it;
   `POST /auth/logout` ends the member's sign-in on it, not the id. When a
   sign-in lapses, the member reads as signed out and is asked to sign in
   again on the same session.
3. Choose a conversation id yourself (your chat or ticket id: 1–128 printable
   ASCII characters, no spaces) and send it as `metadata.conversation_id` on
   every call of that conversation. Without it, every call starts a new
   conversation.
4. Send `"model": "plancover-assistant"`. The server ignores the model name,
   but never use a `gpt-*` or `o<N>*` name: some clients change behaviour for
   those.
5. Each call carries **one new user message**. The server keeps the history;
   `system` and `developer` messages are dropped unread.

Pages: [Conversations](/docs/api/conversations),
[Authentication](/docs/api/authentication).

## Clients

- **OpenAI Python / JavaScript:** `base_url` / `baseURL` = `${BASE_URL}/v1`,
  the session in `default_headers` / `defaultHeaders`, and the conversation id
  in the `metadata` parameter. In Python, `plancover` and other extra fields
  are on `chunk.model_extra`.
- **Vercel AI SDK:** use `@ai-sdk/openai-compatible`, or `@ai-sdk/openai` with
  `openai.chat("plancover-assistant")`. Plain `openai("plancover-assistant")`
  calls the Responses API and gets a `404`. Put `metadata` in
  `providerOptions.<provider name>.metadata` (`providerOptions.openai.metadata`
  for `@ai-sdk/openai`).
- **LangChain (`langchain-openai`):** pass `extra_body={"metadata": …}`, not
  `model_kwargs`.
- These fields are refused with a `400`: `n` above 1, `response_format`,
  `audio`, `modalities`, and `logprobs: true`. Other unsupported fields, such
  as `temperature`, are accepted and ignored.

Pages: [Quickstart](/docs/api/quickstart), [Streaming](/docs/api/streaming),
[Citations and documents](/docs/api/citations).

## Answers

- Stream with `"stream": true`. The final chunk carries a top-level `plancover`
  object: `finish_reason`, `citations` (the sources behind `[n]` markers in
  the text) and `documents`. A non-streaming completion carries the same object.
- `plancover.finish_reason` says how the turn really ended, including
  `awaiting_confirmation` and `awaiting_sign_in`.

## Confirmations and sign-in

The assistant pauses before it acts for a member, and when it needs the
member's own records on a signed-out session. To receive a pause as a tool
call:

- declare the tools `confirm_action` and `sign_in` in `tools` (empty
  `parameters`; with the AI SDK, no `execute`). Never run them yourself;
- name the conversation and send the member's session.

Otherwise (its tool not declared, `tool_choice: "none"`, no conversation id of
yours, an anonymous call, or a sign-in that cannot be resumed) the pause
arrives as plain text with `finish_reason: "stop"`; `plancover.finish_reason`
still says `awaiting_confirmation` or `awaiting_sign_in`.

On a `confirm_action` call, show the member its `summary` and send their
decision on the same conversation and session: the assistant message that
carried the call, then one `tool` message with that call's `tool_call_id` and
the content `{"decision": "approve"}` or `{"decision": "reject"}`. Only the
member decides; never approve on their behalf. On a `sign_in` call, sign the
member in on the same session (`POST /auth/login`, then `POST /auth/verify`),
then answer the call with any content, such as `"signed in"`, but only after
`/auth/verify` succeeded. A `403 permission_denied` means the call is not
waiting for that answer (expired, already answered, or replaced by a newer
message): don't retry it with the same id, let the member ask again.

Never tell the member an action succeeded until the assistant's answer says so.

Page: [Confirmations and sign-in](/docs/api/confirmations-and-sign-in).

## Errors, retries and timeouts

- Errors under `/v1` use OpenAI's envelope (`message`, `type`, `param`,
  `code`), so the SDKs raise their usual exceptions. Log the `x-request-id`
  response header, never the request body.
- Retry `429` after `Retry-After`, and `503` before the turn with backoff.
  Don't retry `400`, `401`, `403` or `404`: fix the request. A `401` with
  `code: "unauthorised"` is about the member session, not the key.
- Streaming calls may keep the SDK's default retries.
- Non-streaming calls: set the client timeout to at least 150 seconds and turn
  automatic retries off (`max_retries=0` / `maxRetries: 0`); retry a `429`
  yourself. A timed-out retry would run the turn a second time.
- A failure after the turn started (an error frame mid-stream, or a `5xx` with
  `x-should-retry: false`) must not be retried automatically: an action may
  already have run. Show what streamed and let the member ask again.
- Limits: 120 requests per minute per key by default, and 20 chat turns per
  minute per member session.

Page: [Errors and rate limits](/docs/api/errors-and-rate-limits).

On this page