Documentation/API reference

The router API

Request fields, authentication, responses and error formats for the /v1 API and public model catalog.

Authentication#

Use an active API key from your workspace for the /v1 API. Bearer tokens and the Anthropic X-Api-Key header are accepted. The two /public/models catalog endpoints require no API key.

FieldDescription
AuthorizationBearer YOUR_API_KEY
Content-Typeapplication/json for JSON requests.
X-Api-KeyAlternative API-key header; Authorization takes precedence when both are present.

Chat#

POST/v1/chat/completionsGenerate a chat response+

Send a conversation in the OpenAI-compatible message format. Set stream to true for server-sent events.

FieldTypeDescription
modelstring · requiredExact model ID from the catalog.
messagesarray · requiredConversation messages with role and content, including tool calls and results where supported.
max_tokensintegerMaximum output-token budget.
streambooleanWhether to stream output as SSE.
tools / tool_choicearray / objectClient-side tool definitions and selection.
response_formatobjectStructured-output request where supported by the route.
reasoning_effort / reasoning.effortstringEquivalent effort fields. Equal values are accepted; conflicting values return 400. No effort default or value clamp is added. reasoning: null is treated as omitted.
stopstring / string[] / nullNull or [] means no constraint. A string or nonempty array requires a compatible Chat or Messages route; Responses cannot silently discard it.
min_discount_percentnumberMinimum input and output discount versus verified list price; 0–99.99.
rankingenumbalance (default), speed or discount.
zdrbooleanRequire verified zero-data-retention routes. Adds to the account setting; false cannot override it. No eligible route returns 503.
chat/completionscURL
curl 'https://your-router-domain/v1/chat/completions' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
  "model": "deepseek-v4-flash",
  "messages": [
    {
      "role": "user",
      "content": "Hello!"
    }
  ],
  "max_tokens": 128
}'

Response: OpenAI Chat Completion object with choices, usage and settled billing metadata. Stream ends with [DONE].

Text completions#

POST/v1/completionsComplete a text prompt+

Send one string in prompt, or a single-element string array. The shared gateway applies the same key limits, automatic routing and billing. Responses use text_completion with choices[].text; streaming ends with [DONE]. echo: true includes your prompt once before the output.

completionscURL
curl 'https://your-router-domain/v1/completions' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
  "model": "deepseek-v4-flash",
  "prompt": "Hello!",
  "max_tokens": 128
}'

The output budget defaults to 128 tokens. Batched prompts, token IDs, multiple samples, legacy logprobs and suffix insertion are unsupported. Request history records the underlying Chat protocol.

Responses#

POST/v1/responsesCreate a stateless response+
FieldTypeDescription
modelstring · requiredExact model ID.
inputstring / arrayText or supported conversation items.
storefalse · requiredResponses are stateless. Stored-response access is unsupported.
instructionsstringSystem instructions for this request.
max_output_tokensinteger / nullMaximum output-token budget. Null is treated as omitted; existing budget rules still apply.
reasoning / reasoning_effortobject or null / stringreasoning: null is omitted. Top-level reasoning_effort aliases reasoning.effort; equal values are accepted and conflicts return 400. Responses routes receive nested effort without an added default.
streambooleanStream named Responses API events.
toolsarraySupported client-side function and custom local tools.
min_discount_percentnumberMinimum input and output discount versus verified list price; 0–99.99.
rankingenumbalance (default), speed or discount.
zdrbooleanRequire verified zero-data-retention routes. Adds to the account setting; false cannot override it. No eligible route returns 503.
responsescURL
curl 'https://your-router-domain/v1/responses' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
  "model": "deepseek-v4-flash",
  "input": "Hello!",
  "store": false,
  "max_output_tokens": 128,
  "stream": true
}'
Replay supported text and client-tool input for the next turn. Signed or encrypted thinking, item references, and other opaque history are rejected with 400, including on native routes. previous_response_id, conversations, background execution and provider-hosted tools are not supported.

