Optimized Router API
Send category-routed completions. Omit thread_id to start a stored chat; send it back to continue. The server receives the full message history on every turn. All routes below require an inbound client key (rk_live_...).
Request flow
Threads are owned by the inbound API key. Send only the latest prompt. Do not create a thread first and do not resend prior messages. POST /v1/optimized-router creates a thread when thread_id is omitted or null, and continues that thread when thread_id is present.
- New chat:
POST /v1/optimized-routerwithcategoryandprompt(nothread_id). The server creates a thread, sends that prompt, stores both turns, and returnsthread_id. - Continue: send the same endpoint with that
thread_idplus the nextprompt. - The server loads the thread (404 if missing or owned by another key), rebuilds history in
seqorder, appends the new user message, and sends the full array upstream. - On success it stores the user message and assistant reply, updates
last_message_at, and returns{ "response", "thread_id" }. GET /threadslists chats for the sidebar.GET /threads/{thread_id}/messagesloads a previous chat for the UI.
Keep the returned thread_id on the client and send it on the next turn. Omitting it starts a new chat. POST /threads is optional and not required to send a message.
Authentication
| Header | Example |
|---|---|
Authorization | Bearer rk_live_... |
X-API-Key | rk_live_... |
Bearer takes precedence when both are sent. Missing or invalid keys return 401 with WWW-Authenticate: Bearer.
POST /v1/optimized-router
Client picks the category (no app-side classifier). Category selects the model. This is the only call needed to start or continue a stored chat.
Request
| Field | Type | Required | Description |
|---|---|---|---|
category | string | yes | See categories |
prompt | string | yes | Latest user turn. Prior messages are loaded from the thread |
thread_id | string | no | Continue this thread. Omit or null to create a new one |
New thread
curl -s https://gateway.pixis.ai/v1/optimized-router \
-H 'Authorization: Bearer rk_live_YOUR_KEY' \
-H 'Content-Type: application/json' \
-d '{
"category": "sql",
"prompt": "Write a PostgreSQL query to calculate ROAS."
}'
Continue a thread
curl -s https://gateway.pixis.ai/v1/optimized-router \
-H 'Authorization: Bearer rk_live_YOUR_KEY' \
-H 'Content-Type: application/json' \
-d '{
"category": "sql",
"prompt": "Now filter to last 7 days.",
"thread_id": "THREAD_ID"
}'
Response
{
"response": "SELECT ...",
"thread_id": "THREAD_ID"
}
thread_id is always returned. Use it on the next request to keep context. This endpoint does not set X-Router-* headers.
POST /threads
Optional. Create an empty thread without sending a message. Starting a chat does not require this — omit thread_id on POST /v1/optimized-router instead. Title is optional; if omitted, the first persisted user prompt becomes the title (max 255 characters).
{
"title": "ROAS query"
}
{
"id": "THREAD_ID",
"title": "ROAS query",
"status": "active",
"last_message_at": null,
"created_at": "2026-08-18T10:00:00+00:00",
"updated_at": "2026-08-18T10:00:00+00:00"
}
GET /threads
List active threads for the calling key, newest activity first (last_message_at, then created_at).
| Query | Type | Description |
|---|---|---|
limit | int | Page size, default 50, max 100 |
cursor | string | Opaque cursor from the previous page |
{
"success": true,
"threads": [
{
"id": "THREAD_ID",
"title": "ROAS query",
"status": "active",
"last_message_at": "2026-08-18T10:01:00+00:00",
"created_at": "2026-08-18T10:00:00+00:00",
"updated_at": "2026-08-18T10:01:00+00:00"
}
],
"next_cursor": null
}
GET /threads/{thread_id}
Fetch one thread. Returns 404 not_found if the id is unknown, deleted, or owned by another key.
GET /threads/{thread_id}/messages
Return stored messages in seq order for the UI. Same ownership rules as GET thread.
| Query | Type | Description |
|---|---|---|
after_seq | int | Return rows with seq greater than this value (default 0) |
limit | int | Page size, default 200, max 500 |
{
"success": true,
"thread_id": "THREAD_ID",
"messages": [
{ "id": 1, "seq": 1, "role": "user", "content": "Write a PostgreSQL query to calculate ROAS.", "created_at": "2026-08-18T10:00:30+00:00" },
{ "id": 2, "seq": 2, "role": "assistant", "content": "SELECT ...", "model_requested": "sql", "latency_ms": 420, "created_at": "2026-08-18T10:00:31+00:00" }
],
"next_seq": null
}
next_seq is set when another page exists; pass it as after_seq on the next request.
Errors
{
"success": false,
"error": {
"type": "invalid_category",
"message": "Unknown category 'foo'. Valid: simple, coding, ..."
}
}
| type | Status |
|---|---|
missing_client_key / invalid_client_key | 401 |
client_rate_limit | 429 |
invalid_api_key | 401 |
bad_request | 400 |
not_found | 404 |
invalid_model / invalid_category / invalid_policy | 400 / 404 |
router_model_unconfigured | 400 |
model_not_allowed | 403 |
rate_limit | 429 |
timeout | 504 |
upstream_unavailable / provider_unavailable | 503 / 502 |
provider_error / unexpected_api_response | 502 |
internal_error | 500 |
On retryable upstream failures (timeout, rate_limit, provider_unavailable, upstream_unavailable), the app retries once with FALLBACK_MODEL_DEFAULT (if configured and different from the primary). A failed upstream call is not written to the thread.
Categories
Category values select which model the endpoint uses. Hyphens and spaces normalize to underscores.
| Category | Env | Default model |
|---|---|---|
simple | MODEL_SIMPLE | google/gemini-2.5-flash |
coding | MODEL_CODING | anthropic/claude-sonnet-4-5 |
sql | MODEL_SQL | openai/gpt-5.1 |
analysis | MODEL_ANALYSIS | google/gemini-2.5-pro |
reasoning | MODEL_REASONING | deepseek/deepseek-reasoner |
extraction | MODEL_EXTRACTION | openai/gpt-4.1-mini |
backend_machinery | MODEL_BACKEND_MACHINERY | openai/gpt-5.1 |
routine | MODEL_ROUTINE | google/gemini-2.5-flash |
analytical | MODEL_ANALYTICAL | google/gemini-2.5-pro |
judgment | MODEL_JUDGMENT | anthropic/claude-sonnet-4-5 |
default | MODEL_DEFAULT | falls back to DEFAULT_MODEL |
If a category model is unset, the router uses the matching Von-layer model, then MODEL_DEFAULT.