<!-- toc: Primeros pasos -->
# Hyperouter API {#introduction}

*Versión v1 · Actualizado el 25 de octubre de 2026*

> Esta documentación está disponible en Markdown para asistentes de IA. Empieza en [/llms.txt](https://hyperouter.app/llms.txt), u obtén [/llms-full.txt](https://hyperouter.app/llms-full.txt) para tener todas las páginas en un solo archivo. Esta página es [/docs.md](https://hyperouter.app/docs.md).

Hyperouter es una API HTTP única delante de muchos proveedores de modelos. Envías una solicitud que indica un modelo. Hyperouter elige un proveedor que pueda atenderla, le reenvía la solicitud y cobra de tu saldo el precio de lista de ese proveedor.

`BASE https://api.hyperouter.app/v1`

Todos los endpoints siguen las estructuras de solicitud y respuesta de OpenAI. Los SDK de OpenAI funcionan cuando cambias la URL base y la clave de API. Hyperouter añade algunos campos a los cuerpos de OpenAI. En esta referencia están marcados como añadidos.

| Endpoint | Finalidad | Autenticación |
| --- | --- | --- |
| `GET /v1/models` | Enumera los modelos que tu cuenta puede llamar. | Clave de API |
| `POST /v1/chat/completions` | Chat completion, con streaming opcional. | Clave de API |
| `POST /v1/images/generations` | De texto a imagen. | Clave de API |
| `POST /v1/images/edits` | Edita imágenes. | Clave de API |
| `POST /v1/videos` | Crea un vídeo a partir de un prompt, con una imagen opcional. | Clave de API |
| `POST /v1/videos/edits` | Edita o escala un vídeo. | Clave de API |
| `POST /v1/videos/extensions` | Extiende un vídeo. | Clave de API |
| `POST /v1/videos/{id}/remix` | Crea una nueva versión de un vídeo con un nuevo prompt. | Clave de API |
| `GET /v1/videos`, `GET /v1/videos/{id}`, `GET /v1/videos/{id}/content`, `DELETE /v1/videos/{id}` | Lista, consulta, descarga y elimina vídeos. | Clave de API |
| `GET /v1/generations/{id}` | Consulta el registro de cualquier solicitud: estado, consumo y cargo. | Clave de API |
| `GET /v1/credits` | Tu saldo, lo que retienen las solicitudes abiertas y lo que te queda por gastar (`balance_usd`, `held_usd`, `available_usd`, cadenas decimales en USD). | Clave de API |

> [Inicia sesión en dash.hyperouter.app](https://dash.hyperouter.app) con Google o GitHub para crear una clave de API. `GET /v1/models` devuelve los modelos que puedes llamar en el momento de la solicitud.

## Inicio rápido {#quickstart}

1. [Inicia sesión](https://dash.hyperouter.app) y crea una clave de API. Las claves empiezan por `sk-hr-v1-` y se muestran una sola vez.
2. Guárdala en una variable de entorno, por ejemplo `HYPEROUTER_API_KEY`.
3. Añade crédito: abre una solicitud de recarga en el panel. El equipo acredita tu saldo cuando se confirma el pago.
4. Apunta cualquier SDK de OpenAI a la URL base anterior y elige un modelo.

```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)
```

## Autenticación {#authentication}

Envía tu clave de API como token bearer en cada solicitud.

```
Authorization: Bearer sk-hr-v1-...
```

Las claves dan acceso a tu saldo. Guárdalas en tu servidor. Nunca las pongas en código de navegador ni de móvil. Hyperouter guarda solo un hash de cada clave, así que una clave perdida no se puede volver a mostrar: crea una nueva.

En el [panel](https://dash.hyperouter.app/keys) puedes ponerle nombre a cada clave, asignarle un límite de crédito y desactivarla. Una clave desactivada deja de funcionar al instante. Una solicitud que llevaría a una clave más allá de su límite se rechaza con `402 insufficient_credits`.

### Errores de autenticación

| Estado | type | code | Cuándo | message |
| --- | --- | --- | --- | --- |
| `401` | `authentication_error` | `invalid_api_key` | Falta el encabezado, no es `Bearer`, o la clave no empieza por `sk-hr-v1-`. | `The API key is missing or malformed. Send Authorization: Bearer sk-hr-v1-…` |
| `401` | `authentication_error` | `invalid_api_key` | La clave es desconocida o está desactivada. | `The API key is not valid.` |
| `403` | `permission_error` | `account_suspended` | La cuenta está suspendida. La comprobación se hace en cada solicitud. | `This account is suspended. Open a support ticket.` |
| `429` | `rate_limit_error` | `too_many_auth_failures` | Más de 30 solicitudes desde tu dirección en un minuto usaron una clave ausente o no válida. Todas las solicitudes a `/v1` desde esa dirección se rechazan durante 15 minutos, incluso con una clave válida. `Retry-After` indica los segundos restantes. | `Too many failed API key checks from this address. Try again after the time in Retry-After.` |
| `500` | `server_error` | `internal` | Hyperouter no pudo comprobar la clave. | `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 } }
```

## Errores {#errors}

Todos los errores usan el objeto de error de OpenAI y un estado HTTP correspondiente.

```json
{
  "error": {
    "message": "n must be an integer from 1 to 128",
    "type": "invalid_request_error",
    "code": "invalid_body",
    "param": "n"
  }
}
```

| Campo | Tipo | Descripción |
| --- | --- | --- |
| `message` | string | Una frase que dice qué falla. No la analices programáticamente. |
| `type` | string | La clase de error. Consulta la lista siguiente. |
| `code` | string | Un identificador estable de la causa. Compara por este campo. |
| `param` | string \| null | El campo de la solicitud que causa el error, o `null`. |

### Tipos y estados

| Estado | type | Significado |
| --- | --- | --- |
| `400` | `invalid_request_error` | La solicitud no es válida. `code` indica la causa y `param` el campo. |
| `401` | `authentication_error` | La clave de API falta o no es válida. |
| `402` | `insufficient_credits` | Tu saldo, o el límite de la clave, no cubre lo máximo que podría costar la solicitud. |
| `403` | `permission_error` | La cuenta está suspendida. |
| `404` | `not_found` | El id no existe, pertenece a otra cuenta, o un vídeo ha superado su `expires_at`. |
| `409` | `conflict` | El objeto no está en un estado que permita la llamada (`video_not_finished`), o hay un conflicto de `Idempotency-Key` (consulta [Reintentos e idempotencia](#idempotency)). |
| `422` | `invalid_request_error` | La solicitud era válida, pero su respuesta se rechazó (`expansion_refused`). No se factura. |
| `429` | `rate_limit_error` | Todos los proveedores del modelo están al límite de su capacidad, tu cuenta supera sus [límites de velocidad](#rate-limits), o han fallado demasiadas comprobaciones de clave de API desde tu dirección. Reintenta después del tiempo indicado en el encabezado `Retry-After`. |
| `500`, `501` | `server_error` | Hyperouter ha fallado, o el endpoint no está implementado. No se factura. |
| `503` | `upstream_error` | Un proveedor falló o no respondió a tiempo: `code` indica cuál (`all_venues_failed`, `upstream_error` o `timeout`). El encabezado `X-Upstream-Status` contiene `502` (fallo) o `504` (tiempo agotado). |

Hyperouter nunca responde `502` ni `504`: te llegarían como una página simple de nuestra red perimetral, sin el cuerpo del error. Donde una pasarela respondería con ellos, recibes `503` con el cuerpo de más arriba y el estado de la pasarela en `X-Upstream-Status`.

### Códigos que puedes recibir

| code | Estado | Cuándo |
| --- | --- | --- |
| `invalid_body` | 400 | Un campo del cuerpo o un parámetro de consulta no se puede analizar, tiene el tipo equivocado o está fuera de sus valores permitidos. `param` lo nombra. |
| `unsupported_content_type` | 400 | `/v1/images/generations` recibió un cuerpo `multipart/form-data`. Envía JSON. `param` es `null`. |
| `request_too_large` | 413 | Una subida `multipart/form-data` supera los 50 MiB. Envía los medios grandes como URL https. |
| `unknown_model` | 400 | El modelo no existe para este endpoint, o ningún proveedor puede atenderlo con tus preferencias de `provider`. |
| `unsupported_parameter` | 400 | Un parámetro que existe para otros modelos no está disponible en este modelo. Consulta [Parámetros específicos de cada modelo](#parameters). |
| `invalid_combination` | 400 | El proveedor no permite estos parámetros juntos, por ejemplo medios de referencia con fotogramas clave. |
| `invalid_size` | 400 | `size` no es `WIDTHxHEIGHT`, o no está permitido para el modelo. |
| `too_many_images` | 400 | `n` o el número de imágenes de entrada supera el límite. |
| `invalid_image`, `invalid_video` | 400 | La URL de una imagen o un vídeo no es https a un host público: una URL `data:`, una URL `http://`, una dirección IP o un nombre interno. `param` nombra el campo. |
| `image_required` | 400 | El modelo necesita una imagen de entrada y no enviaste ninguna (o solo edita y llamaste a generations). |
| `invalid_seconds` | 400 | `seconds` no está permitido para el modelo, o falta en un vídeo indicado por URL. |
| `unsupported_operation` | 400 | El modelo no admite la operación (editar, extender o remix, o edición de imágenes). |
| `video_not_ready` | 400 | El vídeo de origen no ha terminado. |
| `context_length_exceeded` | 400 | El prompt del chat y `max_tokens` no caben en el contexto del modelo. |
| `content_rejected` | 400 | El filtro de seguridad del proveedor rechazó la entrada. |
| `vendor_rejected` | 400 | El modelo rechazó la solicitud por no ser válida. Revisa sus parámetros y entradas. |
| `clip_too_long` | 400 | Un clip de referencia más los `seconds` solicitados superan el límite del modelo (30 s). No se factura. |
| `prompt_rejected` | 400 | El prompt pide al modelo de expansión del prompt sus instrucciones. No se factura. Consulta [Expansión del prompt](#prompt-expansion-model). |
| `expansion_refused` | 422 | La reescritura del modelo de expansión del prompt se rechazó o llegó vacía. No se factura. Consulta [Expansión del prompt](#prompt-expansion-model). |
| `invalid_api_key` | 401 | Consulta [Autenticación](#authentication). |
| `insufficient_credits` | 402 | Consulta [Facturación y límites](#billing). |
| `account_suspended` | 403 | La cuenta está suspendida. |
| `moderation_not_enabled` | 403 | `moderation: "low"` no está habilitado para la cuenta. Consulta [Moderación](#moderation). |
| `feature_not_enabled` | 403 | `prompt_expand` no está habilitado para la cuenta, en un modelo que lo ofrece. Abre un ticket de soporte para solicitar el acceso. No se retiene nada ni se envía al proveedor. Consulta [Expansión del prompt](#prompt-expansion). |
| `model_not_enabled` | 403 | El modelo requiere que se conceda acceso a tu cuenta. Abre un ticket de soporte para solicitarlo. No se retiene nada ni se envía nada al proveedor. |
| `not_found` | 404 | El objeto no existe. |
| `video_not_finished` | 409 | Eliminaste un vídeo que todavía se está creando. |
| `venue_busy` | 429 | Todos los proveedores del modelo de chat superan su presupuesto de velocidad. |
| `rate_limited` | 429 | Tu cuenta supera sus solicitudes por minuto o sus solicitudes concurrentes. Consulta [Límites de velocidad](#rate-limits). |
| `too_many_auth_failures` | 429 | Más de 30 solicitudes desde tu dirección en un minuto usaron una clave de API no válida. Consulta [Límites de velocidad](#rate-limits). |
| `all_venues_failed` | 503 | Ningún proveedor está disponible, o todos han fallado. |
| `upstream_error` | 503 | Un trabajo de imagen o vídeo falló en el modelo. No se te cobra. |
| `internal`, `server_error`, `no_price` | 500 | Hyperouter ha fallado (`no_price`: el modelo no se puede valorar ahora). No se factura. |
| `not_implemented` | 501 | `This endpoint is not implemented yet.` |
| `invalid_idempotency_key` | 400 | El encabezado `Idempotency-Key` supera los 255 caracteres o no es ASCII imprimible. Consulta [Reintentos e idempotencia](#idempotency). |
| `idempotency_key_reused` | 409 | La `Idempotency-Key` ya se usó con una solicitud distinta. |
| `idempotency_key_in_use` | 409 | Una solicitud con esta `Idempotency-Key` todavía se está iniciando. Reintenta con la misma clave. |
| `idempotency_key_replayed` | 409 | Un chat completion con esta `Idempotency-Key` ya se ejecutó. `error.generation_id` es su id. |
| `timeout` | 503 | Una imagen síncrona no estuvo lista en 90 segundos. Todavía se está creando: `error.generation_id` es el id que debes consultar. |

## Reintentos e idempotencia {#idempotency}

Un error de red o un tiempo de espera agotado no te dice si Hyperouter recibió la solicitud. Si la vuelves a enviar, puedes iniciar un segundo trabajo y pagar dos veces. Una `Idempotency-Key` hace segura la repetición: Hyperouter reconoce la repetición, no inicia un segundo trabajo, no hace una segunda retención y responde a partir de la primera solicitud.

**Envía una en cada solicitud que cueste dinero.** Genera un valor aleatorio nuevo, como un UUID v4, para cada solicitud nueva. Guárdalo junto con la solicitud y envía el mismo valor cada vez que repitas esa solicitud.

### Enviar una clave

El encabezado se acepta en estos endpoints:

| Endpoint | Una repetición devuelve |
| --- | --- |
| `POST /v1/images/generations`, `POST /v1/images/edits` | la imagen de la primera solicitud, o su `202` con el mismo `id` bajo `Prefer: respond-async` |
| `POST /v1/videos`, `/v1/videos/edits`, `/v1/videos/extensions`, `/v1/videos/{id}/remix` | el objeto Video de la primera solicitud, tal como está ahora |
| `POST /v1/chat/completions` | `409` `idempotency_key_replayed` con el id de la primera solicitud (consulta [Chat completions](#idempotency-chat)) |

Los SDK de OpenAI no envían este encabezado por su cuenta. Pásalo como encabezado adicional; los SDK lo reenvían sin cambios en sus reintentos automáticos.

```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 } },
);
```

### Qué devuelve una repetición

| Envías | Hyperouter responde |
| --- | --- |
| Una clave nueva | Ejecuta la solicitud con normalidad. |
| La misma clave y la misma solicitud, después de que la primera fuera aceptada | El resultado de la primera solicitud, como en la tabla anterior. Ni segundo trabajo, ni segunda retención, ni segundo cargo. |
| La misma clave y la misma solicitud, mientras la primera todavía se está aceptando | Espera a la primera y luego responde como arriba. Si la primera no ha iniciado su trabajo en 60 segundos: `409` `idempotency_key_in_use`. Reintenta con la misma clave. |
| La misma clave y una solicitud distinta | `409` `idempotency_key_reused`. No se ejecuta nada. Usa una clave nueva. |
| La misma clave, después de que la primera fuera rechazada antes de iniciar ningún trabajo (un `400`, un `402` o un `5xx` enviado sin `x-should-retry: false`) | Ejecuta la solicitud como nueva, para que puedas corregirla y volver a enviarla con la misma clave. |
| La misma clave, más de 30 minutos después de la primera solicitud | Ejecuta la solicitud como nueva. |
| Una clave de más de 255 caracteres, o que no sea ASCII imprimible | `400` `invalid_idempotency_key`. No se ejecuta nada. |

### Reglas

- **Ámbito.** Una clave pertenece a la clave de API que la envió. Dos claves de API pueden usar el mismo valor sin afectarse entre sí.
- **Ventana.** Una clave se respeta durante 30 minutos después de la primera solicitud. Reintenta dentro de ese plazo.
- **Misma solicitud.** Dos solicitudes son la misma cuando coinciden el método, la ruta y el cuerpo. El JSON se compara después de analizarlo, así que el orden de los campos y los espacios no importan. Una imagen o un vídeo es una URL en el cuerpo, así que se compara la URL, nunca los bytes que sirve.
- **Facturación.** Una solicitud hecha con una clave se factura como máximo una vez, sea cual sea el número de repeticiones. Una repetición respondida a partir de la primera solicitud no cuesta nada.
- **Garantía.** Una clave nunca inicia más de un trabajo, tampoco cuando dos repeticiones llegan al mismo tiempo.

### Chat completions {#idempotency-chat}

Las respuestas de chat no se reproducen. Una repetición con la misma clave responde `409` con el código `idempotency_key_replayed`, y `error.generation_id` es el id de la primera solicitud. La solicitud no se vuelve a ejecutar ni a facturar. Consulta la primera solicitud con [GET /v1/generations/{id}](#generations) para ver su estado, consumo y coste. Si el primer chat completion falló y no se facturó, su clave queda libre y una repetición se ejecuta como solicitud nueva.

```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"
  }
}
```

### Reintentos automáticos en los SDK de OpenAI

Los SDK de OpenAI reintentan dos veces, por defecto, una respuesta `408`, `409`, `429` o `5xx`, salvo que la respuesta incluya el encabezado `x-should-retry: false`. Hyperouter envía `x-should-retry: false` en toda respuesta en la que un reintento iniciaría un segundo trabajo o un segundo cargo:

- el `503` `timeout` de una imagen síncrona, cuyo trabajo continúa y se factura si tiene éxito; consulta [GET /v1/generations/{id}](#generations) con su `generation_id`;
- un `500` después de crear el trabajo, por ejemplo cuando no se puede volver a leer una imagen terminada;
- `409` `idempotency_key_reused` e `idempotency_key_replayed`, que un reintento no puede cambiar.

Nunca lo envía en un `429`, en un `409` `idempotency_key_in_use` ni en un `5xx` anterior a que exista cualquier trabajo. Esos son seguros de reintentar. Con una `Idempotency-Key`, todos los reintentos son seguros.

<!-- toc: Referencia de la API -->
## Modelos {#models}

`GET /v1/models`

Enumera todos los modelos que tu cuenta puede llamar ahora mismo, con sus precios y lo que admiten. Un modelo sin un proveedor habilitado no aparece. Algunos modelos requieren que se conceda acceso a tu cuenta: aparecen una vez concedido. Este endpoint necesita una clave de API y enumera los modelos que puede llamar la cuenta de esa clave. La respuesta puede almacenarse en caché durante 60 segundos, solo por tu cliente.

### Parámetros

Ninguno.

### Respuesta

```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": {}
  }]
}
```

| Campo | Tipo | Descripción |
| --- | --- | --- |
| `id` | string | El id del modelo, `<provider>/<name>`, por ejemplo `x-ai/grok-4.7` o `qwen/qwen-image-2.0`. Envíalo como `model`. Un modelo renombrado sigue respondiendo a su id anterior. |
| `response_modes` | string[] | Cómo responde el modelo: `sync` (una respuesta retenida), `async` (envía `Prefer: respond-async`, recibe `202` y consulta periódicamente) y `stream` (`"stream": true`). Un modelo que solo lista `async` necesita el encabezado `Prefer`, y `stream` solo funciona donde aparece listado. El vídeo es siempre asíncrono a través de la API de vídeos, con o sin el encabezado. |
| `name` | string | Nombre para mostrar. |
| `kind` | string | `chat`, `image` o `video`. |
| `context_length` | integer | Ventana de contexto en tokens. `0` para modelos de imagen y vídeo. |
| `architecture` | object | `input_modalities` y `output_modalities`. La salida es `["text"]`, `["image"]` o `["video"]`. |
| `pricing` | object of strings | USD por unidad, como cadenas decimales, en el nivel base: `prompt` y `completion` (por token), `input_cache_read` (por token en caché), `request`, `image` (por imagen), `video_second` (por segundo de vídeo). Un vídeo con precio según la resolución muestra el precio más bajo. Una unidad que no se aplica es `"0"`. |
| `top_provider` | object | `context_length` y `max_completion_tokens`. |
| `supported_parameters` | string[] | En chat, los parámetros de OpenAI que admite el modelo. En imagen y vídeo, las funciones compartidas que admite (`resolution`, `ratio`, `audio`, `seed`, `negative_prompt`, `first_frame`, `last_frame`, `reference_images`, `reference_videos`, `prompt_extend`). |
| `operations` | string[] | Solo en modelos de vídeo: `edit`, `extend` y `remix`, para cada una que el modelo admita. Ausente cuando el modelo no admite ninguna. |
| `extra_parameters` | object | Los campos de primer nivel específicos del modelo, por nombre. Cada uno tiene `type`, `values` (valores permitidos; vacío significa cualquiera), `price_condition` (el campo cambia el precio) y `description`. Consulta [Parámetros específicos de cada modelo](#parameters). |

```bash
curl https://api.hyperouter.app/v1/models \
  -H "Authorization: Bearer $HYPEROUTER_API_KEY"
```

### Errores

Ninguno específico de este endpoint.

### Modelos de chat

Precios por 1 M de tokens: entrada / entrada en caché / salida.

<!-- BEGIN GENERATED models-chat: sitemd writes the rows from models.json; edit the header row only. Run cd tools && go run ./sitemd -->
| Modelo | Contexto | Precio |
| --- | --- | --- |
| `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 por solicitud |
<!-- END GENERATED models-chat -->

En los modelos `x-ai/`, una solicitud con 200 000 tokens de prompt o más se factura por completo con las tarifas de contexto largo, que son el doble de las anteriores. Los modelos `deepseek/` tienen una única tarifa para cualquier longitud de prompt, razonan antes de responder por defecto (`reasoning_effort: "none"` lo desactiva) y cuentan su razonamiento dentro de `max_tokens`, con un máximo de 384 000. `z-ai/glm-5.3` siempre razona: `reasoning_effort` es `low`, `high` o `max`, y `none` se rechaza. Los modelos `openai/` también razonan y aceptan prompts de 272 000 tokens como máximo.

## Chat completions {#chat-completions}

`POST /v1/chat/completions`

Crea la respuesta de un modelo a una conversación. El cuerpo es la solicitud de Chat Completions de OpenAI. Hyperouter añade `models`, `provider` y `moderation`.

### Parámetros

| Nombre | Tipo | Obligatorio | Valores permitidos | Por defecto | Descripción |
| --- | --- | --- | --- | --- | --- |
| `model` | string | Sí | Un id de modelo de chat de `GET /v1/models` |  | El modelo que se usará, por ejemplo `x-ai/grok-4.7`. |
| `messages` | object[] | Sí | No vacío. Roles `system`, `user`, `assistant`, `tool` |  | La conversación hasta ahora. Se aceptan imágenes (partes `image_url`) cuando el modelo admite imágenes. |
| `models` | string[] | No | Como máximo 5 ids de modelo |  | Añadido. Modelos de reserva, probados en orden después de que fallen todos los proveedores de `model`. |
| `provider` | object | No | Ver más abajo |  | Añadido. Preferencias de enrutamiento. |
| `moderation` | string | No | `auto`, `low` | `auto` | Añadido. Consulta [Moderación](#moderation). |
| `stream` | boolean | No |  | `false` | Devuelve eventos enviados por el servidor. |
| `stream_options` | object | No | `{"include_usage": true}` |  | Con `include_usage`, el stream termina con un fragmento que incluye `usage`. |
| `max_tokens`, `max_completion_tokens` | integer | No | 1 o más | El máximo de salida del modelo | Límite superior de tokens generados. También fija lo máximo que puede costar una solicitud, que Hyperouter retiene antes de la llamada (consulta [Facturación](#billing)). |
| `n` | integer | No | De 1 a 128 | `1` | Número de opciones. |
| `temperature`, `top_p`, `seed`, `frequency_penalty`, `presence_penalty`, `logit_bias` | number / integer / object | No | Como en OpenAI |  | Controles de muestreo, pasados al proveedor. |
| `stop` | string \| string[] | No |  |  | Secuencias que terminan la generación. |
| `tools`, `tool_choice`, `parallel_tool_calls` | object[], string \| object, boolean | No | Formato de function calling de OpenAI |  | Function calling. Solo herramientas de tipo `function`: cualquier otro tipo se rechaza con `400 unsupported_parameter`. |
| `response_format` | object | No | `{"type":"json_object"}` o un esquema JSON |  | Salida estructurada. |
| `reasoning_effort`, `logprobs`, `top_logprobs`, `user` | como en OpenAI | No |  |  | Se pasan al proveedor cuando el modelo los admite. |

Los demás campos se ignoran. `supported_parameters` de cada modelo en [GET /v1/models](#models) enumera exactamente lo que admite. Grok 4.20 y posteriores ignoran `logprobs` y `top_logprobs`, por lo que no aparecen en la lista.

### El objeto provider {#routing}

Por defecto, Hyperouter envía cada solicitud de chat al proveedor disponible con el precio más bajo para ese modelo, y desempata por el menor tiempo reciente hasta el primer token. Un proveedor que falla repetidamente se omite hasta que se recupera. El objeto `provider` sigue la estructura de solicitud de OpenRouter.

| Nombre | Tipo | Obligatorio | Valores permitidos | Por defecto | Descripción |
| --- | --- | --- | --- | --- | --- |
| `sort` | string | No | `price`, `latency`, `throughput` | `price` | Cómo ordenar los proveedores. |
| `order` | string[] | No | Nombres de proveedores |  | Proveedores que se probarán primero, en orden, por ejemplo `["xai"]`. El nombre de un proveedor es su nombre público en cualquier capitalización (`xAI`, `Qwen`) o la primera parte de sus ids de modelo (`x-ai`, `qwen`). |
| `only` | string[] | No | Nombres de proveedores |  | Enruta solo a estos proveedores. |
| `ignore` | string[] | No | Nombres de proveedores |  | No enruta nunca a estos proveedores. |

### Respuesta

El chat completion de OpenAI. El `id` es el id de la solicitud: consúltalo con [GET /v1/generations/{id}](#generations). Hyperouter añade `provider` (el nombre público del proveedor que atendió la solicitud, por ejemplo `xAI`) y `usage.cost` (el cargo, en USD, como número JSON).

```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
  }
}
```

Un modelo de reescritura de prompts responde con una opción, un `usage` de cero tokens y un `cost` fijo por solicitud.

### Streaming {#streaming}

Con `"stream": true` la respuesta es un flujo de eventos enviados por el servidor en el formato de fragmentos de OpenAI. Termina con `data: [DONE]`. Cuando defines `stream_options.include_usage`, el último fragmento antes de `[DONE]` tiene un array `choices` vacío y un objeto `usage` que incluye `cost`, además de un campo `provider`. El consumo siempre se registra para la facturación, pidas o no el fragmento.

Hasta que llega el primer token, el stream envía la línea de comentario `: processing` cada 15 segundos para mantener abierta la conexión. Los clientes SSE ignoran los comentarios.

La conmutación a otro proveedor solo ocurre antes de enviar el primer token. Si un proveedor falla a mitad del stream, este termina con `data: {"error": {...}}` seguido de `data: [DONE]`, y se te facturan los tokens ya producidos.

Se te facturan los tokens que produce el proveedor, los leas o no. Un stream que abortas, o una respuesta que dejas de esperar, no se detiene en el proveedor, que continúa y factura toda su respuesta: Hyperouter la lee hasta el final (durante 5 minutos más como máximo) y factura eso.

Un proveedor que ha aceptado una solicitud puede estar ejecutándola. Si luego deja de responder (5 minutos sin ningún token en un stream, o más de lo que tarda una respuesta de `max_tokens`) o su respuesta se corta antes del primer token, la solicitud no se envía a otro proveedor: responde `503` con el código `timeout` o `upstream_error` con `error.generation_id`, y se factura según su retención, con `usage_estimated: true` en su registro.

### Ejemplo

```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" }]
  }'
```

### Errores

| Estado | type | code | param | Cuándo |
| --- | --- | --- | --- | --- |
| `400` | `invalid_request_error` | `invalid_body` | el campo | El cuerpo no es JSON válido o supera los 32 MiB; falta `model`; `models` tiene más de 5 entradas; `n` está fuera del rango de 1 a 128; `max_tokens` no es positivo; `provider.sort` no está permitido; `moderation` no es `auto` ni `low` (`moderation must be auto or low.`); `messages` está vacío. También cuando todos los proveedores rechazaron la solicitud por no ser válida. |
| `400` | `invalid_request_error` | `unknown_model` | `model` o `models` | El modelo no es un modelo de chat, o no tiene un proveedor habilitado. |
| `400` | `invalid_request_error` | `unsupported_parameter` | `tools`, `response_format`, `messages` o `max_tokens` | El modelo no admite herramientas, un formato de respuesta ni imágenes; una herramienta no es de tipo `function`; o ningún proveedor del modelo puede acotar su salida. |
| `400` | `invalid_request_error` | `context_length_exceeded` | `messages` | El prompt y `max_tokens` superan el contexto del modelo. |
| `402` | `insufficient_credits` | `insufficient_credits` | `max_tokens` | El saldo o el límite de la clave no cubre lo máximo que podría costar la solicitud. El mensaje indica cuántos tokens puedes permitirte. |
| `429` | `rate_limit_error` | `venue_busy` |  | Todos los proveedores del modelo superan su presupuesto de velocidad. |
| `429` | `rate_limit_error` | `rate_limited` |  | Tu cuenta supera sus solicitudes por minuto o sus solicitudes concurrentes (consulta [Límites de velocidad](#rate-limits)). No se retiene ni se reenvía nada. |
| `409` | `conflict` | `idempotency_key_replayed`, `idempotency_key_reused`, `idempotency_key_in_use` |  | Consulta [Reintentos e idempotencia](#idempotency). |
| `503` | `upstream_error` | `all_venues_failed` |  | Ningún proveedor está disponible, o todos han fallado. No se factura. |
| `503` | `upstream_error` | `upstream_error` |  | El proveedor aceptó la solicitud y su respuesta se cortó antes del primer token. Se factura según la retención (consulta [Streaming](#streaming)). |
| `503` | `upstream_error` | `timeout` |  | El proveedor aceptó la solicitud y no la terminó a tiempo. Se factura según la retención (consulta [Streaming](#streaming)). |
| `401`, `403`, `500` |  |  |  | Consulta [Autenticación](#authentication). |

## Imágenes {#images}

`POST /v1/images/generations`

`POST /v1/images/edits`

Ambos siguen la API de imágenes de OpenAI y aceptan un cuerpo JSON. `/v1/images/edits` acepta la forma JSON de OpenAI: las imágenes son `images`, de 1 a 5 URL https. El `images.edit` de los SDK de OpenAI envía los archivos de imagen como `multipart/form-data`, y eso también funciona: el edge de Hyperouter guarda cada archivo (PNG, JPEG o WebP, 20 MiB cada uno, 50 MiB por solicitud) durante 3 días en una URL `https://api.hyperouter.app/u/...` imposible de adivinar que el proveedor descarga, y luego ejecuta la solicitud como JSON. Un archivo de vídeo se rechaza (`400 invalid_video`): envía un vídeo como URL https. Ambos aceptan las funciones compartidas que siguen y cualquier [parámetro específico del modelo](#parameters) como campos de primer nivel.

### Parámetros

| Nombre | Tipo | Obligatorio | Valores permitidos | Por defecto | Descripción |
| --- | --- | --- | --- | --- | --- |
| `model` | string | Sí | Un id de modelo de imagen |  | El modelo que se usará. |
| `prompt` | string | Generations: sí. Edits: según lo que necesite el modelo | Texto no vacío |  | Qué crear. |
| `images` | object[] | Edits: sí | De 1 a 5 objetos `{"image_url": "https://..."}`, tantos como admita el modelo. Cada URL es https a un host público, nunca una URL `data:`, una dirección IP ni un nombre interno. No se acepta `file_id`. |  | Las imágenes que se editarán. Cada URL se pasa al proveedor tal cual, por lo que el proveedor debe poder descargarla; si no puede descargarla o leerla, el trabajo falla con `vendor_rejected` y no se cobra nada. Un modelo que las acepta en un orden determinado lo indica en sus notas de la referencia de modelos. |
| `n` | integer | No | De 1 a 10, y como máximo el límite del modelo | `1` | Número de imágenes. |
| `size` | string | No | `WIDTHxHEIGHT`, por ejemplo `1024x1024`; el modelo puede permitir menos tamaños | `1024x1024` | Tamaño de salida. |
| `quality` | string | No | Según lo que permita el modelo |  | Se pasa al modelo. |
| `response_format` | string | No | `url` | `url` | Cómo se devuelve la imagen: siempre como URL. `b64_json` se rechaza con `400 unsupported_parameter`. |
| `seed` | integer | No | Si el modelo lista `seed` |  | Semilla para una salida repetible. |
| `negative_prompt` | string | No | Si el modelo lo lista |  | Qué evitar. |
| `prompt_extend` | boolean | No | Si el modelo lo lista | Desactivado salvo que lo pidas | Permite que el modelo amplíe el prompt. |
| `prompt_expand` | boolean | No | Si el modelo lo lista | `false` | Añadido. Hyperouter reescribe el prompt para el modelo antes de generar. Consulta [Expansión del prompt](#prompt-expansion). |
| `prompt_expand_model` | string | No | El slug de un modelo de chat que tu cuenta puede llamar | `x-ai/grok-4.6` | Añadido. Con `prompt_expand`: el modelo que escribe la reescritura. Sin `prompt_expand: true` es `400 invalid_body`. Consulta [Expansión del prompt](#prompt-expansion). |
| `locale` | string | No | Una etiqueta BCP 47, por ejemplo `zh-CN` | Inglés | Añadido. Con `prompt_expand`: el idioma de los diálogos, si el modelo lo habla. |
| `ratio` | string | No | Si el modelo lo lista |  | Relación de aspecto. |
| `resolution` | string | No | `480p`, `720p`, `1080p`, `2K`, `4K`, según lo que permita el modelo |  | Resolución de salida. |
| `models` | string[] | No | Ids de modelos de imagen |  | Añadido. Modelos de reserva. |
| `provider` | object | No | `only`, `ignore`, `order` (arrays de nombres de proveedores) |  | Añadido. Preferencias de enrutamiento. |
| `moderation` | string | No | `auto`, `low` | `auto` | Añadido. Consulta [Moderación](#moderation). |
| campos específicos del modelo | según se declare | Según se declare | Según se declare |  | Consulta [Parámetros específicos de cada modelo](#parameters). |

Una función compartida que el modelo no lista, como `seed` en un modelo que no la tiene, se rechaza con `400 unsupported_parameter`. Nunca se descarta en silencio, porque cambiaría la salida y el precio.

### Respuesta

```json
{
  "created": 1730000000,
  "data": [{ "url": "https://..." }],
  "usage": { "cost": 0.02 },
  "id": "0194f2c4-7a1e-7b3c-9d02-5f6a1c0e8b21"
}
```

| Campo | Tipo | Descripción |
| --- | --- | --- |
| `created` | integer | Hora Unix de la solicitud. |
| `data[].url` | string | Con `response_format: "url"`: un enlace firmado, válido durante 24 horas. `GET /v1/generations/{id}` firma uno nuevo. |
| `usage.cost` | number | El cargo en USD, como número JSON. |
| `id` | string | El id de la solicitud. Úsalo con [GET /v1/generations/{id}](#generations). |
| `expanded_prompt` | string \| null | Añadido. Con `prompt_expand`: el prompt con el que se generó. `null` si no se reescribió nada. |

Hyperouter almacena cada resultado durante 30 días, de modo que un enlace nunca apunta a un proveedor. Pasados los 30 días el registro permanece, pero su `outputs` queda vacío.

### Esperar el resultado

- En un modelo cuyos `response_modes` incluyen `sync`, la solicitud espera la imagen hasta 90 segundos y responde con su estado real: `200` con las imágenes, o el error. Los errores de validación, saldo y capacidad van primero, con su propio estado.
- Una imagen que no está lista tras 90 segundos responde `503` con el código `timeout` y el encabezado de respuesta `x-should-retry: false`, de modo que los SDK de OpenAI no la reintentan ni inician una segunda imagen. El trabajo continúa, y el error lleva su id como `generation_id`: consulta [GET /v1/generations/{id}](#generations) para obtener el resultado. Solo se te factura si tiene éxito. Para reintentar con seguridad por tu cuenta, envía una [Idempotency-Key](#idempotency): un reintento con la misma clave espera al mismo trabajo.
- Un modelo cuyos `response_modes` son solo `async` necesita `Prefer: respond-async`. Sin él, la respuesta es `400` con el código `async_required`.
- Para evitar una conexión retenida en cualquier modelo que liste `async`, envía `Prefer: respond-async` (RFC 7240). El encabezado puede repetirse y puede llevar otras preferencias, por ejemplo `Prefer: wait=10, respond-async`. Recibes `202` de inmediato con el encabezado de respuesta `Preference-Applied: respond-async`:

  ```json
  { "id": "0194f2c4-7a1e-7b3c-9d02-5f6a1c0e8b21", "status": "queued" }
  ```

  Consulta periódicamente [GET /v1/generations/{id}](#generations), o recibe un [webhook](#webhooks) `image.completed` o `image.failed`. Los webhooks se disparan para imágenes solo en los trabajos que enviaste con este encabezado.

### Ejemplos

```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"}]}'
```

### Errores

| Estado | code | param | Cuándo |
| --- | --- | --- | --- |
| `400` | `unsupported_content_type` |  | El cuerpo es `multipart/form-data`: `Send a JSON body; pictures and videos are https URLs.` |
| `400` | `invalid_body` | el campo | El cuerpo no es JSON válido; `images` no es un array de objetos con `image_url`; falta `prompt` en generations; `n` es menor que 1; `response_format` no es `url` (`response_format must be url.`; `b64_json` es `unsupported_parameter`); `resolution` no está permitida; un campo tipado tiene el tipo equivocado; `moderation` no es `auto` ni `low`. |
| `400` | `unknown_model` | `model` | El modelo no existe para imágenes, o ningún proveedor coincide con tus preferencias de `provider`. |
| `400` | `invalid_size` | `size` | `size` no tiene la forma `1024x1024`, o el modelo no la permite. |
| `400` | `too_many_images` | `n` o `images` | `n` supera 10 o el límite del modelo; más de 5 imágenes; más imágenes de las que admite el modelo. |
| `400` | `image_required` | `images` | Una edición tiene menos imágenes de las que necesita el modelo, o llamaste a generations en un modelo que solo edita (usa `/v1/images/edits`). |
| `400` | `unsupported_operation` | `images` | El modelo no edita imágenes. |
| `400` | `unsupported_parameter` | el campo | El modelo no admite el campo; `images[i].file_id`. |
| `400` | `invalid_combination` | el campo | El proveedor no permite estos campos juntos. |
| `400` | `async_required` |  | El modelo solo responde de forma asíncrona: envía `Prefer: respond-async`. |
| `400` | `unsupported_parameter` | `stream` | `"stream": true` en un modelo cuyos `response_modes` no incluyen `stream`. |
| `400` | `invalid_image` | `images[i].image_url` | La URL no es https a un host público: `images[i].image_url must be an https URL.` para una URL `data:` o `http://`. |
| `400` | `content_rejected` |  | El filtro de seguridad del proveedor rechazó la entrada: `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` |  | Consulta [Reintentos e idempotencia](#idempotency). |
| `402` | `insufficient_credits` |  | El saldo o el límite de la clave no cubre lo máximo que podría costar la solicitud. |
| `409` | `idempotency_key_reused`, `idempotency_key_in_use` |  | Consulta [Reintentos e idempotencia](#idempotency). |
| `429` | `rate_limit_error` |  | Todos los proveedores están al límite de su capacidad (sin `code`), o tu cuenta supera sus solicitudes por minuto o sus solicitudes concurrentes (`code: rate_limited`, consulta [Límites de velocidad](#rate-limits)). |
| `503` | `upstream_error` |  | `The model failed to produce a result. You were not charged.` |
| `503` | `provider_busy` |  | El proveedor siguió al límite de su capacidad durante unos dos minutos y no aceptó el trabajo. Reintenta más tarde. No se te cobró. |
| `500` | `server_error`, `no_price` |  | Hyperouter ha fallado, o el modelo no se puede valorar ahora. Un `500` después de crear la imagen lleva `x-should-retry: false`. |
| `503` | `timeout` |  | La imagen no estuvo lista en 90 segundos. Todavía se está creando: `error.generation_id` es el id que debes consultar. Lleva `x-should-retry: false`. |

El mensaje de un trabajo fallido termina con el motivo del propio proveedor, si lo dio, tras `Provider:`, para distinguir un prompt rechazado, un proveedor saturado y una caída. Las URL que contenga se sustituyen por `<url>`.

### Modelos de imagen

<!-- BEGIN GENERATED models-image: sitemd writes the ids and prices from models.json; write each model's Does cell. Run cd tools && go run ./sitemd -->
| Modelo | Función | Precio |
| --- | --- | --- |
| `x-ai/grok-imagine-image` | Genera y edita. Hasta 10 imágenes, hasta 5 imágenes de entrada. | $0.02 por imagen |
| `x-ai/grok-imagine-image-2.0` | Genera y edita, igual que el anterior. | Desde $0.04 por imagen |
| `qwen/qwen-image-2.0` | Genera y edita (de 1 a 3 imágenes de entrada). `n` de 1 a 6. | $0.035 por imagen |
| `bytedance/seedream-5.0-flash` | Genera y edita (de 1 a 10 imágenes de entrada). Una imagen 2K por solicitud, sin marca de agua. | $0.018 por imagen |
| `bytedance/seedream-5.0-lite` | Genera y edita, como el anterior. | $0.035 por imagen |
<!-- END GENERATED models-image -->

Algunos modelos requieren que se conceda acceso a tu cuenta. La [referencia de modelos](https://dash.hyperouter.app/docs/models) del panel enumera todos los modelos que tu cuenta puede llamar, con sus parámetros, precios y notas.

## Vídeos {#videos}

El vídeo sigue la API de vídeos de OpenAI. Un vídeo es un trabajo. Una llamada de creación, edición, extensión o remix responde `200` de inmediato con el objeto Video en estado `queued`. Consultas `GET /v1/videos/{id}` hasta que el estado sea `completed`, o recibes un [webhook](#webhooks). `Prefer` no cambia nada aquí: los vídeos son siempre asíncronos.

| Endpoint | Función |
| --- | --- |
| `POST /v1/videos` | Crea un vídeo a partir de un prompt, con un `input_reference` opcional. |
| `POST /v1/videos/edits` | Edita o escala un vídeo completado. Solo modelos que listan `edit` en `operations`. |
| `POST /v1/videos/extensions` | Extiende un vídeo completado. Solo modelos que listan `extend`. |
| `POST /v1/videos/{id}/remix` | Nuevo prompt sobre un vídeo completado, con el mismo modelo. Solo modelos que listan `remix`. |
| `GET /v1/videos` | Lista tus vídeos. |
| `GET /v1/videos/{id}` | El objeto Video. |
| `GET /v1/videos/{id}/content` | El MP4 o la miniatura. |
| `DELETE /v1/videos/{id}` | Elimina un vídeo. |

### Crear un vídeo

`POST /v1/videos`

El cuerpo es JSON, en todos los endpoints de vídeo; el `videos.create` de los SDK de OpenAI con un archivo de imagen `input_reference` también se acepta, y el archivo se trata como en las [ediciones de imágenes](#images). Toda imagen y todo vídeo es una URL https a un host público, que se pasa al proveedor tal cual: el proveedor debe poder descargarla, y si no puede descargarla o leerla, el vídeo falla con `vendor_rejected` y no se cobra nada. Una URL `data:`, una URL `http://`, una dirección IP o un nombre interno se rechaza con `400 invalid_image` o `400 invalid_video`. Un cuerpo `multipart/form-data` se rechaza con `400 unsupported_content_type`.

| Nombre | Tipo | Obligatorio | Valores permitidos | Por defecto | Descripción |
| --- | --- | --- | --- | --- | --- |
| `model` | string | Sí | Un id de modelo de vídeo |  | El modelo que se usará. |
| `prompt` | string | Sí | Texto no vacío |  | Qué crear. |
| `seconds` | string \| integer | No | Según lo que permita el modelo | El primer valor permitido del modelo; si no, 4 s (o el máximo del modelo si es menor) | Duración del vídeo. `"8"` y `8` son lo mismo. |
| `size` | string | No | `WIDTHxHEIGHT` |  | La altura determina la resolución salvo que envíes `resolution`: hasta 480 es `480p`, hasta 720 es `720p`, hasta 1080 es `1080p`, hasta 1440 es `2K`, por encima es `4K`. El modelo debe permitir esa resolución. |
| `input_reference` | object | Si el modelo necesita una imagen | `{"image_url": "https://..."}`, una URL https |  | La primera imagen del vídeo. No se acepta `file_id`. |
| `resolution` | string | No | `480p`, `720p`, `1080p`, `2K`, `4K`, según lo que permita el modelo | La primera resolución del modelo | Resolución de salida. Cambia el precio. |
| `ratio` | string | No | Si el modelo lo lista |  | Relación de aspecto. |
| `audio` | boolean | No | Si el modelo genera sonido | `false` | Genera sonido. |
| `seed` | integer | No | Si el modelo lo lista |  | Semilla para una salida repetible. |
| `negative_prompt` | string | No | Si el modelo lo lista |  | Qué evitar. |
| `prompt_extend` | boolean | No | Si el modelo lo lista | Desactivado salvo que lo pidas | Permite que el modelo amplíe el prompt. |
| `prompt_expand`, `locale` | boolean, string | No | Como en [imágenes](#images) | `false` | Añadido. Consulta [Expansión del prompt](#prompt-expansion). |
| `prompt_expand_model` | string | No | Como en [imágenes](#images) | `x-ai/grok-4.6` | Añadido. Con `prompt_expand`: el modelo que escribe la reescritura. Consulta [Expansión del prompt](#prompt-expansion). |
| `first_frame`, `last_frame` | string | No | Una URL https de una imagen |  | Fotogramas clave. |
| `reference_images`, `reference_videos` | string[] | No | URL https |  | Medios de referencia. No se pueden combinar con fotogramas clave, según lo establece el proveedor. |
| `models`, `provider` | string[], object | No | Como en [imágenes](#images) |  | Añadido. Modelos de reserva y preferencias de enrutamiento. |
| `moderation` | string | No | `auto`, `low` | `auto` | Añadido. Consulta [Moderación](#moderation). |
| campos específicos del modelo | según se declare | Según se declare | Según se declare |  | Consulta [Parámetros específicos de cada modelo](#parameters). |

### Editar, extender y remix

| Endpoint | Cuerpo (JSON) |
| --- | --- |
| `POST /v1/videos/edits` | `model`, `video` (obligatorio), `prompt`, `resolution` y las funciones compartidas. `video` es `{"id": "<your video id>"}` (un vídeo tuyo completado) o `{"url": "https://..."}`. `model` toma por defecto el modelo del vídeo de origen cuando envías un id. |
| `POST /v1/videos/extensions` | `model`, `video` (obligatorio), `prompt`, `seconds`. Los nuevos segundos siguen las mismas reglas que en una creación. |
| `POST /v1/videos/{id}/remix` | `prompt` (obligatorio). El modelo es el del vídeo de origen. |

El vídeo de origen debe haberse completado. Un vídeo indicado por URL es una URL https, que se pasa al proveedor tal cual, igual que una imagen. Si indicas un vídeo por URL, `seconds` es obligatorio en los modelos que no tienen un máximo fijo.

### Listar, consultar, descargar, eliminar

| Endpoint | Parámetros | Respuesta |
| --- | --- | --- |
| `GET /v1/videos` | Consulta: `after` (un id de vídeo), `limit` (de 1 a 100, por defecto 20), `order` (`desc` por defecto, o `asc`) | `{"object":"list","data":[Video],"first_id","last_id","has_more"}`. Los vídeos que han superado su `expires_at` se omiten. |
| `GET /v1/videos/{id}` |  | El objeto Video. |
| `GET /v1/videos/{id}/content` | Consulta: `variant` (`video` por defecto, o `thumbnail`) | `302 Found` a un enlace firmado a los bytes (`video/mp4`, o el tipo de la miniatura), válido durante 15 minutos. Los clientes HTTP y los SDK de OpenAI lo siguen. `404 not_found` antes de completarse o cuando no hay miniatura. |
| `DELETE /v1/videos/{id}` |  | `{"id","object":"video.deleted","deleted":true}`. Responde `409 video_not_finished` mientras el trabajo se ejecuta. Un vídeo eliminado responde `404`. |

### El objeto Video

```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 }
}
```

| Campo | Tipo | Descripción |
| --- | --- | --- |
| `status` | string | `queued`, `in_progress`, `completed` o `failed`. |
| `progress` | integer | De 0 a 100. |
| `prompt` | string \| null | El prompt solo se devuelve mientras el trabajo se ejecuta. El objeto terminado devuelve `null`. |
| `seconds` | string | Duración del vídeo, como cadena, igual que en OpenAI. |
| `size` | string | El `size` que enviaste, si lo hiciste. |
| `expires_at` | integer | Hora Unix: `created_at` más 30 días. Después, `GET /v1/videos/{id}` y `/content` responden `404 not_found` y el vídeo se omite de la lista. Descarga lo que quieras conservar antes de esa fecha. |
| `error` | object \| null | En un trabajo fallido: `{"code", "message"}`, por ejemplo `content_rejected`. |
| `remixed_from_video_id` | string \| null | El vídeo de origen de una edición, extensión o remix. |
| `usage` | object \| null | Se define cuando el trabajo se liquida: `cost` (USD, número JSON) y `seconds` (entero). |
| `expanded_prompt` | string \| null | Añadido. Con `prompt_expand`: el prompt con el que se generó, una vez enviado el trabajo. Se conserva 30 días; después, `null`. |

Un vídeo se factura según los segundos que solicitas. El importe se retiene por adelantado y se libera por completo si el trabajo falla.

### Ejemplo: x-ai/grok-imagine-video

El modelo `x-ai/grok-imagine-video` crea un vídeo a partir de un prompt, con una imagen opcional como punto de partida. `seconds` va de 1 a 15. Precio: $0.050 por segundo.

```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
}
```

A este trabajo se le cobra 8 × $0.050 = $0.40, así que el objeto liquidado muestra `"usage": {"cost": 0.4, "seconds": 8}`.

### Errores

| Estado | code | param | Cuándo |
| --- | --- | --- | --- |
| `400` | `unsupported_content_type` |  | El cuerpo es `multipart/form-data`: `Send a JSON body; pictures and videos are https URLs.` |
| `400` | `invalid_body` | el campo | El cuerpo no es JSON válido; `prompt` está vacío; `video` no es un objeto con `id` o `url`; `input_reference` no es un objeto con `image_url`; `limit`, `after` o `variant` no es válido; un campo tipado tiene el tipo equivocado; `resolution` no está permitida; `moderation` no es `auto` ni `low`. |
| `400` | `unknown_model` | `model` | El modelo no existe para vídeos. |
| `400` | `invalid_seconds` | `seconds` | `seconds` no está permitido para el modelo (el mensaje enumera los valores permitidos), o hace falta para un vídeo indicado por URL. |
| `400` | `invalid_size` | `size` | `size` no tiene la forma `1280x720`. |
| `400` | `unsupported_parameter` | el campo | El modelo no admite el campo; la resolución no está disponible (`resolution` o `size`); `audio` en un modelo sin sonido; `input_reference.file_id`. |
| `400` | `unsupported_operation` | `model` | El modelo no lista la operación (editar, extender o remix). |
| `400` | `invalid_combination` | el campo | Medios de referencia junto con fotogramas clave, según lo establece el proveedor. |
| `400` | `image_required` | `input_reference` | `This model needs an input_reference picture.` |
| `400` | `video_not_ready` | `video` | El vídeo de origen no ha terminado. |
| `400` | `invalid_image`, `invalid_video` | el campo | La URL de una imagen (`invalid_image`) o de un vídeo (`invalid_video`: `reference_videos[i]`, `video.url`) no es https a un host público, por ejemplo una URL `data:` o `http://`: `<field> must be an https URL.` `param` es el campo, por ejemplo `input_reference.image_url`, `first_frame` o `reference_images[1]`. Una URL se pasa al proveedor tal cual: si el proveedor no puede descargarla o leerla, el vídeo falla con `vendor_rejected` y no se cobra nada. |
| `400` | `clip_too_long` |  | El clip de referencia más los `seconds` solicitados superan el límite del modelo: `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.` Aparece en `error.code` del objeto Video. |
| `400` | `content_rejected`, `vendor_rejected` |  | El trabajo falló en el proveedor. Aparece en `error.code` del objeto Video. |
| `402` | `insufficient_credits` |  | El saldo o el límite de la clave no cubre el importe retenido. |
| `404` | `not_found` |  | El vídeo no existe, no es tuyo, se eliminó o ha superado `expires_at`; o el contenido no está listo. |
| `409` | `video_not_finished` |  | Eliminaste un vídeo que todavía se está creando. |
| `409` | `idempotency_key_reused`, `idempotency_key_in_use` |  | Consulta [Reintentos e idempotencia](#idempotency). |
| `429`, `503`, `500` |  |  | Igual que en imágenes. Un `500` después de crear el vídeo lleva `x-should-retry: false`. |

### Modelos de vídeo

<!-- BEGIN GENERATED models-video: sitemd writes the ids and prices from models.json; write each model's Notes cell. Run cd tools && go run ./sitemd -->
| Modelo | Precio por segundo de vídeo | Notas |
| --- | --- | --- |
| `x-ai/grok-imagine-video` | Desde $0.050 | Entrada de texto, imagen o vídeo. 15 s como máximo. Admite `edit`. |
| `x-ai/grok-imagine-video-1.5` | Desde $0.080 | Entrada de texto, imagen o referencia. 15 s como máximo. |
| `x-ai/grok-imagine-video-1.5-lite` | Desde $0.020 | Igual que el anterior, más pequeño y barato. |
| `bytedance/seedance-2.0` | Desde $0.0431892 | Entrada de texto, primer y último fotograma o imágenes de referencia, audio opcional. De 4 a 15 s. 480p, 720p, 1080p o 4K; la tarifa indicada es la de 480p. |
| `bytedance/seedance-2.0-fast` | Desde $0.0331452 | Entrada de texto, primer y último fotograma o imágenes de referencia, audio opcional. De 4 a 15 s. 480p o 720p; la tarifa indicada es la de 480p. |
| `bytedance/seedance-2.0-mini` | Desde $0.0210924 | Entrada de texto, primer y último fotograma o imágenes de referencia, audio opcional. De 4 a 15 s. 480p o 720p; la tarifa indicada es la de 480p. |
| `bytedance/seedance-2.5` | Desde $0.0642816 | Entrada de texto, primer y último fotograma o imágenes de referencia, audio opcional. De 4 a 15 s. 480p, 720p o 1080p; la tarifa indicada es la de 480p. |
| `lumina/berry-1.0` | Desde $0.050 | Entrada de texto, imagen o referencia, audio opcional. 30 s como máximo. 480p, 720p o 1080p; la tarifa indicada es la de 480p. |
| `minimax/minimax-h3` | Desde $0.080 | Entrada de texto, primer y último fotograma o imágenes de referencia, sin audio. De 4 a 15 s, a 768p o 2K. |
| `alibaba/wan3.0` | Desde $0.050 | Entrada de texto, imagen o referencia, audio opcional. 30 s como máximo. 480p, 720p o 1080p; la tarifa indicada es la de 480p. |
<!-- END GENERATED models-video -->

La [referencia de modelos](https://dash.hyperouter.app/docs/models) del panel enumera todos los modelos de vídeo que tu cuenta puede llamar.

## Generaciones {#generations}

`GET /v1/generations/{id}`

Devuelve el registro de una de tus solicitudes, de cualquier tipo: su estado, consumo, cargo, tiempos y, para imágenes y vídeos, enlaces firmados a las salidas. Úsalo para consultar periódicamente una solicitud de imagen que enviaste con `Prefer: respond-async` y para conciliar el gasto. El texto del prompt y de la respuesta nunca está en el registro. Hyperouter lo conserva cifrado durante 180 días solo para cumplir obligaciones legales, y ninguna API ni página del panel lo devuelve ([Política de privacidad](privacy.md#p3)).

### Parámetros

| Nombre | En | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- | --- |
| `id` | path | string (UUID) | Sí | El `id` de un chat completion, una respuesta de imagen o un objeto Video. |

### Respuesta

```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": []
  }
}
```

| Campo | Tipo | Descripción |
| --- | --- | --- |
| `kind` | string | `chat`, `image` o `video`. |
| `provider_name` | string \| null | El proveedor que atendió una solicitud de chat terminada. `null` para imágenes y vídeos. |
| `status` | string | `queued`, `submitting`, `running`, `done` o `failed`. |
| `created_at`, `finished_at` | string \| null | RFC 3339 UTC. `finished_at` es `null` hasta que la solicitud termina. |
| `usage` | object | `input_tokens`, `output_tokens`, `cache_read_tokens`, `image`, `video_seconds` y `cost`. `cost` es el cargo en USD, un número JSON. Los contadores son `0` hasta que la solicitud termina. |
| `usage_estimated` | boolean | `true` cuando el proveedor no envió el recuento de tokens y Hyperouter lo calculó. |
| `ttft_ms`, `latency_ms` | integer \| null | Tiempo hasta el primer token y tiempo total, en milisegundos. |
| `error` | object \| null | `{"code", "message"}` cuando la solicitud falló. |
| `outputs` | object[] | Para imágenes y vídeos: `[{"url", "expires_at"}]`, enlaces firmados válidos durante 24 horas. Disponibles durante 30 días tras la solicitud; después `outputs` queda vacío y el registro permanece. Siempre vacío en chat. |
| `expanded_prompt` | string \| null | Para imágenes y vídeos enviados con `prompt_expand`: el prompt con el que se generó. Se conserva 30 días tras la solicitud; después, `null`. |

```bash
curl https://api.hyperouter.app/v1/generations/0194f2c4-7a1e-7b3c-9d02-5f6a1c0e8b21 \
  -H "Authorization: Bearer $HYPEROUTER_API_KEY"
```

### Errores

| Estado | type | code | param | Cuándo |
| --- | --- | --- | --- | --- |
| `404` | `not_found` | `not_found` | `id` | `No generation with that id.` El id no es un UUID, no existe, pertenece a otra cuenta o se eliminó. |
| `500` | `server_error` | `server_error` |  | Hyperouter no pudo leer ni firmar el registro. |
| `401`, `403` |  |  | Consulta [Autenticación](#authentication). |

<!-- toc: Conceptos -->
## Parámetros específicos de cada modelo {#parameters}

Algunos modelos tienen campos que otros modelos no tienen. Son los *parámetros adicionales*. Los envías como campos de primer nivel del cuerpo de la solicitud, como cualquier otro campo. Cualquier SDK de OpenAI puede enviarlos (`extra_body` en Python, propiedades adicionales en JavaScript).

`GET /v1/models` enumera los parámetros adicionales de cada modelo en `extra_parameters`. Cada entrada tiene:

| Campo | Tipo | Descripción |
| --- | --- | --- |
| `type` | string | Uno de `string`, `integer`, `number`, `boolean`, `array`, `object`. |
| `values` | array | Los valores permitidos. Vacío significa cualquier valor del tipo. |
| `price_condition` | boolean | `true` cuando el campo cambia el precio. |
| `description` | string | Qué hace el campo. |

La [referencia de modelos](https://dash.hyperouter.app/docs/models) del panel describe los parámetros adicionales de cada modelo que tu cuenta puede llamar.

### Reglas

- Un parámetro conocido que el modelo no tiene se rechaza: `400 invalid_request_error` con el código `unsupported_parameter`, `param` con el nombre del campo (por ejemplo `loras`) y el mensaje `This model does not support "loras".` Un parámetro es conocido cuando algún modelo del catálogo lo declara. La misma regla cubre las funciones compartidas (`resolution`, `ratio`, `audio`, `seed`, `negative_prompt`, `first_frame`, `last_frame`, `reference_images`, `reference_videos`, `prompt_extend`). Nada se descarta en silencio, porque cambiaría la salida y el precio.
- Un valor de tipo incorrecto se rechaza con `400 invalid_body`, `param` con el nombre y un mensaje como `loras must be of type array.`
- Un valor fuera de `values` se rechaza con `400 invalid_body` y el mensaje `<name> must be one of [...]`.
- Un campo que ningún modelo declara se ignora, igual que la API de OpenAI ignora los campos desconocidos.
- Cada modelo comprueba sus parámetros con su propio esquema: valores permitidos, mínimo y máximo, campos obligatorios y longitudes de lista. Un valor fuera del esquema responde `400 invalid_body` con `param` nombrando el campo, por ejemplo `seconds must be one of 5, 8.`
- Una combinación que un proveedor no permite, como medios de referencia junto con fotogramas clave, responde `400 invalid_combination` con el motivo. Las comprobaciones se ejecutan antes de cobrar nada.
- Los campos que cambian el precio, como `resolution` y `audio`, se valoran en la retención previa, por lo que la retención es exacta.

### Funciones compartidas

Estos campos tienen el mismo nombre en todos los modelos que los tienen. Un modelo lista los que admite en `supported_parameters`.

| Campo | Significado |
| --- | --- |
| `resolution` | `480p`, `720p`, `1080p`, `2K` o `4K`, según lo que permita el modelo. |
| `ratio`, `seed`, `negative_prompt`, `prompt_extend`, `audio` | Según su nombre. `audio` y `prompt_extend` están desactivados salvo que los pidas. |
| `first_frame`, `last_frame` | Fotogramas clave para vídeo. |
| `reference_images`, `reference_videos` | Medios de referencia para vídeo y edición de imágenes. |

### Expansión del prompt {#prompt-expansion}

Con `"prompt_expand": true`, Hyperouter reescribe tu prompt para el modelo antes de generar: una descripción más completa para una imagen y un guion segundo a segundo para un vídeo. Está desactivada por defecto, así que el prompt se envía tal cual. Solo la acepta un modelo cuyo `supported_parameters` incluya `prompt_expand`, y solo para crear una imagen o un vídeo; en cualquier otro modelo, y en la edición, extensión o remezcla de un vídeo, se rechaza con `400 unsupported_parameter`. Tampoco se puede combinar con `prompt_extend: true` (`400 invalid_body`).

La expansión del prompt está disponible para una cuenta cuando Hyperouter la habilita; abre un ticket de soporte para solicitar el acceso. Hasta entonces, `GET /v1/models` no lista `prompt_expand` en ningún modelo, y una solicitud con `prompt_expand: true` en un modelo que lo ofrece responde `403 permission_error`, código `feature_not_enabled`, `param` `prompt_expand`, sin retener nada ni enviarlo al proveedor.

- **Lo que se generó.** La respuesta de imagen, el objeto de vídeo y `GET /v1/generations/{id}` incluyen `expanded_prompt`, el prompt que recibió el modelo. Se conserva 30 días tras la solicitud; después se devuelve `null`. `prompt` sigue siendo tu propio texto.
- **Modelo que escribe.** `prompt_expand_model` indica el modelo de chat que escribe la reescritura; por defecto, `x-ai/grok-4.6`. Solo es válido con `prompt_expand: true` (si no, `400 invalid_body`). Un modelo que no existe o que no es de chat da `400 unknown_model`, y uno que tu cuenta no puede llamar da `403 model_not_enabled`, ambos antes de retener nada. Si el modelo lee imágenes, se le muestran las imágenes de la solicitud; si no, escribe solo a partir del texto.
- **Precio.** La reescritura se cobra en la misma solicitud, a los precios por token del modelo que la escribió (`prompt_expand_model`; si no, `x-ai/grok-4.6`) según los tokens que usó, y su importe máximo se retiene por adelantado junto con la generación. La referencia de modelos detalla cada modelo.
- **Cuándo no se ejecuta.** Si la reescritura falla, agota el tiempo o no se puede usar, se genera con tu prompt tal cual, se devuelve `expanded_prompt: null` y no cuesta nada.
- **Idioma.** Los diálogos de un vídeo siguen `locale` (si no, el inglés) cuando el modelo habla ese idioma.

#### La reescritura como modelo de chat {#prompt-expansion-model}

Una cuenta con expansión del prompt también tiene un modelo de chat que devuelve la reescritura en lugar de generarla; tu [referencia de modelos](#models) lo nombra. Llámalo en `POST /v1/chat/completions` con exactamente un mensaje `user`: su texto es el prompt, y sus partes `image_url` (URL https, 5 como máximo) son las imágenes; una URL `data:` o `http://` devuelve `400 invalid_image` con `param` igual a `messages`. `target_model` es obligatorio: un modelo que liste `prompt_expand`. `seconds`, `locale` y `negative_prompt` son opcionales. Un destino de vídeo con una imagen se escribe a partir de ese primer fotograma. No admite streaming (`stream: true` devuelve `400 unsupported_parameter`), y cualquier otro mensaje, incluido un mensaje system, devuelve `400 invalid_body`.

Este modelo de chat usa siempre `x-ai/grok-4.6`; no admite `prompt_expand_model`. `choices[0].message.content` es una cadena JSON, `{"prompt": "...", "negative_prompt": "..."}`, más `"beats"` (una línea por segundo) para un destino de vídeo. Se factura a los precios por token de `x-ai/grok-4.6`, que figuran en `usage`. Un prompt que pide las instrucciones del modelo devuelve `400 prompt_rejected`; una reescritura rechazada o vacía devuelve `422 expansion_refused`, y una que falla, `503 upstream_error`. Ninguno de estos casos se cobra.

## Moderación {#moderation}

El campo `moderation` es de OpenAI. Funciona en `/v1/chat/completions`, `/v1/images/generations`, `/v1/images/edits` y `/v1/videos` con sus llamadas de edición, extensión y remix.

| Valor | Significado |
| --- | --- |
| `auto` (por defecto) | Se aplica el filtrado propio del proveedor. Hyperouter no envía nada adicional. |
| `low` | Hyperouter envía la exclusión voluntaria del filtrado del proveedor, en los modelos donde el proveedor la ofrece. En los demás modelos `low` se ignora sin error, como hace OpenAI. La entrada de cada modelo en tu [referencia de modelos](https://dash.hyperouter.app/docs/models) indica si la ofrece. Envía `moderation` en el cuerpo: una cabecera o un campo de filtrado propio del proveedor no se reenvía en estos endpoints. |

Cualquier otro valor se rechaza: `400 invalid_request_error`, código `invalid_body`, `param` `moderation`, mensaje `moderation must be auto or low.`

`low` está disponible para una cuenta una vez que Hyperouter lo ha habilitado; abre un ticket de soporte para solicitarlo. Hasta entonces, una solicitud con `low` responde `403 permission_error`, código `moderation_not_enabled`, `param` `moderation`, y no se envía al proveedor.

```json
{
  "model": "qwen/qwen-image-2.0",
  "prompt": "...",
  "moderation": "low"
}
```

La respuesta no indica si se usó la exclusión voluntaria. Las políticas de los [Términos del servicio](terms.md) siguen aplicándose. Una solicitud que el filtro del proveedor rechaza falla con `400 content_rejected` y no se cobra.

## Webhooks {#webhooks}

En lugar de consultar periódicamente, un endpoint tuyo puede recibir una solicitud cuando termina un trabajo. Añade endpoints en el [panel](https://dash.hyperouter.app/webhooks). La estructura es la de los webhooks de OpenAI, firmados con el esquema Standard Webhooks, por lo que los SDK de OpenAI los verifican.

| Evento | Se envía cuando |
| --- | --- |
| `video.completed`, `video.failed` | Termina un trabajo de vídeo. |
| `image.completed`, `image.failed` | Termina un trabajo de imagen que enviaste con `Prefer: respond-async`. |
| `raw.completed`, `raw.failed` | Termina la tarea de una llamada directa (cuentas con llamadas directas): cobrada, o su retención liberada. Una llamada directa que responde en el momento no envía ninguno. |

La carga útil lleva un id, nunca un resultado, un prompt ni un cargo. Obtén el objeto con tu clave de API: `data.id` es el id que aceptan [GET /v1/videos/{id}](#videos) y [GET /v1/generations/{id}](#generations). En una tarea directa es el `X-Generation-Id` que devolvió la creación.

```json
{
  "id": "evt_0194f2c4-7a1e-7b3c-9d02-5f6a1c0e8b21",
  "object": "event",
  "type": "video.completed",
  "created_at": 1761300000,
  "data": { "id": "0194f2c4-7a1e-7b3c-9d02-5f6a1c0e8b21" }
}
```

### Encabezados y verificación

Cada entrega es un `POST` con `content-type: application/json` y estos encabezados:

- `webhook-id`: el id del evento. Es el mismo en cada reintento, así que úsalo para eliminar duplicados.
- `webhook-timestamp`: segundos Unix.
- `webhook-signature`: `v1,` y el HMAC-SHA256 en base64 de `id.timestamp.body`, con la clave secreta del endpoint.

El secreto de firma empieza por `whsec_`. Se muestra una sola vez, cuando creas el endpoint y cuando lo rotas. Verifica el cuerpo bruto de la solicitud, antes de analizarlo:

```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` lanza una excepción cuando la firma no se verifica; responde a una solicitud así con un estado de error. La llamada posterior usa la misma URL base que el resto de esta API.

### Endpoints, reintentos y límites

- Un endpoint debe ser `https` en el puerto 443 (o sin puerto) y un nombre de host, no una dirección IP. El host debe resolverse a una dirección pública; Hyperouter lo vuelve a comprobar al conectarse. No se siguen las redirecciones.
- Una cuenta tiene como máximo 10 endpoints. Cada endpoint elige los tipos de evento que quiere.
- Un `2xx` en 10 segundos es un éxito. Cualquier otra cosa se reintenta tras unos 5 segundos, 1 minuto, 5 minutos, 30 minutos, 2 horas, 5 horas, 10 horas y 10 horas, y después la entrega se marca como fallida.
- Un endpoint cuyas entregas fallan todas durante 3 días se desactiva, y el panel indica el motivo. Vuelve a activarlo después de arreglar el receptor.
- El panel lista las entregas de cada endpoint con su estado, intentos, último estado HTTP y último error. Tiene Resend (mismo id de evento) y Send test event, que entrega un evento `webhook.test` cuyo `data.id` es el id del endpoint.

## Servidor MCP {#mcp}

`POST https://api.hyperouter.app/mcp`

Un asistente de IA puede trabajar con tu cuenta de Hyperouter a través del Model Context Protocol (MCP). El servidor gestiona el panel: claves de API, actividad, créditos, tickets, webhooks y la lista de modelos. No ejecuta modelos. Las llamadas a modelos siguen yendo a `/v1` con una clave de API.

El servidor habla Streamable HTTP y JSON-RPC 2.0. Admite las revisiones de MCP `2025-11-25` y `2025-06-18`. No tiene estado: no hay sesiones, y `GET /mcp` responde `405`.

### Tokens de acceso

Un cliente MCP se autentica con un token de acceso del panel, no con una clave de API.

- Crea un token en el [panel](https://dash.hyperouter.app/tokens). Dale un nombre, un ámbito y, opcionalmente, una fecha de caducidad.
- Un token empieza por `hr_dt_`. Se muestra una sola vez. Hyperouter guarda solo un hash.
- Un usuario de la cuenta puede tener como máximo 25 tokens no caducados ni revocados. Revoca un token en el panel para detenerlo al instante.
- Un token no funciona en `/v1`, y una clave de API no funciona en `/mcp`.
- Un token actúa como el usuario que lo creó, con los permisos de ese usuario. Los permisos se comprueban en cada mensaje, así que un cambio de rol se aplica en la siguiente llamada.

| Ámbito | Herramientas que el token puede llamar |
| --- | --- |
| `read` | Herramientas que solo leen datos. |
| `write` | Todas las herramientas que el usuario tiene permiso para usar. |

Envía el token como token bearer:

```
Authorization: Bearer hr_dt_...
```

### Protocolo

| Método | Respuesta |
| --- | --- |
| `initialize` | La versión del protocolo (la del cliente, si se admite; si no, `2025-11-25`), la capacidad `tools` y la información del servidor. |
| `ping` | Un resultado vacío. |
| `tools/list` | Las herramientas que permiten el usuario y el ámbito del token. |
| `tools/call` | El resultado de una herramienta. |

Una notificación, o una respuesta al servidor, responde `202` sin cuerpo. Un lote JSON-RPC se rechaza con el error `-32600`. Un mensaje no debe superar 1 MiB. Un encabezado `MCP-Protocol-Version` desconocido responde `400`. Una solicitud de navegador debe proceder de `https://dash.hyperouter.app`; un cliente que no envía el encabezado `Origin` no se ve afectado.

```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"}'
```

### Herramientas

`tools/list` devuelve el conjunto actual de herramientas. Cada herramienta corresponde a una ruta del panel y lleva un esquema JSON Schema para sus argumentos. Las herramientas cubren:

- Cuenta y claves: `get_me`, `list_api_keys`, `create_api_key`, `update_api_key`.
- Actividad y créditos: `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`.
- Modelos: `list_models`.
- Las herramientas de personal, con el prefijo `admin_`, aparecen solo para los usuarios que tienen el permiso de personal.

Una herramienta responde con un elemento de contenido de texto que contiene el cuerpo JSON de la ruta. Cuando la ruta falla, el resultado tiene `isError: true` y el texto `<message> (<code>, HTTP <status>)`. Una herramienta desconocida, o una que tu token no puede ver, es el error JSON-RPC `-32602`.

### Aprobaciones

Una escritura sensible, como acreditar una cuenta, no se ejecuta de inmediato. La herramienta no la ejecuta. Responde con `status` igual a `approval_required` y una `approval_url` en el panel. El usuario abre el enlace, confirma con un segundo factor y aprueba la acción. Una aprobación caduca a las 24 horas. El usuario también puede rechazarla.

<!-- toc: Cuenta -->
## Facturación y límites {#billing}

- Las solicitudes se pagan con un saldo prepagado en USD, facturadas al precio de lista del proveedor que las atendió, sin recargo.
- **Retener y liquidar.** Antes de ejecutar una solicitud, Hyperouter retiene lo máximo que puede costar: tu entrada más `max_tokens` en chat, la cantidad solicitada en imágenes y los segundos en vídeo. Cuando termina, se te cobra lo que se usó y el resto de la retención se libera. El saldo nunca es negativo.
- Una solicitud fallida, o un trabajo de imagen o vídeo fallido, no cuesta nada. Un intento de reserva que falla no cuesta nada.
- Cada respuesta incluye su cargo como `usage.cost`, un número JSON en USD: en chat, en imágenes, en un vídeo liquidado y en `GET /v1/generations/{id}`. El panel muestra todas las solicitudes y tu gasto por día, modelo y clave.
- Los importes son exactos: el dinero se cuenta en milmillonésimas de dólar y nunca se redondea a través de un float. Los precios de `GET /v1/models` son cadenas decimales.
- Por ahora las recargas son manuales. Abre una solicitud de recarga en el [panel](https://dash.hyperouter.app/credits) con el importe, la referencia del pago y una captura de pantalla del pago. El equipo comprueba el pago y acredita tu saldo, y responde en el mismo ticket.
- **Comisión por recarga.** Cada recarga lleva una comisión del 5,5 % del importe pagado, con un mínimo de $0.80, que se descuenta cuando se acredita la recarga: un pago de $100 acredita $94.50, y un pago de $10 acredita $9.20. La comisión vigente se muestra en la página [Credits](https://dash.hyperouter.app/credits) antes de que solicites una recarga, y cada recarga de tu historial muestra el importe pagado y la comisión. Las solicitudes en sí no llevan ninguna comisión.

### Límites de velocidad {#rate-limits}

Cada cuenta tiene un límite de solicitudes por minuto y de solicitudes en curso a la vez. Ambos cuentan juntas todas las claves de API de la cuenta, y se aplican a toda solicitud que inicia trabajo: chat completions, creaciones de imagen y vídeo, y creaciones directas. Leer un resultado (`GET /v1/generations/{id}`, `GET /v1/videos…`, una consulta directa de tarea) no cuenta; las consultas directas se cuentan aparte, por proveedor, con el mismo límite de solicitudes por minuto.

Tu nivel depende del total que hayas recargado, antes de la comisión por recarga:

| Nivel | Recargado | Solicitudes por minuto | Solicitudes concurrentes |
| --- | --- | --- | --- |
| 1 | Menos de $10 | 60 | 10 |
| 2 | $10 o más | 300 | 30 |
| 3 | $100 o más | 1,200 | 100 |
| 4 | $1,000 o más | 6,000 | 300 |

Tus límites actuales aparecen en la página [Credits](https://dash.hyperouter.app/credits). Son los valores por defecto; el personal puede cambiarlos para la plataforma o para una cuenta concreta, y el cambio se aplica en un minuto.

Si se supera cualquiera de los dos límites, la solicitud se rechaza antes de retener o reenviar nada, con `429`, `type: rate_limit_error`, `code: rate_limited` y un encabezado `Retry-After` en segundos. El mensaje indica el límite alcanzado. Una solicitud cuenta para el minuto aunque se rechace. Una solicitud en curso, incluido un stream, cuenta hasta que termina.

Como protección contra el intento de adivinar claves, Hyperouter cuenta las comprobaciones fallidas de clave de API por dirección de cliente. Más de 30 en un minuto bloquean esa dirección en todas las rutas `/v1` durante 15 minutos: `429`, `rate_limit_error`, `too_many_auth_failures`, con `Retry-After`. Una clave válida no levanta el bloqueo.

## Soporte {#support}

Abre un ticket en el [panel](https://dash.hyperouter.app/tickets) para dudas sobre facturación, recargas y la API; el equipo responde en el mismo hilo. También puedes escribir a [account@hyperouter.app](mailto:account@hyperouter.app). Incluye el `id` de la respuesta cuando informes de una solicitud concreta.