Response: Responses object; streamed settlement appears in the final response.completed or response.incomplete event.

Messages#

POST/v1/messagesAnthropic-compatible messages+
FieldTypeDescription
modelstring · requiredAn exact catalog model ID.
messagesarray · requiredUser and assistant conversation turns, including client tools and tool results.
max_tokensinteger · requiredMaximum output-token budget.
systemstring / arraySystem instruction content.
streambooleanReturn Anthropic-compatible SSE events.
stop_sequencesstring[]Sequences that should end generation. [] adds no constraint; null is invalid. A nonempty array needs a Chat or Messages route and is never silently dropped for Responses.
thinkingobjectReasoning hints for models with compatible support.
min_discount_percentnumberMinimum input and output discount versus verified list price; 0–99.99.
rankingenumbalance (default), speed or discount.
zdrbooleanRequire verified zero-data-retention routes. Adds to the account setting; false cannot override it. No eligible route returns 503.
messagescURL
curl 'https://your-router-domain/v1/messages' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
  "model": "deepseek-v4-flash",
  "max_tokens": 1024,
  "messages": [
    {
      "role": "user",
      "content": "Hello!"
    }
  ]
}'

Response: Anthropic Message shape with content blocks and usage. The final streamed message_delta contains settlement metadata. A matched stop_sequence is preserved in JSON and SSE; Chat conversions use finish_reason: stop.

Token counting
POST /v1/messages/count_tokens estimates input_tokens locally for text, system instructions and client function tools. An inference-scoped key is required; max_tokens is not. It makes no supplier call and charges nothing. The result is a Unicode heuristic, not an official tokenizer or billing count. The x-ci-token-count-estimated response header is true.

Direct image input#

Chat Completions, Responses and Messages accept raw images in ordinary user messages when a compatible route is available. Check capabilities.vision in GET /v1/models. Each anonymous route on the model detail page separately identifies its input support and price. A lower-priced text-only route is never a fallback for an image request.

FieldDescription
Chat · messages[].content[]Use type: image_url with image_url: {url, detail?} in a user message.
Responses · input[].content[]Use type: input_image with image_url: string and optional detail in a user message. Keep store: false.
Messages · messages[].content[]Use type: image with source: {type: url, url} or source: {type: base64, media_type, data}. Messages has no detail field.
URL / base64Complete HTTP(S) URLs without embedded credentials, or canonical base64 data URLs. Base64 MIME labels: image/png, image/jpeg, image/webp, image/gif. Image bytes and animation are validated by the serving provider.
detail · Chat / Responseslow, high, auto or original; omission uses auto for reservation. Any explicit detail cannot be converted to Messages, which has no equivalent field.
Request limitsThe full UTF-8 JSON body, including base64, is limited to 2 MiB. There is no separate image-count limit; the body limit and conservative input bound both apply.
Image content-part examples · replace the URLJSON
{
  "chat": {
    "type": "image_url",
    "image_url": {
      "url": "https://example.com/image.png",
      "detail": "low"
    }
  },
  "responses": {
    "type": "input_image",
    "image_url": "https://example.com/image.png",
    "detail": "low"
  },
  "messages": {
    "type": "image",
    "source": {
      "type": "url",
      "url": "https://example.com/image.png"
    }
  }
}

Place these parts alongside text in the corresponding user content array. For Messages base64 input, set source.type to base64, media_type to a supported image MIME and data to the canonical base64 string without a data-URL prefix. External image URLs are forwarded to the compatible provider; router does not download them. Temporary Router uploads can also be referenced as described below. Image output and images nested inside tool results or non-user messages are not supported. Additional native image fields cannot be silently converted across protocols.

Image requests skip optional text techniques, exact response caching and automatic model routing. An experiment that changes the request or explicitly enables techniques returns 400 incompatible_image_input; unchanged experiment attribution remains available.

