API reference
This page lists every endpoint Zabio serves, field by field, with a runnable example for each in curl, Python and Node. The other pages explain the ideas: Models and routing for the three lines and how a job is classified, Chat completions for images, tools and structured output, Responses API and conversations for stored conversations, Errors for every error code. Come here when you need the exact shape of a request or a response.
Every field name, header and code on this page comes from the code. A machine readable version of the same routes is in openapi.yaml.
Basics
Section titled “Basics”Base URLs
Section titled “Base URLs”| Host | Use |
|---|---|
https://api.zabio.ai/v1 |
default; resolves to the US region |
https://api-us.zabio.ai/v1 |
the US region by name |
The Python and Node examples below use the openai package with base_url set to the first host. Everything else in them is a normal SDK call.
from openai import OpenAI
client = OpenAI(base_url="https://api.zabio.ai/v1", api_key="insert your zabio.ai key here")import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://api.zabio.ai/v1", apiKey: "insert your zabio.ai key here" });Authentication
Section titled “Authentication”Send your API key as a bearer token. Keys look like zb_live_ followed by 43 letters and digits.
Authorization: Bearer zb_live_...Zabio also reads the key from an x-api-key header when there is no Authorization header, for SDKs that send it that way. All /v1 routes need a key, including GET /v1/models, except the public ones: GET /v1/status, POST /v1/public/analyze, GET /v1/public/free-tier, GET /v1/public/plan, GET /v1/public/auto-models and GET /v1/public/catalog-export (the model tables of these docs, as the API serves them).
| Status | code | When |
|---|---|---|
| 401 | invalid_api_key |
the key is missing, incorrect or revoked. The message says which. A missing key also returns WWW-Authenticate: Bearer realm="zabio" |
| 403 | organization_suspended |
the key is valid but its organization is suspended |
| 403 | insufficient_scope |
the key lacks the scope for this endpoint |
| 503 | auth_unavailable |
the key could not be checked right now |
Keys carry scopes. New keys have all three.
| Scope | Endpoints |
|---|---|
chat |
POST /v1/chat/completions and, together with conversations, POST /v1/responses |
embeddings |
POST /v1/embeddings |
conversations |
everything under /v1/responses and /v1/conversations |
Versioning
Section titled “Versioning”The version is the /v1 prefix in the path. There is no version header. Zabio adds fields to OpenAI’s objects (the zabio object on every response, zabio on model objects) rather than changing existing ones. Read the fields you need and ignore the rest.
Request ids
Section titled “Request ids”Every response carries an x-request-id header of the form req_ followed by 32 hex characters. Zabio assigns it; an X-Request-Id you send is not reused. Quote it when you write to support.
Rate limit headers
Section titled “Rate limit headers”The key’s two per minute buckets are reported on every response from /v1/chat/completions, /v1/embeddings and /v1/responses:
| Header | Meaning |
|---|---|
x-ratelimit-limit-requests |
requests per minute allowed for this key (default 600) |
x-ratelimit-remaining-requests |
requests left in the current minute |
x-ratelimit-reset-requests |
time until the bucket is full again, as 12s or 500ms |
x-ratelimit-limit-tokens |
tokens per minute allowed for this key (default 200000) |
x-ratelimit-remaining-tokens |
tokens left. The bucket is charged with an estimate (input tokens plus max_tokens) at admission and refunded with the real count after the call |
x-ratelimit-reset-tokens |
time until the token bucket is full again |
Retry-After |
on 429, whole seconds to wait (at least 1). Also 5 on 502, 503 and 504 when a retry makes sense |
Two more limits have their own headers:
| Header | Meaning |
|---|---|
x-zabio-quota-scope, x-zabio-quota-limit, x-zabio-quota-remaining |
present when a spend limit applies. scope is key (the key’s own limit, over its reset period: a day, a week from Monday or a month from 00:00 UTC, or the key’s whole life) or member (the monthly quota of the member who created the key). Values are in micros (millionths of a US dollar) |
x-ratelimit-limit-ip, x-ratelimit-remaining-ip, x-ratelimit-reset-ip |
the per address limit, checked before authentication. Sent only on the 429 ip_rate_limit_exceeded refusal, never on normal responses |
The Free plan (see Billing, “The Free plan”) adds its own:
| Header | Meaning |
|---|---|
x-zabio-plan |
free on every call served under the Free limits; absent on a paid call |
x-ratelimit-limit-requests-day, x-ratelimit-remaining-requests-day, x-ratelimit-reset-requests-day |
the Free plan’s day: 100 successful requests, what is left, and the time to 00:00 UTC. Only with x-zabio-plan: free; on such calls x-ratelimit-*-requests is the Free minute bucket (20), which is tighter than the key’s own |
x-zabio-free-limit |
on a 429 or 403 free_tier_limit, which limit was met: requests_per_day, requests_per_minute, daily_cost, input_tokens, max_tokens, members or api_keys |
Other headers on every call
Section titled “Other headers on every call”| Header | Meaning |
|---|---|
x-zabio-vendor |
the vendor that served the call (anthropic, openai, google, deepseek, xai, mistral, together, typesafe); also set on vendor errors to name the vendor that failed. Absent on a call kept alive past 90 seconds (see Long calls): its headers went out before the vendor was chosen, and the body’s zabio object carries the vendor, the model and the routing |
x-zabio-byok |
true when your organization’s own vendor key served the call |
x-zabio-balance-micros |
your organization’s credit balance after the admission check, when a balance check ran |
Idempotency
Section titled “Idempotency”Zabio has no idempotency keys. A repeated POST is a new call and is billed again. Retry on 429, 502, 503 and 504 after Retry-After; do not retry a 200 you did not receive without checking your own records first.
Limits
Section titled “Limits”Request bodies are limited to 32 MB (413 payload_too_large). Other limits are listed with each endpoint.
Error envelope
Section titled “Error envelope”Every error is JSON in OpenAI’s shape, so SDKs raise their usual exception types:
{"error": {"message": "'temperature' must be between 0 and 2.", "type": "invalid_request_error", "param": "temperature", "code": "invalid_value"}}type is invalid_request_error, permission_error, billing_error, rate_limit_error or server_error. param is present when a field is at fault. The full code list is on the Errors page; each endpoint below lists the codes specific to it.
GET /v1/models
Section titled “GET /v1/models”Lists the four auto models followed by the catalog, priced for your organization. Keys with an allow list only see their models. No scope is needed beyond a valid key.
Response: {"object": "list", "data": [Model, ...]}. A Model is OpenAI’s model object plus a zabio object:
| Field | Type | Notes |
|---|---|---|
id |
string | auto, auto/best, auto/balanced, auto/cheap or a catalog id |
object |
string | model |
created |
integer | the same launch timestamp for every model |
owned_by |
string | zabio for the auto models, else the vendor: anthropic, openai, google, deepseek, xai, mistral, together, typesafe |
zabio.tier |
string | the model’s class: fast, balanced, powerful, frontier. A label for the pages, not a routing rule. For the auto ids it is the line |
zabio.auto_eligible |
boolean | whether auto can pick it today: it has a skill score for at least one kind of work, or the earlier rule can choose it for a request of kind other (see Models and routing). It follows the daily skill data, the same list as GET /v1/public/auto-models |
zabio.named_only |
boolean | the opposite of auto_eligible: the model is reached by naming it; omitted when false |
zabio.virtual |
boolean | true on the auto models only |
zabio.embedding |
boolean | true on embedding models only |
zabio.capabilities |
object | chat models only: vision, tools, json_schema (booleans), context_window, max_output (tokens) |
zabio.pricing |
object | input, output, cache_read, cache_write in USD per million tokens with your plan’s fee included, currency (USD). cache_write is the 5 minute rate |
zabio.residency |
string | where inference may run, used by routing: global for most models, us for models served only from the United States (today the xAI models) |
zabio.location |
string | where the vendor processes the request, as shown on the Models page (for example United States, China); with zabio.label, the display name when the id alone is ambiguous |
zabio.free_tier |
boolean | true on the models of the free model list (see GET /v1/public/free-tier) |
zabio.availability |
string | available or locked: whether this key may name the model right now. locked only while the organization runs on the Free limits and the model is not on the free list; the auto ids are always available. Absent for a paid organization with credit |
curl
curl https://api.zabio.ai/v1/models \ -H "Authorization: Bearer insert your zabio.ai key here"Python
models = client.models.list()for m in models.data: z = m.model_extra.get("zabio", {}) print(m.id, m.owned_by, z.get("auto_eligible"), (z.get("pricing") or {}).get("input"))Node
const models = await client.models.list();for (const m of models.data) console.log(m.id, m.owned_by, m.zabio.auto_eligible, m.zabio.pricing?.input);GET /v1/models/{id}
Section titled “GET /v1/models/{id}”Returns one model object. Virtual ids with a slash work: GET /v1/models/auto/cheap and the percent encoded GET /v1/models/auto%2Fcheap are both accepted.
Errors: 404 model_not_found when the id is unknown, switched off for your organization or missing from the key’s allow list.
curl
curl https://api.zabio.ai/v1/models/claude-sonnet-5 \ -H "Authorization: Bearer insert your zabio.ai key here"Python
m = client.models.retrieve("auto/cheap")print(m.id, m.model_extra["zabio"])Node
const m = await client.models.retrieve("auto/balanced");console.log(m.id, m.zabio);GET /v1/public/free-tier
Section titled “GET /v1/public/free-tier”Public, no key: the free model list in force and the Free limits, for a pricing page or a client that wants to know what a Free account gets before signing up. Cache-Control: public, max-age=60. The fair-use limit is reported as existing, never with its amount. See Billing, “The Free plan”.
{ "models": ["deepseek-flash@together", "gemini-3.5-flash-lite", "gpt-5.4-nano", "gpt-oss-120b", "mistral-small-2603", "qwen3.5-9b"], "limits": {"requests_per_day": 100, "requests_per_minute": 20, "max_input_tokens": 16000, "max_output_tokens": 2000, "fair_use_daily_limit": true}}Errors: 503 free_tier_unavailable when the list cannot be read. The same list marks each model in GET /v1/models as zabio.free_tier: true, and for a key on the Free limits zabio.availability says available or locked per model.
curl https://api.zabio.ai/v1/public/free-tierGET /v1/public/auto-models
Section titled “GET /v1/public/auto-models”Public, no key: the catalog ids auto can pick today, for a page or a client that wants to show them. A model is in the list when it has a skill score for at least one kind of work, or when the earlier rule can choose it for a request of kind other (see Models and routing). The list follows the daily skill data and routing’s own settings; as_of is the date of the skill data it was read from. Cache-Control: public, max-age=600. The same answer is zabio.auto_eligible in GET /v1/models.
{"object": "auto_models", "as_of": "2026-10-06T00:00:00Z", "models": ["claude-haiku-4-5", "deepseek-flash", "gpt-6-luna"]}curl https://api.zabio.ai/v1/public/auto-modelsPOST /v1/chat/completions
Section titled “POST /v1/chat/completions”One stateless chat turn in OpenAI’s shape. Needs the chat scope. Zabio stores nothing from these calls unless the key has body logging on.
Request
Section titled “Request”| Field | Type | Required | Notes |
|---|---|---|---|
model |
string | no | a catalog id or auto, auto/best, auto/balanced, auto/cheap. Omitted means auto |
messages |
array | yes | 1 to 4096 messages with role system, developer, user, assistant or tool. At least one must be user, assistant or tool. system and developer texts are joined with a blank line into one system prompt |
messages[].content |
string or array | yes | user messages accept text and image_url parts; the other roles accept text parts only. assistant messages may have content or tool_calls or both |
messages[].tool_calls |
array | no | on assistant messages: {"id", "type": "function", "function": {"name", "arguments"}}. Invalid arguments JSON is replaced with {} |
messages[].tool_call_id |
string | on tool messages |
the id of the call being answered |
tools |
array | no | up to 256 tools of type function with function.name, function.description, function.parameters (defaults to an empty object schema) |
tool_choice |
string or object | no | auto, none, required or {"type": "function", "function": {"name": "..."}}. Needs tools unless it is none |
parallel_tool_calls |
boolean | no | passed to the vendor where supported |
max_tokens, max_completion_tokens |
integer | no | max_completion_tokens wins when both are present. Default 16000, capped at the model’s max_output |
temperature |
number | no | 0 to 2 |
top_p |
number | no | 0 to 1 |
stop |
string or array | no | up to 4 sequences |
stream |
boolean | no | server sent events, below |
stream_options.include_usage |
boolean | no | accepted; the usage chunk is always sent |
response_format |
object | no | {"type": "text"}, {"type": "json_object"} or {"type": "json_schema", "json_schema": {"name", "schema", "strict"}}; schema is required |
reasoning_effort |
string | no | none, minimal, low, medium, high or xhigh; mapped onto the served model’s own control, see Reasoning levels |
user |
string | no | forwarded as request metadata |
metadata |
object | no | string values, forwarded as request metadata |
store |
boolean | no | accepted and has no effect; chat completions store nothing |
n |
integer | no | only 1; larger values are refused |
seed, logit_bias, presence_penalty, frequency_penalty |
no | accepted and ignored; reported in zabio.ignored_parameters when they carry a non default value |
|
logprobs, top_logprobs, audio, prediction, web_search_options, non text modalities |
no | refused with 400 unsupported_parameter |
|
zabio |
object | no | Zabio’s options, next table |
The zabio request object
Section titled “The zabio request object”| Field | Type | Values | What it does |
|---|---|---|---|
session_id |
string | up to 128 characters | groups calls so the routing decision is reused; echoed in the response |
vendors |
array | anthropic, openai, google, deepseek, xai, mistral, together |
only these vendors are candidates |
fallback |
string | same_vendor, cross_vendor, none |
what happens when an explicitly named model is unavailable. Default: the key’s fallback_policy |
cache.ttl |
string | 5m, 1h, none |
prompt cache lifetime where the vendor offers one. Caching is automatic on every call, to auto or to a named model, and none turns it off. Default: 5m (1h in a session whose turns are far apart), written on a long prompt’s second appearance (see cache.ttl); inside a batch, none, except that lines sharing a long prefix on an Anthropic model read it from a cache Zabio writes once for the batch |
cache.breakpoints |
string | auto, none |
none disables caching for the call and forces ttl to none |
split |
string | auto, off |
whether Zabio may split a big request under auto into an extract on a cheap model and an answer on the routed model, when that is cheaper. Default auto; off sends the request whole. Never on a named model. See Split and route on big inputs |
deadline |
string | a duration with units s, m or h (15m, 1h, 24h), up to 168h |
how long the answer may take. Absent or under 15 minutes: nothing changes. From 15 minutes up, the call may be served on the vendor’s flex tier where the model has one, the same model at about half the price, slower. Not a guarantee of a delivery time. See The flex tier |
The same values can travel as headers, which win over the body: x-zabio-session-id, x-zabio-cache-ttl, x-zabio-fallback, x-zabio-split, x-zabio-deadline. There is no header for vendors or cache.breakpoints. Bad values return 400 invalid_value with param set to zabio.<field>.
Response
Section titled “Response”A chat.completion object with exactly one choice:
| Field | Type | Notes |
|---|---|---|
id |
string | chatcmpl- followed by 32 hex characters |
object |
string | chat.completion |
created |
integer | Unix seconds |
model |
string | the model that answered (same as zabio.model_served) |
choices[0].index |
integer | 0 |
choices[0].message |
object | role assistant, content (string, or null when only tool calls were produced), tool_calls when present. arguments is always a JSON string, {} when empty |
choices[0].finish_reason |
string | stop, length, tool_calls or content_filter. A reasoning model that spends its whole output budget thinking returns an empty answer with length: raise max_tokens so it leaves room for the answer above the thinking, or lower reasoning_effort. A max_tokens above the served model’s maximum output is lowered to that limit |
choices[0].logprobs |
null | always null |
usage |
object | prompt_tokens (every input token, cached or not), completion_tokens, total_tokens, prompt_tokens_details.cached_tokens when any tokens were read from cache, and completion_tokens_details.reasoning_tokens when the vendor reports its thinking count |
system_fingerprint |
null | always null |
zabio |
object | next table |
The zabio response object
Section titled “The zabio response object”The same object is returned by /v1/chat/completions (in the body and in the final streaming chunk) and by /v1/responses. Fields with empty values are omitted.
| Field | Type | Meaning |
|---|---|---|
call_id |
string | the call’s id, call_ and 32 hex characters: the same id Logs and the call list show, and the same hex as the response’s chatcmpl- id. Quote it to find the call later |
vendor |
string | who served the call: anthropic, openai, google, deepseek, xai, mistral, together or typesafe |
model_served |
string | the model that answered; differs from model you sent for auto and fallbacks |
routing |
object | line (the tier as you asked for it: auto, cheap, balanced or best), floor (the skill floor applied, none, middle or top, after a hard or high-stakes request moved it one step up and after any move to a stronger model; absent when the request was routed by the earlier rule, see Models and routing), task (the job as routing reads it), task_type (the kind of work, one of the tags in Kinds of work; absent when the classifier named none), difficulty (1 to 5), band (easy, medium, hard, frontier), stakes (normal or high), biased (true when low confidence raised the difficulty), cell (the knowledge used, task/band), confidence (0 to 1; 0 when no classifier ran), reason (the classifier’s sentence), trigger, classifier (who decided: typesafe/jev-1.13.0, or the text classifier it fell back to; absent on explicit models), effort (the reasoning setting applied with the model), effort_reason (why: the band’s evidence, high stakes, the best line, no evidence, or your explicit level) and sticky (present only when the session’s warm prompt cache decided the pick; names the model kept and the one cold pricing would have chosen, see Sessions and stickiness). Triggers: no_prior_decision, new_human_message, cache_hit, low_confidence_prior, long_idle, tool_failure_streak, customer_feedback, explicit, escalation. Absent when there is nothing to report |
reasoning |
object | requested (the level you sent, omitted when none) and applied (what the served model got: default, off, the vendor’s level name, budget N, on, always on or not supported) |
cost_micros |
integer | what you were billed, in millionths of a US dollar |
cost |
object | vendor_micros (provider list price), service_fee_micros, call_fee_micros (the fixed fee an auto request pays once, the price of automatic model choice; 0 on a named model), savings_fee_micros and classifier_micros (both always 0, kept so existing clients keep parsing them), direct_micros (the same call on your key’s baseline model with the same cache reads and writes at that model’s rates; the provider price itself on a named model), saved_micros (direct_micros minus cost_micros; can be negative; left out on an auto call while baseline.pending is true), baseline (on auto only: {pending, model} once your key has a baseline, or {pending: true, calls, calls_needed} while it is still under the 20 successful calls the baseline needs) |
byok |
boolean | present and true when your own vendor key served the call |
cache |
object | always present: read_tokens, write_tokens, ttl_applied (5m, 1h or "") |
session_id |
string | echoes the session id you sent. On /v1/responses it is the conversation id |
escalated_from |
string | line/model of the model whose answer auto replaced, for example cheap/gpt-5.4-nano: on the Responses API when that model asked for a stronger one, and on any request that is not streamed when its answer failed Zabio’s check |
compacted |
boolean | Responses API only: the stored history was summarized before this turn |
vendor_stop_reason |
string | the vendor’s own stop reason |
ignored_parameters |
array | request fields Zabio accepted and ignored |
dropped_parameters |
array | sampling parameters the model that answered does not take (temperature, top_p), left out of its request; also the x-zabio-dropped-params header. See dropped_parameters |
attempts |
integer | how many providers were tried before one answered (1 when the first worked) |
decision |
object | when jev-1.13.0 answered (named, or chosen by auto for a classification job, see Classification jobs on auto): one entry per property of your schema, in schema order. A choice: {"type": "choice", "probabilities": {value: p, ...}, "confidence"}; a boolean: {"type": "noul", "probability": p} (the probability of yes; true at 0.5 and above); a score: {"type": "score", "value": the weighted level, "probabilities": {level: p, ...}, "confidence"}. See Decision models |
split |
object | present while split and route is on (see Split and route on big inputs): applied (boolean, true when the answer came from an excerpt of your source), pattern (distill, once the split ran), reason (why the request was not split: named_model, opt_out, not_eligible, below_threshold, jev_no, jev_failed, price_check; empty on a split and on a fallback), estimate (unsplit_micros, split_micros; present once the gate passed), steps (a list of {role, vendor, model, input_tokens, output_tokens, micros, billed}; roles questions, extract, check, answer; micros is the call’s list price; billed is false for Zabio’s own costs and for every step of a fallback), excerpt (lines_kept, lines_total, tokens, verbatim_rate), check_p (the check’s probability), fallback (the guard that sent the request whole: extractor_declined, empty_extract, extract_too_big, low_verbatim, check_rejected, need_more_source, extract_failed, check_failed), saved_micros (on a split that held, what the split saved: the request sent whole to the answer’s model less what the split’s calls cost, at list price; separate from cost.saved_micros, and present while your key’s baseline is pending too). Empty fields are omitted |
tier |
object | the service tier of the call (see The flex tier): requested (standard, flex or batch, what your deadline mapped to), applied (what served the call) and fallback (capacity when flex was requested and the vendor refused it for capacity, so standard served and was billed; synchronous when a deadline of 24 hours or more asked for batch on a synchronous call, so flex or standard served; no_batch_tier on a background response whose model has no batch tier, and on a batch line at the standard price under zabio.allow_standard_price; decision on a batch line Jev answered at its own price, below the text pick’s batch price). Present on every response |
plan |
object | present on a call served under the Free limits (see Billing, “The Free plan”): tier (free), reason (free_plan, the organization never topped up; zero_balance, a paid organization at zero continuing on the free tier), paid_would_use (on auto, the model a paid account would have been given, when it differs from the one that answered) and topup_url. Absent on a paid call. Not to be confused with tier, the vendor processing tier |
Kinds of work
Section titled “Kinds of work”zabio.routing.task_type names the kind of work the classifier saw in an auto request, as one of these tags. The dashboard’s spend by kind of work and the Logs page show the name, and the dashboard’s CSV export of your calls carries the tag. The list can grow: treat a tag you do not know as a new kind of work, not as an error.
| Tag | Name |
|---|---|
classification_tagging |
Classification |
data:extraction |
Data Extraction |
agent:workflow_execution |
Workflow Execution |
roleplay_fiction |
Roleplay & Fiction |
content_writing |
Content Writing |
data:transformation |
Data Transformation |
code:general_impl |
Code Generation |
agent:multi_step_planning |
Multi-step Planning |
qa_knowledge |
Q&A & Knowledge |
translation |
Translation |
conversational_reply |
Conversation |
code:debugging |
Debugging |
summarization |
Summarization |
code:file_read_write |
File I/O |
customer_support |
Customer Support |
code:review_security |
Code Review |
agent:tool_dispatch |
Tool Dispatch |
code:shell_execution |
Shell Execution |
agent:memory_extraction |
Memory Extraction |
code:frontend_ui |
Frontend & UI |
finance_trading |
Finance & Trading |
research_report |
Research & Reports |
agent:web_search |
Web Search |
security_audit |
Security Audit |
code:repo_scan |
Repo Scanning |
math |
Math |
code:devops_config |
DevOps & Config |
code:sql_database |
SQL & Database |
devops |
DevOps |
other |
Other |
Streaming
Section titled “Streaming”With stream: true the status is 200 and the body is text/event-stream. Each event is one data: line holding a chat.completion.chunk with the same id, created and model as a unary response would have. Order:
- a chunk with
delta: {"role": "assistant", "content": ""} - content chunks with
delta.content, and tool call chunks withdelta.tool_calls[]carryingindex,id,typeandfunction.nameon the first chunk of a call andfunction.argumentsfragments afterwards - a chunk with an empty
deltaandfinish_reason - one usage chunk with
choices: [], theusageblock and thezabioobject data: [DONE]
Every chunk carries system_fingerprint: null, and every choice logprobs: null. If the vendor connection fails after the stream started, one event with {"error": {"message", "type": "server_error", "code"}} is sent, then data: [DONE]. Errors before the first chunk are ordinary JSON errors with their HTTP status.
Long calls
Section titled “Long calls”A vendor attempt may run up to 600 seconds on powerful and frontier models (180 seconds on the others). To keep the connection alive that long, a call with no vendor answer after 90 seconds commits its 200 and writes one space every 20 seconds until the JSON body, which parsers read unchanged. A failure after that commit is delivered in the body as a chat.completion with choices: [] and an error object (message, type, code). Streams send an SSE comment (: keepalive) every 20 seconds while the vendor opens or is silent, and a failure before any data is data: {"error": {...}} then data: [DONE]. Set client timeouts to at least 600 seconds for powerful and frontier models. A disconnect cancels the vendor call and, for a call that was not streamed, costs nothing.
Errors specific to this endpoint
Section titled “Errors specific to this endpoint”| Status | code | When |
|---|---|---|
| 400 | missing_messages |
messages is absent or has no user, assistant or tool entry |
| 400 | too_many_messages, too_many_tools |
more than 4096 messages or 256 tools |
| 400 | invalid_value |
a field is out of range; param names it (temperature, top_p, max_tokens, stop, reasoning_effort, tool_choice, tools[i].type, messages[i].role, messages[i].content, response_format.type, response_format.json_schema, zabio.session_id, zabio.cache.ttl, zabio.cache.breakpoints, zabio.fallback, zabio.split, zabio.deadline) |
| 400 | unsupported_parameter |
n above 1, logprobs, top_logprobs, audio, prediction, web_search_options, a modalities entry other than text, the legacy function role, a content part type other than text and image_url, tools sent to a named model that cannot take them through Zabio (today gpt-6-astra) |
| 400 | invalid_image_url |
an image could not be fetched or is not allowed; param points at the part |
| 400 | byok_key_invalid |
your organization’s own vendor key was rejected by the vendor |
| 400 | upstream_invalid_request |
the vendor rejected the request; the message carries the vendor’s reason |
| 400 | unsupported_for_model |
the model you named cannot do what the request asks; param names the field. On jev-1.13.0: no json_schema, a free text or nested property, tools, images, tool results or stream: true; the message says what Jev answers and how to ask |
| 402 | insufficient_credits |
no credits left; only a named model served on your own vendor key skips this check, auto never does |
| 403 | insufficient_scope |
the key lacks chat |
| 404 | model_not_found |
unknown model, or not allowed for the key |
| 429 | rate_limit_exceeded, quota_exceeded, upstream_rate_limited |
see the headers above and Errors |
| 502, 503, 504 | upstream_error, no_provider_available, billing_unavailable, upstream_timeout |
see Errors |
Examples
Section titled “Examples”A unary call with auto, then reading what was served and what it cost.
curl
curl https://api.zabio.ai/v1/chat/completions \ -H "Authorization: Bearer insert your zabio.ai key here" \ -H "Content-Type: application/json" \ -d '{ "model": "auto", "messages": [ {"role": "system", "content": "Answer in one sentence."}, {"role": "user", "content": "Why is the sky blue?"} ], "zabio": {"session_id": "docs-demo"} }'Python
r = client.chat.completions.create( model="auto", messages=[ {"role": "system", "content": "Answer in one sentence."}, {"role": "user", "content": "Why is the sky blue?"}, ], extra_body={"zabio": {"session_id": "docs-demo"}},)print(r.choices[0].message.content)z = r.model_extra["zabio"]print(z["model_served"], z["cost_micros"], z["routing"]["line"], z["routing"]["task"])Node
const r = await client.chat.completions.create({ model: "auto", messages: [ { role: "system", content: "Answer in one sentence." }, { role: "user", content: "Why is the sky blue?" }, ], zabio: { session_id: "docs-demo" },});console.log(r.choices[0].message.content);console.log(r.zabio.model_served, r.zabio.cost_micros, r.zabio.routing?.line, r.zabio.routing?.task);The same call streamed. The zabio object arrives in the last chunk, the one with an empty choices list.
curl
curl -N https://api.zabio.ai/v1/chat/completions \ -H "Authorization: Bearer insert your zabio.ai key here" \ -H "Content-Type: application/json" \ -d '{"model": "auto/cheap", "messages": [{"role": "user", "content": "Count to five."}], "stream": true}'Python
stream = client.chat.completions.create( model="auto/cheap", messages=[{"role": "user", "content": "Count to five."}], stream=True,)for chunk in stream: if chunk.choices and chunk.choices[0].delta.content: print(chunk.choices[0].delta.content, end="", flush=True) if chunk.usage: print("\n", chunk.usage.total_tokens, chunk.model_extra.get("zabio"))Node
const stream = await client.chat.completions.create({ model: "auto/cheap", messages: [{ role: "user", content: "Count to five." }], stream: true,});for await (const chunk of stream) { process.stdout.write(chunk.choices[0]?.delta?.content ?? ""); if (chunk.usage) console.log("\n", chunk.usage.total_tokens, chunk.zabio);}A tool call round trip. The assistant answers with tool_calls; you run the function and send the result back as a tool message.
curl
curl https://api.zabio.ai/v1/chat/completions \ -H "Authorization: Bearer insert your zabio.ai key here" \ -H "Content-Type: application/json" \ -d '{ "model": "auto", "messages": [{"role": "user", "content": "What is the weather in Lima? Use the tool."}], "tools": [{"type": "function", "function": {"name": "get_weather", "description": "Weather for a city", "parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}}}], "tool_choice": "required" }'Python
tool = {"type": "function", "function": {"name": "get_weather", "description": "Weather for a city", "parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}}}messages = [{"role": "user", "content": "What is the weather in Lima? Use the tool."}]
first = client.chat.completions.create(model="auto", messages=messages, tools=[tool], tool_choice="required")call = first.choices[0].message.tool_calls[0]messages.append(first.choices[0].message.model_dump(exclude_none=True))messages.append({"role": "tool", "tool_call_id": call.id, "content": "Sunny, 22C"})
second = client.chat.completions.create(model="auto", messages=messages, tools=[tool])print(second.choices[0].message.content)Node
const tools = [{ type: "function", function: { name: "get_weather", description: "Weather for a city", parameters: { type: "object", properties: { city: { type: "string" } }, required: ["city"] } } }];const messages = [{ role: "user", content: "What is the weather in Lima? Use the tool." }];
const first = await client.chat.completions.create({ model: "auto", messages, tools, tool_choice: "required" });const call = first.choices[0].message.tool_calls[0];messages.push(first.choices[0].message, { role: "tool", tool_call_id: call.id, content: "Sunny, 22C" });
const second = await client.chat.completions.create({ model: "auto", messages, tools });console.log(second.choices[0].message.content);POST /v1/embeddings
Section titled “POST /v1/embeddings”Embeddings in OpenAI’s shape. Models are always explicit; there is no auto. Needs the embeddings scope.
Request
Section titled “Request”| Field | Type | Required | Notes |
|---|---|---|---|
model |
string | yes | text-embedding-3-small, text-embedding-3-large or gemini-embedding-001 |
input |
string or array | yes | one string or 1 to 2048 strings |
dimensions |
integer | no | passed to the vendor |
encoding_format |
string | no | float (default) or base64 (little endian float32) |
user |
string | no | accepted |
The zabio request object does not apply here. Rate limit and quota headers are the same as for chat completions; the token bucket is charged with an estimate of one token per four characters.
Response
Section titled “Response”| Field | Type | Notes |
|---|---|---|
object |
string | list |
data[] |
array | {"object": "embedding", "index", "embedding"}; embedding is an array of numbers or a base64 string |
model |
string | the model that answered |
usage |
object | prompt_tokens and total_tokens (equal) |
zabio |
object | call_id, vendor, model_served, cost_micros, tokens_estimated. tokens_estimated is true for Google models, which do not report token counts; Zabio estimates and bills on the estimate |
Errors specific to this endpoint
Section titled “Errors specific to this endpoint”| Status | code | When |
|---|---|---|
| 400 | missing_model |
model absent |
| 400 | invalid_value |
input empty or over 2048 items, or encoding_format not float or base64 |
| 400 | byok_key_invalid |
your own vendor key was rejected |
| 404 | model_not_found |
not an embedding model, or not allowed for the key |
| 503 | no_provider_available |
the provider for this model is not configured on this server |
Examples
Section titled “Examples”curl
curl https://api.zabio.ai/v1/embeddings \ -H "Authorization: Bearer insert your zabio.ai key here" \ -H "Content-Type: application/json" \ -d '{"model": "text-embedding-3-small", "input": ["first text", "second text"]}'Python
res = client.embeddings.create(model="text-embedding-3-small", input=["first text", "second text"])print(len(res.data), len(res.data[0].embedding), res.model_extra["zabio"]["cost_micros"])Node
const res = await client.embeddings.create({ model: "text-embedding-3-small", input: ["first text", "second text"] });console.log(res.data.length, res.data[0].embedding.length, res.zabio.cost_micros);POST /v1/responses
Section titled “POST /v1/responses”One turn of a stored conversation in the OpenAI Responses API shape. Zabio assembles the history, routes the turn, stores the answer and returns it. Needs the conversations scope; the routing itself reuses the chat path.
Request
Section titled “Request”| Field | Type | Required | Notes |
|---|---|---|---|
model |
string | no | a catalog id or an auto model. Omitted means auto |
input |
string or array | yes | a string is one user message. Items: {"type": "message", "role": "user" or "assistant" or "system" or "developer", "content": string or parts} (type may be omitted), {"type": "function_call_output", "call_id", "output"}, {"type": "function_call", "call_id", "name", "arguments"}. Content parts: input_text, text and output_text (all text), input_image with image_url |
instructions |
string | no | stored with the turn as a developer item |
previous_response_id |
string | no | resp_... to continue from. Pointing at an older response branches the conversation |
conversation |
string or object | no | conv_... or {"id": "conv_..."} to continue from the conversation’s latest turn. With previous_response_id, both must belong to the same conversation |
store |
boolean | no | default true. false computes the turn from the stored history and stores nothing; the response id cannot be retrieved afterwards |
tools |
array | no | flat function tools: {"type": "function", "name", "description", "parameters", "strict"} |
tool_choice |
string or object | no | auto, none, required or {"type": "function", "name"}; needs tools unless none |
parallel_tool_calls |
boolean | no | passed to the vendor where supported |
max_output_tokens |
integer | no | at least 1; default 4096 |
temperature |
number | no | 0 to 2 |
top_p |
number | no | above 0 and up to 1 |
text.format |
object | no | {"type": "text"}, {"type": "json_object"} or {"type": "json_schema", "name", "schema", "strict"} |
reasoning.effort |
string | no | none, minimal, low, medium, high or xhigh, the same scale as reasoning_effort on chat completions |
metadata |
object | no | up to 16 keys; keys up to 64 characters, values up to 512 |
stream |
boolean | no | server sent events, below |
background |
boolean | no | default false. true returns the response at once with status queued and runs the turn later; poll GET /v1/responses/{id}. Needs store true and no stream. With a zabio.deadline of 24 hours or more on a model that can be batched, the turn joins your organization’s next vendor batch at the batch price; otherwise it runs as its own job at flex or standard and zabio.tier says why. See Background responses |
include, truncation, user |
no | accepted and ignored; listed in zabio.ignored_parameters |
|
zabio |
object | no | next table |
The zabio request object
Section titled “The zabio request object”Everything from chat completions (session_id, vendors, fallback, cache, split, deadline) plus:
| Field | Type | Values | What it does |
|---|---|---|---|
retention_days |
integer | 0 to 365 | retention of a conversation created by this call. 0 deletes it as soon as the response is returned. Ignored when the conversation already exists |
compaction |
string | auto (default), off |
off fails with context_length_exceeded instead of summarizing old history |
session_id is overwritten: within a conversation the conversation id is the session, so zabio.session_id in the response is conv_.... The x-zabio-* headers work here too.
Response
Section titled “Response”| Field | Type | Notes |
|---|---|---|
id |
string | resp_ followed by 32 hex characters |
object |
string | response |
created_at |
integer | Unix seconds |
status |
string | completed, incomplete (hit max_output_tokens; incomplete_details.reason is max_output_tokens) or failed (streaming, after a vendor error; or a background response that failed). A background response is also queued and cancelled |
background |
boolean | true when the turn was created with background |
error |
object or null | on a failed or cancelled background response, {"code", "message"} |
model |
string | the model that answered |
output[] |
array | {"type": "message", "id": "msg_...", "status", "role": "assistant", "content": [{"type": "output_text", "text", "annotations": []}]} and {"type": "function_call", "id": "fc_...", "call_id", "name", "arguments", "status": "completed"} |
usage |
object | input_tokens (cached included), input_tokens_details.cached_tokens, output_tokens, output_tokens_details.reasoning_tokens (the vendor’s thinking count when it reports one, else 0), total_tokens |
previous_response_id |
string or null | what you sent |
conversation |
object or null | {"id": "conv_..."}; null only when store is false and no conversation was given |
instructions |
string or null | what you sent |
metadata |
object | what you sent, or {} |
store |
boolean | what applied |
max_output_tokens |
integer or null | what applied; null on GET /v1/responses/{id} |
tools, tool_choice, parallel_tool_calls |
what applied; tool_choice defaults to auto. GET returns [], auto and true |
|
temperature, top_p |
number | present when you sent them |
incomplete_details |
object or null | {"reason": "max_output_tokens"} when status is incomplete |
error |
null | always null on this endpoint; errors are HTTP errors or a stream error event |
zabio |
object | the zabio response object, with escalated_from and compacted filled in when they apply |
Streaming
Section titled “Streaming”With stream: true the body is text/event-stream with event: and data: lines. Every payload carries type and a sequence_number starting at 0. The sequence for a text answer is response.created, response.in_progress, response.output_item.added, response.content_part.added, response.output_text.delta (repeated), response.output_text.done, response.content_part.done, response.output_item.done, then response.completed (or response.incomplete, response.failed) with the full response object under response. Tool calls use response.output_item.added, response.function_call_arguments.delta (repeated), response.function_call_arguments.done, response.output_item.done. A vendor failure after the stream started sends event: error with {"type": "error", "code", "message", "param": null}.
Delta events carry output_index, item_id, content_index (text only) and delta. Item ids inside a stream are msg_<response id>_<index> and fc_<response id>_<index>; the stored ids returned by response.completed and by GET are msg_ and fc_ followed by 32 hex characters.
Long calls
Section titled “Long calls”The same keepalive as chat completions: after 90 seconds without an answer a non-streamed call commits its 200 and writes a space every 20 seconds until the object; a failure after that is the object with status: "failed" and error: {"code", "message"}. A stream sends : keepalive comments every 20 seconds while the vendor opens or is silent, before response.created if need be; a failure before any output is a response.failed event carrying that object.
Errors specific to this endpoint
Section titled “Errors specific to this endpoint”| Status | code | When |
|---|---|---|
| 400 | missing_field |
input, an item’s role, content or call_id is missing; param names it |
| 400 | invalid_value |
a field is out of range; includes previous_response_id and conversation that are not ids, zabio.retention_days, zabio.compaction, an input with no user message or tool output, and background with stream or with store: false |
| 400 | background_unavailable |
background responses are not available on this server |
| 402 | insufficient_credits |
on a background response headed for a batch: its estimated cost exceeds the available balance; nothing is stored |
| 400 | conversation_mismatch |
previous_response_id belongs to another conversation |
| 400 | conversation_too_large |
the thread reached 4000 entries or 2000000 estimated tokens |
| 400 | context_length_exceeded |
the history plus this turn does not fit and compaction is off, impossible (store: false, no conversation) or not enough; the message carries the numbers |
| 403 | insufficient_scope, conversations_closed |
the key lacks conversations; stored conversations are closed for the organization |
| 404 | previous_response_not_found, conversation_not_found |
unknown or deleted |
| 503 | conversations_unavailable |
stored conversations are not available on this server |
Everything listed for chat completions applies as well: model_not_found, insufficient_credits, the 429 family and vendor errors.
Examples
Section titled “Examples”Two turns, the second continuing from the first.
curl
curl https://api.zabio.ai/v1/responses \ -H "Authorization: Bearer insert your zabio.ai key here" \ -H "Content-Type: application/json" \ -d '{"model": "auto", "input": "My name is Ana. Remember it.", "zabio": {"retention_days": 7}}'
# then, with the id from the first answer:curl https://api.zabio.ai/v1/responses \ -H "Authorization: Bearer insert your zabio.ai key here" \ -H "Content-Type: application/json" \ -d '{"model": "auto", "input": "What is my name?", "previous_response_id": "resp_..."}'Python
first = client.responses.create(model="auto", input="My name is Ana. Remember it.", extra_body={"zabio": {"retention_days": 7}})second = client.responses.create(model="auto", input="What is my name?", previous_response_id=first.id)print(second.output_text)print(second.conversation.id, second.model_extra["zabio"]["model_served"])Node
const first = await client.responses.create({ model: "auto", input: "My name is Ana. Remember it.", zabio: { retention_days: 7 } });const second = await client.responses.create({ model: "auto", input: "What is my name?", previous_response_id: first.id });console.log(second.output_text);console.log(second.conversation.id, second.zabio.model_served);Streaming a turn and reading the final object.
curl
curl -N https://api.zabio.ai/v1/responses \ -H "Authorization: Bearer insert your zabio.ai key here" \ -H "Content-Type: application/json" \ -d '{"model": "auto", "input": "Count to three.", "stream": true}'Python
with client.responses.stream(model="auto", input="Count to three.") as stream: for event in stream: if event.type == "response.output_text.delta": print(event.delta, end="", flush=True) final = stream.get_final_response()print("\n", final.status, final.model_extra["zabio"])Node
const stream = client.responses.stream({ model: "auto", input: "Count to three." });for await (const event of stream) { if (event.type === "response.output_text.delta") process.stdout.write(event.delta);}const final = await stream.finalResponse();console.log("\n", final.status, final.zabio);GET /v1/responses/{id}
Section titled “GET /v1/responses/{id}”Returns a stored response with its output. model is the model that served it. Errors: 404 response_not_found when the id is malformed, unknown, deleted, or was created with store: false.
curl
curl https://api.zabio.ai/v1/responses/resp_... \ -H "Authorization: Bearer insert your zabio.ai key here"Python
r = client.responses.retrieve("resp_...")print(r.status, r.output_text)Node
const r = await client.responses.retrieve("resp_...");console.log(r.status, r.output_text);POST /v1/responses/{id}/cancel
Section titled “POST /v1/responses/{id}/cancel”Cancels a background response. Queued, or still waiting for its batch to be sent: the response ends cancelled and nothing is billed. Running as a job: the vendor call is cut and the response ends cancelled. Already sent to the model vendor inside a batch: 409 not_cancellable, the answer arrives and is billed. Already ended: the object as it is. Returns the response object. Errors: 400 not_background when the response was not created with background; 404 response_not_found.
curl
curl -X POST https://api.zabio.ai/v1/responses/resp_.../cancel \ -H "Authorization: Bearer insert your zabio.ai key here"Python
r = client.responses.cancel("resp_...")print(r.status) # cancelledNode
const r = await client.responses.cancel("resp_...");console.log(r.status); // cancelledDELETE /v1/responses/{id}
Section titled “DELETE /v1/responses/{id}”Deletes the response with its turn (the inputs it stored and its outputs) and everything stored after it on the same thread. The conversation continues from where that turn started. Returns {"id": "resp_...", "object": "response", "deleted": true}. Errors: 404 response_not_found.
curl
curl -X DELETE https://api.zabio.ai/v1/responses/resp_... \ -H "Authorization: Bearer insert your zabio.ai key here"Python
client.responses.delete("resp_...")Node
await client.responses.delete("resp_...");POST /v1/conversations
Section titled “POST /v1/conversations”Creates an empty or seeded conversation. The body may be empty.
| Field | Type | Required | Notes |
|---|---|---|---|
metadata |
object | no | up to 16 string keys |
items |
array | no | input items in the same shapes as input on POST /v1/responses; stored without a model turn |
zabio.retention_days |
integer | no | 0 to 365; default is the organization’s retention |
Response, also returned by GET and by POST /v1/conversations/{id}:
| Field | Type | Notes |
|---|---|---|
id |
string | conv_ followed by 32 hex characters |
object |
string | conversation |
created_at |
integer | Unix seconds |
metadata |
object | |
zabio.entries |
integer | stored items, all threads |
zabio.token_estimate |
integer | estimated tokens of the stored items |
zabio.expires_at |
string | RFC 3339; when the conversation is deleted unless a new turn resets the clock |
zabio.last_turn_at |
string | RFC 3339 |
zabio.retention_days |
integer | present when the conversation has its own retention |
Errors: 400 invalid_json, 400 invalid_value on metadata or zabio.retention_days, 400 missing_field or invalid_value on a bad item.
curl
curl https://api.zabio.ai/v1/conversations \ -H "Authorization: Bearer insert your zabio.ai key here" \ -H "Content-Type: application/json" \ -d '{"metadata": {"customer": "acme"}, "items": [{"role": "user", "content": "Hello"}], "zabio": {"retention_days": 30}}'Python
conv = client.conversations.create(metadata={"customer": "acme"}, items=[{"role": "user", "content": "Hello"}], extra_body={"zabio": {"retention_days": 30}})print(conv.id, conv.model_extra["zabio"]["expires_at"])Node
const conv = await client.conversations.create({ metadata: { customer: "acme" }, items: [{ role: "user", content: "Hello" }], zabio: { retention_days: 30 },});console.log(conv.id, conv.zabio.expires_at);GET /v1/conversations/{id}
Section titled “GET /v1/conversations/{id}”Returns the conversation object above. Errors: 404 conversation_not_found.
curl
curl https://api.zabio.ai/v1/conversations/conv_... \ -H "Authorization: Bearer insert your zabio.ai key here"Python
conv = client.conversations.retrieve("conv_...")print(conv.model_extra["zabio"]["entries"])Node
const conv = await client.conversations.retrieve("conv_...");console.log(conv.zabio.entries);POST /v1/conversations/{id}
Section titled “POST /v1/conversations/{id}”Replaces the conversation’s metadata with {"metadata": {...}} (up to 16 keys) and returns the conversation object. Errors: 400 invalid_json, 400 invalid_value, 404 conversation_not_found.
curl
curl https://api.zabio.ai/v1/conversations/conv_... \ -H "Authorization: Bearer insert your zabio.ai key here" \ -H "Content-Type: application/json" \ -d '{"metadata": {"customer": "acme", "ticket": "4711"}}'Python
conv = client.conversations.update("conv_...", metadata={"customer": "acme", "ticket": "4711"})Node
const conv = await client.conversations.update("conv_...", { metadata: { customer: "acme", ticket: "4711" } });DELETE /v1/conversations/{id}
Section titled “DELETE /v1/conversations/{id}”Deletes the conversation and everything in it, immediately and permanently. Returns {"id": "conv_...", "object": "conversation.deleted", "deleted": true}. Errors: 404 conversation_not_found.
curl
curl -X DELETE https://api.zabio.ai/v1/conversations/conv_... \ -H "Authorization: Bearer insert your zabio.ai key here"Python
client.conversations.delete("conv_...")Node
await client.conversations.delete("conv_...");GET /v1/conversations/{id}/items
Section titled “GET /v1/conversations/{id}/items”Lists every stored item in creation order, all threads, including compaction summaries.
| Query parameter | Default | Notes |
|---|---|---|
limit |
20 | 1 to 100 |
after |
an item id (msg_..., fc_..., fco_..., cmp_...) to continue after |
|
order |
asc |
asc or desc |
Response: {"object": "list", "data": [...], "has_more", "first_id", "last_id"}. Items:
type |
Shape |
|---|---|
message |
{"id": "msg_...", "role", "status": "completed", "content": [...]}. User, system and developer content is input_text and input_image parts; assistant content is output_text parts |
function_call |
{"id": "fc_...", "call_id", "name", "arguments", "status"} |
function_call_output |
{"id": "fco_...", "call_id", "output", "status": "completed"} |
compaction |
{"id": "cmp_...", "summary"}: the summary that replaced older history |
Errors: 400 invalid_value on limit or after, 404 conversation_not_found.
curl
curl "https://api.zabio.ai/v1/conversations/conv_.../items?limit=50&order=asc" \ -H "Authorization: Bearer insert your zabio.ai key here"Python
page = client.conversations.items.list("conv_...", limit=50)for item in page.data: print(item.type, getattr(item, "role", ""))Node
const page = await client.conversations.items.list("conv_...", { limit: 50 });for (const item of page.data) console.log(item.type, item.role ?? "");POST /v1/conversations/{id}/items
Section titled “POST /v1/conversations/{id}/items”Appends items without running a model turn, for example to seed history from another system. Body: {"items": [...]} with at least one item in the input shapes. Returns {"object": "list", "data": [the stored items], "has_more": false}. Errors: 400 missing_field (items), 400 conversation_too_large, 404 conversation_not_found.
curl
curl https://api.zabio.ai/v1/conversations/conv_.../items \ -H "Authorization: Bearer insert your zabio.ai key here" \ -H "Content-Type: application/json" \ -d '{"items": [{"role": "user", "content": "Earlier question"}, {"role": "assistant", "content": "Earlier answer"}]}'Python
client.conversations.items.create("conv_...", items=[ {"role": "user", "content": "Earlier question"}, {"role": "assistant", "content": "Earlier answer"},])Node
await client.conversations.items.create("conv_...", { items: [ { role: "user", content: "Earlier question" }, { role: "assistant", content: "Earlier answer" },] });POST /v1/files
Section titled “POST /v1/files”Upload a file in OpenAI’s shape, multipart/form-data. Today the only purpose is batch: a JSONL file of chat completion requests for Batches. Files are encrypted per organization and kept for the organization’s retention period from the upload, or until deleted, or until the expiry you set. Needs the chat scope.
Request
Section titled “Request”| Part | Required | Notes |
|---|---|---|
file |
yes | the file; 100 MiB at most |
purpose |
yes | batch |
expires_after[anchor] |
no | created_at |
expires_after[seconds] |
no | 3600 to 2592000; shortens the retention |
A batch input is checked line by line on upload: each line is a JSON object with a unique custom_id, method POST, a url and a body object; one line may be up to 10 MiB.
Response
Section titled “Response”| Field | Type | Notes |
|---|---|---|
id |
string | file_... |
object |
string | file |
bytes |
integer | the size |
created_at, expires_at |
integer | Unix seconds; expires_at is the retention or the expiry you set, whichever is sooner |
filename, purpose |
string | as uploaded |
status |
string | processed |
Errors specific to this endpoint
Section titled “Errors specific to this endpoint”| Status | code | When |
|---|---|---|
| 400 | invalid_multipart, missing_field |
not multipart, no file part, no purpose |
| 400 | invalid_value |
a purpose other than batch, or a bad expires_after |
| 400 | invalid_file |
the JSONL is not valid; the message names the line |
| 413 | file_too_large |
over 100 MiB |
GET /v1/files
Section titled “GET /v1/files”Lists your files, newest first. purpose filters; limit (1 to 10000) and after (a file id) page. Returns {"object": "list", "data": [file...], "first_id", "last_id", "has_more"}.
GET /v1/files/{id}
Section titled “GET /v1/files/{id}”Returns the file object, or 404 not_found.
DELETE /v1/files/{id}
Section titled “DELETE /v1/files/{id}”Deletes the content at once and permanently; returns {"id", "object": "file", "deleted": true}. A file a running batch still needs answers 409 file_in_use.
GET /v1/files/{id}/content
Section titled “GET /v1/files/{id}/content”Streams the file’s bytes, with Content-Disposition naming the file.
Examples
Section titled “Examples”curl
curl https://api.zabio.ai/v1/files \ -H "Authorization: Bearer insert your zabio.ai key here" \ -F purpose=batch -F file=@requests.jsonlcurl https://api.zabio.ai/v1/files/file_.../content \ -H "Authorization: Bearer insert your zabio.ai key here" -o output.jsonlPython
f = client.files.create(file=open("requests.jsonl", "rb"), purpose="batch")print(client.files.content(f.id).text)POST /v1/batches
Section titled “POST /v1/batches”Creates a batch from an uploaded file: every line is routed at the vendors’ batch prices and answered within 24 hours. See Batches for the whole flow, the models and the money. Needs the chat scope.
Request
Section titled “Request”| Field | Type | Required | Notes |
|---|---|---|---|
input_file_id |
string | yes | a processed upload with purpose batch, at most 50,000 lines |
endpoint |
string | yes | /v1/chat/completions |
completion_window |
string | no | 24h, the only value |
metadata |
object | no | up to 16 string pairs; keys at most 64 characters, values at most 512 |
zabio.allow_standard_price |
boolean | no | default false. true runs the lines that name a model without a batch tier as synchronous calls at the standard price instead of failing the batch; each such line says zabio.tier.applied: "standard" with fallback: "no_batch_tier". See Lines Zabio answers directly |
Response
Section titled “Response”The batch object:
| Field | Type | Notes |
|---|---|---|
id |
string | batch_... |
object |
string | batch |
endpoint, input_file_id, completion_window |
string | as created |
status |
string | validating, failed, in_progress, finalizing, completed, expired, cancelling, cancelled |
output_file_id, error_file_id |
string or null | set when the batch ends; the error file only when some lines failed |
errors |
object | on a failed batch: {"object": "list", "data": [{"code", "message", "param", "line"}]} |
created_at, expires_at, in_progress_at, finalizing_at, completed_at, failed_at, expired_at, cancelling_at, cancelled_at |
integer | Unix seconds; expires_at is 24 hours after creation |
request_counts |
object | total, completed, failed (failed, expired and cancelled lines) |
metadata |
object | as created |
zabio |
object | tier (batch) and cost: vendor_micros, billed_micros, direct_micros, saved_micros, classifier_micros (always 0), the running totals of the lines billed so far. saved_micros adds each line’s own saved_micros, so an auto line on a key whose baseline is pending adds nothing |
Errors specific to this endpoint
Section titled “Errors specific to this endpoint”| Status | code | When |
|---|---|---|
| 400 | missing_field |
input_file_id or endpoint absent |
| 400 | invalid_value |
endpoint not /v1/chat/completions, completion_window not 24h, metadata too big, the file not a processed batch upload |
| 400 | batch_too_large |
more than 50,000 lines |
| 404 | not_found |
no file with that id |
| 429 | too_many_open_batches |
20 batches already open for the organization |
A batch that was created can still fail before running, with status: failed and the reasons in errors: model_not_batchable (a line names a model without a batch tier and zabio.allow_standard_price is not set; the message lists the batch-capable models and the opt-in), model_not_found, no_batch_model, insufficient_credits (the estimate and the available balance are in the message), api_key_revoked, empty_file, input_file_missing, or a request code from a line (invalid_url, invalid_request, missing_messages, invalid_json). Nothing is billed on a failed batch.
GET /v1/batches
Section titled “GET /v1/batches”Lists your batches, newest first; limit (1 to 100, default 20) and after (a batch id) page. Returns {"object": "list", "data": [batch...], "first_id", "last_id", "has_more"}.
GET /v1/batches/{id}
Section titled “GET /v1/batches/{id}”Returns the batch object, or 404 not_found.
POST /v1/batches/{id}/cancel
Section titled “POST /v1/batches/{id}/cancel”Cancels the lines not yet at a vendor and asks the vendors to stop the rest; what they finish is kept and billed. Returns the batch object with status cancelling. A batch that has ended answers 400 batch_not_cancellable.
Examples
Section titled “Examples”curl
curl https://api.zabio.ai/v1/batches \ -H "Authorization: Bearer insert your zabio.ai key here" \ -H "Content-Type: application/json" \ -d '{"input_file_id": "file_...", "endpoint": "/v1/chat/completions", "completion_window": "24h"}'curl https://api.zabio.ai/v1/batches/batch_... -H "Authorization: Bearer insert your zabio.ai key here"Python
b = client.batches.create(input_file_id=f.id, endpoint="/v1/chat/completions", completion_window="24h")b = client.batches.retrieve(b.id)print(b.status, b.request_counts, b.model_extra["zabio"]["cost"])Node
const b = await client.batches.create({ input_file_id: f.id, endpoint: "/v1/chat/completions", completion_window: "24h" });console.log((await client.batches.retrieve(b.id)).status);POST /v1/public/analyze
Section titled “POST /v1/public/analyze”Reads a chat completions request and says what it costs today, what it would cost through Zabio and how to cut it, without running it. This is the engine behind the analyzer on zabio.ai and the tips the Playground shows after each call. Public: no API key. Rate limited per address, on top of the general per-IP limit, and capped for everyone by a daily allowance.
The only vendor call is Zabio’s routing classifier, which reads the same excerpt it reads on any auto call (see Privacy); no model answers the request and Zabio pays the classification. The request is never stored: Zabio logs its size, the job the classifier named and a hash of the caller’s address, nothing else.
Request
Section titled “Request”| Field | Type | Required | Notes |
|---|---|---|---|
request |
object | yes | a chat completions request body, exactly as you would send it to POST /v1/chat/completions, with the same validation |
calls_per_month |
integer | no | projects the month in monthly and is reported on the batch tip |
Bodies over 1 MB answer 413 payload_too_large. Unknown fields are ignored.
Response
Section titled “Response”| Field | Type | Notes |
|---|---|---|
job |
object | the classifier’s view: task, difficulty (1 to 5), stakes (normal or high) |
tokens |
object | input (estimated, characters over four), output (what every price assumes) and output_basis: typical for the job, or max_tokens when the request’s cap is lower |
today |
object or null | the request on the model it names, at the vendor’s list price: model, vendor, usd. Null when the request names auto or a model Zabio does not know |
lines[] |
array | one row per virtual model: line, the model and vendor it would pick (by the same choice a live auto request makes, see Models and routing), provider_usd, fee_usd (the service fee), call_fee_usd (the fixed fee of an auto request, the price of automatic model choice), classifier_usd (always 0), total_usd and saving_usd against today, or against the plain auto pick when there is no today |
normal_stakes |
object | only when the classifier read the job as high stakes and that moved the lines. A high-stakes job moves every line one step up, so the rows above show what a live call does. This field says what auto/cheap would pick and cost per call had the stakes been normal: model, vendor, total_usd, on the same tokens and against the same baseline as the rows. Absent when the stakes did not change the picks |
switches[] |
array | the six feature levers, always in this order: split, cache, effort, jev, flex, batch. Each has id, applicable (whether the lever can change this request’s price), default (the product’s: split, cache, effort and jev on, flex and batch off) and status (active or coming_soon) |
scenarios |
object | all 64 combinations of the switches. The key is one 0 or 1 per switch in the switches’ order, so 111100 is the defaults and 000000 every lever off. Each value is {"total_usd", "saving_usd"}: the per-call bill through Zabio on the auto pick with those levers on, priced together so overlapping savings are never counted twice, and the saving against today, or against 000000 when there is no today. With flex and batch both on, batch wins. Batching is priced from the catalog’s batch tier, the price the vendor’s batch API charges. effort never changes a price today, because reasoning tokens are not priced yet, so its scenarios equal the ones without it |
tips[] |
array | each tip only when it applies: id, status (active or coming_soon), saving_usd (per call, or null when the saving cannot be priced) and params, numbers and names only, listed below |
monthly |
object or null | with calls_per_month: calls, today_usd (null without a today) and zabio_usd on the plain auto line |
Every amount is USD per call at list prices, six decimals.
The tips and their parameters
Section titled “The tips and their parameters”The site and the Playground word each tip from its parameters; the API sends no sentences. Each parameter’s meaning and unit:
id |
When | Parameter | Type | Meaning |
|---|---|---|---|---|
line |
a cheaper line whose pick the knowledge base does not rate probably worse than the best line’s pick, with a saving | line |
string | the line to send the request to: auto/cheap, auto/balanced or auto/best |
model |
string | the model that line would pick | ||
vendor |
string | its vendor | ||
total_usd |
number | the per-call bill on that line, USD | ||
max_tokens |
the request’s max_tokens is at least four times the typical output for the job and at least 1,000; saving_usd is null, the bill follows the actual output |
max_tokens |
integer | the cap the request sent, tokens |
typical_output |
integer | the output the job usually needs, tokens | ||
cache |
the system prompt plus the history before the last message is at least the pick’s smallest cacheable prefix, on a vendor that caches; saving_usd is per repeated call |
model |
string | the pick the prefix would be cached on |
vendor |
string | its vendor | ||
prefix_tokens |
integer | the stable prefix, tokens | ||
min_prefix_tokens |
integer | the smallest prefix that vendor caches, tokens | ||
ttl |
string | the cache lifetime priced (5m) |
||
first_call_extra_usd |
number | what the first call pays on top to write the cache, USD (0 where the vendor charges no write premium) | ||
jev |
the json_schema maps onto Jev’s questions and Jev is cheaper than the pick |
model |
string | jev-1.13.0 |
fields |
integer | the schema’s typed fields | ||
jev_usd |
number | Jev’s price for the request, USD | ||
pick_model |
string | the chat model the line would use instead | ||
pick_usd |
number | that model’s price with the schema’s output, USD | ||
split |
a big plain text source with a question, on a job that is not whole-document work, where Smart Splitting’s own gate and price check pass on the estimate | model |
string | the model that answers on the excerpt |
extractor |
string | the cheap model that copies the relevant lines | ||
source_tokens |
integer | the last user message, tokens | ||
unsplit_usd |
number | the request sent whole on the pick, USD | ||
split_usd |
number | the extract, the check and the answer on the excerpt, USD | ||
flex |
the pick has a flex tier | model |
string | the pick |
vendor |
string | its vendor | ||
deadline |
string | the shortest zabio.deadline that asks for flex (15m) |
||
standard_usd |
number | the call at the standard tier, USD | ||
flex_usd |
number | the same call at the flex tier, USD | ||
batch |
active. The pick has a batch tier; the saving is the request at its batch price against its standard price (see Batches) |
model |
string | the pick |
vendor |
string | its vendor | ||
discount |
number | the batch discount as a share of the standard price (0.5 is 50 percent) |
||
window |
string | the vendor’s completion window (24h) |
||
standard_usd |
number | the call at the standard tier, USD | ||
calls_per_month |
integer | the volume the request carried, 0 when none | ||
effort |
the named model reasons, the job is easy or medium with normal stakes, and the setting in force is above the model’s lowest; saving_usd is null because reasoning tokens are not priced yet |
model |
string | the named model |
current |
string | the reasoning setting in force, as zabio.reasoning.applied reports it |
||
suggested |
string | the model’s lowest setting, in the same form | ||
suggested_level |
string | the reasoning_effort value that selects it, when the setting maps to the scale |
||
requested |
string | the reasoning_effort the request sent, empty when none |
| Status | code | When |
|---|---|---|
| 400 | missing_parameter, invalid_value and the chat completions request errors |
the body or the request inside it is not valid |
| 413 | payload_too_large |
the body is over 1 MB |
| 429 | ip_rate_limit_exceeded |
too many analyses from one address; wait for Retry-After |
| 503 | analyzer_daily_cap |
the day’s allowance for everyone is spent; sign up to run the call for real |
| 503 | analyzer_unavailable |
the analyzer cannot run right now |
curl
curl https://api.zabio.ai/v1/public/analyze \ -H "Content-Type: application/json" \ -d '{ "request": { "model": "claude-opus-5-5", "messages": [{"role": "user", "content": "Hola, tengo una pregunta sobre un cargo duplicado en mi factura."}] }, "calls_per_month": 2000 }'{ "job": {"task": "conversation", "difficulty": 1, "stakes": "normal"}, "tokens": {"input": 20, "output": 150, "output_basis": "typical"}, "today": {"model": "claude-opus-5-5", "vendor": "anthropic", "usd": 0.00385}, "lines": [ {"line": "auto", "model": "qwen3.5-9b", "vendor": "together", "provider_usd": 0.000041, "fee_usd": 0.000002, "call_fee_usd": 0.00025, "classifier_usd": 0, "total_usd": 0.000293, "saving_usd": 0.003557}, {"line": "auto/best", "model": "gemini-3.8-flash", "vendor": "google", "provider_usd": 0.000375, "fee_usd": 0.000019, "call_fee_usd": 0.00025, "classifier_usd": 0, "total_usd": 0.000644, "saving_usd": 0.003206}, {"line": "auto/balanced", "model": "qwen3.5-9b", "vendor": "together", "provider_usd": 0.000041, "fee_usd": 0.000002, "call_fee_usd": 0.00025, "classifier_usd": 0, "total_usd": 0.000293, "saving_usd": 0.003557}, {"line": "auto/cheap", "model": "qwen3.5-9b", "vendor": "together", "provider_usd": 0.000041, "fee_usd": 0.000002, "call_fee_usd": 0.00025, "classifier_usd": 0, "total_usd": 0.000293, "saving_usd": 0.003557} ], "switches": [ {"id": "split", "applicable": false, "default": true, "status": "active"}, {"id": "cache", "applicable": false, "default": true, "status": "active"}, {"id": "effort", "applicable": false, "default": true, "status": "active"}, {"id": "jev", "applicable": false, "default": true, "status": "active"}, {"id": "flex", "applicable": false, "default": false, "status": "active"}, {"id": "batch", "applicable": true, "default": false, "status": "active"} ], "scenarios": { "000000": {"total_usd": 0.000887, "saving_usd": 0.002963}, "111100": {"total_usd": 0.000887, "saving_usd": 0.002963}, "000001": {"total_usd": 0.000867, "saving_usd": 0.002983} }, "tips": [ {"id": "line", "status": "active", "saving_usd": 0.002963, "params": {"line": "auto/cheap", "model": "qwen3.5-9b", "vendor": "together", "total_usd": 0.000887}}, {"id": "batch", "status": "active", "saving_usd": 0.00002, "params": {"model": "gpt-5.4-nano", "vendor": "openai", "discount": 0.5, "window": "24h", "standard_usd": 0.000041, "batch_usd": 0.000021, "calls_per_month": 2000}} ], "monthly": {"calls": 2000, "today_usd": 7.7, "zabio_usd": 1.774}}The numbers above are illustrative and scenarios is cut to three of its 64 keys; the live check in build note 32 carries real responses.
GET /v1/status
Section titled “GET /v1/status”The public service status, the same document the page at zabio.ai/status shows. No API key is needed; it is one of the two public routes under /v1, with POST /v1/public/analyze. The per-IP rate limit still applies.
Response: {"status", "updated_at", "source", "components": [Component, ...]}.
| Field | Type | Notes |
|---|---|---|
status |
string | operational, degraded, major_outage, or unknown when source is unavailable. The worst component wins, except that a vendor counts as degraded at most because calls fail over to another vendor |
updated_at |
string | RFC 3339 time the numbers were computed. The document is recomputed every 30 seconds |
source |
string | live; stale when the metrics backend could not be reached and the last good document (up to 5 minutes old) is served; unavailable when there is nothing usable |
components[].id |
string | api, vendor:anthropic, vendor:openai, vendor:google, vendor:deepseek, vendor:xai, vendor:mistral, vendor:together, vendor:typesafe, vendor:alibaba-us, vendor:alibaba-intl, web |
components[].name |
string | Display name |
components[].status |
string | operational under 2 percent of failed requests in the last 15 minutes, degraded under 20 percent, major_outage above. No requests in the window is operational with a null rate |
components[].success_rate_15m |
number or null | Share of requests without a failure in the last 15 minutes (0.995 is 99.5 percent). Not present on the web row |
components[].success_rate_24h |
number or null | The same over the last 24 hours |
components[].p95_latency_ms |
integer or null | 95th percentile request duration over the last 15 minutes. Not present on the web row |
The document contains nothing about your organization or your calls.
curl
curl https://api.zabio.ai/v1/status{ "status": "operational", "updated_at": "2026-09-21T12:00:00Z", "source": "live", "components": [ {"id": "api", "name": "API", "status": "operational", "success_rate_15m": 0.999, "success_rate_24h": 0.998, "p95_latency_ms": 420}, {"id": "vendor:anthropic", "name": "Anthropic", "status": "operational", "success_rate_15m": 0.998, "success_rate_24h": 0.997, "p95_latency_ms": 2100}, {"id": "vendor:openai", "name": "OpenAI", "status": "operational", "success_rate_15m": null, "success_rate_24h": 0.999, "p95_latency_ms": null}, {"id": "vendor:google", "name": "Google", "status": "operational", "success_rate_15m": 1, "success_rate_24h": 1, "p95_latency_ms": 1300}, {"id": "web", "name": "Web app", "status": "operational"} ]}Zabio extensions
Section titled “Zabio extensions”Everything Zabio adds on top of OpenAI’s shapes, one worked example each. The Python SDK rejects unknown keyword arguments, so zabio travels in extra_body; the Node SDK passes unknown fields through.
The auto models
Section titled “The auto models”auto names a line, how skilled the model must be: plain auto (or no model) and auto/cheap take the cheapest model skilled enough for the job’s kind of work, auto/balanced the cheapest whose skill for it is in the middle third of the candidates or above, and auto/best the cheapest in the top third; a hard or high-stakes request moves one step up. Every line classifies the job first (kind of work, difficulty, stakes) and applies its floor to the models enabled for the key (see Models and routing). These four are the only virtual ids. zabio.routing.trigger is explicit for a named model.
curl
curl https://api.zabio.ai/v1/chat/completions \ -H "Authorization: Bearer insert your zabio.ai key here" \ -H "Content-Type: application/json" \ -d '{"model": "auto/balanced", "messages": [{"role": "user", "content": "Refactor this function to remove the nested loops: ..."}]}'Python
r = client.chat.completions.create(model="auto/balanced", messages=[{"role": "user", "content": "Refactor this function: ..."}])print(r.model_extra["zabio"]["routing"]) # {'line': 'balanced', 'task': 'conversation', 'difficulty': 1, ...}Node
const r = await client.chat.completions.create({ model: "auto/balanced", messages: [{ role: "user", content: "Refactor this function: ..." }] });console.log(r.zabio.routing); // { line: 'balanced', floor: 'middle', task: 'conversation', difficulty: 1, ... }Lines: auto/best, auto/balanced, auto/cheap
Section titled “Lines: auto/best, auto/balanced, auto/cheap”Name the line in model. There is nothing else to set: the classifier names the job and the line applies its floor. A hard job, or a high-stakes one (a wrong answer can cause legal, medical, financial or security harm, or changes a production system), moves one step up: cheap to the middle third, balanced to the top third. zabio.routing.line shows the line you named and zabio.routing.floor the floor applied. The line lives in the request: there is no key setting for it.
curl https://api.zabio.ai/v1/chat/completions -H "Authorization: Bearer $ZABIO_API_KEY" -H "Content-Type: application/json" \ -d '{"model": "auto/best", "messages": [{"role": "user", "content": "Summarize this contract clause: ..."}]}'r = client.chat.completions.create(model="auto/cheap", messages=[{"role": "user", "content": "Summarize this contract clause: ..."}])print(r.model_extra["zabio"]["routing"]["line"], r.model_extra["zabio"]["model_served"])const r = await client.chat.completions.create({ model: "auto/cheap", messages: [{ role: "user", content: "Summarize this contract clause: ..." }] });console.log(r.zabio.routing.line, r.zabio.model_served);session_id
Section titled “session_id”Send the same session_id (up to 128 characters) on calls that belong together. What is reused is the job the classifier named for the session’s current user message: the tool-result turns of an agent loop report trigger: "cache_hit" and are not classified again; each request still pays the fee for automatic model choice once. A new user message is classified again (trigger: "new_human_message"), as is any turn after a signal (low_confidence_prior, long_idle, tool_failure_streak, customer_feedback). Without a session_id, Zabio keys the reuse on your system prompt and the last user message, so an agent loop still classifies once per user message. Session state is metadata only: line, model, confidence, a few counters and the last 200 characters of the last user message for the feedback signal. It lives one hour after the last call. Prompts are never stored for it. The response echoes the id in zabio.session_id.
curl
curl https://api.zabio.ai/v1/chat/completions \ -H "Authorization: Bearer insert your zabio.ai key here" \ -H "x-zabio-session-id: support-4711" \ -H "Content-Type: application/json" \ -d '{"model": "auto", "messages": [{"role": "user", "content": "Second question in the same session"}]}'Python
for question in ["First question", "Second question"]: r = client.chat.completions.create(model="auto", messages=[{"role": "user", "content": question}], extra_body={"zabio": {"session_id": "support-4711"}}) print(r.model_extra["zabio"]["routing"]["trigger"]) # no_prior_decision, then cache_hitNode
for (const question of ["First question", "Second question"]) { const r = await client.chat.completions.create({ model: "auto", messages: [{ role: "user", content: question }], zabio: { session_id: "support-4711" } }); console.log(r.zabio.routing.trigger); // no_prior_decision, then cache_hit}cache.ttl and cache.breakpoints
Section titled “cache.ttl and cache.breakpoints”Prompt caching where the vendor offers it. cache.ttl is 5m, 1h or none; cache.breakpoints is auto or none.
| Vendor | What the values do |
|---|---|
| Anthropic | auto places cache markers on the system prompt, the last tool, the last message and the previous user turn, so the conversation history is read from the cache on every turn and only the new part is written. ttl picks the 5 minute or the 1 hour cache; the 1 hour cache costs more to write (2 times the input price against 1.25 times). Anthropic does not cache prefixes shorter than the model’s minimum (from 512 to 4,096 tokens depending on the model), so short prompts report 0 |
| OpenAI | caching is automatic on prefixes of 1024 tokens or more; ttl is accepted and ignored |
caching is automatic (implicit) on prompts of 4,096 tokens or more, with no write charge; cache reads are billed at Google’s cache price and reported in zabio.cache.read_tokens; ttl and breakpoints are accepted and ignored |
|
| Other vendors | where the model has a published cache price (DeepSeek, Mistral, and the DeepSeek models on Together, for example), caching is automatic and cache reads are billed at that price; ttl is accepted and ignored |
breakpoints: "none" disables caching for the call and forces ttl to none. Caching is automatic on every call, whether it names auto or a model; a call turns it off with ttl: "none", breakpoints: "none" or the header. The default ttl is 5m, the lifetime used when Zabio writes (keys no longer carry a default of their own). A long prompt (1,024 tokens or more) is written to the cache on its second appearance, not its first: the first time it is sent without a write, and when the same prompt or the same system prompt and tools come back within the lifetime it is written, and later calls read it. A prompt sent once never pays the write. Calls in a session_id, requests that carry earlier turns of the conversation, and requests that set cache.ttl themselves are written at once. On the Responses API a stored response (the default), a conversation and a previous_response_id are written at once. The rule matters on Anthropic, whose writes are billed; the other vendors cache by themselves. With that default, a call in a session_id whose previous turn was more than five minutes ago gets 1h, because a 5 minute cache would have expired and be rewritten on every turn, which costs more than no cache. Read the result in zabio.cache: read_tokens, write_tokens, ttl_applied. The header x-zabio-cache-ttl carries ttl; there is no header for breakpoints. A line inside a batch, and a background response served from the pool, gets none unless the line sets ttl itself: nothing reads a batch line’s cache entry back, and the write costs more than fresh input.
curl
curl https://api.zabio.ai/v1/chat/completions \ -H "Authorization: Bearer insert your zabio.ai key here" \ -H "Content-Type: application/json" \ -d '{"model": "claude-sonnet-5", "messages": [{"role": "system", "content": "<a long system prompt>"}, {"role": "user", "content": "Question"}], "zabio": {"cache": {"ttl": "1h", "breakpoints": "auto"}}}'Python
r = client.chat.completions.create(model="claude-sonnet-5", messages=[{"role": "system", "content": long_system_prompt}, {"role": "user", "content": "Question"}], extra_body={"zabio": {"cache": {"ttl": "1h", "breakpoints": "auto"}}})print(r.model_extra["zabio"]["cache"]) # {'read_tokens': 0, 'write_tokens': 5120, 'ttl_applied': '1h'} on the first callNode
const r = await client.chat.completions.create({ model: "claude-sonnet-5", messages: [{ role: "system", content: longSystemPrompt }, { role: "user", content: "Question" }], zabio: { cache: { ttl: "1h", breakpoints: "auto" } } });console.log(r.zabio.cache); // { read_tokens: 5120, write_tokens: 0, ttl_applied: '1h' } on the second callvendors and fallback
Section titled “vendors and fallback”vendors limits the candidates to the vendors you list (anthropic, openai, google, deepseek, xai, mistral, together). It applies to auto and to the fallback set of an explicit model.
fallback applies to explicitly named models when the model is unhealthy, unavailable or fails a capability check: same_vendor (default from the key’s fallback_policy) tries other models of the same vendor and class, cross_vendor any model of the same class, none fails with 503 no_provider_available. Whatever happened, zabio.model_served and zabio.vendor tell you what answered and zabio.attempts how many tries it took.
curl
curl https://api.zabio.ai/v1/chat/completions \ -H "Authorization: Bearer insert your zabio.ai key here" \ -H "Content-Type: application/json" \ -d '{"model": "gpt-5.4", "messages": [{"role": "user", "content": "Hello"}], "zabio": {"vendors": ["openai", "anthropic"], "fallback": "cross_vendor"}}'Python
r = client.chat.completions.create(model="gpt-5.4", messages=[{"role": "user", "content": "Hello"}], extra_body={"zabio": {"vendors": ["openai", "anthropic"], "fallback": "cross_vendor"}})z = r.model_extra["zabio"]print(z["vendor"], z["model_served"], z.get("attempts"))Node
const r = await client.chat.completions.create({ model: "gpt-5.4", messages: [{ role: "user", content: "Hello" }], zabio: { vendors: ["openai", "anthropic"], fallback: "cross_vendor" } });console.log(r.zabio.vendor, r.zabio.model_served, r.zabio.attempts);Reading zabio.cost
Section titled “Reading zabio.cost”cost_micros is what you were billed; cost breaks it down. direct_micros is what the same call would have cost on your baseline model with the cache it had: for a named model, that same model, so it equals the provider price; for auto, your key’s baseline model with the same tokens, cache reads and cache writes at that model’s own rates, priced uncached where its vendor has no prompt cache (Google, Together AI). A cache discount is never counted as a saving: we never take a cut of your cache or batch discounts. The baseline model is the model your key used at the 95th percentile of list price per token over the last 30 days, recomputed nightly: the most expensive 5 percent of your calls are set aside and the next one sets it, so a burst does not raise it. The rule needs 20 successful calls before it says anything, because under 20 the 5 percent set aside is zero calls and one expensive call would set the baseline alone. Until then a new key’s calls are their own baseline and save nothing: saved_micros is left out and cost.baseline shows the progress; the baseline is set usually within an hour of the 20th call. Those calls stay without a saving after the baseline is set: the dashboard, Usage, the CSV export, the CLI and batch totals count none for them, so every total adds up the calls as each one showed. classifier_micros is always 0: the classifier that reads an auto request is never passed on. Before version 1.2.6 it carried that call’s provider price. saved_micros is direct_micros minus cost_micros. service_fee_micros is the plan’s percentage of vendor_micros, and call_fee_micros is the fixed fee an auto request pays once, the price of automatic model choice (see Billing). savings_fee_micros is always 0 since version 1.2.7: Zabio takes no share of the saving. Divide by 1000000 for US dollars.
curl
curl -s https://api.zabio.ai/v1/chat/completions \ -H "Authorization: Bearer insert your zabio.ai key here" \ -H "Content-Type: application/json" \ -d '{"model": "auto", "messages": [{"role": "user", "content": "Say hello."}]}' | jq .zabio.costPython
r = client.chat.completions.create(model="auto", messages=[{"role": "user", "content": "Say hello."}])z = r.model_extra["zabio"]print(f"billed ${z['cost_micros'] / 1e6:.6f}, direct ${z['cost']['direct_micros'] / 1e6:.6f}, saved ${z['cost'].get('saved_micros', 0) / 1e6:.6f}")Node
const r = await client.chat.completions.create({ model: "auto", messages: [{ role: "user", content: "Say hello." }] });const { cost_micros, cost } = r.zabio;console.log(`billed $${cost_micros / 1e6}, direct $${cost.direct_micros / 1e6}, saved $${(cost.saved_micros ?? 0) / 1e6}`);Split and route on big inputs
Section titled “Split and route on big inputs”Under auto, a request that carries a large source (a document, a log, a thread, a set of records) and a question that needs only part of it can be answered in two steps when that is cheaper: a cheap model copies the relevant lines verbatim, a check confirms the excerpt holds what the question names, and the routed model answers on the excerpt. The response then has zabio.split.applied: true, zabio.split.steps lists every call with its model and list price, and zabio.cost covers the whole request (vendor_micros is both calls, cost_micros the whole request). When any guard fails, the request goes whole to the routed model, zabio.split.fallback names the guard, and you pay exactly the unsplit price. A named model is never split. zabio.split: "off" (or the header x-zabio-split: off) sends a request whole; there is no key-level default. Details in Models and routing and Billing.
curl
curl -s https://api.zabio.ai/v1/chat/completions \ -H "Authorization: Bearer insert your zabio.ai key here" \ -H "Content-Type: application/json" \ -d @request.json | jq '.zabio.split'request.json holds "model": "auto" and a long user message with the source and the question; add "zabio": {"split": "off"} to it, or the header -H "x-zabio-split: off", to send the request whole.
Python
r = client.chat.completions.create(model="auto", messages=[{"role": "user", "content": contract + "\n\nQUESTION: what is the notice period for termination?"}])s = r.model_extra["zabio"].get("split") or {}print(s.get("applied"), s.get("reason") or s.get("fallback"), [(st["role"], st["model"], st["micros"]) for st in s.get("steps", [])])# The same request sent whole:r = client.chat.completions.create(model="auto", messages=[{"role": "user", "content": contract + "\n\nQUESTION: what is the notice period for termination?"}], extra_body={"zabio": {"split": "off"}})print(r.model_extra["zabio"]["split"]["reason"]) # opt_outNode
const r = await client.chat.completions.create({ model: "auto", messages: [{ role: "user", content: `${contract}\n\nQUESTION: what is the notice period for termination?` }] });const s = r.zabio.split ?? {};console.log(s.applied, s.reason ?? s.fallback, (s.steps ?? []).map((st) => [st.role, st.model, st.micros]));// The same request sent whole:const whole = await client.chat.completions.create({ model: "auto", messages: [{ role: "user", content: `${contract}\n\nQUESTION: what is the notice period for termination?` }], zabio: { split: "off" } });console.log(whole.zabio.split.reason); // opt_outThe flex tier
Section titled “The flex tier”When your request can wait, say so with zabio.deadline (or the header x-zabio-deadline), a duration of 15 minutes or more. Zabio then serves the call on the vendor’s flex tier where the model has one (OpenAI gpt-5.4-nano, gpt-5-mini, gpt-5.4-mini, gpt-5.4, gpt-5.5, gpt-6-astra; Google gemini-3.5-flash-lite, gemini-3.8-flash, gemini-3.5-flash, gemini-3.1-pro-preview): the same model at about half the price, slower, with best-effort capacity. Under auto the candidates are compared at their flex prices, so auto can pick a model because it is cheaper in flex. When the vendor refuses a flex request for capacity, Zabio retries the same model at the standard tier at once and bills the price actually used; a flex request never fails for that reason. zabio.tier reports requested, applied and, after a refusal, fallback: "capacity". A flex attempt may take up to 900 seconds, so set your client’s timeout accordingly. The saving is measured against the flex price of your key’s baseline model, or its standard price when that model has no flex tier: Zabio never takes a cut of the vendor’s discount. A deadline of 24 hours or more asks for the batch tier, which only a background response or a batch can wait for; a synchronous call with such a deadline is served at flex (or standard) and zabio.tier reports fallback: "synchronous". Details in Chat completions and Billing.
curl
curl -s https://api.zabio.ai/v1/chat/completions \ -H "Authorization: Bearer insert your zabio.ai key here" \ -H "Content-Type: application/json" \ -H "x-zabio-deadline: 30m" \ -d '{"model": "auto", "messages": [{"role": "user", "content": "Summarize the attached report."}]}' | jq '.zabio.tier'Python
r = client.chat.completions.create(model="auto", messages=[{"role": "user", "content": "Summarize the attached report."}], extra_body={"zabio": {"deadline": "30m"}}, timeout=900)tier = r.model_extra["zabio"]["tier"]print(tier["requested"], tier["applied"], tier.get("fallback")) # flex flex None, or flex standard capacityNode
const r = await client.chat.completions.create({ model: "auto", messages: [{ role: "user", content: "Summarize the attached report." }], zabio: { deadline: "30m" } }, { timeout: 900_000 });console.log(r.zabio.tier.requested, r.zabio.tier.applied, r.zabio.tier.fallback); // flex flex undefined, or flex standard capacitybyok in responses
Section titled “byok in responses”When your organization has added its own vendor key for the vendor that served the call, the response says so: zabio.byok is true and the header x-zabio-byok: true is set. zabio.cost.vendor_micros is 0 because Zabio has no provider cost (the vendor bills you directly) and cost_micros is 0 on a named model and the fee for automatic model choice on an auto request; direct_micros is still filled in so the numbers stay comparable. Calls that your own keys can serve entirely are not refused for missing credits. If the vendor rejects your key, the call fails with 400 byok_key_invalid and Zabio never falls back to its own key. See Own keys.
curl
curl -si https://api.zabio.ai/v1/chat/completions \ -H "Authorization: Bearer insert your zabio.ai key here" \ -H "Content-Type: application/json" \ -d '{"model": "claude-sonnet-5", "messages": [{"role": "user", "content": "Hello"}]}' | grep -i x-zabio-byokPython
r = client.chat.completions.create(model="claude-sonnet-5", messages=[{"role": "user", "content": "Hello"}])print(r.model_extra["zabio"].get("byok", False))Node
const r = await client.chat.completions.create({ model: "claude-sonnet-5", messages: [{ role: "user", content: "Hello" }] });console.log(r.zabio.byok ?? false);Escalation in the Responses API
Section titled “Escalation in the Responses API”On auto turns of POST /v1/responses with tool_choice auto or absent, Zabio gives the model a hidden tool. When the model calls it to say the task is beyond it, Zabio re-runs the same turn on a stronger model, with the full history, and returns that answer: the cheapest model one step up the skill floor for the kind of work, or, for a request routed by the earlier rule, the next model up the curve that its knowledge says is probably better. The response then carries zabio.escalated_from as line/model (for example cheap/gpt-5.4-nano), zabio.routing.trigger is escalation, and later turns of the conversation stay on the stronger model. When there is no stronger model to go to (the top floor, or nothing probably better under the earlier rule), the tool is not offered. The hidden call never appears in output. Escalation is not offered on /v1/chat/completions, on explicit models or when tool_choice is none, required or a named function.
curl
curl -s https://api.zabio.ai/v1/responses \ -H "Authorization: Bearer insert your zabio.ai key here" \ -H "Content-Type: application/json" \ -d '{"model": "auto", "input": "Prove that there are infinitely many primes, then explain each step."}' | jq '.zabio | {model_served, escalated_from, routing}'Python
r = client.responses.create(model="auto", input="Prove that there are infinitely many primes, then explain each step.")z = r.model_extra["zabio"]if z.get("escalated_from"): print("escalated from", z["escalated_from"], "to", z["model_served"])Node
const r = await client.responses.create({ model: "auto", input: "Prove that there are infinitely many primes, then explain each step." });if (r.zabio.escalated_from) console.log("escalated from", r.zabio.escalated_from, "to", r.zabio.model_served);Compaction and retention
Section titled “Compaction and retention”When the stored history plus a new turn would not fit the context window of the model the turn is served by, Zabio summarizes the oldest part with a fast model, stores the summary as a compaction item and continues; the response has zabio.compacted: true and the summary call appears in the conversation’s billing. The summary runs on Zabio’s key, so a turn that needs compaction needs credit even when it names a model served on your own key; at zero balance it gets 402 insufficient_credits before any summary is made. zabio.compaction: "off" turns this off and the request fails with 400 context_length_exceeded instead. zabio.retention_days (0 to 365) sets the retention of a conversation created by the call; the clock restarts on every turn.
curl
curl https://api.zabio.ai/v1/responses \ -H "Authorization: Bearer insert your zabio.ai key here" \ -H "Content-Type: application/json" \ -d '{"model": "auto", "input": "Continue", "conversation": "conv_...", "zabio": {"compaction": "off"}}'Python
r = client.responses.create(model="auto", input="Continue", conversation="conv_...", extra_body={"zabio": {"compaction": "off"}})print(r.model_extra["zabio"].get("compacted", False))Node
const r = await client.responses.create({ model: "auto", input: "Continue", conversation: "conv_...", zabio: { compaction: "off" } });console.log(r.zabio.compacted ?? false);dropped_parameters
Section titled “dropped_parameters”Some models do not take every sampling parameter: OpenAI’s reasoning models take no temperature or top_p, recent Claude models neither, Gemini models ignore both. Zabio never fails a request for that. It leaves the parameter out of the request to the model that answers, names it in zabio.dropped_parameters and in the x-zabio-dropped-params header, and the answer comes back as usual. Under auto this depends on the model chosen, so read the field rather than assuming. stop is never dropped: where the model’s provider does not take it, Zabio cuts the answer at the first sequence itself, streamed or not. The list per model is on the Models page.
ignored_parameters
Section titled “ignored_parameters”Fields Zabio accepts so SDK defaults do not break, but does not act on, are listed in zabio.ignored_parameters. On chat completions: seed, logit_bias, presence_penalty and frequency_penalty, reported only when they carry a non default value. On the Responses API: include, truncation and user, reported whenever present. The list is omitted when empty.
curl
curl -s https://api.zabio.ai/v1/chat/completions \ -H "Authorization: Bearer insert your zabio.ai key here" \ -H "Content-Type: application/json" \ -d '{"model": "auto", "messages": [{"role": "user", "content": "Hello"}], "seed": 42, "presence_penalty": 0.5}' | jq .zabio.ignored_parametersPython
r = client.chat.completions.create(model="auto", messages=[{"role": "user", "content": "Hello"}], seed=42, presence_penalty=0.5)print(r.model_extra["zabio"].get("ignored_parameters")) # ['seed', 'presence_penalty']Node
const r = await client.chat.completions.create({ model: "auto", messages: [{ role: "user", content: "Hello" }], seed: 42, presence_penalty: 0.5 });console.log(r.zabio.ignored_parameters); // [ 'seed', 'presence_penalty' ]Differences from OpenAI
Section titled “Differences from OpenAI”Refused with 400 unsupported_parameter:
ngreater than 1; Zabio serves exactly one choicelogprobsandtop_logprobsaudiooutput and anymodalitiesentry other thantextpredictionandweb_search_options- the legacy
functionmessage role (usetool) - content parts other than
textandimage_urlon chat completions; input items other thanmessage,function_callandfunction_call_output, and content parts other thaninput_text,text,output_textandinput_imageon the Responses API - tools other than
type: "function"(400invalid_value)
Accepted and ignored:
seed,logit_bias,presence_penalty,frequency_penaltyon chat completions (reported inzabio.ignored_parameters)stream_options.include_usage: the usage chunk is always sentstoreon chat completions: nothing is stored either wayinclude,truncation,useron the Responses API (reported inzabio.ignored_parameters);backgroundis honoured (see Background responses)image_url.detail: images are always sent as bytescache.ttlon OpenAI models: caching is automatic there
Behaves differently:
modelmay beauto,auto/best,auto/balancedorauto/cheap; the responsemodelis the model that answered, not the one you sent- every response carries a
zabioobject, and model objects carryzabiowith class, capabilities and pricing systemanddevelopermessages are joined into one system promptusage.prompt_tokensincludes cached tokens;prompt_tokens_details.cached_tokensis the cached partresponse_format: json_objecton Anthropic models is best effort through prompting, see Chat completions- the Responses API stores by default and creates a conversation when none is given;
store: falsestill reads the stored history but keeps nothing max_output_tokenson the Responses API defaults to 4096;max_tokenson chat completions defaults to 16000reasoning_effortandreasoning.efforttake one scale,nonetoxhigh, mapped onto each model’s own control;zabio.reasoningreports what was applied, and onautowith no level set Zabio chooses the setting with the model- streamed Responses API item ids are
msg_<response id>_<index>untilresponse.completedreports the stored ids - images are downloaded by Zabio and sent to the vendor as bytes; private and internal addresses are refused
- errors keep OpenAI’s envelope but use Zabio’s codes (
model_not_found,insufficient_credits,quota_exceeded,upstream_errorand the rest on Errors); the vendor’s own error body never reaches you - the request id is in the
x-request-idheader, never in the body, and one you send is not reused - there are no idempotency keys, no fine tuning, no assistants and no audio or image generation endpoints;
/v1/filesaccepts thebatchpurpose only and/v1/batchesthe/v1/chat/completionsendpoint only