PlanCover Docs
API

Chat completions

The full request and response reference for POST /v1/chat/completions.

POST /v1/chat/completions

One call is one turn of a conversation: the member's latest message in, the assistant's answer out. With "stream": true the answer arrives as chunks; otherwise as one JSON completion, described below.

Headers

HeaderRequiredMeaning
Authorization: Bearer pcai_…YesYour client API key (Authentication)
X-PlanCover-SessionNoThe member session. Wins over metadata.session_id. Without either, the turn is anonymous
X-Conversation-IdNoThe conversation id, when metadata.conversation_id is not set
Content-Type: application/jsonYes

The request

{
  "model": "plancover-assistant",
  "messages": [{ "role": "user", "content": "What is the maternity waiting period?" }],
  "stream": true,
  "stream_options": { "include_usage": true },
  "metadata": { "conversation_id": "support-4821", "channel": "web" }
}
FieldTypeHandling
modelstringAccepted with any value and ignored; echoed in the response's model, or plancover-assistant if empty. PlanCover chooses the model that answers
messagesarray, requiredAt least one message. See Messages
streambooleantrue streams chunks; false or absent returns one completion
stream_options.include_usagebooleanWith stream: true, adds a usage chunk before [DONE]
metadataobjectSee Metadata

Every other field is accepted and ignored, including temperature, top_p, max_tokens, max_completion_tokens, stop, seed, user, and parallel_tool_calls. tools and tool_choice are read for one thing only: whether you declared confirm_action or sign_in, so that a paused turn can be offered to you as a tool call. The assistant never sees or calls a client's tools.

These fields are refused with a 400 whose param names the field, because a client that sends them expects a response this endpoint does not give:

FieldRefused when
ngreater than 1
response_formatset, and not null
audioset, and not null
modalitiesset, and not null, even ["text"]
logprobstrue

Messages

RoleHandling
userThe last one is this turn's message. Earlier ones may seed a new conversation
assistantEarlier answers. May seed a new conversation. content may be null
system, developerDropped unread. The assistant has its own instructions
toolA trailing tool message answers a confirm_action or sign_in call. Earlier ones are skipped

content is a string, or an array of parts of which only {"type": "text", "text": …} parts are read, joined with a newline.

This turn's message, the last user message, must (unless the request answers a tool call):

  • be the last message other than system and developer ones;
  • be text only: an image, audio or file part is a 400;
  • not be empty or blank;
  • be at most 8000 characters.

Earlier messages are never refused for their length or their content parts. OpenAI-style clients resend the whole transcript on every call, so a long transcript, or an image sent earlier, must not break later turns. Each message still needs one of the five roles above, and content must be a string, an array of parts or null. Non-text parts are dropped from them, and an empty one is skipped. How they are used depends on whether the conversation is new; see Conversations.

Metadata

KeyTypeMeaning
conversation_idstring, 1–128 printable ASCII characters, no spacesThe conversation this turn belongs to. Wins over X-Conversation-Id. Without either, the server mints conv_<32 hex>
session_idstring, at most 256 charactersThe member session, for clients that cannot set X-PlanCover-Session. The header wins
channelweb | mobile | whatsappWhere the member is chatting. Default web. On whatsapp, member questions get a "sign in through the PlanCover app" answer instead of a sign-in pause

Other keys are ignored. A conversation id that is not 1–128 printable ASCII characters is a 400 before the turn runs, with param naming where it came from: metadata.conversation_id or X-Conversation-Id.

The response

Without stream, the answer is one chat.completion:

{
  "id": "chatcmpl-6f1d0c2a9b8e4f7d9a3c5e1b2d4f6a8c",
  "object": "chat.completion",
  "created": 1793260800,
  "model": "plancover-assistant",
  "choices": [
    {
      "index": 0,
      "finish_reason": "stop",
      "message": {
        "role": "assistant",
        "content": "Maternity has a nine month waiting period [1].",
        "annotations": [
          {
            "type": "url_citation",
            "url_citation": {
              "url": "https://docs.plancover.example/kb/gmc-2026",
              "title": "Group Medical Cover — Maternity — p.12",
              "start_index": 42,
              "end_index": 45
            }
          }
        ]
      }
    }
  ],
  "usage": { "prompt_tokens": 1841, "completion_tokens": 212, "total_tokens": 2053 },
  "plancover": {
    "conversation_id": "support-4821",
    "message_id": "6f1d0c2a9b8e4f7d9a3c5e1b2d4f6a8c",
    "finish_reason": "stop",
    "citations": [
      {
        "index": 1,
        "id": "chunk-gmc-14",
        "title": "Group Medical Cover — Maternity — p.12",
        "document_id": "gmc-2026",
        "url": "https://docs.plancover.example/kb/gmc-2026",
        "snippet": "Maternity benefits are payable after a waiting period of nine months…"
      }
    ],
    "documents": []
  }
}
FieldMeaning
idchatcmpl- followed by the turn's message_id
modelThe request's model, echoed
choicesAlways exactly one
choices[0].message.contentThe answer. [n] markers point at plancover.citations
choices[0].message.annotationsOne url_citation per cited source that has a URL. See Citations
choices[0].finish_reasonstop, or tool_calls when the turn ends in a tool call. The real reason is plancover.finish_reason
usageAlways present. See Usage
plancoverPlanCover's extension object, described in Streaming, where the final chunk carries the same object

Fields that are not set, such as system_fingerprint, or tool_calls when there is no call, are left out rather than sent as null.

Response headers

HeaderMeaning
X-Conversation-IdThe conversation id in use: yours, or the one the server minted
x-request-idThe request's id, on every response. Quote it when reporting a problem
x-should-retry: falseOn a 5xx after the turn started. See Errors

finish_reason

choices[0].finish_reason is stop, so that every OpenAI client accepts the answer. The one exception is a turn that ends in a confirm_action or sign_in call: that is tool_calls. See Confirmations and sign-in. How the turn really ended is in plancover.finish_reason:

plancover.finish_reasonMeans
stopThe turn ran to completion
awaiting_sign_inPaused for the member to sign in. plancover.auth_required is set
awaiting_confirmationPaused on an action that needs the member's approval. plancover.confirmation is set
sign_in_requiredA member question on a channel with no sign-in card (whatsapp)
member_data_missingA member question that read none of the member's records
escalatedHanded over to a person
insufficient_evidenceThe answer cites none of the evidence it found: treat it with care
tool_budget_exhausted, model_budget_exhaustedA step budget ran out and the turn wound down
empty_responseThe model returned no text
cancelledThe turn was stopped at a safe point before it finished

Treat it as an open set: a value added later should be logged, not rejected. The Native SSE API documents each value in more depth.

Usage

usage is {prompt_tokens, completion_tokens, total_tokens}, and total_tokens is always the sum of the other two.

The figures are the totals of every model call in the turn: the step that plans the turn, the steps that gather evidence and call tools, the answer, and any fallback model that stood in for one of them. Each call counts once. The reranker that orders search results is not a language-model call and is not counted. A turn in which no model ran reports zeros.

A turn that pauses counts the calls made before the pause. The request that answers it counts the calls made after.

usage has no cost. PlanCover records each turn's tokens, and the cost its model provider reports, against the key that made the call.

Non-streaming calls and retries

A non-streaming call sends nothing until the turn ends: up to TURN_TIMEOUT_SECONDS (120 seconds by default), plus about 10 seconds to wind down. Set your client's timeout to at least 150 seconds and turn its automatic retries off, or a timeout replays the turn. See Retries and timeouts.

On this page