Reservations and unavailable image routes
Image admission uses a conservative input bound: serialized UTF-8 body bytes excluding image URL/base64 string payloads, plus 8,192, plus 308 per low-detail image, 3,000 per high-detail image, or 36,000 per auto/original image. A bound above 272,000 returns 400 image_input_limit. This is a reservation bound, not a tokenizer result; actual supplier usage determines the settled charge. A model without an image-capable route returns 400 unsupported_parameter. Unavailable eligible vision capacity returns 503 image_capacity_unavailable; an unmet explicit discount requirement remains 503 min_discount_unavailable.

Temporary image uploads#

Store a temporary image, then use its returned ID in an ordinary user image block. The limits and handle rules below are Router's contract. This is not the OpenAI or Anthropic Files API, and does not accept general documents, audio or video.

POST/v1/uploadsUpload one image+
FieldTypeDescription
filebinary · requiredExactly one multipart/form-data file, with no other fields. PNG, JPEG, WebP or GIF; matching MIME and image signature; 1 byte to 10 MiB. Complete multipart body at most 10 MiB plus 64 KiB.
Authenticationinference-scoped Bearer or X-Api-Key, or a same-origin signed-in browser session.
Expiry and quotasNew uploads expire after one hour; earlier uploads retain their recorded expiry. 32 stored objects and 64 MiB per account, including pending writes and queued deletions. Rolling limits: 20 admitted uploads per minute and 120 per day, including admitted storage failures. Tombstones remain for 24 hours after physical deletion; deleting does not immediately free storage or reset rate counters.
Upload an imagecURL
curl "$ROUTER_BASE_URL/v1/uploads" \
  -H "Authorization: Bearer $ROUTER_API_KEY" \
  -F "file=@image.png;type=image/png"

Set ROUTER_BASE_URL to this deployment's origin without /v1. HTTP 201 returns id, bytes, content_type and expires_at. The ID is rtr_upload_ followed by 32 lowercase hexadecimal characters. Uploading does not call a model. The one-hour lifetime follows the reference's public DPA statement; this does not establish how its storage behaves at runtime.

Use your upload ID · user content partsJSON
{
  "chat": {
    "type": "image_url",
    "image_url": {
      "url": "router-upload://rtr_upload_0123456789abcdef0123456789abcdef"
    }
  },
  "responses": {
    "type": "input_image",
    "file_id": "rtr_upload_0123456789abcdef0123456789abcdef"
  },
  "messages": {
    "type": "image",
    "source": {
      "type": "file",
      "file_id": "rtr_upload_0123456789abcdef0123456789abcdef"
    }
  }
}

Replace the example ID with your upload's id. Responses and Messages use their standard image-file field shapes; the router-upload:// Chat URL is a Router extension. IDs belong to this account and are resolved locally before forwarding. Vendor file IDs are not accepted. Choose one source per image; Responses permits image_url: null alongside its file_id. Unsupported extra fields on upload blocks are rejected. Existing image capacity, price, detail and input-budget rules still apply; a stored image does not enable a text-only route. The complete inference JSON body remains limited to 2 MiB.

DELETE/v1/uploads/{id}Delete your upload+

Requires the same authentication as upload. Returns HTTP 204 without a body; invalid, expired, deleted and foreign IDs return 404. Deletion or expiry blocks new reads, while physical cleanup runs asynchronously. Bytes already downloaded by a provider cannot be recalled.

FieldDescription
400 invalid_uploadMalformed multipart body or anything other than one file field.
413 upload_too_largeEmpty file, file above 10 MiB, or multipart envelope above its bounded limit.
415 invalid_content_type / unsupported_file_typeWrong request content type, unrecognized image signature, or MIME mismatch.
429 upload_rate_limit / upload_quota_exceededRolling upload admission or retained-storage quota exceeded.
503 uploads_unavailableStorage or signing configuration is missing or temporarily unavailable.

Account data protection#

GET/api/data-protectionRead the account restriction+

Requires an account:read key or a signed-in session. Returns zero_data_retention, updated_at and verified_zdr_capacity: false.

