# Quickstart (/docs/api/quickstart)



`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`](/docs/api/errors-and-rate-limits#status-codes).

## Before you start [#before-you-start]

You need:

* a **client API key** for the environment, `pcai_live_…` in prod and
  `pcai_test_…` elsewhere
  ([Authentication](/docs/api/authentication#api-keys));
* the environment's **base URL** (see [the overview](/docs/api#base-url));
* 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:


```bash
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](/docs/api/conversations)).

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

## curl [#curl]


```bash
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](/docs/api/chat-completions#the-response) instead of
[chunks](/docs/api/streaming).

## OpenAI Python [#openai-python]


```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 [#openai-javascript]


```ts
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 [#vercel-ai-sdk]

Use either provider. Both call `/chat/completions`.

### `@ai-sdk/openai-compatible` [#ai-sdkopenai-compatible]


```ts
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()` [#ai-sdkopenai-with-openaichat]


```ts
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);
```

<Callout type="error" title="openai.chat(), not openai()">
  `openai("plancover-assistant")` calls the Responses API, which answers
  `404 unsupported_endpoint`. Use `openai.chat(…)`.
</Callout>

To read the [`plancover` object](/docs/api/streaming#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]

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


```ts
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?");
```

<Callout type="warn" title="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.
</Callout>

## LangChain [#langchain]


```python
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)
```

<Callout type="warn" title="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`.
</Callout>

## Client integration notes [#client-integration-notes]

### Model name [#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` [#setting-metadata]

`metadata` carries the conversation id, and the session for clients that cannot
set headers. See [the request reference](/docs/api/chat-completions#metadata).

| Client                             | Where `metadata` goes                                                      |
| ---------------------------------- | -------------------------------------------------------------------------- |
| OpenAI SDKs                        | the `metadata` parameter                                                   |
| `@ai-sdk/openai-compatible`        | `providerOptions.<name>.metadata`, where `<name>` is the provider's `name` |
| `@ai-sdk/openai` (`openai.chat()`) | `providerOptions.openai.metadata`                                          |
| AgentKit                           | `defaultParameters: { metadata }`                                          |
| LangChain                          | `extra_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 [#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](/docs/api/confirmations-and-sign-in).

### Retries and timeouts [#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](/docs/api/streaming#pings) 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`.


```python
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 [#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.
