unopenrouter Log in

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.
Needs
A runner or an exit node of yours online. Without one, requests answer 503.

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 pricing
GET/v1/models/{model}One model

Models

API price, per 1M tokens
ModelProviderContextWith your subscriptionInputCache readOutput
claude-opus-5-5claude1M$0*$4$0.20$20
claude-fable-5-1claude1M$0*$10$0.25$50
claude-sonnet-5-5claude1M$0*$2$0.20$10
claude-haiku-4-5-20251001claude200k$0*$1$0.10$5
gpt-6.1-solcodex272k$0*$2$0.10$10
gpt-6-astracodex272k$0*$10$1$50
gpt-6-solcodex272k$0*$2$0.20$10
gpt-6-lunacodex272k$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_usage and a closing data: [DONE]. Responses streams use the typed response.* 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, and tool_choice auto, none, required or a named function.
JSON mode
Best effort. response_format json_object or json_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_key or previous_response_id. Cached tokens are in usage.prompt_tokens_details.cached_tokens (Responses: usage.input_tokens_details.cached_tokens).
Responses API
With previous_response_id and store (default true).
Images
image_url parts, 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's model says which one answered.
Effort and length
reasoning_effort (Responses: reasoning.effort) maps to the model's effort levels. max_tokens, max_completion_tokens and max_output_tokens are best effort, Claude only.
Request ids
Every response has an x-request-id header.

Not supported

Return 400
n above 1, logprobs and top_logprobs.
Ignored
temperature, top_p, seed, stop, the penalties and logit_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: false works on the server's private network but not through api.unopenrouter.com.

Errors

OpenAI's error object, {"error": {"message", "type", "param", "code"}}, and its status codes.

StatusWhen
400A bad request, or a parameter that isn't supported
401A missing, unknown, disabled or revoked key
403A model the key may not use
404An unknown model or response
422JSON mode failed after one retry
429A limit was hit. Wait for retry-after seconds; with x-should-retry: false, don't retry soon
503runner_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 scope admin. Admin keys work for /v1 too. 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_models
DELETE/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 link

A 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

# 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"