PATCH/api/data-protectionSet the account restriction+

Only a same-origin signed-in browser session may change this setting. Send {"zero_data_retention": true} or false. API keys receive 403 session_required; malformed settings receive 400 invalid_data_protection. Manage it in the data protection settings.

No verified ZDR capacity
Enabling the account restriction makes new inference return 503 zdr_capacity_unavailable, even with request zdr: false. It blocks new uploads and uploaded-image access with 403 data_protection_restriction, clears response-cache entries and schedules existing uploads for deletion. It does not erase saved separate experiment prompts or pre-existing browser drafts. It cancels Prompt Studio runs and clears their saved prompts, answers and results; billing records remain. It cannot recall requests already sent to a provider. Background cleanup is not an immediate physical-erasure guarantee.

Models#

GET/public/modelsBrowse the catalog without an API key+

Returns a models array of visible models with price-eligible configured supply. This does not guarantee credentials, health or capacity at inference time. Prices are decimal USD-per-million strings. No account data or internal serving-supplier identities are included.

FieldTypeDescription
zdrboolean · optionalOnly the literal query value true enables the filter. No verified ZDR capacity is configured, so zdr=true returns an empty models array.
discount_percentstringA signed percentage; unknown discount is represented as 0.00 on this compatibility endpoint. Null reference prices identify the unknown case.
supports_reasoningboolean · optionalOmitted together with reasoning_capability_mode when the capability is unverified. Absence is not false.
Public catalog · no credentials
GET /public/models
GET /public/models?zdr=true
Unknown reference prices remain unknown
The 0.00 compatibility value is not a verified zero discount when reference input/output prices are null. The existing UI endpoint /api/public/models keeps unknown discount as null and also includes display fields and anonymous route details.
GET/public/models/{model_id}Read one public model without an API key+

Use an exact, case-sensitive canonical ID or a listed alias. Aliases with a slash are supported, and the response contains the canonical ID. IDs are not lowercased or trimmed. The optional zdr=true filter also applies here.

Public model lookup
GET /public/models/deepseek-v4-flash

Success returns one model object. Unknown, hidden, unavailable or ZDR-excluded models return HTTP 404 with {"detail":"The model is not currently available."}. A catalog failure returns HTTP 503 with {"detail":"The catalog is temporarily unavailable."}. Both public endpoints use Cache-Control: public, max-age=0, must-revalidate.

GET/v1/modelsList available models+

Read model identifiers, current catalog prices, aliases, capability flags and context limits. Requires an API key.

modelscURL
curl 'https://your-router-domain/v1/models' \
  -H 'Authorization: Bearer YOUR_API_KEY'
GET/v1/models/compareEstimate a workload across models+

Requires usage:read. This read-only calculation uses configured customer catalog prices. It does not run inference or reserve supply.

FieldTypeDescription
modelsstring · requiredOne to ten comma-separated model IDs or aliases. Unknown or disallowed models return found: false.
prompt_tokens / completion_tokensintegerTokens per request, each 0–10,000,000. Default 0; estimates assume uncached input.
requestsintegerNumber of identical requests. Default 1. Unsupported totals return 400.
models/compare?models=deepseek-v4-flash&prompt_tokens=1000&completion_tokens=250&requests=100cURL
curl 'https://your-router-domain/v1/models/compare?models=deepseek-v4-flash&prompt_tokens=1000&completion_tokens=250&requests=100' \
  -H 'Authorization: Bearer YOUR_API_KEY'

Returns object: model.comparison, currency: USD, and data sorted by estimated billed_usd. Each found model includes input_per_m, output_per_m, list_usd, saved_usd and anonymous suppliers. Provider identifies the model maker; supplier entries show only Provider numbers and customer rates. List and savings estimates are null without a verified reference.

