PlanCover Docs
API

Quickstart

Call the assistant from curl, the OpenAI SDKs, the Vercel AI SDK, AgentKit or LangChain.

POST /v1/chat/completions speaks OpenAI's Chat Completions protocol, so any OpenAI-style client can call the assistant: point its base URL at <base URL>/v1 and give it your API key. Chat Completions is the only OpenAI endpoint served; the Responses API and /v1/models answer 404 unsupported_endpoint.

Before you start

You need:

  • a client API key for the environment, pcai_live_… in prod and pcai_test_… elsewhere (Authentication);
  • the environment's base URL (see the overview);
  • a member session, if the conversation should carry on from call to call or reach the member's own data. Mint one once per member and keep it:
export BASE_URL=https://chat.142.93.215.84.sslip.io  # staging: https://chat-staging.142.93.215.84.sslip.io
export CHAT_API_KEY=pcai_live_...                     # staging: pcai_test_...
export SESSION=$(curl -s -X POST "$BASE_URL/auth/session" \
  -H "Authorization: Bearer $CHAT_API_KEY" | jq -r .session)
  • a conversation id that you choose and send on every call of one conversation. Without one, every call starts a new conversation (Conversations).

All the examples below run on your server. The key never goes to a browser or an app.

curl

curl -N "$BASE_URL/v1/chat/completions" \
  -H "Authorization: Bearer $CHAT_API_KEY" \
  -H "X-PlanCover-Session: $SESSION" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "plancover-assistant",
    "messages": [{ "role": "user", "content": "What is the maternity waiting period?" }],
    "stream": true,
    "metadata": { "conversation_id": "support-4821" }
  }'

Drop "stream": true to get one JSON completion instead of chunks.

OpenAI Python

import os

from openai import OpenAI

client = OpenAI(
    base_url=f"{os.environ['BASE_URL']}/v1",
    api_key=os.environ["CHAT_API_KEY"],
    default_headers={"X-PlanCover-Session": os.environ["SESSION"]},
)

stream = client.chat.completions.create(
    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"},
)
for chunk in stream:
    if chunk.choices:
        print(chunk.choices[0].delta.content or "", end="", flush=True)
    if plancover := (chunk.model_extra or {}).get("plancover"):
        print("\n", plancover["finish_reason"], plancover["citations"])

Fields outside OpenAI's schema, like plancover and the reasoning_content status lines, are on model_extra.

OpenAI JavaScript

import OpenAI from "openai";

const client = new OpenAI({
  baseURL: `${process.env.BASE_URL}/v1`,
  apiKey: process.env.CHAT_API_KEY,
  defaultHeaders: { "X-PlanCover-Session": process.env.SESSION },
});

const stream = await client.chat.completions.create({
  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" },
});
for await (const chunk of stream) {
  process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}

Vercel AI SDK

Use either provider. Both call /chat/completions.

@ai-sdk/openai-compatible

import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
import { streamText } from "ai";

const plancover = createOpenAICompatible({
  name: "plancover",
  baseURL: `${process.env.BASE_URL}/v1`,
  apiKey: process.env.CHAT_API_KEY,
  headers: { "X-PlanCover-Session": process.env.SESSION },
  includeUsage: true,
});

const result = streamText({
  model: plancover("plancover-assistant"),
  prompt: "What is the maternity waiting period?",
  providerOptions: { plancover: { metadata: { conversation_id: "support-4821" } } },
});
for await (const text of result.textStream) process.stdout.write(text);
console.log(await result.finishReason); // "stop"

This provider surfaces the tool status lines as reasoning (result.reasoningText, or reasoning-delta parts).

@ai-sdk/openai with openai.chat()

import { createOpenAI } from "@ai-sdk/openai";
import { streamText } from "ai";

const openai = createOpenAI({
  baseURL: `${process.env.BASE_URL}/v1`,
  apiKey: process.env.CHAT_API_KEY,
  headers: { "X-PlanCover-Session": process.env.SESSION },
});

const result = streamText({
  model: openai.chat("plancover-assistant"),
  prompt: "What is the maternity waiting period?",
  providerOptions: { openai: { metadata: { conversation_id: "support-4821" } } },
});
for await (const text of result.textStream) process.stdout.write(text);

