HiAPI
Blog

Tutorial

API de texto a video Happyhorse 1.1: tutorial ejecutable con curl y Python

Jun 26, 2026
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:

  • status pasa por queued → handling → archiving → success (o fail). Cualquier estado anterior a success o fail no debe considerarse terminal.
  • El tiempo de generación crece con duration y 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[].url se 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:

  1. 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.
  2. Desacopla la confirmación del procesamiento. En cuanto encoles el payload, devuelve 200 OK desde 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.
  3. Recurre al sondeo. Guarda cada taskId que envíes junto con created_at en 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

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 →

Empieza a crear con artefactos de IA persistentes

Consigue tu API key y llama a modelos de imagen, vídeo y audio a través de una única API basada en tasks.

Leer la documentación de la API