Build on the NaraRouter API
One stable, OpenAI-compatible endpoint across leading models. Point any OpenAI SDK at our base URL, authenticate with your key, and ship.
Quickstart
Three steps to your first response: create a key, set the base URL, send a request. The API speaks the OpenAI Chat Completions format, so existing OpenAI clients work by changing two lines.
2. Set the base URL
Send all requests to the gateway base URL. The chat endpoint lives at /v1/chat/completions.
https://router.bynara.id/v13. Send your first request
Use cURL or any OpenAI-compatible SDK. Pass a model alias (see Models), your messages, and your key as a Bearer token.
1curl https://router.bynara.id/v1/chat/completions \2 -H "Authorization: Bearer sk-nry-xxxxxxxx" \3 -H "Content-Type: application/json" \4 -d '{5 "model": "deepseek-v4-flash",6 "messages": [7 { "role": "user", "content": "Hello!" }8 ]9 }'Because the API is OpenAI-compatible, the official OpenAI SDKs work unchanged apart from the base URL and key — no provider-specific client required.
Authentication & keys
Every request to the API must carry your secret key in the Authorization header as a Bearer token.
Authorization header
Authorization: Bearer sk-nry-xxxxxxxxxxxxxxxxxxxxxxxxxxxxKey format
Keys are prefixed with sk-nry- followed by a random secret. The full secret is returned only once, at creation or rotation; afterwards only a masked form is shown.
Rotation & revocation
Manage keys from the dashboard. You can rotate a key (issue a new secret and invalidate the old one), revoke a key (it stays listed but no longer authenticates), or delete it. Rotated and revoked secrets stop working immediately.
Keep keys secret
Treat keys like passwords. Never embed them in client-side code, mobile apps, or public repositories. Use server-side environment variables and rotate any key you suspect is exposed.
Endpoint support
The gateway exposes four API surfaces under the same base URL and Bearer key. Point any compatible SDK at the matching path.
/v1/responsesOpenAI Responses API
The stateful Responses API. Send input plus optional tools and receive a structured response object. Compatible with the OpenAI Responses SDK.
/v1/messagesAnthropic Messages API
The Anthropic-native Messages format. Point the Anthropic SDK at the base URL and call this path with your key as the Bearer token.
/v1/chat/completionsOpenAI Chat Completions
The primary OpenAI-compatible chat endpoint. Send a list of messages and receive a model response; supports streaming and reasoning_effort.
https://api-images.bynara.id/v1/images/generationshttps://api-images.bynara.id/v1/images/editsImage generation & edit
Generate images from a prompt (/v1/images/generations) or edit an existing image (/v1/images/edits). Returns a URL or base64 payload.
Request
{
"model": "deepseek-v4-flash",
"messages": [
{ "role": "system", "content": "You are helpful." },
{ "role": "user", "content": "Hello!" }
],
"temperature": 0.7,
"max_tokens": 256
}Response
{
"id": "chatcmpl-...",
"object": "chat.completion",
"model": "deepseek-v4-flash",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "Hello! How can I help?" },
"finish_reason": "stop"
}
],
"usage": { "prompt_tokens": 18, "completion_tokens": 8, "total_tokens": 26 }
}The gateway validates the model and payload before forwarding. Streaming requests against a model that does not support streaming are rejected.
Streaming
Set stream to true to receive the response incrementally as Server-Sent Events (SSE). Each event carries a JSON delta in the OpenAI streaming format.
Wire format
The connection uses content-type text/event-stream. Each chunk arrives as a data: line containing a JSON delta. The stream terminates with a final data: [DONE] sentinel.
data: {"choices":[{"delta":{"content":"Hel"}}]}
data: {"choices":[{"delta":{"content":"lo"}}]}
data: [DONE]Consuming the stream
Most OpenAI SDKs expose streaming natively. With the official SDK, iterate the streamed chunks and read each delta's content.
stream = client.chat.completions.create(
model="deepseek-v4-flash",
messages=[{"role": "user", "content": "Tell me a story."}],
stream=True,
)
for chunk in stream:
delta = chunk.choices[0].delta.content
if delta:
print(delta, end="", flush=True)If an error occurs mid-stream, the gateway emits a clean error event followed by [DONE]; an error before the first byte is returned as a normal JSON error response instead.
Models
Pass a model alias in the model field. The list below is loaded live from the public plans endpoint, so it always reflects the models currently offered and which plan tier grants each.
Live from /api/plans. The authenticated /v1/models returns exactly the aliases your own plan entitles.
Loading models...
Some models are reasoning models (for example kimi-k2.5 and gemini-3.1-pro) that spend part of the token budget on internal reasoning before producing an answer. If you set a very low max_tokens, the visible content may come back empty because the budget was used up while reasoning. For these models, use a higher max_tokens.
Reasoning
Reasoning ("thinking") models spend part of the token budget on internal reasoning before answering. Control how deeply they think per request with reasoning_effort.
reasoning_effort
Higher effort means deeper thinking — better on hard tasks, but more output tokens and higher latency. Use none to disable reasoning, ultra as an alias for max. On a non-reasoning model the field is ignored. Higher levels are clamped to what each model supports (e.g. max becomes xhigh/high where max is unsupported) instead of erroring.
| Effort | Thinking | Cost & speed | Use for |
|---|---|---|---|
none | Off | Cheapest, no thinking tokens | Disable reasoning when you want a direct answer |
minimal | Glance | Near-none cost, fastest thinking | Trivial tasks that need a touch of care |
low | Short | Cheapest, fastest | Everyday chat, simple Q&A |
medium | Moderate | Balanced | Most tasks - the sensible default |
high | Long, deep | More tokens, slower | Hard math, multi-step coding, planning |
xhigh | Very long, exhaustive | Heavy tokens, slowest | Frontier coding, long-horizon agents |
max | Unconstrained | Maximum tokens and latency | Deepest analysis; ultra is an alias for max |
{
"model": "deepseek-v4-pro",
"messages": [
{ "role": "user", "content": "Prove that sqrt(2) is irrational." }
],
"reasoning_effort": "high"
}
// Same 7 levels on the other two chat endpoints:
// POST /v1/responses -> "reasoning": { "effort": "high" }
// POST /v1/messages -> "output_config": { "effort": "high" }
// Levels: none | minimal | low | medium | high | xhigh | max (ultra = max).
// Higher levels clamp to what each model supports instead of erroring.Which models support it
Call /v1/models and check the reasoning flag on each model — true means it is a reasoning model. The Models catalog also shows a Reasoning badge.
Safe to always send
reasoning_effort is a no-op on non-reasoning models, so you can send it on every request without gating per model. Unsupported levels never error: they are clamped to the highest level the model supports. Providers without a reasoning knob (Qoder, CommandCode, byNara event-stream) ignore the level and use their own default.
Output budget
For reasoning models the gateway guarantees enough output budget so thinking never starves the visible answer. A very low max_tokens can still return empty content on these models — leave room or omit it.
If you omit reasoning_effort, the provider's own default applies (medium on most models, high where reasoning cannot be disabled). The same 7 levels work on /v1/chat/completions (reasoning_effort), /v1/responses (reasoning.effort), and /v1/messages (output_config.effort).
Embeddings
Turn text into vectors for semantic search, clustering, and retrieval-augmented generation (RAG). Compatible with the OpenAI Embeddings API: send input and receive float vectors.
https://router.bynara.id/v1/embeddingsPOST /v1/embeddings maps to the provider's embeddings model. Use it to index documents; at query time, embed the question, search by cosine similarity, then optionally rerank.
Request
curl https://router.bynara.id/v1/embeddings \
-H "Authorization: Bearer sk-nry-xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"model": "text-embedding-3-small",
"input": "The quick brown fox jumps over the lazy dog"
}'from openai import OpenAI
client = OpenAI(base_url="https://router.bynara.id/v1", api_key="sk-nry-xxxxxxxx")
res = client.embeddings.create(model="text-embedding-3-small", input="The quick brown fox jumps over the lazy dog")
print(res.data[0].embedding[:5]) # [0.012, -0.034, ...]Response
{
"object": "list",
"data": [{ "object": "embedding", "index": 0, "embedding": [0.012, -0.034, 0.041, ...] }],
"model": "text-embedding-3-small",
"usage": { "prompt_tokens": 9, "total_tokens": 9 }
}Parameters
input can be a string or an array of strings. encoding_format, dimensions, and user are forwarded verbatim.
Embeddings are non-streaming. A stream flag is rejected. Usage reports input tokens only (no output tokens).
Rerank
Re-order retrieved candidates so the most relevant documents come first. Rerank uses a cross-encoder that reads query + document together, more accurate than cosine similarity for picking the top context to feed the LLM.
https://router.bynara.id/v1/rerankPOST /v1/rerank maps to the provider's rerank model. Send a query and a list of candidate documents (e.g. top 20 from vector search) and receive ranked results with relevance_score.
Typical RAG pipeline: embed 20 candidates via /v1/embeddings -> rerank to top 5 -> feed to chat completions. Rerank adds 100-300ms but lifts precision 20-30%.
Request
curl https://router.bynara.id/v1/rerank \
-H "Authorization: Bearer sk-nry-xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"model": "rerank-v3",
"query": "cara reset password nararouter",
"documents": [
"panduan billing reset saldo",
"cara reset password nararouter via dashboard",
"reset password email token"
],
"top_n": 3
}'# RAG: embed 20 docs via /v1/embeddings, then rerank top candidates
import requests
res = requests.post(f"https://router.bynara.id/v1/rerank", headers={"Authorization": "Bearer sk-nry-xxxxxxxx"}, json={
"model": "rerank-v3",
"query": "cara reset password nararouter",
"documents": docs, # 10-20 candidates from vector search
"top_n": 5
})
for r in res.json()["results"]:
print(r["index"], r["relevance_score"], docs[r["index"]][:60])Response
{
"object": "list",
"results": [
{ "index": 1, "relevance_score": 0.98 },
{ "index": 2, "relevance_score": 0.85 },
{ "index": 0, "relevance_score": 0.21 }
],
"model": "rerank-v3",
"usage": { "prompt_tokens": 142, "total_tokens": 142 }
}Parameters
queryThe search query (string).
documentsArray of candidate document strings (the snippets to score).
top_nOptional. Limit the number of results returned (e.g. 5). When omitted the provider returns all documents ranked.
modelRerank model alias.
Video generation
Generate short MP4 clips from text or images with one endpoint. The gateway handles the upstream async task (create + poll) server-side, so a single request returns the finished video.
https://api-images.bynara.id/v1/videosText-to-video uses a JSON body. Image-to-video and reference-to-video send the source images as multipart form data (image, image2, image3), or as public URLs via image_urls[] in JSON.
Generation modes
| Mode | How the image is used | Images |
|---|---|---|
t2v · Text-to-video | No image. The video is generated purely from the text prompt. | 0 |
i2v · Image-to-video | The uploaded image becomes the video's FIRST frame; the model animates from that exact composition. | Exactly 1 |
r2v · Reference-to-video | Images act as subject/style references (character, object, clothing); the model merges them into a NEW scene from the prompt, not bound to any source frame. | 1-3 |
Example r2v: image 1 a wizard, image 2 a fire dragon, image 3 a castle, prompt 'The wizard battles the fire dragon in the castle courtyard' — the model composes a new scene using elements from all three images.
Request
curl --request POST \
--url https://api-images.bynara.id/v1/videos \
--header 'Authorization: Bearer sk-nry-xxxxxxxx' \
--header 'Content-Type: application/json' \
--data '{
"model": "happyhorse-1.1-t2v",
"prompt": "A miniature city built from cardboard comes to life at night",
"mode": "t2v",
"resolution": "720p",
"ratio": "16:9",
"duration": 5
}'curl --request POST \
--url https://api-images.bynara.id/v1/videos \
--header 'Authorization: Bearer sk-nry-xxxxxxxx' \
--form [email protected] \
--form model=happyhorse-1.1-i2v \
--form mode=i2v \
--form prompt='The wizard raises his staff, wind blows his cloak, camera slowly zooms in' \
--form resolution=720p \
--form duration=5curl --request POST \
--url https://api-images.bynara.id/v1/videos \
--header 'Authorization: Bearer sk-nry-xxxxxxxx' \
--form [email protected] \
--form [email protected] \
--form [email protected] \
--form model=agnes-video-v2.0 \
--form mode=r2v \
--form prompt='The wizard battles the fire dragon in the castle courtyard' \
--form duration=5Response
# POST /v1/videos -> 202 Accepted
{ "id": "01934...", "status": "pending", "created": 1780457477 }
# GET /v1/videos/{id} (poll every ~5s)
{ "id": "01934...", "status": "succeeded",
"url": "/v1/videos/01934.../download",
"duration": 5, "created": 1780457477, "expires_at": "..." }Parameters
modelVideo alias, e.g. happyhorse-1.1-t2v or agnes-video-v2.0.
modet2v, i2v, or r2v. i2v requires exactly 1 image; r2v requires 1-3.
promptPositive prompt (and optional negative_prompt). Up to 3,500 characters each.
resolution720p or 1080p (provider default when omitted).
ratio16:9, 9:16, 1:1, 4:3, 3:4, 4:5, 5:4, 9:21, 21:9. Text-to-video only; i2v follows the first frame's aspect ratio.
durationClip length in seconds, 3-15. Billed per second (price_per_second × duration).
seedRandom seed [0, 2147483647] for reproducibility.
watermarkProvider watermark toggle.
imagesMultipart image, image2, image3 (i2v/r2v), or image_urls[] in JSON for public URLs.
The Videos API is asynchronous: POST /v1/videos immediately returns an ID with pending status (HTTP 202), then poll GET /v1/videos/[id] every ~5s until the status is succeeded — the response then includes the download URL. The MP4 is stored temporarily and expires after 3 hours. Generation typically takes 1-5 minutes.
Rate limits & quotas
Limits depend on your plan. Subscription plans are governed by a per-minute request rate and a daily token quota; the free tier and per-model fair-use caps apply otherwise.
Request rate (per minute)
Each plan sets a maximum number of requests per minute. Exceeding it returns a 429 with a rate_limited error. The window resets every minute.
Daily token quota
Subscription plans count input + output tokens against a daily quota but the quota is per model class, not one account-wide ceiling. Each class (base, Lite, Mocin, Pro) has its own separate daily cap. When a class bucket is reached, only that class returns 429 until the next day; other class buckets keep working. A null cap means fair-use (no hard daily ceiling).
Per-class daily token quotas
Each model belongs to a quota class — base, Lite, Mocin, or Pro — and each class has its own daily token quota. A subscription gets a separate daily quota for every class it can access, and the quotas are independent: using a model only counts against its own class quota and never reduces another. When one quota is exhausted, models in the other classes you have still work until the quotas reset at the start of the next day.
Concurrency
Plans also bound how many requests may run at once. Excess concurrent requests are rejected with 429; retry once an in-flight request completes.
429 behavior
On any limit breach the response status is 429 with the rate_limited error type. A daily token breach is per model tier — the message reads that this model tier's quota is reached, while other model tiers you have access to still work. Back off and retry after the relevant window resets. Per-plan limits are shown on the pricing page and load live below.
Errors
Errors use a single, stable JSON envelope. Switch on the type field rather than parsing the message. The request_id helps correlate a failure with server logs.
Error shape
{
"error": {
"type": "rate_limited",
"message": "Rate limit exceeded. Please retry later.",
"request_id": "req_..."
}
}Status codes
| Status | Type | Meaning |
|---|---|---|
400 | validation_error | The request was malformed or failed validation (for example, a missing model or messages field). |
401 | unauthorized | Missing or invalid API key. Check the Authorization header. |
403 | forbidden | Authenticated, but your plan does not include the requested model, or the account is suspended. |
404 | not_found | The requested model alias or endpoint does not exist. |
413 | bad_request | The request body or input is too large. |
415 | unsupported_media_type | Content-Type must be application/json. |
429 | rate_limited | Rate limit or a per-model-tier daily token quota was exceeded. A quota breach affects only that model tier — other tiers you have access to still work. Retry after the window resets. |
503 | service_unavailable | The model service is temporarily unavailable. Retry with backoff. |
500 | internal_error | An unexpected internal error. Retry; if it persists, contact support with the request_id. |
Model combos
Model combos are user-owned virtual model ids of the form combo/'<name>' that bundle multiple real models under one fallback strategy. When the first model in the panel fails, the combo automatically falls over to the next one — perfect for keeping free models resilient.
What is a combo?
A combo bundles 1-5 model aliases under a single combo/'<name>' id. Requests to that id are routed to the panel models in order: if the first model fails, the combo immediately tries the next one within the same request. A Redis-backed circuit breaker tracks failures across requests so a known-bad model is skipped before the engine is even called.
How fallback works
When you call combo/'<name>', the gateway tries each panel model one-by-one within the SAME request: target 1 → if it fails (402, 403, 500, timeout) → release billing → target 2 → if it fails → target 3, and so on. The first target that succeeds returns its response. If all targets fail, the combo returns the last error.
Circuit breaker
A circuit breaker prevents the combo from repeatedly hitting a model that is down. After a failure, the circuit for that model opens for a 10-minute cooldown — during which that model is skipped immediately (no engine call wasted). After the cooldown expires, the model is retried. A successful request closes the circuit and resets the counter. Deterministic failures (402 insufficient balance, 403 not entitled) open the circuit immediately; transient failures (500, timeout) require 3 consecutive failures before opening.
Fallback strategy
Panel models are tried top-to-bottom in the order you configure them. Put your most reliable model first and use the others as safety nets. Circuit-open targets are skipped before the engine is called, so a known-down model costs zero latency.
Example request
curl https://router.bynara.web.id/v1/chat/completions \
-H "Authorization: Bearer sk-nry-yourapikey" \
-H "Content-Type: application/json" \
-d '{
"model": "combo/free-pack",
"messages": [{"role": "user", "content": "Hello!"}]
}'Naming rules
The '<name>' part must be 1-19 characters of lowercase letters, numbers, dots, and hyphens — no double punctuation, no underscores, no spaces. The full id combo/'<name>' must be 25 characters or fewer. Malformed names are auto-normalized (e.g. combo/-aneh---.. becomes combo/aneh).
Private to owner
Combos are private to the API key owner who created them. Another user calling your combo/'<name>' id gets a 404 (model not found), not a 403 — so the combo's existence is never leaked. This prevents name conflicts between users.
Managing combos
Create and manage combos in the dashboard under Developer > Model combos. Each combo shows per-model request counts and token usage. The request log displays via combo/'<name>' on each routed row, with the underlying model that actually served the request.
Pricing is in Rupiah, billed per day or per week. Each tier grants a model set, a request rate, and a daily token quota. Tiers load live below.