Chat
Generate a single in-character reply for a persona and a recent message buffer. Phantom Router retrieves relevant memories from your vector store, injects them into the prompt, runs inbound moderation on the user's message, and (on a cadence) extracts new memories in the background.
POST /api/v1/companion/chat
- Auth: Bearer key with the
chat:companionscope. - Requires: a configured vector store (otherwise
400).
Construct a chat request interactively and watch the response come back in the Playground - edit the persona and per-turn options, send a turn, then copy the exact request and response JSON straight into your own code.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
chat_id | string | ✓ | Opaque per-user conversation id. Pattern ^[A-Za-z0-9][A-Za-z0-9_.:@-]{0,127}$, max 128 chars. Reuse it across turns to accumulate memory. |
persona | object | ✓ | The character. See below. |
messages | array | ✓ | 1–100 turns. The last message must be role: "user". |
extract_memory | boolean | - | Default true. Set false to skip background memory write-back (retrieval still runs). |
temperature and max_tokens are not request parameters. Generation
settings are configured per endpoint by the Phantom Router team and applied to
every call.
persona
Every field describes the companion character (e.g. Luna), not the user. All
fields except name are optional free text.
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | ✓ | 1–120 chars. The character's display name. |
nickname | string | - | Up to 120 chars. What the user calls the character. |
gender | string | - | Up to 200 chars. |
date_of_birth | string | - | Up to 200 chars. Free text — a date, an age, or a range. |
language | string | - | Up to 200 chars, e.g. "English, Portuguese". |
lives_in_country | string | - | Up to 200 chars. |
lives_in_city | string | - | Up to 200 chars. |
comes_from_country | string | - | Up to 200 chars. |
comes_from_city | string | - | Up to 200 chars. |
living_situation | string | - | Up to 2000 chars. |
daily_routine | string | - | Up to 2000 chars. |
goals | string | - | Up to 2000 chars. |
bio | string | - | Up to 4000 chars. Background, personality, backstory. |
boundaries | string | - | Up to 2000 chars. Lines the character will not cross. |
chat_style | string | - | Up to 80 chars, e.g. "playful, teasing". Slots into the generated preamble. |
custom_instructions | string | - | Up to 8000 chars. Appended as a strong directive; does not replace the preamble. |
The persona is stateless - send it on every turn. Phantom Router doesn't store it.
messages
Each item is { "role": "user" | "assistant", "content": "..." }, with content 1–16000 chars.
Send a rolling window of recent turns; the last one must be from the user.
Example request
curl -X POST https://api.phantomrouter.ai/api/v1/companion/chat \
-H "Authorization: Bearer $PHANTOM_KEY" \
-H "Content-Type: application/json" \
-d '{
"chat_id": "user-42",
"persona": {
"name": "Luna",
"bio": "A warm, witty companion who loves astronomy.",
"chat_style": "playful, curious"
},
"messages": [
{ "role": "user", "content": "Morning! Remember I had that big presentation?" }
]
}'
Response
The response is a conversation chain: your request messages echoed back with the
in-character reply appended as the final assistant turn. Append it to your buffer for the next
turn.
{
"chat_id": "user-42",
"messages": [
{ "role": "user", "content": "Morning! Remember I had that big presentation?" },
{
"role": "assistant",
"content": "Morning, you! Of course - the big one you were dreading. How'd it land?"
}
],
"moderation": { "enabled": true, "inbound_prohibited": false }
}
| Field | Type | Notes |
|---|---|---|
chat_id | string | Echoes the request. |
messages | array | The request buffer with the reply appended. The last turn (role: "assistant") is the new reply. |
moderation.enabled | boolean | true when the inbound check ran (an Omnifence key is configured). false means it was not run — inbound_prohibited is then always false because nothing was checked, not because the content passed. |
moderation.inbound_prohibited | boolean | true if the user's message was refused. Only inbound (user-supplied) content is moderated; the generated reply is never checked. |
moderation.reason | string? | Present when moderation acted. |
Reading the result: enabled: false → moderation didn't run (connect an Omnifence key to turn it on). enabled: true with inbound_prohibited: false → ran and passed. inbound_prohibited: true → the user's turn was blocked.
Memory retrieval and extraction still happen automatically on every eligible turn — they're just not surfaced in the response body. To see exactly what was retrieved and whether extraction fired, use the Playground, which renders the full pipeline.
Moderation behavior
Moderation is opt-in and inbound-only: screening runs on the user-supplied
message buffer only when you have connected an Omnifence API key (in the console,
under Chat). The generated reply is never moderated. Without a key,
moderation.inbound_prohibited is always false. When enabled, moderation runs
inline and returns 200, not an error:
- Inbound refusal - if the user's message is prohibited, the appended
assistantturn is a canned refusal,moderation.inbound_prohibitedistrue, and nothing is generated or extracted.
Memory & cadence
Retrieval runs on every turn. Extraction is cadence-gated - by default Phantom Router extracts new
memories every few user turns, not on every message, and the job runs in the background. Set
extract_memory: false to opt out of write-back for a turn while still retrieving.
Errors
| Status | Code | When |
|---|---|---|
| 400 | INVALID_REQUEST | Body failed validation, or no vector store is configured. |
| 401 | UNAUTHORIZED | Missing or invalid key. |
| 402 | PAYMENT_REQUIRED | Insufficient credit balance. |
| 403 | FORBIDDEN | Key lacks chat:companion. |
| 503 | SERVICE_UNAVAILABLE | Transient model-provider or vector-store failure. Retry with backoff. |