openai.chat(), not openai()

openai("plancover-assistant") calls the Responses API, which answers 404 unsupported_endpoint. Use openai.chat(…).

To read the plancover object with either provider, pass includeRawChunks: true to streamText and take it from the raw part whose rawValue.plancover is set.

AgentKit

AgentKit cannot set headers, so the session and the conversation id ride in metadata. It does not stream.

import { createAgent, openai } from "@inngest/agent-kit";

const assistant = createAgent({
  name: "plancover-assistant",
  system: "Answer health-insurance questions.", // sent, and ignored by the server
  model: openai({
    model: "plancover-assistant",
    baseUrl: `${process.env.BASE_URL}/v1`,
    apiKey: process.env.CHAT_API_KEY,
    defaultParameters: {
      metadata: { conversation_id: "support-4821", session_id: process.env.SESSION },
    },
  }),
});

const { output } = await assistant.run("What is the maternity waiting period?");

Compliance: AgentKit inside Inngest functions

Inside an Inngest function, step.ai.infer sends the request through Inngest's cloud, so member messages and the session id leave PlanCover's infrastructure. That is fine for knowledge-only bots with no session. A bot that acts for members this way needs PlanCover's sign-off first.

LangChain

import os

from langchain_openai import ChatOpenAI

llm = ChatOpenAI(
    model="plancover-assistant",
    base_url=f"{os.environ['BASE_URL']}/v1",
    api_key=os.environ["CHAT_API_KEY"],
    default_headers={"X-PlanCover-Session": os.environ["SESSION"]},
    extra_body={"metadata": {"conversation_id": "support-4821"}},
)

for part in llm.stream("What is the maternity waiting period?"):
    print(part.content, end="", flush=True)

extra_body, not model_kwargs

model_kwargs={"metadata": …} does not reach the server: langchain-openai 1.6 moves metadata into LangChain's own run metadata and never sends it. Pass it in extra_body.

Client integration notes

Model name

The server ignores model and echoes it back. The model that answers is set by PlanCover. Use plancover-assistant. Never use a gpt-* or o<N>* name: @ai-sdk/openai changes how it converts messages for those names, and LangChain moves some of them to the Responses API.

Setting metadata

metadata carries the conversation id, and the session for clients that cannot set headers. See the request reference.

ClientWhere metadata goes
OpenAI SDKsthe metadata parameter
@ai-sdk/openai-compatibleproviderOptions.<name>.metadata, where <name> is the provider's name
@ai-sdk/openai (openai.chat())providerOptions.openai.metadata
AgentKitdefaultParameters: { metadata }
LangChainextra_body={"metadata": …}

Send the session as the X-PlanCover-Session header wherever the client can set headers. metadata.session_id is for clients that cannot; the header wins when both are sent.

Confirmations and sign-in

To let members approve actions and sign in from your client, declare the confirm_action and sign_in tools, without running them, name the conversation yourself, and send the member's session. See Confirmations and sign-in.

Retries and timeouts

OpenAI-style SDKs retry 429 and 5xx twice by default, honouring Retry-After. For a streaming call that is safe: every refusal comes before the turn starts, and the stream sends a byte at once and a ping every 15 seconds after that.

A non-streaming call sends nothing until the turn ends, which can take up to TURN_TIMEOUT_SECONDS (120 seconds by default) plus about 10 seconds to wind down. If your client gives up first and retries, the turn runs a second time on the same conversation. So for non-streaming calls:

  • set the client timeout to at least 150 seconds;
  • turn automatic retries off (maxRetries: 0, max_retries=0), and retry a 429 yourself after Retry-After.
client = OpenAI(base_url=..., api_key=..., max_retries=0, timeout=150)

A non-streaming turn that fails after it started answers with x-should-retry: false, which the OpenAI SDKs obey.

Keys and system prompts

  • Keep the key in a server environment variable, never in a browser or app bundle.
  • system and developer messages are dropped unread. The assistant has its own instructions, and a client's cannot change them.

On this page