Tutorial
API de texto a video Happyhorse 1.1: tutorial ejecutable con curl y Python
Esta página te guiará por la solicitud mínima para generar un video con happyhorse-1.1/text-to-video, explicará qué valores acepta realmente cada campo de entrada y presentará las dos formas de esperar el resultado (sondeo y callback). Todo lo siguiente se ha probado contra la API en producción; la estructura de las solicitudes y los mensajes de error se reproducen literalmente.
Qué vas a construir
Una función: recibe un prompt de texto y devuelve una URL de MP4 descargable. El modelo es happyhorse-1.1/text-to-video, disponible a través del endpoint unificado /v1/tasks. Envías una tarea, recibes inmediatamente un taskId y luego o bien sondeas hasta que termine, o bien dejas que hiapi envíe el resultado final por POST a tu webhook.
Requisitos previos:
- Una clave de API de hiapi. Las cuentas nuevas reciben 1 dólar de crédito gratuito; consíguela en la página de claves de API.
- Una forma de enviar solicitudes HTTPS. No hace falta instalar ningún SDK.
No pongas la clave en el código del cliente ni la subas a git. Léela desde una variable de entorno:
export HIAPI_API_KEY="sk-..."
Solicitud mínima funcional
El único campo de entrada obligatorio es prompt. Todos los demás tienen valores predeterminados.
curl
curl -X POST https://api.hiapi.ai/v1/tasks \
-H "Authorization: Bearer $HIAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "happyhorse-1.1/text-to-video",
"input": {
"prompt": "a calm ocean wave at sunset, slow motion, golden hour"
}
}'
Respuesta:
{
"code": 200,
"data": { "taskId": "tk-hiapi-01KW23VDG2EZ20J7RQH8S67MNC" },
"message": "success"
}
Ese taskId es tu credencial para operar con la tarea. Guárdalo bien.
Python (solo biblioteca estándar)
import os
import urllib.request
import json
API_KEY = os.environ["HIAPI_API_KEY"]
def submit_task(prompt: str, **input_overrides) -> str:
body = {
"model": "happyhorse-1.1/text-to-video",
"input": {"prompt": prompt, **input_overrides},
}
req = urllib.request.Request(
"https://api.hiapi.ai/v1/tasks",
method="POST",
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
},
data=json.dumps(body).encode("utf-8"),
)
with urllib.request.urlopen(req) as resp:
payload = json.loads(resp.read())
if payload.get("code") != 200:
raise RuntimeError(payload)
return payload["data"]["taskId"]
task_id = submit_task("a calm ocean wave at sunset, slow motion, golden hour")
print(task_id) # e.g. tk-hiapi-01KW23VDG2EZ20J7RQH8S67MNC
Tabla completa de parámetros
La estructura de parámetros es pequeña y estricta: los campos desconocidos se rechazan con 400 en lugar de descartarse silenciosamente. Estos son los campos dentro de input que la API acepta realmente, tomados directamente de los mensajes de error de validación:
| Campo | Tipo | Obligatorio | Valores permitidos | Comportamiento predeterminado |
|---|---|---|---|---|
prompt |
string | Sí | Cualquier texto no vacío que describa la escena | — |
aspect_ratio |
string | No | 16:9, 9:16, 3:4, 4:3, 4:5, 5:4, 1:1, 9:21, 21:9 |
Valor predeterminado del modelo |
resolution |
string | No | 720p, 1080p |
Valor predeterminado del modelo |
duration |
integer | No | 3 a 15 (segundos) | Valor predeterminado del modelo |
Campos que parecen obvios pero que actualmente no se aceptan: size, audio, seed, negative_prompt. Enviar cualquiera de ellos devuelve additional properties '<field>' not allowed. Si has usado otros modelos de hiapi —qwen-image-2.0 usa size, z-image usa aspect_ratio— ten en cuenta que happyhorse-1.1/text-to-video pertenece a la familia de relación de aspecto, no a la de tamaño explícito.
Una solicitud que usa todos los parámetros admitidos:
curl -X POST https://api.hiapi.ai/v1/tasks \
-H "Authorization: Bearer $HIAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "happyhorse-1.1/text-to-video",
"input": {
"prompt": "a vintage paper plane gliding above neon Tokyo at night, cinematic, 24fps",
"aspect_ratio": "16:9",
"resolution": "1080p",
"duration": 8
}
}'
Sobre el coste y la duración
Este modelo se factura por los segundos de video generados; consulta la tarifa actual en la página de precios. duration es la palanca de coste; resolution no lo es. Pasar de 5 a 15 segundos triplica el coste; en el momento de escribir esto, pasar de 720p a 1080p no lo hace. Durante el desarrollo, usa por defecto duration: 3 y una resolución más baja; sube a 1080p solo con los prompts ya confirmados definitivamente.
Obtener el resultado: sondeo
La forma más sencilla de recuperar la salida es hacer GET de la tarea por id hasta que status alcance un estado terminal.
import time
import urllib.request
import json
def get_task(task_id: str) -> dict:
req = urllib.request.Request(
f"https://api.hiapi.ai/v1/tasks/{task_id}",
headers={"Authorization": f"Bearer {API_KEY}"},
)
with urllib.request.urlopen(req) as resp:
return json.loads(resp.read())["data"]
def wait_for_task(task_id: str, timeout_s: int = 600, poll_interval_s: int = 10) -> dict:
deadline = time.time() + timeout_s
while time.time() < deadline:
task = get_task(task_id)
status = task["status"]
if status == "success":
return task
if status == "fail":
raise RuntimeError(f"task failed: {task}")
time.sleep(poll_interval_s)
raise TimeoutError(f"task {task_id} still {status} after {timeout_s}s")
task = wait_for_task(submit_task("a quiet coffee shop on a rainy afternoon"))
video_url = task["output"][0]["url"]
print(video_url)
Algunos detalles importantes:
statuspasa porqueued → handling → archiving → success(ofail). Cualquier estado anterior asuccessofailno debe considerarse terminal.- El tiempo de generación crece con
durationy la profundidad actual de la cola. Una tarea de 5 segundos a 720p suele completarse en pocos minutos; una de 15 segundos a 1080p necesita reservar más tiempo. output[].urlse guarda en almacenamiento temporal. Descarga los bytes de inmediato y vuelve a alojarlos por tu cuenta; no guardes solo la URL para entregarla a los usuarios finales: caduca.
import urllib.request
def download(video_url: str, out_path: str) -> None:
with urllib.request.urlopen(video_url) as resp, open(out_path, "wb") as f:
f.write(resp.read())
download(video_url, "ocean.mp4")
Obtener el resultado: callback (modo producción)
El sondeo va bien para scripts y notebooks, pero en un backend real no querrás que un worker quede ocupado varios minutos por cada video. Pasa un objeto callback al enviar la tarea y hiapi enviará por POST el cuerpo de la tarea completada exactamente una vez a tu URL.
curl -X POST https://api.hiapi.ai/v1/tasks \
-H "Authorization: Bearer $HIAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "happyhorse-1.1/text-to-video",
"input": {
"prompt": "an astronaut planting a flag on a glass beach",
"aspect_ratio": "16:9",
"resolution": "1080p",
"duration": 6
},
"callback": {
"url": "https://example.com/hooks/hiapi",
"when": "final"
}
}'
Actualmente solo se admite when: "final"; enviar otro valor devuelve:
{ "code": 400, "error_code": "INVALID_REQUEST",
"message": "invalid callback.when: only 'final' is supported" }
Diseña un receptor robusto
Tres reglas garantizan que la ruta de callback sea segura:
- Idempotencia. Persiste por
taskId. Si recibes el mismo callback dos veces —algo que al final ocurrirá, por los reintentos—, el procesamiento duplicado debe ser una operación nula. - Desacopla la confirmación del procesamiento. En cuanto encoles el payload, devuelve
200 OKdesde el webhook. La descarga, la re-subida y la escritura en base de datos reales van en una tarea en segundo plano. Un webhook lento provoca tiempos de espera agotados y entregas duplicadas. - Recurre al sondeo. Guarda cada
taskIdque envíes junto concreated_aten tu propia base de datos. Ejecuta un barrendero que sondee/v1/tasks/{id}para cualquier fila que supere unos 30 minutos sin estado terminal. El callback es la vía rápida, no la única vía.
Un receptor mínimo con FastAPI:
from fastapi import FastAPI, Request
app = FastAPI()
@app.post("/hooks/hiapi")
async def hiapi_callback(req: Request):
body = await req.json()
task = body.get("data", body) # callback wraps the same shape as GET
task_id = task["taskId"]
if task["status"] == "success":
url = task["output"][0]["url"]
# enqueue, don't download here
await queue.put({"task_id": task_id, "url": url})
return {"ok": True}
Errores que realmente encontrarás
La forma del sobre de error es siempre la misma —{code, error_code, message, data}—, así que puedes escribir un único manejador. Los que merece la pena tratar de forma especial para este modelo:
| HTTP | error_code |
Condición que lo activa | Cómo solucionarlo |
|---|---|---|---|
| 400 | INVALID_REQUEST |
Campo desconocido, valor de enumeración incorrecto, duration fuera de 3–15 |
Consulta message; indicará el campo, por ejemplo aspect_ratio: value must be one of '16:9'... |
| 401 | — | Clave ausente o incorrecta, o permission_denied |
Revisa de nuevo la cabecera Authorization: Bearer sk-... |
| 402 | — | Saldo insuficiente para cubrir el coste estimado | Recarga en hiapi.ai/en/pricing y vuelve a intentarlo |
| 429 | — | Estás machacando el endpoint | Retrocede con backoff y reintenta con jitter |
Un fallo de validación real tiene este aspecto; puedes copiarlo como fixture de tus pruebas:
{
"code": 400,
"data": null,
"error_code": "INVALID_REQUEST",
"message": "invalid input: aspect_ratio: value must be one of '16:9', '9:16', '3:4', '4:3', '4:5', '5:4', '1:1', '9:21', '21:9'; resolution: value must be one of '720p', '1080p'; duration: maximum: got 99, want 15"
}
Fíjate en que cada campo se informa en la misma respuesta. Valida localmente cuando puedas, pero cuando no puedas, confía en esto.
Contenido relacionado en hiapi
- Página del modelo happyhorse-1.1/text-to-video: playground, interfaz completa de parámetros y precios en tiempo real
- happyhorse-1.1/image-to-video: la misma familia de modelos, pero para animar imágenes estáticas
- veo-3.1-fast/text-to-video y seedance-2.0: modelos alternativos de texto a video sobre la misma API
/v1/tasks - Referencia de la API de tareas unificada: fuente autoritativa de
POST /v1/tasks,GET /v1/tasks/:id, callbacks y sobres de error - Explora todos los modelos y precios
Preguntas frecuentes
¿happyhorse-1.1/text-to-video genera audio?
A través de la API de tareas, no: la estructura de la solicitud actualmente no tiene un campo audio, y enviarlo devuelve additional properties 'audio' not allowed. La salida es un MP4 sin sonido. Añade la banda sonora en una etapa posterior.
¿Qué relaciones de aspecto hay disponibles para vertical / video corto?
Para salidas estilo TikTok y Reels, 9:16 es la opción estándar. La API también acepta 4:5 (vertical para el feed de Instagram) y 9:21 (vertical extra largo), por si necesitas un encuadre más estrecho.
¿Cuál es la duración mínima y máxima?
duration es un entero de 3 a 15 segundos (extremos incluidos). Enviar 0 muestra duration: minimum: got 0, want 3; enviar 99 muestra duration: maximum: got 99, want 15. Elige el valor mínimo que capture el movimiento que necesitas: la duración es la parte por la que pagas.
¿Puedo pasar seed para obtener una salida reproducible?
Este modelo actualmente no lo admite. La solicitud {"input": {"prompt": "...", "seed": 42}} falla y devuelve additional properties 'seed' not allowed. Si la reproducibilidad es importante, registra el cuerpo completo de la solicitud y reintenta con el mismo prompt; obtendrás resultados similares, pero no idénticos píxel a píxel.
¿Se admite negative_prompt?
No. Guía la salida añadiendo restricciones en el propio prompt (por ejemplo, "...clean composition, no text, no watermarks").
¿Cuánto tiempo es válida la URL de salida?
Con el plan predeterminado, el campo storage de la tarea es temp y output[].url es un enlace firmado de corta duración. Descarga el archivo y vuelve a alojarlo en cuanto la tarea llegue a success. No guardes la URL original para servirla más tarde.
¿Cómo controlo el gasto durante el desarrollo?
Dos hábitos que ahorran: mantén duration: 3 hasta fijar los prompts y envía con resolution: "720p" hasta confirmar el storyboard. Sube a 1080p y duraciones más largas solo para el render final.
¿Sondeo o callback? ¿Cuál debo usar?
Usa ambos, en este orden. Primero el sondeo, porque tiene una pieza menos en movimiento. Pasa al callback la primera vez que tengas un backend en el que no puedas permitirte bloquear un worker en wait_for_task. En cualquier caso, mantén el sondeo como barrendero de respaldo; el callback es de mejor esfuerzo, no una entrega garantizada.
¿Por qué recibo un 402 si tengo crédito para llamar? La API estima el límite de coste de la tarea —duración × precio por segundo— y lo reserva por adelantado. Si el saldo no cubre esa estimación, devuelve 402 de inmediato. Recarga, vuelve a intentarlo y, si tiene éxito, solo se cobra el coste real.
Artículos relacionados
Ver todos los artículos →-
Tutorial
Cómo llamar a hailuo-2.3-fast/image-to-video a través de la API de HiAPI: práctica con curl y Python
La primera solicitud ejecutable del nivel rápido y de bajo coste de imagen a vídeo: del ID del modelo a la descarga del MP4
-
Tutorial
Cómo llamar a hailuo-2.3/text-to-video mediante la API de HiAPI: curl, Python y ejemplos de solicitud ejecutables
-
Tutorial
Uso de Kandinsky 6.0 Pro texto a video en HiAPI: tutorial con curl y Python
Demostración práctica centrada en la solicitud: construir la llamada de prompt a clip de video, enviar la solicitud, sondear el estado de finalización y descargar el resultado