An estimate, not a settled bill
Each request is rounded before multiplication. Actual route and token usage determine the bill. Source is configured_catalog and pricing_checked_at is null; no live supplier price check or availability guarantee is claimed.
GET/v1/models/supplyCheck supply under a price ceiling+
FieldTypeDescription
modelstring · requiredThe model to check.
min_discount_percentnumberThe required minimum discount from list.
zdrbooleanRestrict to zero-retention sources.
models/supply?model=deepseek-v4-flash&min_discount_percent=30cURL
curl 'https://your-router-domain/v1/models/supply?model=deepseek-v4-flash&min_discount_percent=30' \
  -H 'Authorization: Bearer YOUR_API_KEY'

Candidate counts describe current pricing. They are not a reservation, a health check or a guarantee of fulfillment.

Price changes#

GET/v1/pricing/changesRead changes to the configured catalog+

Requires usage:read. Returns the latest change for each allowed model at or after since, including that timestamp. Initial observations are updated entries with null previous prices. Removed entries have a null model and null current price fields.

FieldTypeDescription
sinceRFC 3339 · requiredInclusive timestamp with an explicit timezone, such as 2026-09-06T00:00:00Z.
limitinteger1–100 records per page. Default 100.
cursorstringOpaque next_cursor from the previous page. Omit to begin a new query.
pricing/changes?since=2026-09-06T00%3A00%3A00Z&limit=100cURL
curl 'https://your-router-domain/v1/pricing/changes?since=2026-09-06T00%3A00%3A00Z&limit=100' \
  -H 'Authorization: Bearer YOUR_API_KEY'

The response includes data, has_more, next_cursor, pricing_version and pricing_updated_at. Entries contain model_id, change_type, changed_at, model, previous_pricing and current_pricing. Decimal prices are strings in USD per million tokens. This endpoint gives the latest change per model, rather than every intermediate observation.

Consistent pagination
A cursor freezes the catalog version from the first page. Reuse the same since and credential/model-policy context; mismatches return 400. Start without a cursor to see newer changes. The version is a hash of the allowed catalog. Source is configured_catalog, pricing_checked_at is null, and pricing_observed_at records this observation time. Responses are not cached.

Usage#

GET/v1/usage/requestsRead request history+

Requires usage:read. Read model, request status, token usage, settled ledger cost and latency. Supplier identities stay private. Requests still in flight have status pending and no settled amount.

FieldTypeDescription
start_at / end_attimestampInclusive start, exclusive end. Offsetless timestamps are UTC; use explicit offsets for local dates.
api_key_idUUIDOnly this account’s key, including revoked keys. An unknown or foreign key returns 404.
limitinteger1–100 records, default 100. Records are newest first.
cursorstringUse next_cursor from the preceding page with the same account, date bounds and key filter. Newer requests do not enter that pagination window.
usage/requests?limit=25cURL
curl 'https://your-router-domain/v1/usage/requests?limit=25' \
  -H 'Authorization: Bearer YOUR_API_KEY'

New requests retain the published standard input/output token prices at admission. Settled requests compare those prices with the actual ledger charge. This baseline does not estimate cache savings or alternative models. Historical requests without a recorded price return null for provider_list_price_usd and savings_usd. A negative saving is preserved.

GET/v1/usage/dailyRead daily usage+

Requires usage:read. Returns a usage.daily summary and daily_spend rows, filling empty days with zeros. Failed and pending requests are counted separately; only settled ledger entries contribute to spend and billed tokens.

FieldTypeDescription
start_at / end_attimestampInclusive start and exclusive end, with a maximum duration of 366 days. Defaults to the most recent 30 calendar days, including today.
api_key_idUUIDFilter all counts and amounts to one key in this account.
timezone_offset_minutesintegerFixed minutes east of UTC, from −840 to 840; default 0. For example, 480 groups by UTC+8 calendar days.
start / enddate · legacyInclusive UTC YYYY-MM-DD dates. Cannot be mixed with timestamp bounds or a nonzero timezone offset.
usage/daily?timezone_offset_minutes=480cURL
curl 'https://your-router-domain/v1/usage/daily?timezone_offset_minutes=480' \
  -H 'Authorization: Bearer YOUR_API_KEY'
