PlanCover Docs
API

Streaming

The chunk sequence of a streamed completion, the plancover object, pings and [DONE].

With "stream": true, POST /v1/chat/completions answers 200 with text/event-stream and sends the turn as chat.completion.chunk objects, the way OpenAI does. Any OpenAI-style SDK reads it as is. This page is for reading the stream yourself, and for the PlanCover data that rides inside it.

Every refusal (a bad key, a bad request, a limit) is an HTTP status before the stream opens; see Errors and rate limits. Once the 200 is sent, the stream ends in either [DONE] or one error frame.

Native SSE API

PlanCover's own frontends use the richer Native SSE API (/chat/stream), with eleven named events. This page covers only the OpenAI-compatible stream.

The wire

Every frame is one data: line with a JSON chunk, followed by a blank line. There is no event: line and no id: line. A whole turn looks like this:

data: {"id": "chatcmpl-6f1d…", "object": "chat.completion.chunk", "created": 1793260800, "model": "plancover-assistant", "choices": [{"index": 0, "delta": {"role": "assistant", "content": ""}, "finish_reason": null}], "usage": null}

data: {"id": "chatcmpl-6f1d…", "object": "chat.completion.chunk", "created": 1793260800, "model": "plancover-assistant", "choices": [{"index": 0, "delta": {"reasoning_content": "Searching policy documents…"}, "finish_reason": null}], "usage": null}

: ping

data: {"id": "chatcmpl-6f1d…", "object": "chat.completion.chunk", "created": 1793260800, "model": "plancover-assistant", "choices": [{"index": 0, "delta": {"content": "Maternity has a nine month "}, "finish_reason": null}], "usage": null}

data: {"id": "chatcmpl-6f1d…", "object": "chat.completion.chunk", "created": 1793260800, "model": "plancover-assistant", "choices": [{"index": 0, "delta": {"content": "waiting period [1]."}, "finish_reason": null}], "usage": null}

data: {"id": "chatcmpl-6f1d…", "object": "chat.completion.chunk", "created": 1793260800, "model": "plancover-assistant", "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}], "usage": null, "plancover": {"conversation_id": "support-4821", "message_id": "6f1d…", "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": []}}

data: {"id": "chatcmpl-6f1d…", "object": "chat.completion.chunk", "created": 1793260800, "model": "plancover-assistant", "choices": [], "usage": {"prompt_tokens": 1841, "completion_tokens": 212, "total_tokens": 2053}}

data: [DONE]

Every chunk of a turn has the same id (chatcmpl- and the turn's message_id), the same created, and the request's model.

The chunks, in order

Chunkchoices[0].deltaNotes
First{"role": "assistant", "content": ""}Sent as soon as the turn starts
Tool status{"reasoning_content": "<status line>"}One each time the assistant starts a tool. See Status lines
Text{"content": "<text>"}The answer, in order. Join every content to get it
Tool call{"tool_calls": [ … ]}Only when the turn pauses on a tool you declared. One complete call. See Confirmations and sign-in
Final{}The finish_reason and the top-level plancover object
Usagenone: choices is []Only with stream_options.include_usage. See The usage chunk
[DONE]The literal data: [DONE], not JSON

finish_reason is null on every chunk but the final one. There it is stop, or tool_calls after a tool call chunk. The real reason is plancover.finish_reason. A stream that ends normally always has a final chunk with a finish_reason.

Status lines

While a tool runs, the stream would otherwise be quiet, so each tool start sends a short, fixed line in reasoning_content. It never contains the tool's arguments, such as a claim number. Show it as a transient "working" hint, not as part of the answer.

ToolStatus line
search_policy_knowledgeSearching policy documents…
list_claimsChecking your claims…
get_claim_detailLooking up that claim…
get_claim_statusChecking the claim status…
list_my_policiesChecking your policies…
get_policy_detailsLooking up your policy…
get_policy_validityChecking your policy dates…
get_plan_detailsLooking up your plan…
get_covered_benefitsChecking what is covered…
get_insured_membersChecking who is covered…
get_company_detailsLooking up your company…
get_ecardFetching your e-card…
get_policy_documentsFetching your policy documents…
find_network_hospitalsFinding network hospitals…
request_callbackArranging a callback…
any otherWorking on it…

reasoning_content is not in OpenAI's chunk schema. @ai-sdk/openai-compatible surfaces it as reasoning; the OpenAI Python SDK puts it on delta.model_extra; @ai-sdk/openai ignores it.

The plancover object

The final chunk carries a top-level plancover object with everything about the turn that has no place in OpenAI's schema. A non-streaming completion carries the same object.

FieldAlwaysMeaning
conversation_idYesThe conversation id in use: yours, or the one the server minted
message_idYesThe turn's id, 32 hexadecimal characters. Also the tail of the chunk id
finish_reasonYesHow the turn really ended. See finish_reason
citationsYesThe sources the answer rests on, [] if none. See Citations
documentsYesMember documents ready to download, [] if none. See Document handles
auth_requiredNoSet when the turn needs the member to sign in
confirmationNoSet when the turn paused on an action that needs the member's approval

auth_required

{ "reason": "sign_in_required", "resumable": true, "resume_id": "78d8933d80fd576aaa549d0de2bee768" }

The answer text already tells the member to sign in. resumable: true means the turn is paused (finish_reason: "awaiting_sign_in"). Sign the member in with in-chat OTP on the same session. Then answer the sign_in tool call if you declared it, and the paused question is answered; otherwise send the question again on the same conversation. A non-resumable card has resumable: false and resume_id: null.

An anonymous call can get this card too, but cannot act on it: it has no session to sign in on, and its next call is a new identity. Mint a session first for members who may ask about their own policies.

confirmation

{
  "confirmation_id": "3f9a…",
  "name": "request_callback",
  "summary": "Ask a PlanCover advisor to call you back about: my claim is stuck",
  "arguments_preview": { "reason": "my claim is stuck" },
  "risk": "send",
  "expires_at": "2026-09-12T11:24:07+00:00"
}

Nothing has happened upstream yet. Show the summary, and treat the action as not done until the member approves it. Declare the confirm_action tool to get the confirmation as a tool call you can answer; see Confirmations and sign-in. A new user message on the conversation replaces the pending confirmation.

The usage chunk

With "stream_options": {"include_usage": true}, one more chunk follows the final one, with empty choices and the turn's usage. Every earlier chunk then has "usage": null. Without include_usage, no chunk has a usage key and every chunk has a choice.

What the figures count is described under Usage.

[DONE]

The stream ends with the literal line data: [DONE], never JSON-encoded. Stop reading there. A stream that ends with an error frame instead has no [DONE].

Pings

While the assistant searches or calls tools, nothing else may be sent for a while. To keep proxies and load balancers from closing the connection, the server sends an SSE comment every 15 seconds of silence (SSE_HEARTBEAT_SECONDS):

: ping

SSE parsers skip comments, and every OpenAI-style SDK does too. If you parse the stream by hand, ignore any line that starts with :.

Errors mid-stream

A failure after the stream opened sends one data: frame with an error in the OpenAI envelope, then closes the stream. No [DONE] follows.

data: {"error": {"message": "That took too long. Nothing was changed.", "type": "server_error", "param": null, "code": "upstream_timeout"}}

The OpenAI SDKs and the AI SDK raise this as an error. Text that streamed before it stays valid: show it, marked incomplete. The codes are listed under After the turn started.

Disconnects

If the client hangs up mid-turn, the server stops the turn at its next safe point, so the conversation's history stays consistent. Nothing is sent back. The next message on the same conversation carries on from there.

On this page