Docs
unopenrouter speaks the OpenAI API. Use any OpenAI SDK with the base URL below and a key from your dashboard.
Coding with an agent? Give it this whole page as Markdown.
Quick start
- Base URL
https://api.unopenrouter.com/v1- Key
Authorization: Bearer sk-unor-…, from the dashboard or the management API.
Examples
curl https://api.unopenrouter.com/v1/chat/completions \
-H "Authorization: Bearer $UNOR_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet-5-5",
"messages": [{"role": "user", "content": "Write a haiku about caches."}]
}'
import os
from openai import OpenAI
client = OpenAI(
base_url="https://api.unopenrouter.com/v1",
api_key=os.environ["UNOR_KEY"],
)
reply = client.chat.completions.create(
model="claude-sonnet-5-5",
messages=[{"role": "user", "content": "Write a haiku about caches."}],
)
print(reply.choices[0].message.content)
stream = client.chat.completions.create(
model="gpt-6-luna",
messages=[{"role": "user", "content": "Count to ten, slowly."}],
stream=True,
)
for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://api.unopenrouter.com/v1",
apiKey: process.env.UNOR_KEY,
});
const reply = await client.chat.completions.create({
model: "gpt-6-luna",
messages: [{ role: "user", content: "Write a haiku about caches." }],
});
console.log(reply.choices[0].message.content);
# client = OpenAI(...) as in the Python example
first = client.responses.create(
model="claude-sonnet-5-5",
input="Pick a city at random and remember it.",
)
second = client.responses.create(
model="claude-sonnet-5-5",
previous_response_id=first.id,
input="Which city did you pick?",
)
print(second.output_text)
Endpoints
POST/v1/chat/completionsChat completions, streamed or not
POST/v1/responsesResponses API
GET/v1/responses/{id}A stored response
DELETE/v1/responses/{id}Delete a stored response
GET/v1/responses/{id}/input_itemsA stored response's input items
GET/v1/modelsThe models, with
context_length and pricingGET/v1/models/{model}One model
Models
| API price, per 1M tokens | ||||||
|---|---|---|---|---|---|---|
| Model | Provider | Context | With your subscription | Input | Cache read | Output |
| claude-opus-5-5 | claude | 1M | $0* | $4 | $0.20 | $20 |
| claude-fable-5-1 | claude | 1M | $0* | $10 | $0.25 | $50 |
| claude-sonnet-5-5 | claude | 1M | $0* | $2 | $0.20 | $10 |
| claude-haiku-4-5-20251001 | claude | 200k | $0* | $1 | $0.10 | $5 |
| gpt-6.1-sol | codex | 272k | $0* | $2 | $0.10 | $10 |
| gpt-6-astra | codex | 272k | $0* | $10 | $1 | $50 |
| gpt-6-sol | codex | 272k | $0* | $2 | $0.20 | $10 |
| gpt-6-luna | codex | 272k | $0* | $0.10 | $0.01 | $0.50 |
*Uses your account's usage limits; typically 10–25× cheaper than the API. API prices are list prices in USD. A key's spending limits count in them.
Supported
- Streaming
- Server-sent events with OpenAI's chunks,
stream_options.include_usageand a closingdata: [DONE]. Responses streams use the typedresponse.*events. - Tool calling
- Emulated: your tools are described to the model, and its JSON answer is parsed into
tool_calls, retried once if it doesn't parse. Streamed and parallel calls work, andtool_choiceauto,none,requiredor a named function. - JSON mode
- Best effort.
response_formatjson_objectorjson_schema(Responses:text.format). The reply is checked, against the schema if there is one, and retried once; then a 422. A JSON-mode stream arrives in one piece at the end. - Caching
- A conversation stays on one account, so the provider's prompt cache hits. It is matched by its message
history,
prompt_cache_keyorprevious_response_id. Cached tokens are inusage.prompt_tokens_details.cached_tokens(Responses:usage.input_tokens_details.cached_tokens). - Responses API
- With
previous_response_idandstore(defaulttrue). - Images
image_urlparts, https or data URLs. Responses:input_image.- Fallbacks
"models": ["claude-sonnet-5-5", "gpt-6-luna"]tries each in order, as on OpenRouter. The response'smodelsays which one answered.- Effort and length
reasoning_effort(Responses:reasoning.effort) maps to the model's effort levels.max_tokens,max_completion_tokensandmax_output_tokensare best effort, Claude only.- Request ids
- Every response has an
x-request-idheader.
Not supported
- Return 400
nabove 1,logprobsandtop_logprobs.- Ignored
temperature,top_p,seed,stop, the penalties andlogit_bias: the CLIs don't expose them.- Not offered
- Embeddings, audio, image generation, files, batch, assistants, realtime, and hosted tools (web search, file search, code interpreter).
Keys and limits
- Limits
- Optional, per key: requests per minute and per second, tokens a day, spend a day and a month (at API list prices), and the models it may use. Every IP also has a limit of 600 requests a minute.
- Your accounts only
- A key runs only on its owner's accounts, through their runners or exit nodes.
- Private-only keys
- A key with
public_allowed: falseworks on the server's private network but not throughapi.unopenrouter.com.
Errors
OpenAI's error object, {"error": {"message", "type", "param", "code"}}, and its status codes.
| Status | When |
|---|---|
| 400 | A bad request, or a parameter that isn't supported |
| 401 | A missing, unknown, disabled or revoked key |
| 403 | A model the key may not use |
| 404 | An unknown model or response |
| 422 | JSON mode failed after one retry |
| 429 | A limit was hit. Wait for retry-after seconds; with x-should-retry: false, don't retry soon |
| 503 | runner_offline or exit_offline: none of your runners or exit nodes is online |
Management API
For code that makes keys. Base URL https://api.unopenrouter.com/api/v1. JSON is snake_case, and
errors are {"error": {"message", "code"}}.
- Auth
Authorization: Bearer $ADMIN_KEY, a key with scopeadmin. Admin keys work for/v1too. Inference keys get 403 here. Only an admin can make admin keys.- Rate limit
- 60 calls a minute per admin key, apart from inference. Every call is logged.
Endpoints
GET/api/v1/keysThe keys:
{"data": [Key]}POST/api/v1/keysMake a key: 201
{"data": Key, "key": "sk-unor-…"}. The secret is in this answer only.PATCH/api/v1/keys/{id}Change
name, disabled, public_allowed, limits (null clears one) or allowed_modelsDELETE/api/v1/keys/{id}Revoke a key for good
GET/api/v1/usageUsage by day, key and model:
?days=30&key_id=…&model=…GET/api/v1/accountsYour subscription accounts, with their usage windows
PUT/api/v1/policyHow work spreads over your accounts:
{"mode": "pressure" | "order" | "pinned", "pinned"}GET/api/v1/invitesYour invite codes
POST/api/v1/invitesMake an invite code,
{"note"} optional: 201 with its linkA new key takes name and, optionally, scope (inference or
admin), public_allowed (default true), limits
(rpm, rps, daily_tokens, daily_usd, monthly_usd;
any left out is no limit) and allowed_models (null is every model).
Examples
curl, with an admin key
# Make a key with limits. The secret is in "key", shown once.
curl https://api.unopenrouter.com/api/v1/keys \
-H "Authorization: Bearer $ADMIN_KEY" \
-H 'Content-Type: application/json' \
-d '{"name":"pokemon-bot","limits":{"rpm":60,"daily_usd":5},"allowed_models":["claude-sonnet-5-5"]}'
# List the keys
curl https://api.unopenrouter.com/api/v1/keys \
-H "Authorization: Bearer $ADMIN_KEY"
# Disable one ({"disabled":false} turns it back on)
curl -X PATCH https://api.unopenrouter.com/api/v1/keys/$KEY_ID \
-H "Authorization: Bearer $ADMIN_KEY" \
-H 'Content-Type: application/json' \
-d '{"disabled":true}'
# Revoke it for good
curl -X DELETE https://api.unopenrouter.com/api/v1/keys/$KEY_ID \
-H "Authorization: Bearer $ADMIN_KEY"
# Its usage over the last 7 days
curl "https://api.unopenrouter.com/api/v1/usage?days=7&key_id=$KEY_ID" \
-H "Authorization: Bearer $ADMIN_KEY"