# Hyperouter > Hyperouter is an OpenAI-compatible router for language, image and video models. One HTTP API and one prepaid USD balance give access to models from several providers. Each request goes to a healthy provider and is charged at that provider's list price, with no markup. The API base URL is https://api.hyperouter.app/v1. An MCP server for the dashboard is at https://api.hyperouter.app/mcp. --- Source: https://hyperouter.app/docs.md # Hyperouter API {#introduction} *Version v1 · Updated October 25, 2026* > These docs are available as Markdown for SI assistants. Start at [/llms.txt](https://hyperouter.app/llms.txt), or fetch [/llms-full.txt](https://hyperouter.app/llms-full.txt) for every page in one file. This page is [/docs.md](https://hyperouter.app/docs.md). Hyperouter is one HTTP API in front of many model vendors. You send a request that names a model. Hyperouter picks a provider that can serve it, forwards the request, and charges your balance at that provider’s list price. `BASE https://api.hyperouter.app/v1` Every endpoint follows OpenAI’s request and response shapes. The OpenAI SDKs work when you change the base URL and the API key. Hyperouter adds a few fields to OpenAI’s bodies. They are marked as additions in this reference. | Endpoint | Purpose | Auth | | --- | --- | --- | | `GET /v1/models` | List the models your account can call. | API key | | `POST /v1/chat/completions` | Chat completion, with optional streaming. | API key | | `POST /v1/images/generations` | Text to image. | API key | | `POST /v1/images/edits` | Edit pictures. | API key | | `POST /v1/videos` | Create a video from a prompt, with an optional picture. | API key | | `POST /v1/videos/edits` | Edit or upscale a video. | API key | | `POST /v1/videos/extensions` | Extend a video. | API key | | `POST /v1/videos/{id}/remix` | Make a new version of a video with a new prompt. | API key | | `GET /v1/videos`, `GET /v1/videos/{id}`, `GET /v1/videos/{id}/content`, `DELETE /v1/videos/{id}` | List, read, download and delete videos. | API key | | `GET /v1/generations/{id}` | Read the record of any request: status, usage and charge. | API key | | `GET /v1/credits` | Your balance, what open requests hold, and what is left to spend (`balance_usd`, `held_usd`, `available_usd`, decimal USD strings). | API key | > [Sign in at dash.hyperouter.app](https://dash.hyperouter.app) with Google or GitHub to create an API key. `GET /v1/models` returns the models you can call at the time of the request. ## Quickstart 1. [Sign in](https://dash.hyperouter.app) and create an API key. Keys start with `sk-hr-v1-` and are shown once. 2. Store it in an environment variable, for example `HYPEROUTER_API_KEY`. 3. Add credits: open a top-up request in the dashboard. Staff credit your balance when the payment is confirmed. 4. Point any OpenAI SDK at the base URL above and pick a model. ```bash curl https://api.hyperouter.app/v1/chat/completions \ -H "Authorization: Bearer $HYPEROUTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "x-ai/grok-4.7", "messages": [{"role": "user", "content": "Hello"}] }' ``` ```typescript import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.hyperouter.app/v1", apiKey: process.env.HYPEROUTER_API_KEY, }); const res = await client.chat.completions.create({ model: "x-ai/grok-4.7", messages: [{ role: "user", content: "Hello" }], }); console.log(res.choices[0].message.content); ``` ```python import os from openai import OpenAI client = OpenAI( base_url="https://api.hyperouter.app/v1", api_key=os.environ["HYPEROUTER_API_KEY"], ) res = client.chat.completions.create( model="x-ai/grok-4.7", messages=[{"role": "user", "content": "Hello"}], ) print(res.choices[0].message.content) ``` ## Authentication Send your API key as a bearer token on every request. ``` Authorization: Bearer sk-hr-v1-... ``` Keys carry access to your balance. Keep them on your server. Never put them in browser or mobile code. Hyperouter stores only a hash of each key, so a lost key cannot be shown again: create a new one. In the [dashboard](https://dash.hyperouter.app/keys) you can name each key, give it a credit limit, and disable it. A disabled key stops working at once. A request that would take a key past its limit is refused with `402 insufficient_credits`. ### Authentication errors | Status | type | code | When | message | | --- | --- | --- | --- | --- | | `401` | `authentication_error` | `invalid_api_key` | The header is missing, is not `Bearer`, or the key does not start with `sk-hr-v1-`. | `The API key is missing or malformed. Send Authorization: Bearer sk-hr-v1-…` | | `401` | `authentication_error` | `invalid_api_key` | The key is unknown or disabled. | `The API key is not valid.` | | `403` | `permission_error` | `account_suspended` | The account is suspended. The check runs on every request. | `This account is suspended. Open a support ticket.` | | `429` | `rate_limit_error` | `too_many_auth_failures` | More than 30 requests from your address in a minute used a missing or invalid key. Every `/v1` request from the address is refused for 15 minutes, a valid key included. `Retry-After` gives the seconds left. | `Too many failed API key checks from this address. Try again after the time in Retry-After.` | | `500` | `server_error` | `internal` | Hyperouter could not check the key. | `An internal error occurred. Try again.` | ```bash curl https://api.hyperouter.app/v1/generations/0194f2c4-7a1e-7b3c-9d02-5f6a1c0e8b21 \ -H "Authorization: Bearer $HYPEROUTER_API_KEY" ``` ```json { "error": { "message": "The API key is not valid.", "type": "authentication_error", "code": "invalid_api_key", "param": null } } ``` ## Errors Every error uses OpenAI’s error object and a matching HTTP status. ```json { "error": { "message": "n must be an integer from 1 to 128", "type": "invalid_request_error", "code": "invalid_body", "param": "n" } } ``` | Field | Type | Description | | --- | --- | --- | | `message` | string | A sentence that says what is wrong. Do not parse it. | | `type` | string | The class of error. See the list below. | | `code` | string | A stable identifier for the cause. Match on this field. | | `param` | string \| null | The request field at fault, or `null`. | ### Types and statuses | Status | type | Meaning | | --- | --- | --- | | `400` | `invalid_request_error` | The request is invalid. `code` names the cause and `param` the field. | | `401` | `authentication_error` | The API key is missing or not valid. | | `402` | `insufficient_credits` | Your balance, or the key’s limit, cannot cover the most the request could cost. | | `403` | `permission_error` | The account is suspended. | | `404` | `not_found` | No such id, the id belongs to another account, or a video has passed its `expires_at`. | | `409` | `conflict` | The object is not in a state that allows the call (`video_not_finished`), or an `Idempotency-Key` conflict (see [Retries and idempotency](#idempotency)). | | `422` | `invalid_request_error` | The request was valid but its answer was refused (`expansion_refused`). Not billed. | | `429` | `rate_limit_error` | Every provider for the model is at capacity, your account is over its [rate limits](#rate-limits), or too many API key checks from your address failed. Retry after the time in the `Retry-After` header. | | `500`, `501` | `server_error` | Hyperouter failed, or the endpoint is not implemented. Not billed. | | `503` | `upstream_error` | A provider failed or did not answer in time: `code` says which (`all_venues_failed`, `upstream_error` or `timeout`). The header `X-Upstream-Status` carries `502` (failed) or `504` (timed out). | Hyperouter never answers `502` or `504`: those would reach you as a bare page from our edge network, without the error body. Where a gateway would answer them, you get `503` with the body below and the gateway status in `X-Upstream-Status`. ### Codes you can get | code | Status | When | | --- | --- | --- | | `invalid_body` | 400 | A body field or query parameter does not parse, has the wrong type, or is outside its allowed values. `param` names it. | | `unsupported_content_type` | 400 | `/v1/images/generations` got a `multipart/form-data` body. Send JSON. `param` is `null`. | | `request_too_large` | 413 | A `multipart/form-data` upload is over 50 MiB. Send large media as https URLs. | | `unknown_model` | 400 | The model does not exist for this endpoint, or no provider can serve it with your `provider` preferences. | | `unsupported_parameter` | 400 | A parameter that exists for other models is not available on this model. See [Model-specific parameters](#parameters). | | `invalid_combination` | 400 | The vendor does not allow these parameters together, for example reference media with keyframes. | | `invalid_size` | 400 | `size` is not `WIDTHxHEIGHT`, or is not allowed for the model. | | `too_many_images` | 400 | `n` or the number of input pictures is over the limit. | | `invalid_image`, `invalid_video` | 400 | A picture or video URL is not https to a public host: a `data:` URL, an `http://` URL, an IP address or an internal name. `param` names the field. | | `image_required` | 400 | The model needs an input picture and you sent none (or it only edits, and you called generations). | | `invalid_seconds` | 400 | `seconds` is not allowed for the model, or is missing for a video given by URL. | | `unsupported_operation` | 400 | The model does not support the operation (edit, extend or remix, or picture editing). | | `video_not_ready` | 400 | The source video has not completed. | | `context_length_exceeded` | 400 | The chat prompt and `max_tokens` do not fit in the model’s context. | | `content_rejected` | 400 | The vendor’s safety filter refused the input. | | `vendor_rejected` | 400 | The model refused the request as invalid. Check its parameters and inputs. | | `clip_too_long` | 400 | A reference clip plus the requested `seconds` run past the model’s limit (30 s). Not billed. | | `prompt_rejected` | 400 | The prompt asks the prompt expansion model for its instructions. Not billed. See [Prompt expansion](#prompt-expansion-model). | | `expansion_refused` | 422 | The prompt expansion model's rewrite was refused or came back empty. Not billed. See [Prompt expansion](#prompt-expansion-model). | | `invalid_api_key` | 401 | See [Authentication](#authentication). | | `insufficient_credits` | 402 | See [Billing & limits](#billing). | | `account_suspended` | 403 | The account is suspended. | | `moderation_not_enabled` | 403 | `moderation: "low"` is not enabled for the account. See [Moderation](#moderation). | | `feature_not_enabled` | 403 | `prompt_expand` is not enabled for the account, on a model that offers it. Open a support ticket to request access. Nothing is held or sent to the provider. See [Prompt expansion](#prompt-expansion). | | `model_not_enabled` | 403 | The model needs access granted to your account. Open a support ticket to request it. Nothing is held or sent to the provider. | | `not_found` | 404 | No such object. | | `video_not_finished` | 409 | You deleted a video that is still being made. | | `venue_busy` | 429 | Every provider for the chat model is over its rate budget. | | `rate_limited` | 429 | Your account is over its requests per minute or its concurrent requests. See [Rate limits](#rate-limits). | | `too_many_auth_failures` | 429 | More than 30 requests from your address in a minute used an invalid API key. See [Rate limits](#rate-limits). | | `all_venues_failed` | 503 | No provider is healthy, or all of them failed. | | `upstream_error` | 503 | An image or video job failed at the model. You are not charged. | | `internal`, `server_error`, `no_price` | 500 | Hyperouter failed (`no_price`: the model cannot be priced right now). Not billed. | | `not_implemented` | 501 | `This endpoint is not implemented yet.` | | `invalid_idempotency_key` | 400 | The `Idempotency-Key` header is longer than 255 characters or is not printable ASCII. See [Retries and idempotency](#idempotency). | | `idempotency_key_reused` | 409 | The `Idempotency-Key` was already used with a different request. | | `idempotency_key_in_use` | 409 | A request with this `Idempotency-Key` is still being started. Retry with the same key. | | `idempotency_key_replayed` | 409 | A chat completion with this `Idempotency-Key` already ran. `error.generation_id` is its id. | | `timeout` | 503 | A sync image was not ready within 90 seconds. It is still being made: `error.generation_id` is the id to poll. | ## Retries and idempotency {#idempotency} A network error or a timeout does not tell you whether Hyperouter received the request. If you send it again, you may start a second job and pay twice. An `Idempotency-Key` makes a repeat safe: Hyperouter recognises the repeat, starts no second job, places no second hold, and answers from the first request. **Send one on every request that costs money.** Generate a new random value, such as a UUID v4, for each new request. Keep it with the request, and send the same value every time you send that request again. ### Sending a key The header is accepted on these endpoints: | Endpoint | A repeat returns | | --- | --- | | `POST /v1/images/generations`, `POST /v1/images/edits` | the first request's image, or its `202` with the same `id` under `Prefer: respond-async` | | `POST /v1/videos`, `/v1/videos/edits`, `/v1/videos/extensions`, `/v1/videos/{id}/remix` | the first request's Video object, as it is now | | `POST /v1/chat/completions` | `409` `idempotency_key_replayed` with the first request's id (see [Chat completions](#idempotency-chat)) | The OpenAI SDKs do not send this header on their own. Pass it as an extra header; the SDKs then re-send it unchanged on their automatic retries. ```bash curl https://api.hyperouter.app/v1/images/generations \ -H "Authorization: Bearer $HYPEROUTER_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7" \ -d '{"model": "x-ai/grok-imagine-image", "prompt": "A lighthouse at dusk", "n": 1}' ``` ```python import uuid key = str(uuid.uuid4()) # one per request; reuse it when you retry this request img = client.images.generate( model="x-ai/grok-imagine-image", prompt="A lighthouse at dusk", extra_headers={"Idempotency-Key": key}, ) ``` ```typescript import { randomUUID } from "node:crypto"; const key = randomUUID(); // one per request; reuse it when you retry this request const img = await client.images.generate( { model: "x-ai/grok-imagine-image", prompt: "A lighthouse at dusk" }, { headers: { "Idempotency-Key": key } }, ); ``` ### What a repeat returns | You send | Hyperouter answers | | --- | --- | | A new key | Runs the request as usual. | | The same key and the same request, after the first was accepted | The first request's result as in the table above. No second job, no second hold, no second charge. | | The same key and the same request, while the first is still being accepted | Waits for the first, then answers as above. If the first has not started its job within 60 seconds: `409` `idempotency_key_in_use`. Retry with the same key. | | The same key and a different request | `409` `idempotency_key_reused`. Nothing runs. Use a new key. | | The same key, after the first was refused before any job started (a `400`, a `402`, or a `5xx` sent without `x-should-retry: false`) | Runs the request as new, so you can correct it and send it again with the same key. | | The same key, more than 30 minutes after the first request | Runs the request as new. | | A key longer than 255 characters, or not printable ASCII | `400` `invalid_idempotency_key`. Nothing runs. | ### Rules - **Scope.** A key belongs to the API key that sent it. Two API keys can use the same value without affecting each other. - **Window.** A key is honoured for 30 minutes after the first request. Retry within that time. - **Same request.** Two requests are the same when the method, the path and the body match. JSON is compared after parsing, so field order and whitespace do not matter. A picture or video is a URL in the body, so the URL is compared, never the bytes it serves. - **Billing.** A request made under a key is billed at most once, whatever the number of repeats. A repeat answered from the first request costs nothing. - **Guarantee.** One key never starts more than one job, including when two repeats arrive at the same time. ### Chat completions {#idempotency-chat} Chat replies are not replayed. A repeat with the same key answers `409` with code `idempotency_key_replayed`, and `error.generation_id` is the first request's id. The request is not run or billed again. Look the first request up with [GET /v1/generations/{id}](#generations) to see its status, usage and cost. If the first chat completion failed and was not billed, its key is freed, and a repeat runs as a new request. ```json { "error": { "message": "A request with this Idempotency-Key already ran as generation 019a1f3c-5b7e-7d21-9c4a-2e8f6b0d3a17. Chat replies are not replayed: GET /v1/generations/019a1f3c-5b7e-7d21-9c4a-2e8f6b0d3a17 for its record.", "type": "conflict", "code": "idempotency_key_replayed", "param": null, "generation_id": "019a1f3c-5b7e-7d21-9c4a-2e8f6b0d3a17" } } ``` ### Automatic retries in the OpenAI SDKs The OpenAI SDKs retry a `408`, `409`, `429` or `5xx` answer twice by default, unless the answer carries the response header `x-should-retry: false`. Hyperouter sends `x-should-retry: false` on every answer where a retry would start a second job or a second charge: - the `503` `timeout` of a sync image, whose job goes on and is billed when it succeeds; poll [GET /v1/generations/{id}](#generations) with its `generation_id`; - a `500` after the job was created, for example when a finished image cannot be read back; - `409` `idempotency_key_reused` and `idempotency_key_replayed`, which a retry cannot change. It never sends it on a `429`, on `409` `idempotency_key_in_use`, or on a `5xx` from before any job exists. Those are safe to retry. With an `Idempotency-Key`, every retry is safe. ## Models `GET /v1/models` Lists every model your account can call right now, with its prices and what it supports. A model with no enabled provider is not listed. Some models need access granted to your account: they are listed once it is. This endpoint needs an API key, and lists the models that key's account can call. The answer can be cached for 60 seconds, by your client only. ### Parameters None. ### Response ```json { "data": [{ "id": "x-ai/grok-4.7", "name": "Grok 4.7", "kind": "chat", "context_length": 500000, "architecture": { "input_modalities": ["text", "image"], "output_modalities": ["text"] }, "pricing": { "prompt": "0.000002", "completion": "0.000006", "input_cache_read": "0.0000005", "request": "0", "image": "0", "video_second": "0" }, "top_provider": { "context_length": 500000, "max_completion_tokens": 32768 }, "response_modes": ["sync", "stream"], "supported_parameters": ["max_tokens", "temperature", "top_p", "stop", "stream", "tools", "tool_choice", "response_format", "seed", "frequency_penalty", "presence_penalty"], "extra_parameters": {} }] } ``` | Field | Type | Description | | --- | --- | --- | | `id` | string | The model id, `/`, for example `x-ai/grok-4.7` or `qwen/qwen-image-2.0`. Send it as `model`. A renamed model keeps answering to its old id. | | `response_modes` | string[] | How the model answers: `sync` (one held answer), `async` (send `Prefer: respond-async`, get `202`, and poll) and `stream` (`"stream": true`). A model that lists only `async` needs the `Prefer` header, and `stream` works only where it is listed. Video is always asynchronous through the Videos API, with or without the header. | | `name` | string | Display name. | | `kind` | string | `chat`, `image` or `video`. | | `context_length` | integer | Context window in tokens. `0` for image and video models. | | `architecture` | object | `input_modalities` and `output_modalities`. Output is `["text"]`, `["image"]` or `["video"]`. | | `pricing` | object of strings | USD per unit, as decimal strings, at the base tier: `prompt` and `completion` (per token), `input_cache_read` (per cached token), `request`, `image` (per image), `video_second` (per second of video). A video priced by resolution shows the lowest price. A unit that does not apply is `"0"`. | | `top_provider` | object | `context_length` and `max_completion_tokens`. | | `supported_parameters` | string[] | For chat, the OpenAI parameters the model takes. For image and video, the shared features it takes (`resolution`, `ratio`, `audio`, `seed`, `negative_prompt`, `first_frame`, `last_frame`, `reference_images`, `reference_videos`, `prompt_extend`). | | `operations` | string[] | Video models only: `edit`, `extend` and `remix`, for each one the model supports. Absent when the model supports none. | | `extra_parameters` | object | The model-specific top-level fields, by name. Each has `type`, `values` (allowed values; empty means any), `price_condition` (the field changes the price) and `description`. See [Model-specific parameters](#parameters). | ```bash curl https://api.hyperouter.app/v1/models \ -H "Authorization: Bearer $HYPEROUTER_API_KEY" ``` ### Errors None specific to this endpoint. ### Chat models Prices per 1M tokens: input / cached input / output. | Model | Context | Price | | --- | --- | --- | | `x-ai/grok-4.7` | 500K | $2.00 / $0.50 / $6.00 | | `x-ai/grok-4.6` | 500K | $2.00 / $0.50 / $6.00 | | `x-ai/grok-4.5` | 500K | $2.00 / $0.30 / $6.00 | | `x-ai/grok-4.3` | 1M | $1.25 / $0.20 / $2.50 | | `x-ai/grok-build-0.1` | 256K | $1.00 / $0.20 / $2.00 | | `deepseek/deepseek-v4.1-flash` | 1M | $0.15 / $0.003 / $0.60 | | `deepseek/deepseek-v4.1-flash-1017` | 1M | $0.15 / $0.003 / $0.60 | | `openai/gpt-6-astra` | 272K | $10.00 / $1.00 / $50.00 | | `openai/gpt-6.1-sol` | 272K | $2.00 / $0.10 / $10.00 | | `z-ai/glm-5.3` | 1M | $1.40 / $0.26 / $4.40 | | `lumina/face-detect` | 4K | $0.0013 per request | On the `x-ai/` models, a request with 200,000 prompt tokens or more is billed entirely at the long-context rates, which are twice these. The `deepseek/` models have one rate at every prompt length, reason before they answer by default (`reasoning_effort: "none"` turns it off), and count their reasoning in `max_tokens`, at most 384,000. `z-ai/glm-5.3` always reasons: `reasoning_effort` is `low`, `high` or `max`, and `none` is refused. The `openai/` models reason too and take prompts of at most 272,000 tokens. ## Chat completions `POST /v1/chat/completions` Creates a model response for a conversation. The body is OpenAI’s Chat Completions request. Hyperouter adds `models`, `provider` and `moderation`. ### Parameters | Name | Type | Required | Allowed values | Default | Description | | --- | --- | --- | --- | --- | --- | | `model` | string | Yes | A chat model id from `GET /v1/models` | | The model to use, for example `x-ai/grok-4.7`. | | `messages` | object[] | Yes | Non-empty. Roles `system`, `user`, `assistant`, `tool` | | The conversation so far. Pictures (`image_url` parts) are accepted where the model takes images. | | `models` | string[] | No | At most 5 model ids | | Addition. Fallback models, tried in order after every provider of `model` fails. | | `provider` | object | No | See below | | Addition. Routing preferences. | | `moderation` | string | No | `auto`, `low` | `auto` | Addition. See [Moderation](#moderation). | | `stream` | boolean | No | | `false` | Return server-sent events. | | `stream_options` | object | No | `{"include_usage": true}` | | With `include_usage`, the stream ends with a chunk that carries `usage`. | | `max_tokens`, `max_completion_tokens` | integer | No | 1 or more | The model’s maximum output | Upper bound on generated tokens. It also sets the most a request can cost, which Hyperouter holds before the call (see [Billing](#billing)). | | `n` | integer | No | 1 to 128 | `1` | Number of choices. | | `temperature`, `top_p`, `seed`, `frequency_penalty`, `presence_penalty`, `logit_bias` | number / integer / object | No | As OpenAI | | Sampling controls, passed to the provider. | | `stop` | string \| string[] | No | | | Sequences that end generation. | | `tools`, `tool_choice`, `parallel_tool_calls` | object[], string \| object, boolean | No | OpenAI function-calling format | | Function calling. Only tools of type `function`: any other type is refused with `400 unsupported_parameter`. | | `response_format` | object | No | `{"type":"json_object"}` or a JSON schema | | Structured output. | | `reasoning_effort`, `logprobs`, `top_logprobs`, `user` | as OpenAI | No | | | Passed to the provider where the model takes them. | Other fields are ignored. `supported_parameters` of each model in [GET /v1/models](#models) lists exactly what it takes. Grok 4.20 and newer ignore `logprobs` and `top_logprobs`, so they are not listed. ### The provider object {#routing} By default Hyperouter sends each chat request to the healthy provider with the lowest price for that model, and breaks ties by the lowest recent time to first token. A provider that errors repeatedly is skipped until it recovers. The `provider` object follows OpenRouter’s request shape. | Name | Type | Required | Allowed values | Default | Description | | --- | --- | --- | --- | --- | --- | | `sort` | string | No | `price`, `latency`, `throughput` | `price` | How to rank the providers. | | `order` | string[] | No | Provider names | | Providers to try first, in order, for example `["xai"]`. A provider name is its public name in any case (`xAI`, `Qwen`) or the first part of its model ids (`x-ai`, `qwen`). | | `only` | string[] | No | Provider names | | Route only to these providers. | | `ignore` | string[] | No | Provider names | | Never route to these providers. | ### Response OpenAI’s chat completion. The `id` is the request id: look it up with [GET /v1/generations/{id}](#generations). Hyperouter adds `provider` (the public name of the provider that filled the request, for example `xAI`) and `usage.cost` (the charge, in USD, as a JSON number). ```json { "id": "0194f2c4-7a1e-7b3c-9d02-5f6a1c0e8b21", "object": "chat.completion", "model": "x-ai/grok-4.7", "provider": "xAI", "choices": [{ "index": 0, "message": { "role": "assistant", "content": "Hello! How can I help?" }, "finish_reason": "stop" }], "usage": { "prompt_tokens": 9, "completion_tokens": 8, "total_tokens": 17, "prompt_tokens_details": { "cached_tokens": 0 }, "cost": 0.000066 } } ``` A prompt-rewrite model answers with one choice, a `usage` of zero tokens and a fixed `cost` per request. ### Streaming {#streaming} With `"stream": true` the response is a stream of server-sent events in the OpenAI chunk format. It ends with `data: [DONE]`. When you set `stream_options.include_usage`, the last chunk before `[DONE]` has an empty `choices` array and a `usage` object that includes `cost`, and a `provider` field. Usage is always collected for billing, whether or not you ask for the chunk. Until the first token arrives, the stream sends the comment line `: processing` every 15 seconds to keep the connection open. SSE clients ignore comments. Fallback to another provider happens only before the first token is sent. If a provider fails mid-stream, the stream ends with `data: {"error": {...}}` followed by `data: [DONE]`, and you are billed for the tokens already produced. You are billed for the tokens the provider produces, whether or not you read them. A stream you abort, or a reply you stop waiting for, is not stopped at the provider, which goes on and bills its whole answer: Hyperouter reads it to its end (for at most 5 more minutes) and bills that. A provider that has accepted a request may be running it. If it then stops answering (5 minutes with no token on a stream, or longer than a reply of `max_tokens` takes) or its answer breaks off before the first token, the request is not sent to another provider: it answers `503` with code `timeout` or `upstream_error` with `error.generation_id`, and is billed on its hold, with `usage_estimated: true` on its record. ### Example ```bash curl https://api.hyperouter.app/v1/chat/completions \ -H "Authorization: Bearer $HYPEROUTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "x-ai/grok-4.7", "models": ["x-ai/grok-4.6"], "provider": { "sort": "latency" }, "messages": [{ "role": "user", "content": "Hello" }] }' ``` ### Errors | Status | type | code | param | When | | --- | --- | --- | --- | --- | | `400` | `invalid_request_error` | `invalid_body` | the field | The body is not valid JSON or is over 32 MiB; `model` is missing; `models` has more than 5 entries; `n` is outside 1 to 128; `max_tokens` is not positive; `provider.sort` is not allowed; `moderation` is not `auto` or `low` (`moderation must be auto or low.`); `messages` is empty. Also when every provider refused the request as invalid. | | `400` | `invalid_request_error` | `unknown_model` | `model` or `models` | The model is not a chat model, or it has no enabled provider. | | `400` | `invalid_request_error` | `unsupported_parameter` | `tools`, `response_format`, `messages` or `max_tokens` | The model does not take tools, a response format, or pictures; a tool is not of type `function`; or no provider of the model can bound its output. | | `400` | `invalid_request_error` | `context_length_exceeded` | `messages` | The prompt and `max_tokens` exceed the model’s context. | | `402` | `insufficient_credits` | `insufficient_credits` | `max_tokens` | The balance or key limit cannot cover the most the request could cost. The message says how many tokens you can afford. | | `429` | `rate_limit_error` | `venue_busy` | | Every provider for the model is over its rate budget. | | `429` | `rate_limit_error` | `rate_limited` | | Your account is over its requests per minute or concurrent requests (see [Rate limits](#rate-limits)). Nothing is held or forwarded. | | `409` | `conflict` | `idempotency_key_replayed`, `idempotency_key_reused`, `idempotency_key_in_use` | | See [Retries and idempotency](#idempotency). | | `503` | `upstream_error` | `all_venues_failed` | | No provider is healthy, or all of them failed. Not billed. | | `503` | `upstream_error` | `upstream_error` | | The provider accepted the request and its answer broke off before the first token. Billed on the hold (see [Streaming](#streaming)). | | `503` | `upstream_error` | `timeout` | | The provider accepted the request and did not finish it in time. Billed on the hold (see [Streaming](#streaming)). | | `401`, `403`, `500` | | | | See [Authentication](#authentication). | ## Images `POST /v1/images/generations` `POST /v1/images/edits` Both follow OpenAI’s Images API and take a JSON body. `/v1/images/edits` takes OpenAI’s JSON form: the pictures are `images`, 1 to 5 https URLs. The OpenAI SDKs' `images.edit` sends picture files as `multipart/form-data`, and that works too: Hyperouter's edge stores each file (PNG, JPEG or WebP, 20 MiB each, 50 MiB a request) for 3 days at an unguessable `https://api.hyperouter.app/u/...` URL that the provider fetches, then runs the request as JSON. A video file is refused (`400 invalid_video`): send a video as an https URL. Both take the shared features below and any [model-specific parameters](#parameters) as top-level fields. ### Parameters | Name | Type | Required | Allowed values | Default | Description | | --- | --- | --- | --- | --- | --- | | `model` | string | Yes | An image model id | | The model to use. | | `prompt` | string | Generations: yes. Edits: as the model needs | Non-blank text | | What to make. | | `images` | object[] | Edits: yes | 1 to 5 objects `{"image_url": "https://..."}`, as many as the model takes. Each URL is https to a public host, never a `data:` URL, an IP address or an internal name. `file_id` is not accepted. | | The pictures to edit. Each URL is passed to the provider as it is, so the provider must be able to fetch it; one it cannot fetch or read fails the job with `vendor_rejected`, and nothing is charged. A model that takes them in a set order says so in its notes in the model reference. | | `n` | integer | No | 1 to 10, and at most the model’s limit | `1` | Number of images. | | `size` | string | No | `WIDTHxHEIGHT`, for example `1024x1024`; the model may allow fewer sizes | `1024x1024` | Output size. | | `quality` | string | No | As the model allows | | Passed to the model. | | `response_format` | string | No | `url` | `url` | How the image comes back: always a URL. `b64_json` is refused with `400 unsupported_parameter`. | | `seed` | integer | No | If the model lists `seed` | | Seed for repeatable output. | | `negative_prompt` | string | No | If the model lists it | | What to avoid. | | `prompt_extend` | boolean | No | If the model lists it | Off unless you ask | Let the model expand the prompt. | | `prompt_expand` | boolean | No | If the model lists it | `false` | Addition. Hyperouter rewrites the prompt for the model before it renders. See [Prompt expansion](#prompt-expansion). | | `prompt_expand_model` | string | No | A chat model slug your account can call | `x-ai/grok-4.6` | Addition. With `prompt_expand`: the model that writes the rewrite. Without `prompt_expand: true` it is `400 invalid_body`. See [Prompt expansion](#prompt-expansion). | | `locale` | string | No | A BCP 47 tag, for example `zh-CN` | English | Addition. With `prompt_expand`: the language spoken lines are written in, where the model speaks it. | | `ratio` | string | No | If the model lists it | | Aspect ratio. | | `resolution` | string | No | `480p`, `720p`, `1080p`, `2K`, `4K`, as the model allows | | Output resolution. | | `models` | string[] | No | Image model ids | | Addition. Fallback models. | | `provider` | object | No | `only`, `ignore`, `order` (arrays of provider names) | | Addition. Routing preferences. | | `moderation` | string | No | `auto`, `low` | `auto` | Addition. See [Moderation](#moderation). | | model-specific fields | as declared | As declared | As declared | | See [Model-specific parameters](#parameters). | A shared feature that the model does not list, such as `seed` on a model without it, is refused with `400 unsupported_parameter`. It is never dropped silently, because it would change the output and the price. ### Response ```json { "created": 1730000000, "data": [{ "url": "https://..." }], "usage": { "cost": 0.02 }, "id": "0194f2c4-7a1e-7b3c-9d02-5f6a1c0e8b21" } ``` | Field | Type | Description | | --- | --- | --- | | `created` | integer | Unix time of the request. | | `data[].url` | string | With `response_format: "url"`: a signed link, valid for 24 hours. `GET /v1/generations/{id}` signs a fresh one. | | `usage.cost` | number | The charge in USD, as a JSON number. | | `id` | string | The request id. Use it with [GET /v1/generations/{id}](#generations). | | `expanded_prompt` | string \| null | Addition. With `prompt_expand`: the prompt that rendered. `null` when nothing was expanded. | Hyperouter stores every result for 30 days, so a link never points at a vendor. After 30 days the record stays but its `outputs` is empty. ### Waiting for the result - On a model whose `response_modes` include `sync`, the request waits for the image for up to 90 seconds and answers with its real status: `200` with the images, or the error. Validation, balance and capacity errors come first, with their own status. - An image that is not ready after 90 seconds answers `503` with code `timeout` and the response header `x-should-retry: false`, so the OpenAI SDKs do not retry it and start a second image. The job goes on, and the error carries its id as `generation_id`: poll [GET /v1/generations/{id}](#generations) for the result. You are billed only if it succeeds. To retry safely yourself, send an [Idempotency-Key](#idempotency): a retry with the same key waits on the same job. - A model whose `response_modes` are only `async` needs `Prefer: respond-async`. Without it the answer is `400` with code `async_required`. - To avoid a held connection on any model that lists `async`, send `Prefer: respond-async` (RFC 7240). The header may repeat and may carry other preferences, for example `Prefer: wait=10, respond-async`. You get `202` at once with the response header `Preference-Applied: respond-async`: ```json { "id": "0194f2c4-7a1e-7b3c-9d02-5f6a1c0e8b21", "status": "queued" } ``` Poll [GET /v1/generations/{id}](#generations), or receive an `image.completed` or `image.failed` [webhook](#webhooks). Webhooks fire for images only for jobs you sent with this header. ### Examples ```bash curl https://api.hyperouter.app/v1/images/generations \ -H "Authorization: Bearer $HYPEROUTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "x-ai/grok-imagine-image", "prompt": "A lighthouse at dusk", "n": 1}' ``` ```bash curl https://api.hyperouter.app/v1/images/edits \ -H "Authorization: Bearer $HYPEROUTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "x-ai/grok-imagine-image", "prompt": "make it night", "images": [{"image_url": "https://example.com/photo.png"}]}' ``` ### Errors | Status | code | param | When | | --- | --- | --- | --- | | `400` | `unsupported_content_type` | | The body is `multipart/form-data`: `Send a JSON body; pictures and videos are https URLs.` | | `400` | `invalid_body` | the field | The body is not valid JSON; `images` is not an array of objects with `image_url`; `prompt` is missing on generations; `n` is below 1; `response_format` is not `url` (`response_format must be url.`; `b64_json` is `unsupported_parameter`); `resolution` is not allowed; a typed field has the wrong type; `moderation` is not `auto` or `low`. | | `400` | `unknown_model` | `model` | The model does not exist for images, or no provider matches your `provider` preferences. | | `400` | `invalid_size` | `size` | `size` does not look like `1024x1024`, or the model does not allow it. | | `400` | `too_many_images` | `n` or `images` | `n` is over 10 or over the model’s limit; more than 5 pictures; more pictures than the model takes. | | `400` | `image_required` | `images` | An edit has fewer pictures than the model needs, or you called generations on a model that only edits (use `/v1/images/edits`). | | `400` | `unsupported_operation` | `images` | The model does not edit pictures. | | `400` | `unsupported_parameter` | the field | The model does not support the field; `images[i].file_id`. | | `400` | `invalid_combination` | the field | The vendor does not allow these fields together. | | `400` | `async_required` | | The model answers asynchronously only: send `Prefer: respond-async`. | | `400` | `unsupported_parameter` | `stream` | `"stream": true` on a model whose `response_modes` do not include `stream`. | | `400` | `invalid_image` | `images[i].image_url` | The URL is not https to a public host: `images[i].image_url must be an https URL.` for a `data:` or `http://` URL. | | `400` | `content_rejected` | | The vendor’s safety filter refused the input: `The request was rejected by a content safety filter.` | | `400` | `vendor_rejected` | | `The request was refused by the model. Check its parameters and inputs.` | | `400` | `invalid_idempotency_key` | | See [Retries and idempotency](#idempotency). | | `402` | `insufficient_credits` | | The balance or key limit cannot cover the most the request could cost. | | `409` | `idempotency_key_reused`, `idempotency_key_in_use` | | See [Retries and idempotency](#idempotency). | | `429` | `rate_limit_error` | | Every provider is at capacity (no `code`), or your account is over its requests per minute or concurrent requests (`code: rate_limited`, see [Rate limits](#rate-limits)). | | `503` | `upstream_error` | | `The model failed to produce a result. You were not charged.` | | `503` | `provider_busy` | | The provider stayed at capacity for about two minutes and did not take the job. Retry later. You were not charged. | | `500` | `server_error`, `no_price` | | Hyperouter failed, or the model cannot be priced right now. A `500` after the image was made carries `x-should-retry: false`. | | `503` | `timeout` | | The image was not ready within 90 seconds. It is still being made: `error.generation_id` is the id to poll. Carries `x-should-retry: false`. | A failed job’s message ends with the provider’s own reason when it gave one, after `Provider:`, so a refused prompt, a busy provider and an outage read apart. URLs in it are replaced by ``. ### Image models | Model | Does | Price | | --- | --- | --- | | `x-ai/grok-imagine-image` | Generate and edit. Up to 10 images, up to 5 input images. | $0.02 per image | | `x-ai/grok-imagine-image-2.0` | Generate and edit, as above. | From $0.04 per image | | `qwen/qwen-image-2.0` | Generate and edit (1 to 3 input images). `n` 1 to 6. | $0.035 per image | | `bytedance/seedream-5.0-flash` | Generate and edit (1 to 10 input images). One 2K image per request, no watermark. | $0.018 per image | | `bytedance/seedream-5.0-lite` | Generate and edit, as above. | $0.035 per image | Some models need access granted to your account. The [model reference](https://dash.hyperouter.app/docs/models) in the dashboard lists every model your account can call, with its parameters, prices and notes. ## Videos Video follows OpenAI’s Videos API. A video is a job. A create, edit, extension or remix call answers `200` at once with the Video object in status `queued`. You poll `GET /v1/videos/{id}` until the status is `completed`, or receive a [webhook](#webhooks). `Prefer` changes nothing here: videos are always asynchronous. | Endpoint | Does | | --- | --- | | `POST /v1/videos` | Create a video from a prompt, with an optional `input_reference`. | | `POST /v1/videos/edits` | Edit or upscale a completed video. Only models that list `edit` in `operations`. | | `POST /v1/videos/extensions` | Extend a completed video. Only models that list `extend`. | | `POST /v1/videos/{id}/remix` | New prompt on a completed video, same model. Only models that list `remix`. | | `GET /v1/videos` | List your videos. | | `GET /v1/videos/{id}` | The Video object. | | `GET /v1/videos/{id}/content` | The MP4 or the thumbnail. | | `DELETE /v1/videos/{id}` | Delete a video. | ### Create a video `POST /v1/videos` The body is JSON, on every video endpoint; the OpenAI SDKs' `videos.create` with an `input_reference` picture file is also accepted, the file handled as for [image edits](#images). Every picture and video is an https URL to a public host, which is passed to the provider as it is: the provider must be able to fetch it, and one it cannot fetch or read fails the video with `vendor_rejected`, and nothing is charged. A `data:` URL, an `http://` URL, an IP address or an internal name is refused with `400 invalid_image` or `invalid_video`. A `multipart/form-data` body is refused with `400 unsupported_content_type`. | Name | Type | Required | Allowed values | Default | Description | | --- | --- | --- | --- | --- | --- | | `model` | string | Yes | A video model id | | The model to use. | | `prompt` | string | Yes | Non-blank text | | What to make. | | `seconds` | string \| integer | No | As the model allows | The model’s first allowed value, else 4 s (or the model’s maximum if lower) | Length of the video. `"8"` and `8` are the same. | | `size` | string | No | `WIDTHxHEIGHT` | | The height picks the resolution unless you send `resolution`: up to 480 is `480p`, up to 720 is `720p`, up to 1080 is `1080p`, up to 1440 is `2K`, above is `4K`. The model must allow that resolution. | | `input_reference` | object | If the model needs a picture | `{"image_url": "https://..."}`, an https URL | | The first picture of the video. `file_id` is not accepted. | | `resolution` | string | No | `480p`, `720p`, `1080p`, `2K`, `4K`, as the model allows | The model’s first resolution | Output resolution. It changes the price. | | `ratio` | string | No | If the model lists it | | Aspect ratio. | | `audio` | boolean | No | If the model makes sound | `false` | Make sound. | | `seed` | integer | No | If the model lists it | | Seed for repeatable output. | | `negative_prompt` | string | No | If the model lists it | | What to avoid. | | `prompt_extend` | boolean | No | If the model lists it | Off unless you ask | Let the model expand the prompt. | | `prompt_expand`, `locale` | boolean, string | No | As for [images](#images) | `false` | Addition. See [Prompt expansion](#prompt-expansion). | | `prompt_expand_model` | string | No | As for [images](#images) | `x-ai/grok-4.6` | Addition. With `prompt_expand`: the model that writes the rewrite. See [Prompt expansion](#prompt-expansion). | | `first_frame`, `last_frame` | string | No | An https URL of a picture | | Keyframes. | | `reference_images`, `reference_videos` | string[] | No | https URLs | | Reference media. They cannot be combined with keyframes, as the vendor sets. | | `models`, `provider` | string[], object | No | As for [images](#images) | | Addition. Fallback models and routing preferences. | | `moderation` | string | No | `auto`, `low` | `auto` | Addition. See [Moderation](#moderation). | | model-specific fields | as declared | As declared | As declared | | See [Model-specific parameters](#parameters). | ### Edit, extend and remix | Endpoint | Body (JSON) | | --- | --- | | `POST /v1/videos/edits` | `model`, `video` (required), `prompt`, `resolution` and the shared features. `video` is `{"id": ""}` (a completed video of yours) or `{"url": "https://..."}`. `model` defaults to the source video’s model when you send an id. | | `POST /v1/videos/extensions` | `model`, `video` (required), `prompt`, `seconds`. The new seconds follow the same rules as a create. | | `POST /v1/videos/{id}/remix` | `prompt` (required). The model is the source video’s. | The source video must have completed. A video given by URL is an https URL, passed to the provider as it is, like a picture. If you give a video by URL, `seconds` is needed on models that have no fixed maximum. ### List, read, download, delete | Endpoint | Parameters | Answer | | --- | --- | --- | | `GET /v1/videos` | Query: `after` (a video id), `limit` (1 to 100, default 20), `order` (`desc` default, or `asc`) | `{"object":"list","data":[Video],"first_id","last_id","has_more"}`. Videos past their `expires_at` are left out. | | `GET /v1/videos/{id}` | | The Video object. | | `GET /v1/videos/{id}/content` | Query: `variant` (`video` default, or `thumbnail`) | `302 Found` to a signed link to the bytes (`video/mp4`, or the thumbnail’s type), valid for 15 minutes. HTTP clients and the OpenAI SDKs follow it. `404 not_found` before completion or when there is no thumbnail. | | `DELETE /v1/videos/{id}` | | `{"id","object":"video.deleted","deleted":true}`. Answers `409 video_not_finished` while the job runs. A deleted video answers `404`. | ### The Video object ```json { "id": "0194f2c4-7a1e-7b3c-9d02-5f6a1c0e8b21", "object": "video", "model": "x-ai/grok-imagine-video", "status": "completed", "progress": 100, "prompt": null, "seconds": "8", "size": "", "created_at": 1730000000, "completed_at": 1730000137, "expires_at": 1732592000, "error": null, "remixed_from_video_id": null, "usage": { "cost": 0.4, "seconds": 8 } } ``` | Field | Type | Description | | --- | --- | --- | | `status` | string | `queued`, `in_progress`, `completed` or `failed`. | | `progress` | integer | 0 to 100. | | `prompt` | string \| null | The prompt is returned only while the job runs. The finished object returns `null`. | | `seconds` | string | Length of the video, as a string, as OpenAI’s. | | `size` | string | The `size` you sent, if any. | | `expires_at` | integer | Unix time: `created_at` plus 30 days. After it, `GET /v1/videos/{id}` and `/content` answer `404 not_found` and the video is left out of the list. Download what you want to keep before then. | | `error` | object \| null | On a failed job: `{"code", "message"}`, for example `content_rejected`. | | `remixed_from_video_id` | string \| null | The source video of an edit, extension or remix. | | `usage` | object \| null | Set once the job is settled: `cost` (USD, a JSON number) and `seconds` (integer). | | `expanded_prompt` | string \| null | Addition. With `prompt_expand`: the prompt that rendered, once the job is submitted. Kept 30 days, then `null`. | A video is billed on the seconds you request. The amount is held up front, and released in full if the job fails. ### Example: x-ai/grok-imagine-video Model `x-ai/grok-imagine-video` makes a video from a prompt, with an optional picture to start from. `seconds` is 1 to 15. Price: $0.050 per second. ```bash curl https://api.hyperouter.app/v1/videos \ -H "Authorization: Bearer $HYPEROUTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "x-ai/grok-imagine-video", "prompt": "She turns to the camera and smiles", "input_reference": { "image_url": "https://example.com/portrait.png" }, "seconds": 8 }' ``` ```json { "id": "0194f2c4-7a1e-7b3c-9d02-5f6a1c0e8b21", "object": "video", "model": "x-ai/grok-imagine-video", "status": "queued", "progress": 0, "prompt": "She turns to the camera and smiles", "seconds": "8", "size": "", "created_at": 1730000000, "completed_at": null, "expires_at": 1732592000, "error": null, "remixed_from_video_id": null, "usage": null } ``` This job is charged 8 × $0.050 = $0.40, so the settled object shows `"usage": {"cost": 0.4, "seconds": 8}`. ### Errors | Status | code | param | When | | --- | --- | --- | --- | | `400` | `unsupported_content_type` | | The body is `multipart/form-data`: `Send a JSON body; pictures and videos are https URLs.` | | `400` | `invalid_body` | the field | The body is not valid JSON; `prompt` is blank; `video` is not an object with `id` or `url`; `input_reference` is not an object with `image_url`; `limit`, `after` or `variant` is not valid; a typed field has the wrong type; `resolution` is not allowed; `moderation` is not `auto` or `low`. | | `400` | `unknown_model` | `model` | The model does not exist for videos. | | `400` | `invalid_seconds` | `seconds` | `seconds` is not allowed for the model (the message lists the allowed values), or is needed for a video given by URL. | | `400` | `invalid_size` | `size` | `size` does not look like `1280x720`. | | `400` | `unsupported_parameter` | the field | The model does not support the field; the resolution is not available (`resolution` or `size`); `audio` on a model without sound; `input_reference.file_id`. | | `400` | `unsupported_operation` | `model` | The model does not list the operation (edit, extend or remix). | | `400` | `invalid_combination` | the field | Reference media together with keyframes, as the vendor sets. | | `400` | `image_required` | `input_reference` | `This model needs an input_reference picture.` | | `400` | `video_not_ready` | `video` | The source video has not completed. | | `400` | `invalid_image`, `invalid_video` | the field | A picture URL (`invalid_image`) or video URL (`invalid_video`: `reference_videos[i]`, `video.url`) is not https to a public host, for example a `data:` or `http://` URL: ` must be an https URL.` `param` is the field, for example `input_reference.image_url`, `first_frame` or `reference_images[1]`. A URL is passed to the provider as it is: one the provider cannot fetch or read fails the video with `vendor_rejected`, and nothing is charged. | | `400` | `content_rejected`, `vendor_rejected` | | The job failed at the vendor. It shows in `error.code` of the Video object. | | `400` | `clip_too_long` | | The reference clip plus the requested `seconds` run past the model’s limit: `The reference clip plus the requested seconds run past the model's limit of 30 s. Send a shorter clip or fewer seconds. You were not charged.` It shows in `error.code` of the Video object. | | `402` | `insufficient_credits` | | The balance or key limit cannot cover the held amount. | | `404` | `not_found` | | No such video, it is not yours, it was deleted, or it is past `expires_at`; or the content is not ready. | | `409` | `video_not_finished` | | You deleted a video that is still being made. | | `409` | `idempotency_key_reused`, `idempotency_key_in_use` | | See [Retries and idempotency](#idempotency). | | `429`, `503`, `500` | | | As for images. A `500` after the video was created carries `x-should-retry: false`. | ### Video models | Model | Price per second of video | Notes | | --- | --- | --- | | `x-ai/grok-imagine-video` | From $0.050 | Text, image or video input. 15 s at most. Supports `edit`. | | `x-ai/grok-imagine-video-1.5` | From $0.080 | Text, image or reference input. 15 s at most. | | `x-ai/grok-imagine-video-1.5-lite` | From $0.020 | As above, smaller and cheaper. | | `bytedance/seedance-2.0` | From $0.0431892 | Text, first and last frame, or reference image input, optional audio. 4 to 15 s. 480p, 720p, 1080p or 4K; the rate shown is 480p's. | | `bytedance/seedance-2.0-fast` | From $0.0331452 | Text, first and last frame, or reference image input, optional audio. 4 to 15 s. 480p or 720p; the rate shown is 480p's. | | `bytedance/seedance-2.0-mini` | From $0.0210924 | Text, first and last frame, or reference image input, optional audio. 4 to 15 s. 480p or 720p; the rate shown is 480p's. | | `bytedance/seedance-2.5` | From $0.0642816 | Text, first and last frame, or reference image input, optional audio. 4 to 15 s. 480p, 720p or 1080p; the rate shown is 480p's. | | `lumina/berry-1.0` | From $0.050 | Text, image or reference input, optional audio. 30 s at most. 480p, 720p or 1080p; the rate shown is 480p's. | | `minimax/minimax-h3` | From $0.080 | Text, first and last frame, or reference image input, no audio. 4 to 15 s, at 768p or 2K. | | `alibaba/wan3.0` | From $0.050 | Text, image or reference input, optional audio. 30 s at most. 480p, 720p or 1080p; the rate shown is 480p's. | The [model reference](https://dash.hyperouter.app/docs/models) in the dashboard lists every video model your account can call. ## Generations `GET /v1/generations/{id}` Returns the record of one of your requests, of any kind: its status, usage, charge, timing and, for images and videos, signed links to the outputs. Use it to poll an image request you sent with `Prefer: respond-async` and to reconcile spend. Prompt and completion text are never in the record. Hyperouter keeps them encrypted for 180 days only to meet legal obligations, and no API or dashboard page returns them ([Privacy Policy](privacy.md#p3)). ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string (UUID) | Yes | The `id` of a chat completion, image answer or Video object. | ### Response ```json { "data": { "id": "0194f2c4-7a1e-7b3c-9d02-5f6a1c0e8b21", "kind": "chat", "model": "x-ai/grok-4.7", "provider_name": "xAI", "status": "done", "created_at": "2026-10-24T08:12:00Z", "finished_at": "2026-10-24T08:12:02Z", "usage": { "input_tokens": 120, "output_tokens": 300, "cache_read_tokens": 0, "image": 0, "video_seconds": 0, "cost": 0.00204 }, "usage_estimated": false, "ttft_ms": 320, "latency_ms": 2100, "error": null, "outputs": [] } } ``` | Field | Type | Description | | --- | --- | --- | | `kind` | string | `chat`, `image` or `video`. | | `provider_name` | string \| null | The provider that served a finished chat request. `null` for images and videos. | | `status` | string | `queued`, `submitting`, `running`, `done` or `failed`. | | `created_at`, `finished_at` | string \| null | RFC 3339 UTC. `finished_at` is `null` until the request ends. | | `usage` | object | `input_tokens`, `output_tokens`, `cache_read_tokens`, `image`, `video_seconds` and `cost`. `cost` is the charge in USD, a JSON number. Counts are `0` until the request is done. | | `usage_estimated` | boolean | `true` when the provider sent no token counts and Hyperouter counted them. | | `ttft_ms`, `latency_ms` | integer \| null | Time to first token and total time, in milliseconds. | | `error` | object \| null | `{"code", "message"}` when the request failed. | | `outputs` | object[] | For images and videos: `[{"url", "expires_at"}]`, signed links valid for 24 hours. Available for 30 days after the request; after that `outputs` is empty and the record stays. Always empty for chat. | | `expanded_prompt` | string \| null | For images and videos sent with `prompt_expand`: the prompt that rendered. Kept 30 days after the request, then `null`. | ```bash curl https://api.hyperouter.app/v1/generations/0194f2c4-7a1e-7b3c-9d02-5f6a1c0e8b21 \ -H "Authorization: Bearer $HYPEROUTER_API_KEY" ``` ### Errors | Status | type | code | param | When | | --- | --- | --- | --- | --- | | `404` | `not_found` | `not_found` | `id` | `No generation with that id.` The id is not a UUID, does not exist, belongs to another account, or was deleted. | | `500` | `server_error` | `server_error` | | Hyperouter could not read or sign the record. | | `401`, `403` | | | See [Authentication](#authentication). | ## Model-specific parameters {#parameters} Some models have fields that other models do not. These are *extra parameters*. You send them as top-level fields of the request body, like any other field. Every OpenAI SDK can send them (`extra_body` in Python, extra properties in JavaScript). `GET /v1/models` lists each model’s extra parameters in `extra_parameters`. Each entry has: | Field | Type | Description | | --- | --- | --- | | `type` | string | One of `string`, `integer`, `number`, `boolean`, `array`, `object`. | | `values` | array | The allowed values. Empty means any value of the type. | | `price_condition` | boolean | `true` when the field changes the price. | | `description` | string | What the field does. | The [model reference](https://dash.hyperouter.app/docs/models) in the dashboard describes the extra parameters of each model your account can call. ### Rules - A known parameter that the model does not have is refused: `400 invalid_request_error` with code `unsupported_parameter`, `param` set to the field name (for example `loras`) and the message `This model does not support "loras".` A parameter is known when some model in the catalog declares it. The same rule covers the shared features (`resolution`, `ratio`, `audio`, `seed`, `negative_prompt`, `first_frame`, `last_frame`, `reference_images`, `reference_videos`, `prompt_extend`). Nothing is dropped silently, because it would change the output and the price. - A value of the wrong type is refused with `400 invalid_body`, `param` set to the name and a message such as `loras must be of type array.` - A value outside `values` is refused with `400 invalid_body` and the message ` must be one of [...]`. - A field that no model declares is ignored, as OpenAI’s API ignores unknown fields. - Every model checks its parameters against its own schema: allowed values, minimum and maximum, required fields and list lengths. A value outside it answers `400 invalid_body` with `param` naming the field, for example `seconds must be one of 5, 8.` - A combination a vendor does not allow, such as reference media together with keyframes, answers `400 invalid_combination` with the reason. The checks run before anything is charged. - Fields that change the price, such as `resolution` and `audio`, are priced in the up-front hold, so the hold is exact. ### Shared features These fields have the same name on every model that has them. A model lists the ones it takes in `supported_parameters`. | Field | Meaning | | --- | --- | | `resolution` | `480p`, `720p`, `1080p`, `2K` or `4K`, as the model allows. | | `ratio`, `seed`, `negative_prompt`, `prompt_extend`, `audio` | As named. `audio` and `prompt_extend` are off unless you ask. | | `first_frame`, `last_frame` | Keyframes for video. | | `reference_images`, `reference_videos` | Reference media for video and image edits. | ### Prompt expansion {#prompt-expansion} With `"prompt_expand": true`, Hyperouter rewrites your prompt for the model before it renders: a fuller description for a picture, a second-by-second script for a video. It is off by default, so a prompt goes as written. Only a model whose `supported_parameters` lists `prompt_expand` takes it, and only to create an image or a video; on any other model, and on a video edit, extension or remix, it is refused with `400 unsupported_parameter`, and it cannot be combined with `prompt_extend: true` (`400 invalid_body`). Prompt expansion is available to an account once Hyperouter has enabled it; open a support ticket to request access. Until then `GET /v1/models` does not list `prompt_expand` for any model, and a request with `prompt_expand: true` on a model that offers it answers `403 permission_error`, code `feature_not_enabled`, `param` `prompt_expand`, with nothing held or sent to the provider. - **What rendered.** The image answer, the video object and `GET /v1/generations/{id}` carry `expanded_prompt`, the prompt the model received. It is kept for 30 days after the request, then answered as `null`. `prompt` stays your own text. - **Writer.** `prompt_expand_model` names the chat model that writes the rewrite; the default is `x-ai/grok-4.6`. It is valid only with `prompt_expand: true` (else `400 invalid_body`). A model that does not exist or is not a chat model is `400 unknown_model`, and one your account cannot call is `403 model_not_enabled`, both before anything is held. A writer that reads images is shown the request's pictures; one that does not writes from the text alone. - **Price.** The rewrite is billed on the same request, at the token prices of the model that wrote it (`prompt_expand_model`, else `x-ai/grok-4.6`) for the tokens it used, and its most is held up front with the render. The model reference gives each model's details. - **When it does not run.** A rewrite that fails, times out or cannot be used renders your prompt as sent, answers `expanded_prompt: null` and costs nothing. - **Language.** Spoken lines in a video follow `locale` (else English) where the model speaks that language. #### The rewrite as a chat model {#prompt-expansion-model} An account with prompt expansion also gets a chat model that answers the rewrite instead of rendering it; your [model reference](#models) names it. Call it on `POST /v1/chat/completions` with exactly one `user` message: its text is the prompt, and its `image_url` parts (https URLs, at most 5) are the pictures; a `data:` or `http://` URL is `400 invalid_image` with `param` `messages`. `target_model` is required: a model that lists `prompt_expand`. `seconds`, `locale` and `negative_prompt` are optional. A video target with a picture is written from that first frame. It does not stream (`stream: true` is `400 unsupported_parameter`), and any other message, a system message included, is `400 invalid_body`. This chat model always uses `x-ai/grok-4.6`; it does not take `prompt_expand_model`. `choices[0].message.content` is a JSON string, `{"prompt": "...", "negative_prompt": "..."}`, plus `"beats"` (one line per second) for a video target. It is billed at `x-ai/grok-4.6`'s token prices, shown in `usage`. A prompt that asks for the model's instructions answers `400 prompt_rejected`; a rewrite that is refused or comes back empty answers `422 expansion_refused`, and one that fails `503 upstream_error`. None of these is charged. ## Moderation The `moderation` field is OpenAI’s. It works on `/v1/chat/completions`, `/v1/images/generations`, `/v1/images/edits` and `/v1/videos` with its edit, extension and remix calls. | Value | Meaning | | --- | --- | | `auto` (default) | The vendor’s own screening applies. Hyperouter sends nothing extra. | | `low` | Hyperouter sends the provider’s opt-out from its screening, on models where the provider has one. On other models `low` is ignored without an error, as OpenAI does. Each model’s entry in your [model reference](https://dash.hyperouter.app/docs/models) says whether it has one. Send `moderation` in the body: a provider’s own screening header or field is not forwarded on these endpoints. | Any other value is refused: `400 invalid_request_error`, code `invalid_body`, `param` `moderation`, message `moderation must be auto or low.` `low` is available to an account once Hyperouter has enabled it; open a support ticket to request it. Until then a request with `low` answers `403 permission_error`, code `moderation_not_enabled`, `param` `moderation`, and is not sent to the provider. ```json { "model": "qwen/qwen-image-2.0", "prompt": "...", "moderation": "low" } ``` The response does not say whether the opt-out was used. The policies in the [Terms of Service](terms.md) still apply. A request that the vendor’s filter refuses fails with `400 content_rejected` and is not charged. ## Webhooks Instead of polling, an endpoint of yours can receive a request when a job ends. Add endpoints in the [dashboard](https://dash.hyperouter.app/webhooks). The shape is OpenAI’s webhooks, signed with the Standard Webhooks scheme, so the OpenAI SDKs verify them. | Event | Sent when | | --- | --- | | `video.completed`, `video.failed` | A video job ends. | | `image.completed`, `image.failed` | An image job that you sent with `Prefer: respond-async` ends. | | `raw.completed`, `raw.failed` | A raw call’s task ends (accounts with raw calls): charged, or its hold released. A raw call answered inline sends none. | The payload carries an id, never a result, a prompt or a charge. Fetch the object with your API key: `data.id` is the id that [GET /v1/videos/{id}](#videos) and [GET /v1/generations/{id}](#generations) take. For a raw task it is the `X-Generation-Id` that the create answered. ```json { "id": "evt_0194f2c4-7a1e-7b3c-9d02-5f6a1c0e8b21", "object": "event", "type": "video.completed", "created_at": 1761300000, "data": { "id": "0194f2c4-7a1e-7b3c-9d02-5f6a1c0e8b21" } } ``` ### Headers and verification Each delivery is a `POST` with `content-type: application/json` and these headers: - `webhook-id`: the event id. It is the same on every retry, so use it to de-duplicate. - `webhook-timestamp`: Unix seconds. - `webhook-signature`: `v1,` and the base64 HMAC-SHA256 of `id.timestamp.body`, keyed with the endpoint’s secret. The signing secret starts with `whsec_`. It is shown once, when you create the endpoint and when you rotate it. Verify the raw request body, before you parse it: ```js // Node, with the OpenAI SDK const event = client.webhooks.unwrap(rawBody, req.headers, process.env.HYPEROUTER_WEBHOOK_SECRET); if (event.type === "video.completed") { const video = await client.videos.retrieve(event.data.id); } ``` `unwrap` throws when the signature does not verify; answer such a request with an error status. The follow-up call uses the same base URL as the rest of this API. ### Endpoints, retries and limits - An endpoint must be `https` on port 443 (or no port) and a host name, not an IP address. The host must resolve to a public address; Hyperouter checks again when it connects. Redirects are not followed. - An account has at most 10 endpoints. Each endpoint picks the event types it wants. - A `2xx` within 10 seconds is success. Anything else is retried after about 5 seconds, 1 minute, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 10 hours, then the delivery is marked failed. - An endpoint whose deliveries all fail for 3 days is disabled, and the dashboard says why. Enable it again after you fix the receiver. - The dashboard lists each endpoint’s deliveries with status, attempts, last HTTP status and last error. It has Resend (same event id) and Send test event, which delivers a `webhook.test` event whose `data.id` is the endpoint’s id. ## MCP server {#mcp} `POST https://api.hyperouter.app/mcp` An SI assistant can work with your Hyperouter account through the Model Context Protocol (MCP). The server manages the dashboard: API keys, activity, credits, tickets, webhooks and the model list. It does not run models. Model calls still go to `/v1` with an API key. The server speaks Streamable HTTP and JSON-RPC 2.0. It supports MCP revisions `2025-11-25` and `2025-06-18`. It is stateless: there are no sessions, and `GET /mcp` answers `405`. ### Access tokens An MCP client authenticates with a dashboard access token, not with an API key. - Create a token in the [dashboard](https://dash.hyperouter.app/tokens). Give it a name, a scope and, optionally, an expiry. - A token starts with `hr_dt_`. It is shown once. Hyperouter stores only a hash of it. - An account user can hold at most 25 unexpired, unrevoked tokens. Revoke a token in the dashboard to stop it at once. - A token does not work on `/v1`, and an API key does not work on `/mcp`. - A token acts as the user who created it, with that user's permissions. The permissions are checked on every message, so a role change applies on the next call. | Scope | Tools the token can call | | --- | --- | | `read` | Tools that only read data. | | `write` | Every tool that the user has permission to use. | Send the token as a bearer token: ``` Authorization: Bearer hr_dt_... ``` ### Protocol | Method | Answer | | --- | --- | | `initialize` | The protocol version (the client's, if supported; else `2025-11-25`), the `tools` capability and server information. | | `ping` | An empty result. | | `tools/list` | The tools that the token's user and scope allow. | | `tools/call` | The result of one tool. | A notification, or a response to the server, answers `202` with no body. A JSON-RPC batch is refused with error `-32600`. A message must not exceed 1 MiB. An unknown `MCP-Protocol-Version` header answers `400`. A browser request must come from `https://dash.hyperouter.app`; a client that sends no `Origin` header is not affected. ```bash curl https://api.hyperouter.app/mcp \ -H "Authorization: Bearer $HYPEROUTER_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}' ``` ### Tools `tools/list` returns the current tool set. Each tool maps to one dashboard route and carries a JSON Schema for its arguments. The tools cover: - Account and keys: `get_me`, `list_api_keys`, `create_api_key`, `update_api_key`. - Activity and credits: `list_activity`, `get_activity`, `get_activity_summary`, `get_credits`, `get_topup_fee`. - Tickets: `list_tickets`, `create_ticket`, `get_ticket`, `reply_to_ticket`, `close_ticket`. - Webhooks: `list_webhooks`, `create_webhook`, `update_webhook`, `rotate_webhook_secret`, `delete_webhook`, `list_webhook_deliveries`, `resend_webhook_delivery`, `send_test_webhook`. - Models: `list_models`. - Staff tools, with the `admin_` prefix, appear only for users who hold the staff permission. A tool answers with one text content item that holds the JSON body of the route. When the route fails, the result has `isError: true` and the text ` (, HTTP )`. An unknown tool, or one that your token cannot see, is JSON-RPC error `-32602`. ### Approvals A sensitive write, such as crediting an account, is not run at once. The tool does not execute it. It answers with `status` set to `approval_required` and an `approval_url` on the dashboard. The user opens the link, confirms with a second factor and approves the action. An approval expires after 24 hours. The user can also reject it. ## Billing & limits {#billing} - Requests are paid from a prepaid USD balance, billed at the list price of the provider that served them, with no markup. - **Hold, then settle.** Before a request runs, Hyperouter holds the most it can cost: your input plus `max_tokens` for chat, the requested count for images and the seconds for video. When it finishes you are charged for what was used and the rest of the hold is released. The balance never goes negative. - A failed request, or a failed image or video job, costs nothing. A fallback attempt that fails costs nothing. - Every response carries its charge as `usage.cost`, a JSON number of USD: on chat, on images, on a settled video, and in `GET /v1/generations/{id}`. The dashboard shows every request and your spend by day, model and key. - Amounts are exact: money is counted in billionths of a dollar and never rounded through a float. Prices in `GET /v1/models` are decimal strings. - Top-ups are manual for now. Open a top-up request in the [dashboard](https://dash.hyperouter.app/credits) with the amount, the payment reference and a screenshot of the payment. Staff check the payment and credit your balance, and reply in the same ticket. - **Top-up fee.** Each top-up carries a fee of 5.5% of the amount paid, with a minimum of $0.80, deducted when the top-up is credited: a $100 payment credits $94.50, and a $10 payment credits $9.20. The fee in force is shown on the [Credits](https://dash.hyperouter.app/credits) page before you request a top-up, and each top-up in your history shows the amount paid and the fee. Requests themselves carry no fee. ### Rate limits {#rate-limits} Each account has a limit on requests per minute and on requests in flight at once. Both count every API key of the account together, and apply to every request that starts work: chat completions, image and video creates, and raw creates. Reading a result (`GET /v1/generations/{id}`, `GET /v1/videos…`, a raw task poll) does not count; raw polls have their own count, per provider, with the same requests-per-minute limit. Your tier follows the total you have topped up, before the top-up fee: | Tier | Topped up | Requests per minute | Concurrent requests | | --- | --- | --- | --- | | 1 | Under $10 | 60 | 10 | | 2 | $10 or more | 300 | 30 | | 3 | $100 or more | 1,200 | 100 | | 4 | $1,000 or more | 6,000 | 300 | Your current limits are on the [Credits](https://dash.hyperouter.app/credits) page. These are the defaults; staff can change them for the platform or for one account, and a change applies within a minute. Over either limit, the request is refused before anything is held or forwarded, with `429`, `type: rate_limit_error`, `code: rate_limited` and a `Retry-After` header in seconds. The message names the limit you reached. A request counts toward the minute even when it is refused. A request in flight, a stream included, counts until it ends. As a guard against key guessing, Hyperouter counts failed API key checks per client address. More than 30 in a minute blocks that address from every `/v1` route for 15 minutes: `429`, `rate_limit_error`, `too_many_auth_failures`, with `Retry-After`. A valid key does not lift the block. ## Support Open a ticket in the [dashboard](https://dash.hyperouter.app/tickets) for billing, top-ups and API questions; staff reply in the same thread. You can also write to [account@hyperouter.app](mailto:account@hyperouter.app). Please include the `id` from the response when you report a specific request. --- Source: https://hyperouter.app/terms.md # Terms of Service *Effective November 6, 2026* These Terms govern your access to and use of the Hyperouter website at hyperouter.app, the Hyperouter API, Hyperouter Chat, Hyperouter Studio and related services (together, the “Service”). The Service is provided by Hybermedia Co., Limited, a company incorporated in Hong Kong, of Room A08, 2/F, Lee Ka Industrial Building, 8 Ng Fong Street, San Po Kong, Kowloon, Hong Kong, under the name Hyperouter (“Hyperouter”, “we”, “us”). Please read them carefully. ## 1. Agreement {#t1} By creating an account, requesting or using an API key, or otherwise using the Service, you agree to these Terms and to our [Privacy Policy](privacy.md). If you use the Service on behalf of an organization, you confirm you are authorized to bind that organization, and “you” refers to it. You must be at least 18 years old, or the age of majority where you live, to use the Service. ## 2. Accounts and API keys {#t2} - You must give accurate account information and keep it up to date. - API keys are credentials. Keep them confidential and do not embed them in client-side code. You are responsible for all usage and charges made with your keys, including unauthorized usage, until you ask us to revoke them. - Notify us promptly at [account@hyperouter.app](mailto:account@hyperouter.app) if you believe a key has been exposed. ## 3. The Service {#t3} Hyperouter provides a single interface for sending requests to large language models operated by third parties (“Model Providers”). When you send a request, we route it to a Model Provider according to our routing logic and any preferences you set, and return the provider’s response to you. We do not create, train or control the models, and we do not review outputs before they are returned. Model availability, capabilities and prices are set by Model Providers and can change at any time. We may add or remove models and venues without notice. ## 4. Credits, fees and payment {#t4} - **Prepaid credits.** Usage is paid from a prepaid balance of credits denominated in US dollars. Requests are charged at the Model Provider’s list price for the tokens processed, as shown by our Models API at the time of the request. - **Purchases.** Credits are bought in US dollars, from US$5 to US$10,000 per purchase. Where card payment is offered, cards are processed by our payment processor, Stripe; we do not see or store your full card details. You can also pay by the other methods shown in the dashboard, credited by our staff after you open a top-up ticket and the payment is confirmed. The amount, the fee and the credits you will receive are shown before you pay. - **Prices on our website.** Prices shown on our website, in our documentation and in examples may be updated later than the prices we charge. The price that applies to a request is the one our Models API shows at the time of the request. - **Top-up fees.** A fee of 5.5% of the amount paid, with a minimum of US$0.80, is deducted from each purchase when it is credited to your balance, whatever the payment method. It is shown on the Credits page of the dashboard before you pay. We may change fees for future purchases. - **Failed requests.** Requests that fail on every venue are not charged. For streamed responses that stop partway, including when you disconnect, you are charged for the tokens the Model Provider produced up to that point. - **Rebates.** Where a written agreement with us gives you a rebate, it is credited to your balance as rebate credit after the month's statement is posted. Rebate credit can be used only to pay for usage of the Service. It is never refunded as cash, on request or on closure, and has no cash value. - **Refunds.** You may ask for a refund of a purchase within 24 hours of the time it was processed, by email to [account@hyperouter.app](mailto:account@hyperouter.app) or a ticket in the dashboard. We refund the credits from that purchase that are still unused, to the payment method you paid with, usually within 5 to 10 business days; the top-up fee is not refunded. After 24 hours, purchased credits are non-refundable, except where the law requires a refund. Rebate credit is not refundable. - **Expiry.** Credits do not expire while your account is in good standing. Credits are non-transferable, except that a reseller may move credit to and from the sub-accounts it funds, and have no cash value outside the Service. - **Taxes.** Prices exclude taxes. You are responsible for any taxes that apply to your purchases, other than taxes on our income. - **Disputes and chargebacks.** If you think a charge is wrong, contact us first at [account@hyperouter.app](mailto:account@hyperouter.app); we answer within 2 business days. If a payment is reversed or disputed with your bank, we may suspend the account until the matter is settled, and credits from that payment are removed from your balance. ## 5. Acceptable use {#t5} You agree not to use the Service to: - break any law, or infringe or misappropriate anyone’s rights; - generate or distribute child sexual abuse material, or content that sexualizes minors; - develop weapons capable of mass casualties, or plan or promote violence; - create malware, attack systems, or gain unauthorized access to data or networks; - defraud, impersonate or deceive people, including undisclosed automated messaging at scale; - violate the usage policies of the Model Provider serving your request; - resell raw API access or share keys outside your organization without our written consent; - probe, overload or interfere with the Service, or work around rate limits or billing. Model Providers may apply their own content filters. A request refused by a provider is not a breach of these Terms by us. ## 6. Your content {#t6} You keep all rights to the prompts, files and other inputs you send (“Inputs”) and, as between you and us, to the outputs you receive (“Outputs”). You grant us a limited license to process Inputs and Outputs only as needed to provide, secure and bill for the Service, including passing them to the Model Provider that serves your request. We keep Inputs and Outputs encrypted for 180 days to meet our legal obligations, and disclose them only to authorities that present a valid legal request. We do not use your content to train models. See the [Privacy Policy](privacy.md) for details. You are responsible for your Inputs and for how you use Outputs, and for checking Outputs for accuracy before relying on them. ## 7. Model Providers {#t7} Each Model Provider processes your request under its own terms and data policies. By routing to a provider, you agree to comply with that provider’s usage policies. You can restrict which providers receive your requests using routing settings. We are not responsible for the acts or omissions of Model Providers. ## 8. Beta features {#t8} Features labeled beta, preview or early access, including Hyperouter Chat and the API during private beta, are provided for evaluation. They may change or be discontinued, may be less reliable, and are excluded from any uptime commitment. ## 9. Suspension and termination {#t9} You may stop using the Service at any time and close your account from the dashboard; closure takes effect at once and cannot be undone. A staff account must lose its staff roles before it can be closed. We may suspend or terminate access if you breach these Terms, if your use creates security or legal risk, or if required by law. Where reasonable we will give notice first. On closure, the remaining balance is not refunded; to get back a recent purchase, ask for its refund within 24 hours of it (Section 4) before you close the account. Rebate credit is not refunded, and a sub-account's remaining credit returns to the reseller that funded it. ## 10. Disclaimers {#t10} THE SERVICE AND ALL OUTPUTS ARE PROVIDED “AS IS” AND “AS AVAILABLE”. TO THE FULLEST EXTENT PERMITTED BY LAW, WE DISCLAIM ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND ANY WARRANTY THAT OUTPUTS ARE ACCURATE, COMPLETE OR SAFE. AI outputs can be wrong. Do not rely on them as professional, legal, medical or financial advice. ## 11. Limitation of liability {#t11} TO THE FULLEST EXTENT PERMITTED BY LAW, HYPEROUTER WILL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL OR PUNITIVE DAMAGES, OR FOR LOST PROFITS, REVENUE, DATA OR GOODWILL. OUR TOTAL LIABILITY FOR ALL CLAIMS RELATING TO THE SERVICE IS LIMITED TO THE GREATER OF (A) THE AMOUNTS YOU PAID US IN THE 12 MONTHS BEFORE THE CLAIM AND (B) US$100. These limits do not apply where they are prohibited by law. ## 12. Indemnity {#t12} You will defend and indemnify Hyperouter against third-party claims, and related losses and costs, arising from your Inputs, your use of Outputs, or your breach of these Terms. ## 13. Changes to these Terms {#t13} We may update these Terms. For material changes we will give at least 14 days’ notice by email or on this page before they take effect. Continuing to use the Service after that means you accept the updated Terms. For accounts opened before November 6, 2026, the changes to Sections 4 and 9 on refunds at account closure take effect on November 20, 2026. ## 14. General {#t14} These Terms are the entire agreement between you and us about the Service and replace any earlier understanding. If any part is unenforceable, the rest remains in effect. Our failure to enforce a provision is not a waiver. You may not assign these Terms without our consent; we may assign them in connection with a merger, acquisition or sale of assets. These Terms are governed by the laws of the Hong Kong Special Administrative Region, without regard to conflict-of-law rules, and disputes will be resolved in the courts of Hong Kong unless applicable law requires otherwise. ## 15. Contact {#t15} Hybermedia Co., Limited, Room A08, 2/F, Lee Ka Industrial Building, 8 Ng Fong Street, San Po Kong, Kowloon, Hong Kong. Billing, refunds and questions about these Terms: [account@hyperouter.app](mailto:account@hyperouter.app). We answer within 2 business days. --- Source: https://hyperouter.app/privacy.md # Privacy Policy *Effective November 6, 2026* This policy explains what information Hybermedia Co., Limited, of Room A08, 2/F, Lee Ka Industrial Building, 8 Ng Fong Street, San Po Kong, Kowloon, Hong Kong, which runs the Service under the name Hyperouter (“Hyperouter”, “we”, “us”), collects when you use hyperouter.app, the Hyperouter API, Hyperouter Chat and Hyperouter Studio (the “Service”), how we use it, and the choices you have. We are responsible for this information. Card payments, where offered, are handled by Stripe, which receives your card details directly under its own privacy policy. ## 1. Summary {#p1} - We keep the inputs and outputs of every request encrypted for 180 days to meet our legal obligations. We disclose them only to authorities that present a valid legal request. They are never used to train models, never shown in the dashboard and never sold. - We do not sell personal information, and we do not use your content to train models. - Your prompts are sent to the model provider that serves each request. You control which providers are eligible. - We keep request metadata (model, provider, token counts, cost, time) so we can bill you and keep the Service running. ## 2. Information we collect {#p2} ### Information you give us - **Account and contact details:** name, email, company, and anything you include when you request access or email us. - **Payment information:** we run no payment processor and hold no card or wallet details. When you pay for a top-up, you tell us the payment reference in a support ticket; staff check the payment and credit your balance, and the reference, amount and fee are noted in your account's credit ledger. Any screenshot of the payment you attach to the ticket is stored with the ticket. ### Information collected automatically - **Request metadata:** timestamp, API key ID, model, provider, token counts, cost, latency, status code and the optional `user` field you send. - **Technical data:** IP address, user agent and request headers, used for security, abuse prevention and rate limiting. ## 3. Prompts and outputs {#p3} Prompts, files and model outputs pass through our systems to be routed and returned. To meet our legal obligations, we keep a copy of the inputs and outputs of every request for 180 days, and then it is deleted automatically. The inputs are the request you send, with its prompts, the files you upload, and the files we fetch from links you give to serve it. The outputs are the reply, streamed or not, and the images and videos generated for you. This copy is encrypted before it is stored. We disclose it only to authorities that present a valid legal request, and only the engineers who answer such a request can read it. It is never used to train models, never shown in the dashboard or returned by the API, and never sold. To fulfil a request we send its content to the model provider selected for that request. Providers process it under their own terms; some may retain data for a limited time for abuse monitoring. ## 4. How we use information {#p4} - to provide, route and bill requests, and to show you usage and spend; - to issue API keys and respond to your emails; - to detect and prevent fraud, abuse and security incidents; - to measure provider performance and improve routing, using metadata only; - to send service notices, such as changes to these policies or to pricing; - to meet legal, tax and accounting obligations. Where the GDPR applies, we rely on performance of our contract with you, our legitimate interests in running a secure service, and compliance with legal obligations. ## 5. Who we share information with {#p5} - **Model providers,** which receive request content needed to serve your request. - **Service providers** that host our infrastructure or send email for us, under contracts that limit their use of the data. - **Authorities,** when required by law or to protect the rights, safety or property of our users or others. The copy of your inputs and outputs (section 3) is disclosed only in answer to a valid legal request. - **A successor,** if Hyperouter is involved in a merger, acquisition or sale of assets, subject to this policy. ## 6. Retention {#p6} | Data | Kept for | | --- | --- | | Prompts, files and outputs | 180 days, encrypted, to meet legal obligations (section 3) | | A rewritten prompt (`prompt_expand`), readable through the API | 30 days | | Request metadata | As long as the credit ledger: each request is the billing record of its charge | | Credit ledger and payment references | As long as tax and accounting law requires, typically 7 years | | Sign-in and session logs (IP, user agent) | 90 days | | Audit log of changes to your account, keys and credits, with the IP they came from | Not deleted: it is the permanent record of who changed what | | Support tickets and their attachments | Not deleted, also after the account is closed: they are the record of what was asked, paid and refunded | | Access request emails | Until the request is closed, plus 12 months | ### Closing your account You can close your account yourself on the Account page of the dashboard. Closure takes effect at once and cannot be undone. - **At once:** every API key, dashboard access token and session stops working, and your webhook endpoints are switched off. Requests already running finish and are charged as usual. Nobody can sign in to the account again. - **Deleted when nothing is owed either way:** your linked Google and GitHub sign-ins, passkeys, authenticator app, recovery codes, sign-in log and sessions are deleted, and the name, email address and profile picture on the account are replaced with placeholders. If no credit is left and no request is still running, this happens at closure. - **If credit is left,** we refund it less the top-up fee, as the Terms set out, and write to your account email to arrange the refund. The deletion above follows once the refund is made; until then your email address is kept so we can reach you. - **Kept:** the credit ledger, request metadata, support tickets and the audit log stay for the periods in the table above, tied to the account's ID rather than to your name or email. The encrypted copy of inputs and outputs is deleted 180 days after each request (section 3), whether or not the account is closed. ## 7. Security {#p7} Traffic to the Service is encrypted in transit with TLS. API keys are stored hashed. The copy of your inputs and outputs is encrypted with AES-256 before it is stored. Access to production systems is limited to staff who need it. No system is perfectly secure; if we learn of a breach that affects your personal information, we will notify you as required by law. ## 8. Your rights {#p8} Depending on where you live, you may have the right to access, correct, delete or export your personal information, to object to or restrict certain processing, and to withdraw consent. California residents may request to know what we collect and ask us to delete it; we do not sell or share personal information for cross-context behavioral advertising. To make a request, email [account@hyperouter.app](mailto:account@hyperouter.app) from the address on your account. We will respond within 30 days. You may also complain to your local data protection authority. ## 9. International transfers {#p9} We and our providers may process information in countries other than yours. Where required, we use appropriate safeguards such as Standard Contractual Clauses for these transfers. ## 10. Cookies {#p10} hyperouter.app does not use advertising or analytics cookies. The site loads fonts from Google Fonts, which receives your IP address when you visit. Signed-in areas may use strictly necessary cookies to keep you logged in. ## 11. Children {#p11} The Service is not directed to children under 18, and we do not knowingly collect their information. If you believe a child has given us information, contact us and we will delete it. ## 12. Changes {#p12} We may update this policy. We will post the new version here with a new effective date and, for material changes, notify account holders by email before they take effect. ## 13. Contact {#p13} Privacy questions or requests: [account@hyperouter.app](mailto:account@hyperouter.app).