Developer docs

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.

1. Create an API key

Sign in and open the API keys page to mint a key. The secret is shown once at creation — copy it immediately and store it securely. Keys begin with the prefix sk-nry-.

2. Set the base URL

Send all requests to the gateway base URL. The chat endpoint lives at /v1/chat/completions.

text
https://router.bynara.id/v1

3. 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

http
Authorization: Bearer sk-nry-xxxxxxxxxxxxxxxxxxxxxxxxxxxx

Key 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.

POST/v1/responses

OpenAI Responses API

The stateful Responses API. Send input plus optional tools and receive a structured response object. Compatible with the OpenAI Responses SDK.

POST/v1/messages

Anthropic 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.

POST/v1/chat/completions

OpenAI Chat Completions

The primary OpenAI-compatible chat endpoint. Send a list of messages and receive a model response; supports streaming and reasoning_effort.

POSThttps://api-images.bynara.id/v1/images/generationshttps://api-images.bynara.id/v1/images/edits

Image 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

json
{
  "model": "deepseek-v4-flash",
  "messages": [
    { "role": "system", "content": "You are helpful." },
    { "role": "user", "content": "Hello!" }
  ],
  "temperature": 0.7,
  "max_tokens": 256
}

Response

json
{
  "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.

text
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.

python
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.

EffortThinkingCost & speedUse 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

json
{
  "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.

POSThttps://router.bynara.id/v1/embeddings

POST /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

bash
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"
  }'
python
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

json
{
  "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.

POSThttps://router.bynara.id/v1/rerank

POST /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

bash
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
  }'
python
# 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

json
{
  "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

query

The search query (string).

documents

Array of candidate document strings (the snippets to score).

top_n

Optional. Limit the number of results returned (e.g. 5). When omitted the provider returns all documents ranked.

model

Rerank 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.

POSThttps://api-images.bynara.id/v1/videos

Text-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

ModeHow the image is usedImages
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

bash
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
  }'
bash
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=5
bash
curl --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=5

Response

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

model

Video alias, e.g. happyhorse-1.1-t2v or agnes-video-v2.0.

mode

t2v, i2v, or r2v. i2v requires exactly 1 image; r2v requires 1-3.

prompt

Positive prompt (and optional negative_prompt). Up to 3,500 characters each.

resolution

720p or 1080p (provider default when omitted).

ratio

16: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.

duration

Clip length in seconds, 3-15. Billed per second (price_per_second × duration).

seed

Random seed [0, 2147483647] for reproducibility.

watermark

Provider watermark toggle.

images

Multipart 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

json
{
  "error": {
    "type": "rate_limited",
    "message": "Rate limit exceeded. Please retry later.",
    "request_id": "req_..."
  }
}

Status codes

StatusTypeMeaning
400validation_error

The request was malformed or failed validation (for example, a missing model or messages field).

401unauthorized

Missing or invalid API key. Check the Authorization header.

403forbidden

Authenticated, but your plan does not include the requested model, or the account is suspended.

404not_found

The requested model alias or endpoint does not exist.

413bad_request

The request body or input is too large.

415unsupported_media_type

Content-Type must be application/json.

429rate_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.

503service_unavailable

The model service is temporarily unavailable. Retry with backoff.

500internal_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

bash
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.

Plans & pricing

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.