Read cache coverage with the rate
cache_hit_pct measures cached prompt tokens divided by prompt tokens, using only settled supplier requests with proven cache-read reporting. An explicit zero is a miss and remains in the denominator; missing reporting and historical zero without that evidence remain unknown. Cache writes are reported independently and cannot prove a read miss. Platform exact-cache hits are billed at zero and excluded from supplier cache percentages. Their cache_reporting_state is not_applicable; unsettled requests have a null state.

Account and balance#

GET/v1/accountRead your account+

Requires account:read. Returns object: account, email, role, active_key_count, last_key_use_at and enabled_technique_count. Active keys are unrevoked and unexpired. A workspace key has a fixed organization_id; sessions can select it with the organization_id query parameter. Workspace reads identify the responsible member by email and platform role, and include workspace.id, workspace.name and workspace.role (owner or member). workspace_name is the actual name; funded_by_owner is true when the member differs from the wallet owner. Personal reads omit workspace metadata.

accountcURL
curl 'https://your-router-domain/v1/account' \
  -H 'Authorization: Bearer YOUR_API_KEY'
GET/v1/account/balanceRead wallet and reservation amounts+

Requires account:read. Returns object: account.balance, currency: USD, and numeric balance_usd, available_usd and reserved_usd from one balance snapshot. Workspace reads show the owner-funded wallet; auto-recharge is false and threshold_usd, recharge_amount_usd and card are null, keeping the owner's personal payment configuration private. Personal reads return the account's actual saved-card and auto-recharge state. Responses are not cached.

account/balancecURL
curl 'https://your-router-domain/v1/account/balance' \
  -H 'Authorization: Bearer YOUR_API_KEY'
GET/v1/account/usageRead account usage and recorded savings+

Requires account:read. The days parameter is an integer from 1 to 90, defaulting to 7. The response covers that many trailing 24-hour periods and includes request counts, settled spend and the five models with the highest spend. window_start is inclusive; window_end is exclusive. USD values are numbers, with exact integer values in amounts_nanos.

Savings use the published standard token prices recorded with each request. If any settled request lacks a price, list_usd, saved_usd and savings_percent are null; priced_request_count and savings_status describe that coverage. Savings can be negative.

account/usage?days=7cURL
curl 'https://your-router-domain/v1/account/usage?days=7' \
  -H 'Authorization: Bearer YOUR_API_KEY'
GET/v1/account/savingsRead savings windows and optimization settings+

Requires account:read. Returns 1-day, 7-day and 30-day usage windows with a shared end time, current techniques and up to 100 experiments. experiments_has_more indicates additional experiments. Experiment avg_saved remains null: a price comparison does not establish savings caused by a technique or experiment.

account/savingscURL
curl 'https://your-router-domain/v1/account/savings' \
  -H 'Authorization: Bearer YOUR_API_KEY'

Report channels#

Choose Email, Slack, Discord or Telegram in Reports. Connections belong to your personal account, and summaries exclude Router team activity. Explicitly connect a destination before enabling a schedule. An unavailable platform cannot start a connection.

GET/v1/channelsList connected channels+

Requires account:read or a browser session. Returns object: list, data and available_events. available_events contains the nine supported event definitions as id, name and description; events contains each channel’s saved selection. Reading options does not subscribe a channel. A channel includes its id, platform, workspace_name, integration_active, report_frequency and report_hour in UTC. workspace_name labels the external destination, not a Router organization. integration_active reports configuration eligibility; Email also requires the matching verified address. This does not prove delivery.

channelscURL
curl 'https://your-router-domain/v1/channels' \
  -H 'Authorization: Bearer YOUR_API_KEY'
GET/v1/channels/{channel_id}Read one channel+

Requires account:read or a browser session. Unknown, foreign or disconnected channels return 404.

PATCH/v1/channels/{channel_id}Change frequency or event preferences+

