API
reference.
One base URL, one key, every model. The API speaks the OpenAI and OpenRouter formats: point your client at it and keep the rest of your code.
- Base URL
- https://api.undrlst.com/v1
- Version
- v1
- Status
- Gateway live
- Compatible
- OpenAI · OpenRouter
(01)
Quick start
Sign in on your account page, add credits and create a key. You see the key once: store it right away. Then send your first request.
Already on the OpenAI SDK or an OpenRouter client? Change the base URL and the key. Nothing else.
| Use | Base URL |
|---|---|
| Recommended | https://api.undrlst.com/v1 |
| Also works | https://undrlst.com/api/v1 |
| Method | Path | What it does |
|---|---|---|
| POST | /chat/completions | Run a model. JSON or streamed. |
| GET | /models | Every model you can call, OpenRouter format. |
| GET | /key | Your key's balance and usage, OpenRouter format. |
| GET | /auth/key | Same as /key, for clients that use OpenRouter's path. |
| GET | /credits | Total credits added to your account and total spent. |
export UNDERLIST_API_KEY=sk-smaaart-… curl https://api.undrlst.com/v1/chat/completions \ -H "Authorization: Bearer $UNDERLIST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "anthropic/claude-sonnet-5", "messages": [{ "role": "user", "content": "Say hi in five words." }] }'(02)
Authentication
Send your key in the Authorization header as a bearer token. Clients that use an x-api-key header work too.
Keys start with sk-smaaart-. Older keys starting with sk-inferrr- keep working until you rotate them.
One active key per account. Creating a new key revokes the old one in the same step, and the old key gets a 401 from its very next request. Your balance doesn't move. We only store a hash of the key and its last four characters, so we can't show it to you again.
Authorization: Bearer sk-smaaart-…(03)
Chat completions
POST/v1/chat/completions
The same request and response shapes as OpenAI's chat completions. The common fields are below; any other field the provider accepts is passed through.
| Field | Type | Description |
|---|---|---|
| model | string | Required. The OpenRouter id of the model, e.g. anthropic/claude-sonnet-5. That exact model is called, never a substitute. |
| messages | array | Required. The conversation, as { role, content } objects, in the OpenAI format. |
| stream | boolean | Send tokens back as server-sent events while they are generated. Defaults to false. |
| temperature, top_p, max_tokens, … | various | Standard sampling parameters, passed to the provider unchanged. |
| tools, tool_choice, response_format | various | Tool calls and structured output, passed through as is for models that support them. |
Fixed on our side, so you always get what you asked for:
- Zero-data-retention endpoints only. A
providerobject you send is kept, withzdr: trueanddata_collection: "deny"always set. models,routeandfallbacksare removed: one request runs one model, no silent fallback.- The response keeps the provider's own
idandmodel, so you can check what served it. - Prompts and answers are never stored. We keep model, token counts, cost, latency and time.
import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.undrlst.com/v1", apiKey: process.env.UNDERLIST_API_KEY,}); const res = await client.chat.completions.create({ model: "openai/gpt-6-sol", messages: [{ role: "user", content: "Say hi in five words." }],}); console.log(res.choices[0].message.content);(04)
Streaming
Set stream: true to receive tokens as they are generated, as server-sent events. Every SDK that streams from OpenAI or OpenRouter works unchanged.
Each event is a data: line with a JSON chunk. The last chunk carries token usage and cost, and the stream ends with data: [DONE].
const stream = await client.chat.completions.create({ model: "anthropic/claude-sonnet-5", messages: [{ role: "user", content: "Write a haiku about caching." }], stream: true,}); for await (const chunk of stream) { process.stdout.write(chunk.choices[0]?.delta?.content ?? "");}data: {"id":"gen-…","model":"anthropic/claude-sonnet-5","choices":[{"delta":{"content":"Cold"}}]} data: {"id":"gen-…","model":"anthropic/claude-sonnet-5","choices":[{"delta":{"content":" reads"}}]} data: {"id":"gen-…","choices":[],"usage":{"prompt_tokens":14,"completion_tokens":17,"cost":…}} data: [DONE](05)
Models
GET/v1/models
The full OpenRouter catalogue in OpenRouter's format, refreshed every hour. No key needed. Call any model by its id, like anthropic/claude-sonnet-5. Live prices are in the price list.
curl https://api.undrlst.com/v1/models(06)
Balance and usage
Every response to a valid key, errors included, carries your balance in dollars in the X-Smaaart-Balance header (also sent as X-Inferrr-Balance for older integrations).
A request is accepted while your balance is above zero, and its real cost is deducted once it finishes. The last request before you run out can leave your balance slightly below zero.
GET/v1/key
Your key's balance and usage in OpenRouter's format, so tools that check a key before running work as is. /v1/auth/key returns the same thing.
GET/v1/credits
Everything ever added to your account and everything spent, in dollars.
X-Smaaart-Balance: 37.500000curl https://api.undrlst.com/v1/key \ -H "Authorization: Bearer $UNDERLIST_API_KEY" { "data": { "label": "sk-smaaart-…AbCd", "usage": 12.5, "limit": null, "limit_remaining": 37.5, "is_free_tier": false }}{ "data": { "total_credits": 50, "total_usage": 12.5 }}(07)
Errors
Errors use the OpenAI shape, so your SDK raises them the way it already does. Provider errors pass through with their own status and body.
| Status | What happened |
|---|---|
| 400 | The body isn't a JSON object, or model is missing. |
| 401 | The key is missing, malformed, unknown, revoked or replaced by a newer one. |
| 402 | Your balance is used up. Top up and retry. |
| 429 | The provider is rate-limiting the model. Relayed as is: wait and retry. |
| 502 | The provider couldn't be reached, or returned an error (relayed with its own status and body). No fallback model is tried. |
| 503 | underlist is temporarily unable to serve the request, for example while our upstream account is being topped up. Your balance is not charged. |
{ "error": { "message": "Insufficient balance, top up your credits", "type": "insufficient_balance", "code": 402 }}