API guide for agents
A dense, single-page reference for calling the Cruxy API correctly: auth, tiers, chat, tool calling, embeddings, rerank, moderations, and errors.
One page with everything a model needs to call the API correctly, and nothing
else. The base URL is https://api.cruxy.ai. Every path below is under it. For
the exhaustive per-field spec, see the API Reference;
for the whole corpus as text, fetch /llms-full.txt.
Auth
Every endpoint except device login needs a Bearer key in the Authorization
header. Keys carry the cxy_live_ prefix. A missing or invalid key is 401.
curl https://api.cruxy.ai/api/v1/me \
-H "Authorization: Bearer $CRUXY_API_KEY"GET /api/v1/me echoes { api_key_id, user_id }; use it to confirm a key
works.
Device login
Get a key with an RFC 8628 device flow: no key required, since its job is to mint one.
POST /api/v1/device/code(no body, no auth) returns{ device_code, user_code, verification_uri, verification_uri_complete, expires_in, interval }.- Send the user to
verification_uri_completeto approve beforeexpires_inseconds elapse. POST /api/v1/device/tokenwith{ device_code }, no more often than everyintervalseconds. While unresolved it returns400with acode: onlyauthorization_pendingmeans keep polling;access_denied,expired_token, andinvalid_grantare terminal. On approval it returns200with{ access_token, token_type, key_id, user_id }.
access_token is the cxy_live_ key, returned once; persist it immediately.
Tiers
model is a public tier: kavi, vaani, or mira. It also accepts auto,
which the server-side router resolves to a tier per request, before billing and
the upstream call; an auto request may be budget-downgraded to mira, so read
cruxy.routed_tier on the response for the tier that served the call (see
Auto routing). Never send an upstream provider
id; tiers are the stable contract.
Discover the catalog at runtime instead of hardcoding:
curl https://api.cruxy.ai/api/v1/models \
-H "Authorization: Bearer $CRUXY_API_KEY"GET /api/v1/models returns { object: "list", data: [...] }, each entry
{ id, label, profile, context_window, max_output, latency_target_ms, reasoning, supports_tools, supports_vision, pricing }. Read context_window,
max_output, and supports_tools from the response rather than assuming them.
Choosing a tier compares the tiers.
Chat
POST /api/v1/chat. The request is OpenAI-shaped:
curl https://api.cruxy.ai/api/v1/chat \
-H "Authorization: Bearer $CRUXY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "kavi",
"stream": false,
"messages": [{ "role": "user", "content": "Hello" }]
}'const res = await fetch('https://api.cruxy.ai/api/v1/chat', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.CRUXY_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'kavi',
stream: false,
messages: [{ role: 'user', content: 'Hello' }],
}),
})
const completion = await res.json()Request fields beyond model and messages:
| Field | Type | Notes |
|---|---|---|
messages | array | Each { role, content }; role is system, user, assistant, or tool. |
stream | boolean | true for SSE, false for one JSON object. Optional; false by default. |
temperature | number | 0–2, forwarded upstream unchanged. Omit for the model default. |
max_tokens | integer | Upper bound on generated tokens. Clamped to the tier cap: kavi 8192, vaani 4096, mira 2048. |
reasoning_effort | string | none, low, medium, or high; none disables reasoning. Default per tier: kavi high, vaani low, mira none. Thinking spends max_tokens, so a small max_tokens on kavi can end with finish_reason: "length" and empty content; send none for short answers. See Reasoning effort. |
tools | array | Function tools; see below. |
tool_choice | string or object | none, auto, required, or { type: "function", function: { name } }. |
memory | object | { recall, capture }, each a boolean. Off by default for a developer key; true needs the memory scope. See Memory. |
rules | boolean | Standing-rules injection. Off by default for a developer key; true needs the rules scope. See Standing rules. |
Response shape
The non-streaming and streaming responses differ in shape; read the one that
matches your stream value.
stream: false returns a single object with the reply at the top level (not an
OpenAI choices[] array):
{
"id": "chatcmpl-8f3a1c2d",
"model": "kavi",
"message": { "role": "assistant", "content": "Hi." },
"finish_reason": "stop",
"usage": {
"input_tokens": 12,
"output_tokens": 48,
"billable_input_tokens": 12,
"cache_read_tokens": 0,
"cache_write_tokens": 0,
"cost": { "tier": "kavi", "amount": "<amount>", "currency": "<currency>" }
},
"cruxy": { "request_id": "…", "routed_tier": "kavi", "reasoning_effort": "high" }
}<amount> and <currency> are placeholders; a real amount is a number.
Amounts are specific to your account and shown in your Cruxy Console.
stream: true is an OpenAI-style Server-Sent Events stream: each event is a
chat.completion.chunk on a data: line, terminated by data: [DONE]. Chunks
use the choices[].delta shape:
data: {"object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"Hi"}}]}
data: [DONE]const reader = res.body!.getReader()
const decoder = new TextDecoder()
let buffer = ''
while (true) {
const { value, done } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const events = buffer.split('\n\n')
buffer = events.pop() ?? ''
for (const event of events) {
const line = event.replace(/^data: /, '').trim()
if (line === '[DONE]') break
const chunk = JSON.parse(line) // choices[0].delta.content is the token
}
}The final chunk carries usage as { prompt_tokens, completion_tokens, total_tokens, cost, … }.
Tool calling
Pass tools, each { type: "function", function: { name, description, parameters } } where parameters is a JSON Schema object. When the model calls
one, the turn ends with finish_reason: "tool_calls" and the assistant message
carries tool_calls:
{ "id": "call_abc123", "type": "function",
"function": { "name": "get_weather", "arguments": "{\"city\":\"Paris\"}" } }arguments is a JSON-encoded string; parse it. Run the function, then continue
the conversation with the assistant message that made the call, kept whole,
followed by one tool result per call, tied back by tool_call_id, and send the
same tools again:
messages.push(
message, // the assistant turn as returned, tool_calls included
{ role: 'tool', tool_call_id: 'call_abc123', content: JSON.stringify(result) },
)Repeat while finish_reason is tool_calls; the turn with finish_reason: "stop" carries the answer. Handle tool calls has
the full loop.
Embeddings
POST /api/v1/embeddings with model: "cruxy-embed". input is a string or an
array of strings; one embedding is returned per input, in order.
curl https://api.cruxy.ai/api/v1/embeddings \
-H "Authorization: Bearer $CRUXY_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "model": "cruxy-embed", "input": ["hello", "world"] }'Returns { object: "list", model, data: [{ object: "embedding", index, embedding: [...] }], usage: { prompt_tokens, total_tokens }, cruxy }.
Rerank
POST /api/v1/rerank with model: "cruxy-rerank", a query, and a documents
array of candidate strings. Optional top_n caps how many results come back.
curl https://api.cruxy.ai/api/v1/rerank \
-H "Authorization: Bearer $CRUXY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "cruxy-rerank",
"query": "What is the capital of France?",
"documents": ["Paris is the capital of France.", "Berlin is in Germany."],
"top_n": 3
}'Returns { model, results: [{ index, relevance_score }], usage, cruxy }, ranked
by relevance_score descending. Each result's index points back into the
documents you sent.
Moderations
POST /api/v1/moderations with model: "cruxy-guard". input is a string or
an array; one result is returned per input.
curl https://api.cruxy.ai/api/v1/moderations \
-H "Authorization: Bearer $CRUXY_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "model": "cruxy-guard", "input": "text to classify" }'Returns { id, model, results: [{ flagged, categories, category_scores }] }.
flagged is the overall boolean; categories is a per-label boolean map over
harassment, hate, self-harm, sexual, and violence (with their
/-suffixed subtypes). category_scores mirrors it as numbers, but the guard
model emits labels, so those are binary 1.0/0.0, not calibrated
probabilities. This endpoint returns no cruxy block and no usage.
Errors
Every failure is { code, error }. Branch on code: it is stable; error is
human text that may change.
| Status | code | Meaning |
|---|---|---|
| 400 | invalid_request | Malformed request. Fix it; a retry fails the same way. |
| 400 | context_overflow | Chat input exceeded the tier's context window. |
| 401 | unauthorized | Bearer key missing, malformed, unknown, expired, or revoked. |
| 429 | rate_limited | Requests too fast. Back off, then retry. |
| 429 | budget_exhausted | Spend cap hit. A retry won't clear it until the window resets. |
The device-login endpoints use OAuth-style 400 codes
(authorization_pending, access_denied, expired_token, invalid_grant)
instead of this shape.
The cruxy block
Cruxy-native fields are additive and namespaced under a cruxy key, so a
vanilla OpenAI client ignores them and keeps parsing. What the block carries
differs by endpoint:
- Chat:
{ request_id, routed_tier, reasoning_effort }. Token counts andcostlive in the siblingusageobject, not here. - Embeddings and rerank:
{ request_id, cost }. - Moderations: no top-level block. The request id is the response's
id, formattedmodr-<request_id>, and a flagged result carriesresults[].cruxy.hazard_codes.
request_id is a UUID for the call. Log it; it identifies the call in support
requests. See Request IDs and metadata.