Point your OpenAI client at Realrouter.
The API keeps familiar OpenAI-style routes for responses, chat completions, files, and models. Replace the base URL, use your Realrouter key, and keep your client code small.
Quickstart
Install the official OpenAI SDK and set the Realrouter base URL. The dashboard will show your API key once when it is created.
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.REALROUTER_KEY,
baseURL: "https://api.realrouter.org/v1",
});
const response = await client.responses.create({
model: "gpt-5.4-mini",
reasoning: { effort: "low" },
input: "Summarize this note in Chinese.",
});
console.log(response.output_text);Endpoints
POST /v1/responsesfor the primary OpenAI-compatible responses API.POST /v1/chat/completionsfor legacy chat completion clients.GET /v1/modelsfor available models, capabilities, and pricing metadata.POST /v1/filesto upload a PDF or image for use as input_file / input_image.GET /v1/meto read the key and account metadata for the key you are calling with.GET /v1/usage/summary?days=Nto read token and spend totals for the last N days.GET /v1/billing/transactionsto read the billing ledger for the account.
The three routes above accept a plain sk- key, so an integration can watch its own spend without a dashboard session. Account balance is deliberately not exposed to an API key.
A machine-readable OpenAPI description of every route is served at /openapi.json, with a browsable version at /api-docs.
Parameters that do not reach the model
temperature and top_p are accepted and echoed back in the response object so OpenAI clients keep working, but they are not forwarded and have no effect on output. Use reasoning.effort to trade quality against cost instead. max_output_tokens (and max_tokens / max_completion_tokens on chat completions) are also not forwarded, but they are not inert: they set the estimated output size used to reserve balance, so an inflated value can trigger 402 insufficient_balance or request_cost_limit_exceeded on a request that would otherwise have run.
The API is stateless. previous_response_id and store are accepted and echoed, but nothing is persisted and there is no GET /v1/responses/{id} — send the conversation history with every request.
Error shape
Errors are JSON with an error object. Only error.code and error.message are always present — branch on error.code. The other keys vary by family: error.type (always "invalid_request_error") and error.param appear on request-validation and upstream failures but are absent from 401, 402 and 429, so error.type is never a safe thing to branch on. error.details appears on the quota, balance and rate-limit errors and its contents differ per code; concurrency 429s carry retry_after_seconds instead.
Tools and modalities
Built-in tools are on by default: web_search is added to every request, and image_generation is added on every model that supports it, whether or not you pass a tools array. Pass the tool explicitly to configure it — for example web_search with search_context_size. Tool types the API cannot translate return 400 with unsupported_tool.
web_searchimage_generationfunctioncustomcomputerWeb search
const response = await client.responses.create({
model: "gpt-5.3-codex-spark",
input: "Search the web and summarize today's top AI platform news.",
tools: [{ type: "web_search" }],
});Function calling
Function calling is stateless. Realrouter returns function_call items or chat tool_calls; your app runs the function and sends the result back with the relevant conversation context.
const first = await client.responses.create({
model: "gpt-5.5",
input: "What is the weather in Toronto?",
tools: [{
type: "function",
name: "get_weather",
description: "Get current weather for a city.",
parameters: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
additionalProperties: false,
},
}],
tool_choice: { type: "function", name: "get_weather" },
});
const call = first.output.find((item) => item.type === "function_call");
const toolResult = await getWeather(JSON.parse(call.arguments));
const final = await client.responses.create({
model: "gpt-5.5",
input: [
{ role: "user", content: "What is the weather in Toronto?" },
call,
{ type: "function_call_output", call_id: call.call_id, output: JSON.stringify(toolResult) },
],
tools: first.tools,
});Structured outputs
On GPT models, Responses supports OpenAI-compatible Structured Outputs with text.format.type=json_schema. Use json_schema when you need schema adherence; json_object remains available for plain JSON mode. Claude models accept text.format.type=text only and return 400 unsupported_model_for_text_format for anything else.
const response = await client.responses.create({
model: "gpt-5.4-mini",
input: "Extract the answer. Return exactly pong.",
text: {
format: {
type: "json_schema",
name: "pong_answer",
schema: {
type: "object",
properties: {
answer: { type: "string" },
},
required: ["answer"],
additionalProperties: false,
},
strict: true,
},
},
});
const parsed = JSON.parse(response.output_text);PDF and file input
input_file and file_id references work on /v1/responses only. /v1/chat/completions accepts text and image_url content parts and rejects anything else with 422.
curl https://api.realrouter.org/v1/files \
-H "Authorization: Bearer $REALROUTER_KEY" \
-F purpose=user_data \
-F file=@document.pdfconst response = await client.responses.create({
model: "gpt-5.4-mini",
input: [{
role: "user",
content: [
{ type: "input_file", file_id: "file-..." },
{ type: "input_text", text: "Summarize this PDF." },
],
}],
});Image generation
const response = await client.responses.create({
model: "gpt-5.4-mini",
input: "Generate a small product icon for a code API gateway.",
tools: [{ type: "image_generation" }],
});Model support
Claude models reach the upstream through a different path than GPT models and support a narrower surface. Unsupported tool types return 400 unsupported_tool — including custom and computer on the models below that show No. image_generation is the one exception: on a model that lacks it, it is quietly dropped from the request instead of rejected.
web_searchfunction callingcustom (freeform) toolsimage_generationcomputer useimage inputPDF input_filejson_schema outputweb search streaming eventsfile_search, code_interpreter and mcp are not supported on any model and return 400 unsupported_tool.
Limits and errors
Every error carries error.code and error.message; the codes below also carry an error.details object whose contents depend on the code. Handle these two families explicitly — they are the ones your integration will actually hit.
Rate limits by plan
Your plan sets two published ceilings. Requests per minute is a token bucket that refills continuously, so an idle client can burst a full minute’s worth and a steady one is paced smoothly. Concurrency is how many requests may be in flight at once.
StarterBasicPlus429 — slow down
- plan_rate_limit_exceeded — you went over your plan’s requests-per-minute ceiling. details carries requests_per_minute and the response carries Retry-After.
- quota_exhausted — your own 5h fair-share window is spent. Retry-After and x-ratelimit-* headers are on the response; details carries retry_after_unix.
- upstream_rate_limited — the shared subscription backend is saturated. Retry with backoff; this is not your quota. Concurrency limits surface as 429 too, carrying retry_after_seconds rather than details.
Rate-limit headers accompany successful responses too: x-ratelimit-limit-5h, x-ratelimit-remaining-5h, x-ratelimit-reset-5h and the matching 7d triple. Read them to pace yourself before you are throttled.
402 — balance problem
- insufficient_balance — your remaining balance is below the estimated cost of this request. details carries balance_usd and required_usd.
- request_cost_limit_exceeded — a single request may not cost more than one fifth of your plan’s included usage. Shorten the input, or lower max_output_tokens, which is what sizes the estimate. reasoning.effort does not affect this check.
- subscription_inactive — no active plan on the account.
- subscription_period_expired — the billing period has ended.
File and image retention
Reading your own usage
A plain sk- key can call GET /v1/usage/summary?days=30 for token and spend totals in USD, and GET /v1/me for key metadata. Account balance is deliberately not readable with an API key — sign in to the dashboard for that.