Requires account:write or a same-origin browser session. Omitted fields stay unchanged. At least one field is required; an empty object is invalid. Recipient addresses and report hours cannot be set here.

FieldTypeDescription
report_frequencystring · optionaldaily, weekly, monthly or off. Off disables scheduled digests; a manual report can still summarize the previous day.
eventsarray · optionalChoose unique IDs from available_events. [] disables optional events. Unknown or duplicate IDs reject the whole update. This is independent of report_frequency; Off affects only scheduled digests.
Change frequencyJSON
{"report_frequency":"weekly","events":[]}
POST/v1/channels/{channel_id}/send-reportSend a report now+

Requires account:write or a same-origin browser session. This is an explicit message action. No request body is needed. It summarizes the previous complete UTC day, Monday-based week or calendar month; off uses the previous day. Sending is limited to once per channel per minute and 20 times per UTC day, including admitted no-activity results and failed delivery attempts. Reconnecting does not reset these limits. Manual sending does not consume the next scheduled digest.

Returns object: channel.report, sent, cadence and window_label. A successful no-activity window returns sent: false with a message. Missing configuration returns 503. Provider acceptance is not proof of delivery to an inbox or that a person read the message. A limit returns 429 rate_limited with Retry-After; delivery retries return 503 report_pending. Terminal failures also return 503. An unknown acknowledgement is not automatically resent.

Schedule and connection scope
Scheduled digests become due at 00:05 UTC for the previous complete period; the five-minute scheduler is not an exact delivery-time guarantee. Summaries use actual account activity and settled spending, without prompt or response bodies. Connecting does not send an immediate report or subscribe to events. Save event preferences explicitly in Details; account and security messages are separate.
GET / POST/api/channels/emailPersonal email connection+

Router extension; browser session required. GET returns available, email_verified, email and channel. A same-origin POST requires consent: true and report_frequency; the destination is the account’s current verified email. No arbitrary to address is accepted. An unverified address returns 403 email_verification_required, and missing mail configuration returns 503 email_unavailable. First connection returns 201 with a Channel; an already connected identical configuration returns 200. API keys cannot connect an email channel.

DELETE/api/channels/email/{id}Disconnect in the workspace+

Same-origin browser session required. Returns object: channel.disconnected and id. Disconnecting stops future eligible submissions; already submitted email cannot be recalled. There is no public /v1 channel-create or channel-delete operation.

GET / POST/api/channels/{platform}Connect Slack, Discord or Telegram+

This browser-only extension accepts slack, discord or telegram. GET returns available, channel and optional pending state. available means that connection configuration is present; a saved channel independently reports sending eligibility through integration_active. POST requires consent: true and return_to set to /dashboard/reports or /dashboard/notifications. It returns url, flow_id and expires_at.

Open the returned Slack or Discord authorization URL to select a destination. For Telegram, open the Bot link, press Start, then read connection status again. Confirm a ready recipient with POST /api/channels/telegram/confirm using flow_id and consent: true. Confirmation requires the same unexpired Router session that began the connection. Clients cannot submit a chat ID or webhook URL.

Connections start with the schedule Off and never send a test message. Use DELETE /api/channels/{platform}/{id} from the signed-in browser to disconnect. This cancels unsent work and clears the local credential; it does not delete a remote webhook or message.

Wallet top-up#

POST/v1/billing/topupCreate a Stripe-hosted checkout+

Requires a valid API key with account:write, sent as a Bearer token or X-Api-Key. A browser session alone is insufficient. The customer completes payment on Stripe; this request does not charge or credit the wallet.

FieldTypeDescription
amount_usdnumber · requiredUSD 5–1000 inclusive, with at most two decimal places. Excess precision is rejected without rounding.
Idempotency-Keyheader · optional16–100 letters, digits, underscores or hyphens. Reuse for retries of the same top-up and amount. Without it, each HTTP request creates a fresh checkout.

