Hyperouter API
These docs are available as Markdown for SI assistants. Start at /llms.txt, or fetch /llms-full.txt for every page in one file. This page is /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.
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 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
- Sign in and create an API key. Keys start with
sk-hr-v1-and are shown once. - Store it in an environment variable, for example
HYPEROUTER_API_KEY. - Add credits: open a top-up request in the dashboard. Staff credit your balance when the payment is confirmed.
- Point any OpenAI SDK at the base URL above and pick a model.
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"}] }'
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);
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 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. |
curl https://api.hyperouter.app/v1/generations/0194f2c4-7a1e-7b3c-9d02-5f6a1c0e8b21 \
-H "Authorization: Bearer $HYPEROUTER_API_KEY"
{ "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.
{
"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). |
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, 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. |
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. |
expansion_refused | 422 | The prompt expansion model's rewrite was refused or came back empty. Not billed. See Prompt expansion. |
invalid_api_key | 401 | See Authentication. |
insufficient_credits | 402 | See Billing & limits. |
account_suspended | 403 | The account is suspended. |
moderation_not_enabled | 403 | moderation: "low" is not enabled for the account. See 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. |
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. |
too_many_auth_failures | 429 | More than 30 requests from your address in a minute used an invalid API key. See 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_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
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) |
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.
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}'
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}, )
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
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} 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.
{
"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
503timeoutof a sync image, whose job goes on and is billed when it succeeds; poll GET /v1/generations/{id} with itsgeneration_id; - a
500after the job was created, for example when a finished image cannot be read back; 409idempotency_key_reusedandidempotency_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
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
{
"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, <provider>/<name>, 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. |
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
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. |
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). |
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 lists exactly what it takes. Grok 4.20 and newer ignore logprobs and top_logprobs, so they are not listed.
The provider object
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}. 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).
{
"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
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
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). Nothing is held or forwarded. | |
409 | conflict | idempotency_key_replayed, idempotency_key_reused, idempotency_key_in_use | See Retries and 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). | |
503 | upstream_error | timeout | The provider accepted the request and did not finish it in time. Billed on the hold (see Streaming). | |
401, 403, 500 | See Authentication. | |||
Images
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 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_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. |
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. |
| model-specific fields | as declared | As declared | As declared | See Model-specific 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
{
"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}. |
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_modesincludesync, the request waits for the image for up to 90 seconds and answers with its real status:200with 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
503with codetimeoutand the response headerx-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 asgeneration_id: poll GET /v1/generations/{id} for the result. You are billed only if it succeeds. To retry safely yourself, send an Idempotency-Key: a retry with the same key waits on the same job. - A model whose
response_modesare onlyasyncneedsPrefer: respond-async. Without it the answer is400with codeasync_required. To avoid a held connection on any model that lists
async, sendPrefer: respond-async(RFC 7240). The header may repeat and may carry other preferences, for examplePrefer: wait=10, respond-async. You get202at once with the response headerPreference-Applied: respond-async:{ "id": "0194f2c4-7a1e-7b3c-9d02-5f6a1c0e8b21", "status": "queued" }Poll GET /v1/generations/{id}, or receive an
image.completedorimage.failedwebhook. Webhooks fire for images only for jobs you sent with this header.
Examples
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}'
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. | |
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. | |
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). | |
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 <url>.
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 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. 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
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. 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 | false | Addition. See Prompt expansion. |
prompt_expand_model | string | No | As for images | x-ai/grok-4.6 | Addition. With prompt_expand: the model that writes the rewrite. See 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 | Addition. Fallback models and routing preferences. | |
moderation | string | No | auto, low | auto | Addition. See Moderation. |
| model-specific fields | as declared | As declared | As declared | See Model-specific parameters. |
Edit, extend and remix
| Endpoint | Body (JSON) |
|---|---|
POST /v1/videos/edits | model, video (required), prompt, resolution and the shared features. video is {"id": "<your video 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
{
"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.
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
}'
{
"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: <field> 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. | |
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 in the dashboard lists every video model your account can call.
Generations
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).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (UUID) | Yes | The id of a chat completion, image answer or Video object. |
Response
{
"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. |
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. | |||
Model-specific 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 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_errorwith codeunsupported_parameter,paramset to the field name (for exampleloras) and the messageThis 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,paramset to the name and a message such asloras must be of type array. - A value outside
valuesis refused with400 invalid_bodyand the message<name> 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_bodywithparamnaming the field, for exampleseconds must be one of 5, 8. - A combination a vendor does not allow, such as reference media together with keyframes, answers
400 invalid_combinationwith the reason. The checks run before anything is charged. - Fields that change the price, such as
resolutionandaudio, 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
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}carryexpanded_prompt, the prompt the model received. It is kept for 30 days after the request, then answered asnull.promptstays your own text. - Writer.
prompt_expand_modelnames the chat model that writes the rewrite; the default isx-ai/grok-4.6. It is valid only withprompt_expand: true(else400 invalid_body). A model that does not exist or is not a chat model is400 unknown_model, and one your account cannot call is403 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, elsex-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: nulland costs nothing. - Language. Spoken lines in a video follow
locale(else English) where the model speaks that language.
The rewrite as a chat model
An account with prompt expansion also gets a chat model that answers the rewrite instead of rendering it; your model reference 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 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.
{
"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 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. 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} and GET /v1/generations/{id} take. For a raw task it is the X-Generation-Id that the create answered.
{
"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 ofid.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:
// 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
httpson 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
2xxwithin 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.testevent whosedata.idis the endpoint’s id.
MCP server
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. 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.
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 <message> (<code>, HTTP <status>). 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
- 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_tokensfor 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 inGET /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/modelsare decimal strings. - Top-ups are manual for now. Open a top-up request in the dashboard 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 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
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 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 for billing, top-ups and API questions; staff reply in the same thread. You can also write to [email protected]. Please include the id from the response when you report a specific request.