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.

Base URLhttps://api.sanero.ai/v1
Contents

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:

https://api.sanero.ai/v1

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 header
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.

GET /key
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.

POST/chat/completions
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!"}
    ]
  }'
Response
{
  "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

ParameterTypeDescription
modelstringRequired. A model id from GET /models.
messagesarrayRequired. A list of {role, content} messages. Roles: system, user, assistant. Up to 2000 messages and 2,000,000 characters in total.
streambooleanStream the response as server-sent events. Default false.
temperaturenumber0–2. Forwarded only when you set it; we never send a default, because some reasoning models reject the parameter outright.
top_pnumber0–1. Forwarded only when you set it.
max_tokensintegerUpper bound on generated tokens. Also caps what the request can cost.
max_completion_tokensintegerAlias of max_tokens, for newer OpenAI clients.
stopstring | arrayUp to 4 stop sequences, each up to 64 characters.
presence_penaltynumber−2 to 2. Forwarded only when you set it.
frequency_penaltynumber−2 to 2. Forwarded only when you set it.
seedintegerForwarded to the model when supported. Not a determinism guarantee.
reasoning_effortstringFor reasoning-capable models. The valid values are per-model; see Reasoning effort below.
stream_optionsobject{ "include_usage": true } adds a final usage chunk when streaming.
nintegerOnly 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.

Request
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}
  }'
text/event-stream
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.

text/event-stream
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.

GET/models
Request
curl https://api.sanero.ai/v1/models \
  -H "Authorization: Bearer sk-sanero-…"
Response
{
  "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.

GET/models/{id}
Request
curl "https://api.sanero.ai/v1/models/<model-id>" \
  -H "Authorization: Bearer sk-sanero-…"
Response
{
  "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.

POST /chat/completions
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.

400 invalid_value
{
  "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_usdWhat your balance was actually debited. This is what you paid.
price_usdThe 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_usdThe prompt and completion components.
cache_read_usd / cache_write_usdPrompt-cache components, when the model uses one.
request_usdA per-request component, for models priced that way.
balance_usdYour remaining balance after this charge.
underfundedTrue 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-UsdWhat was actually debited from your balance.
X-Sanero-Price-UsdThe list price of the request.
X-Sanero-Balance-UsdYour balance after this charge.
Python: reading the cost of a request
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.

application/json
{
  "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 typeOpenAI Compatible
Base URLhttps://api.sanero.ai/v1
API keysk-sanero-…
Model<model-id>
  1. 1Create an API key in your Sanero dashboard and copy it.
  2. 2In your editor, open the AI extension settings and add a new model provider of type “OpenAI Compatible”.
  3. 3Paste the base URL above and your API key, then enter a model id from the catalogue.
  4. 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!"}
    ]
  }'
Python: streaming
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.

Response
{
  "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.

Request
curl "https://api.sanero.ai/v1/usage?days=7" \
  -H "Authorization: Bearer sk-sanero-…"
Response
{
  "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.