Developer API
Sanero API reference
A production-grade, OpenAI-compatible REST API. If your tool speaks OpenAI, it already speaks Sanero: point it at our base URL and drop in your key.
https://api.sanero.ai/v1Contents
Getting started
Models & reasoning
Cost & limits
Errors
Tools & recipes
Overview
The Sanero API follows the OpenAI Chat Completions contract. Every request is authenticated with an API key you create in your dashboard and billed pay-as-you-go from your wallet balance. There is no SDK to install: use the official OpenAI client, or any OpenAI-compatible tool, and change exactly two things: the base URL and the API key.
All endpoints are relative to the base URL:
Endpoints
POST /chat/completions | Generate a completion. Streaming and non-streaming. |
GET /models | The models your key can call. |
GET /models/{id} | One model in full detail: prices, reasoning levels, output limit. |
GET /key | What your key is and what it may do. Free, and never billed. |
GET /usage | Your own spend and error breakdown. |
GET /errors | Every error code, with an explanation and a fix. |
GET /rates | Platform billing facts, such as the minimum charge. |
What is and is not supported
Supported: messages with system / user / assistant roles, streaming, temperature, top_p, stop, presence and frequency penalties, seed, max_tokens, reasoning_effort, and stream_options.include_usage.
Not supported yet: n > 1 (returns 400), tool / function calling, image inputs, embeddings, and the legacy /completions endpoint. Anything unsupported fails with a clear error rather than being silently ignored.
Authentication
Create a key in your dashboard, then send it on every request in the Authorization header as a Bearer token. A key spends real money from your balance, so treat it exactly like a password. If a key leaks, revoke it in the dashboard and it stops working immediately.
Authorization: Bearer sk-sanero-…Never embed a key in front-end code, a public repository or a shared notebook. Anyone holding it can spend your balance.
Checking a key without spending anything
GET /key tells you whether a key works, what it is allowed to do and what your balance is. It costs nothing, so use it in setup wizards and health checks instead of sending a throwaway completion.
curl https://api.sanero.ai/v1/key \
-H "Authorization: Bearer sk-sanero-…"Scoped keys
A key can be restricted to specific capabilities (chat, models, usage) and to a list of models. A restricted key sees only its allowed models in GET /models, and a model outside the list returns 404 rather than 403, because we do not confirm that a model exists but is blocked. A key with no restrictions can do everything.
Chat completions
The core endpoint. Send a list of messages, get a completion back. Set stream: true for token-by-token streaming.
/chat/completionscurl https://api.sanero.ai/v1/chat/completions \
-H "Authorization: Bearer sk-sanero-…" \
-H "Content-Type: application/json" \
-d '{
"model": "<model-id>",
"messages": [
{"role": "system", "content": "You are a concise assistant."},
{"role": "user", "content": "Hello, Sanero!"}
]
}'{
"id": "chatcmpl-9f2b1c7e4a8d",
"object": "chat.completion",
"created": 1700000000,
"model": "<model-id>",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "Hello! How can I help you today?" },
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 18,
"completion_tokens": 9,
"total_tokens": 27
},
"sanero": {
"cost": {
"charged_usd": "0.00001512",
"price_usd": "0.00001512",
"input_usd": "0.00000432",
"output_usd": "0.00001080",
"cache_read_usd": "0",
"cache_write_usd": "0",
"request_usd": "0",
"balance_usd": "12.48213600",
"underfunded": false
}
}
}The response is a standard OpenAI chat.completion object, plus a namespaced `sanero` block carrying the cost of the request. SDKs ignore unknown top-level fields, so the extra block breaks nothing.
Request parameters
| Parameter | Type | Description |
|---|---|---|
model | string | Required. A model id from GET /models. |
messages | array | Required. A list of {role, content} messages. Roles: system, user, assistant. Up to 2000 messages and 2,000,000 characters in total. |
stream | boolean | Stream the response as server-sent events. Default false. |
temperature | number | 0–2. Forwarded only when you set it; we never send a default, because some reasoning models reject the parameter outright. |
top_p | number | 0–1. Forwarded only when you set it. |
max_tokens | integer | Upper bound on generated tokens. Also caps what the request can cost. |
max_completion_tokens | integer | Alias of max_tokens, for newer OpenAI clients. |
stop | string | array | Up to 4 stop sequences, each up to 64 characters. |
presence_penalty | number | −2 to 2. Forwarded only when you set it. |
frequency_penalty | number | −2 to 2. Forwarded only when you set it. |
seed | integer | Forwarded to the model when supported. Not a determinism guarantee. |
reasoning_effort | string | For reasoning-capable models. The valid values are per-model; see Reasoning effort below. |
stream_options | object | { "include_usage": true } adds a final usage chunk when streaming. |
n | integer | Only n = 1 is supported. Any other value returns 400. |
Omitted means omitted
A sampling parameter you do not send is not sent upstream either. We do not substitute OpenAI's documented default, because doing so would override a model whose own default differs. Several reasoning models reject temperature and top_p entirely, so a defaulted value would break those requests.
Streaming
With stream: true the API returns server-sent events. Each event is a chat.completion.chunk whose choices[0].delta.content carries the next piece of text. The stream ends with a data: [DONE] line. This is exactly what the OpenAI SDK expects, so streaming works out of the box.
curl -N https://api.sanero.ai/v1/chat/completions \
-H "Authorization: Bearer sk-sanero-…" \
-H "Content-Type: application/json" \
-d '{
"model": "<model-id>",
"messages": [{"role": "user", "content": "Hello, Sanero!"}],
"stream": true,
"stream_options": {"include_usage": true}
}'data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"Hello"},"finish_reason":null}]}
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"!"},"finish_reason":null}]}
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[],"usage":{"prompt_tokens":12,"completion_tokens":4,"total_tokens":16},"sanero":{"cost":{"charged_usd":"0.00000768","price_usd":"0.00000768","balance_usd":"12.48212832","underfunded":false}}}
data: [DONE]Add stream_options: { include_usage: true } to receive a final chunk carrying token usage and the cost of the request.
Errors inside a stream
Once the first byte is sent the HTTP status is already 200, so a later failure cannot change it. Such a failure arrives as a frame containing an error object, followed by data: [DONE]. Handle that frame in your reader: a stream that ends early otherwise looks like a short answer.
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"The answer is"},"finish_reason":null}]}
data: {"error":{"message":"The stream ended before the response was complete.","type":"api_error","param":null,"code":"stream_interrupted"}}
data: [DONE]Tokens already delivered are billed even if the stream breaks or you disconnect: they cannot be undelivered. A request like that appears in your logs as partial, with a charge and an explanation.
Model catalogue & prices
Every model the API can serve right now, with list prices per 1,000,000 tokens. This table is generated from the same configuration the API bills from, so it cannot fall out of date.
Loading…
Models
List the models your key can call. The default response is the exact OpenAI models shape, so an SDK that enumerates models works unchanged. Model ids are Sanero-branded; pass one as the `model` field of a chat request.
/modelscurl https://api.sanero.ai/v1/models \
-H "Authorization: Bearer sk-sanero-…"{
"object": "list",
"data": [
{ "id": "<model-id>", "object": "model", "created": 1771113600, "owned_by": "sanero" }
]
}Extended catalogue
Add ?extended=true (or fetch a single model) to get everything we can tell you about it: display name, the reasoning levels THAT model supports, its maximum output length, and your effective prices. Prices are per 1,000,000 tokens and are returned as decimal strings, never floats, because 0.048 has no exact binary representation, and a client summing thousands of them would drift.
/models/{id}curl "https://api.sanero.ai/v1/models/<model-id>" \
-H "Authorization: Bearer sk-sanero-…"{
"id": "<model-id>",
"object": "model",
"created": 1771113600,
"owned_by": "sanero",
"display_name": "Claude Opus 5",
"company": "Anthropic",
"reasoning_levels": ["off", "low", "medium", "high", "xhigh", "max"],
"max_output_tokens": 128000,
"pricing": {
"input_per_1m": "0.24",
"output_per_1m": "1.2",
"cache_read_per_1m": "0.024",
"cache_write_per_1m": "0.3"
}
}Absent fields, not nulls
A field we have no value for is left out of the response instead of being sent as null. In pricing that is the whole contract: a component that is present bills, and a component that is absent does not. There is no separate type field to consult, and a price is never 0 to mean “free”. Every amount is USD, and the unit is in the field name (input_per_1m is per 1,000,000 tokens), so nothing has to be paired with a currency field to be read correctly.
created is the model's release date as a Unix timestamp, so you can tell a current model from an older one. For a model whose release date we do not publish it falls back to a fixed placeholder; treat it as “unknown”, not as a real date.
The prices in an authenticated response are what YOUR wallet will be charged, including any active promotion. The catalogue on this page shows list prices, because we do not know who is reading it.
The catalogue is read live on every request. When we enable, retire or reprice a model, GET /models reflects it immediately: there is nothing to redeploy and no cached list to invalidate.
Reasoning effort
Reasoning-capable models accept a reasoning_effort. The available levels are a property of each model, not a fixed set: depending on the model you may have minimal, low, medium, high, xhigh, max, or a simple on/off. Read the exact list from the model's reasoning_levels; the catalogue above shows it for every model.
curl https://api.sanero.ai/v1/chat/completions \
-H "Authorization: Bearer sk-sanero-…" \
-H "Content-Type: application/json" \
-d '{
"model": "<model-id>",
"messages": [{"role": "user", "content": "Prove that sqrt(2) is irrational."}],
"reasoning_effort": "high"
}'Any model accepts "off" (and "none"), which means "do not ask for reasoning". Sending a level a model does not support returns 400 and names the levels it does support, so you never have to guess.
{
"error": {
"message": "`galactic` is not a valid value for `reasoning_effort` for model `claude-opus-5`. Supported values: `off`, `low`, `medium`, `high`, `xhigh`, `max`.",
"type": "invalid_request_error",
"param": "reasoning_effort",
"code": "invalid_value"
}
}Higher effort means more generated tokens, and generated tokens are what you pay for. A high-effort answer can cost several times a low-effort one for the same prompt.
What a request costs
Every response tells you what it cost. There is no need to estimate from token counts and a price list: the exact charge, its component breakdown and your remaining balance come back with the answer.
For a streamed request the same block arrives in the final usage chunk (ask for it with stream_options.include_usage).
Cost fields
charged_usd | What your balance was actually debited. This is what you paid. |
price_usd | The price of the request. Differs from charged_usd only if your balance ran out mid-stream, in which case we collected what was left. |
input_usd / output_usd | The prompt and completion components. |
cache_read_usd / cache_write_usd | Prompt-cache components, when the model uses one. |
request_usd | A per-request component, for models priced that way. |
balance_usd | Your remaining balance after this charge. |
underfunded | True when your balance could not cover the full price. |
Every money value is a decimal string, never a JSON number. Parse it into a decimal type: 0.0031 cannot be represented exactly in binary floating point, and a client summing thousands of such values accumulates real error.
Response headers
The same figures are also response headers, for clients that see headers but cannot read the body: a proxy, a logging layer, or an editor extension that wants to show a running total.
X-Sanero-Cost-Usd | What was actually debited from your balance. |
X-Sanero-Price-Usd | The list price of the request. |
X-Sanero-Balance-Usd | Your balance after this charge. |
from decimal import Decimal
import httpx
# The SDK drops unknown fields, so read the raw response to get the cost block.
# Every money value is a decimal STRING: parse it with Decimal, never float.
response = httpx.post(
"https://api.sanero.ai/v1/chat/completions",
headers={"Authorization": "Bearer sk-sanero-…"},
json={
"model": "<model-id>",
"messages": [{"role": "user", "content": "Hello, Sanero!"}],
},
timeout=120,
)
response.raise_for_status()
body = response.json()
cost = Decimal(body["sanero"]["cost"]["charged_usd"])
balance = Decimal(body["sanero"]["cost"]["balance_usd"])
print(f"charged {cost} USD, {balance} USD left")
# Also available as headers, for clients that never parse the body.
print(response.headers["X-Sanero-Cost-Usd"])Billing
The API is pay-as-you-go from your wallet balance, at the per-model prices on our pricing page. There is no subscription and no monthly commitment for API use, and API usage never touches a subscription plan's allowance.
Nothing runs that you cannot pay for
Before a request reaches a model we price it, using your prompt and your max_tokens, and check that your balance covers that estimate. If it does not, you get 402 (insufficient_quota) and nothing is sent and nothing is charged.
While a request runs, its estimated cost is held against your balance, and the hold is released the moment the real charge is known. That is why a burst of parallel requests can hit 402 while your balance still looks sufficient: the requests already running have claimed part of it.
Per-key spending limits
A key can carry a monthly spending ceiling. When it is reached, that key returns 402 with the code spend_limit_exceeded, which is distinct from an empty balance, because the remedy is different: the money is there, the key is not allowed to spend it. Raise or clear the limit in your dashboard.
Rate & concurrency limits
Each key has a requests-per-minute allowance and each account has a cap on simultaneously running requests. Exceeding either returns 429 with a Retry-After header. GET /key reports both for the key you are holding.
Failed requests count too
The per-minute allowance is consumed when a request is admitted, not when it finishes, so a burst of parallel calls and a loop of failing calls are both throttled. A cheap failing request is not a free request.
Why there is a concurrency cap
Requests still in flight are counted against your balance while they run, so a large swarm of your own requests could otherwise lock up your own funds. The cap bounds that, and bounds the damage a runaway script can do.
Errors
Errors use the OpenAI envelope, so SDKs surface them as typed exceptions. The HTTP status tells you the category, and the code field tells you exactly what happened.
{
"error": {
"message": "Insufficient balance to make this request. Top up your account at https://dash.sanero.ai to continue.",
"type": "insufficient_quota",
"param": null,
"code": "insufficient_quota"
}
}Loading…
Retry? says whether repeating the identical request could succeed. Retrying a 400 forever is a bug; giving up on a 502 loses a request that would have worked. When we can be specific about timing, a Retry-After header says how long to wait; honour it.
Every failed request also appears in your dashboard logs with its code, an explanation and a suggested fix, so you can debug an integration without adding logging on your side.
One exception: a request rejected because the key itself is invalid is not recorded against any account. There is no way to tell whose it was meant to be, and attributing it to the claimed owner would let anyone write into a stranger's log.
Use in VS Code / Cursor
Any editor extension with an “OpenAI Compatible” provider works. Add a new provider with these fields:
| Provider type | OpenAI Compatible |
| Base URL | https://api.sanero.ai/v1 |
| API key | sk-sanero-… |
| Model | <model-id> |
- 1Create an API key in your Sanero dashboard and copy it.
- 2In your editor, open the AI extension settings and add a new model provider of type “OpenAI Compatible”.
- 3Paste the base URL above and your API key, then enter a model id from the catalogue.
- 4Open the chat panel: the model list populates and answers stream back live.
Extensions that display a per-request cost read it from the response. Ours is in the usage block and in the X-Sanero-Cost-Usd header, so the figure your editor shows is the exact amount debited, not an estimate.
Verified with OpenAI-Compatible providers such as Continue, Cline, Roo and Kilo Code, and with the official OpenAI Python and Node SDKs.
Code examples
Copy, paste, run. Replace the key placeholder with a real key; the model id is a real id from the live catalogue.
curl https://api.sanero.ai/v1/chat/completions \
-H "Authorization: Bearer sk-sanero-…" \
-H "Content-Type: application/json" \
-d '{
"model": "<model-id>",
"messages": [
{"role": "system", "content": "You are a concise assistant."},
{"role": "user", "content": "Hello, Sanero!"}
]
}'from openai import OpenAI
client = OpenAI(base_url="https://api.sanero.ai/v1", api_key="sk-sanero-…")
stream = client.chat.completions.create(
model="<model-id>",
messages=[{"role": "user", "content": "Hello, Sanero!"}],
stream=True,
stream_options={"include_usage": True},
)
for chunk in stream:
# The final usage chunk carries no choices; guard before indexing.
if chunk.choices:
print(chunk.choices[0].delta.content or "", end="", flush=True)Account endpoints
Read-only endpoints for your own account. They cost nothing and return only your own data.
GET /key: what this key is
Name, last four characters, creation and last-used timestamps, rate and concurrency limits, monthly spending limit and how much of it is used, scopes, allowed models, and your balance. Requires no scope: a key must always be able to describe itself, or a misconfigured key would give you a 403 and no way to find out why.
{
"object": "api_key",
"id": "6f1d0b2c-9a44-4e51-8c07-1b2d3e4f5a6b",
"name": "production",
"last4": "9f3a",
"created_at": "2026-08-01T09:14:22+00:00",
"last_used_at": "2026-08-21T11:03:48+00:00",
"limits": {
"requests_per_minute": 120,
"max_concurrent_requests": 24,
"monthly_spend_limit_usd": "50.00000000",
"monthly_spend_used_usd": "3.41220000",
"monthly_spend_remaining_usd": "46.58780000"
},
"balance_usd": "12.48213600"
}GET /usage: your spend
Totals and a per-model breakdown for the last N days (up to 90), plus a count of failed requests grouped by error code. Failed requests carry no charge, so they appear in the failure counts without inflating your spend.
curl "https://api.sanero.ai/v1/usage?days=7" \
-H "Authorization: Bearer sk-sanero-…"{
"object": "usage",
"period": { "start": "2026-08-14T…", "end": "2026-08-21T…", "days": 7 },
"total": {
"requests": 1842,
"failed_requests": 11,
"prompt_tokens": 2841002,
"completion_tokens": 512338,
"spend_usd": "4.21870000"
},
"models": [
{
"model": "<model-id>",
"requests": 1610,
"failed_requests": 4,
"prompt_tokens": 2510440,
"completion_tokens": 470112,
"spend_usd": "3.88110000"
}
],
"errors": [
{ "code": "rate_limit_exceeded", "count": 7 },
{ "code": "upstream_error", "count": 4 }
]
}GET /errors: the error reference
The whole error table as machine-readable JSON, in English or Russian. The same source this page renders from, so your retry logic can be built against data rather than prose.
GET /rates: billing facts
Platform-wide values you need to interpret a charge, chiefly the minimum per-request charge and the token block prices are quoted against.