A successful response has object: billing.topup, amount_usd, checkout_url, session_id, min_usd: 5 and max_usd: 1000. checkout_url is the Stripe-hosted link. Success and cancel return URLs are set by the server; custom URLs are not supported.

cURL · reuse the key only for this top-upcURL
curl "$ROUTER_BASE_URL/billing/topup" \
  -H "Authorization: Bearer $ROUTER_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: topup-example-0001' \
  -d '{"amount_usd":25.50}'

Set ROUTER_BASE_URL to your API base URL ending in /v1. Replace the example idempotency key for each new top-up intent.

Retries and payment availability
Replaying a credited purchase returns its original persisted Stripe link and session, without creating another charge. Use a new key to top up again. If the completed purchase has no persisted link/session, the API returns 409 checkout_completed. An expired checkout or changed amount for the same key also returns 409. Until payment configuration is present, the endpoint returns 503 payments_unavailable.

API key management#

GET/v1/keysRead key prefixes and policies+

Requires account:read. Returns object: list with data entries; use include_revoked=true to include revoked keys. Secrets and hashes are never returned.

keyscURL
curl 'https://your-router-domain/v1/keys' \
  -H 'Authorization: Bearer YOUR_API_KEY'
POST/v1/keysCreate a scoped API key+

Requires account:write. Returns object: api_key and a one-time key secret. A key caller can grant only its own scope subset and must have no policy restrictions. A signed-in session can create an independently restricted key.

FieldDescription
nameRequired, 1–64 characters.
scopesinference, usage:read, account:read, account:write. Defaults to inference and usage:read.
allowed_models / allowed_ip_cidrsArrays of allowed models or IPv4/IPv6 addresses/CIDRs. [] means no restriction; null is invalid.
expires_atFuture ISO timestamp, or null for no expiry.
rate_limit_per_minute / daily_request_limit / concurrency_limitPositive integers or null for unlimited.
monthly_spend_limit_usd0–1,000,000 USD, at most 9 decimal places; null unlimited, zero blocks positive-cost inference.
DELETE/v1/keys/{key_id}Revoke an owned key+

Requires account:write. Self-revocation is allowed. Subsequent authentication with the revoked key fails.

PATCH/api/keys/{id}Edit a policy in the signed-in workspace+

Browser session and same-origin request required. Only included fields change; null clears a numeric limit or expiry, [] clears model/IP restrictions. Returns key metadata and policy without a secret.

Routing controls#

These fields belong to router. They are validated and removed before the request reaches a provider.

FieldTypeDescription
min_discount_percentnumberMinimum input and output discount versus verified list price; 0–99.99.
rankingenumbalance (default), speed or discount.
zdrbooleanRequire verified zero-data-retention routes. Adds to the account setting; false cannot override it. No eligible route returns 503.

Price and privacy determine eligibility. Ranking determines the order of eligible routes. The model ID never selects a private provider.

Bounded retries
Each eligible route gets at most two physical attempts: the initial attempt and one retry for qualifying transport failures before downstream output or HTTP 404/408/409/425/429/5xx. All attempts share one reservation and the original 180-second deadline. A valid Retry-After is honored in full only if it fits that attempt's remaining budget; otherwise that retry is skipped. A rejected underlying JSON-body read or SSE prebuffer read, or supplier timeout during those reads, can retry even after response headers arrive. Parsing, decoding, invalid stream frames, usage-validation and database failures remain excluded. No retry occurs after a streaming response is returned, settlement begins or the client cancels. Only a valid final success is charged once.

Errors#

FieldDescription
400Invalid input, routing value or protocol feature.
401 / 403Invalid/expired credentials, forbidden scope/model/IP, or unauthorized origin.
402Insufficient balance.
404Unknown model or inaccessible record.
429Rate, concurrency, daily quota, or key monthly budget restriction.
502 / 503 / 504Upstream failure, no eligible capacity, or timeout.

Failed and unsettled requests do not expose a settled billed amount. Retain the request ID when reporting an issue.