HiAPI
Blog

Tutorial

Cómo usar la API de imagen a video de Ray 3.2: curl, Python y una solicitud que funciona

Oct 20, 2026
Cómo usar la API de imagen a video de Ray 3.2: curl, Python y una solicitud que funciona

Ray 3.2 imagen a video anima un único fotograma estático: le das una imagen más un prompt que describe cómo debe moverse, y devuelve un clip corto que comienza con tu imagen. En hiapi, el id del modelo es ray-3.2/image-to-video, y se invoca mediante la misma API de tareas asíncrona unificada que todos los demás modelos de video: POST /v1/tasks, y luego sondeas o recibes un callback.

Es un endpoint distinto de Ray 3.2 texto a video, no solo un prompt diferente. Su schema de entrada es más pequeño y estricto, y faltan dos controles a los que quizá estés acostumbrado con el modelo de texto. Esta guía te da una solicitud que funciona a la primera: el schema exacto (verificado contra la API en producción el 2026-10-20), curl listo para copiar, un script completo de Python para enviar/sondear/descargar, y detalles de producción — callbacks, idempotencia y respuestas de error reales, incluido ese modo de fallo que no aparece donde esperarías.

Qué vas a construir y qué necesitas

Objetivo: enviar una imagen de primer fotograma más un prompt de movimiento, sondear la tarea y descargar un .mp4 silencioso que da vida a tu imagen.

Requisitos previos:

  • Una API key de hiapi — consigue una desde tu panel. La clave tiene la forma sk-... y va en la cabecera Authorization: Bearer. ¿Nuevo con la API de tareas? Empieza por la guía de inicio rápido.
  • Una URL de imagen accesible públicamente para el primer fotograma (detalles abajo). Cualquier cliente HTTP — mostraremos curl y Python (requests).

Ray 3.2 imagen a video se factura por clip. Como la duración del clip es fija (ver abajo), resolution es tu única palanca de coste — un clip en 1080p cuesta varias veces más que uno en 540p. Consulta los precios actuales y prototipa con 540p mientras iteras los prompts.

El schema exacto de entrada (dos campos obligatorios, uno opcional)

Todo va dentro del objeto input. El schema es estricto — rechaza cualquier campo no reconocido. Verificado campo por campo contra la API en producción:

Campo Tipo Obligatorio Valores permitidos Por defecto
prompt string Sí Cómo debe moverse la escena (cámara, movimiento del sujeto) —
image_url string Sí URL pública del primer fotograma —
resolution string No 540p, 720p, 1080p 540p

Tres trampas en las que cae la gente que viene del modelo texto a video:

  1. No hay duration. La duración del clip es fija (unos 5 segundos). Enviar duration no se recorta — se rechaza directamente. Es lo contrario que en Ray 3.2 texto a video, donde duration es un entero seleccionable (5 o 10).
  2. No hay aspect_ratio. La relación de aspecto de salida sigue a tu imagen de entrada, así que no hay nada que ajustar. En el modelo de texto eliges la relación de aspecto; aquí la decide la imagen. (Confirmado abajo con una ejecución real: una entrada cuadrada produjo un clip cuadrado.)
  3. El schema es cerrado. seed, negative_prompt, prompt_optimizer y cualquier otro campo devuelven 400 con additional properties '<field>' not allowed. No portes el cuerpo de la solicitud de otros modelos y asumas que las claves sobrantes se ignoran.

Lo que devuelve realmente la validación

Estas son respuestas reales del endpoint en producción — enviando valores inválidos a propósito, sin crear tarea ni facturar:

// POST with no image_url
{"code":400,"data":null,"error_code":"INVALID_REQUEST",
 "message":"invalid input: image_url: missing required field \"image_url\""}

// POST with "resolution": "2k"
{"code":400,"data":null,"error_code":"INVALID_REQUEST",
 "message":"invalid input: resolution: value must be one of '540p', '720p', '1080p'"}

// POST with a "duration" field
{"code":400,"data":null,"error_code":"INVALID_REQUEST",
 "message":"invalid input: <root>: additional properties 'duration' not allowed"}

// POST with an "aspect_ratio" field
{"code":400,"data":null,"error_code":"INVALID_REQUEST",
 "message":"invalid input: <root>: additional properties 'aspect_ratio' not allowed"}

message señala el campo problemático concreto, así que cuando se te olvide algo, la propia API es tu referencia del schema.

Preparar el primer fotograma

image_url debe ser una URL que el worker de hiapi pueda buscar — un enlace público, no una ruta local ni un blob base64 con data:. La imagen determina la relación de aspecto del clip y la composición inicial, así que parte del fotograma exacto desde el que quieres que empiece el movimiento.

Hay una trampa que merece la pena señalar por adelantado, porque es fácil pasarla por alto: una image_url incorrecta no se detecta al crear la tarea. La llamada de creación solo valida la forma de la solicitud (¿es una cadena?), no si la URL es resoluble. Una URL de imagen inalcanzable o 404 devuelve un 200 y un taskId perfectamente normales — y luego la tarea pasa a fail unos segundos después. Veremos la carga de fallo exacta en la sección de resolución de problemas; la lección es ramificar siempre según el status terminal de la tarea, nunca asumir que un 200 en la creación significa éxito.

Ejemplo mínimo funcional: curl

Crear la tarea:

curl -s -X POST https://api.hiapi.ai/v1/tasks \
  -H "Authorization: Bearer sk-YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "ray-3.2/image-to-video",
    "input": {
      "prompt": "slow cinematic push-in, gentle parallax, soft drifting light, the subject breathing almost imperceptibly",
      "image_url": "https://your-cdn.example.com/first-frame.jpg",
      "resolution": "540p"
    }
  }'

La respuesta de creación trae un id de tarea:

{ "code": 200, "data": { "taskId": "tk-hiapi-01M5..." }, "message": "success" }

Sondea hasta que llegue a un estado terminal:

curl -s https://api.hiapi.ai/v1/tasks/tk-hiapi-01M5... \
  -H "Authorization: Bearer sk-YOUR_KEY"

Mientras la tarea se ejecuta, data.status pasa por varios estados en curso (verás valores como handling y luego archiving). Los dos estados terminales son success y fail. En caso de éxito:

{
  "code": 200,
  "data": {
    "taskId": "tk-hiapi-01M5...",
    "status": "success",
    "created": 1792435735,
    "completed": 1792435781,
    "storage": "temp",
    "output": [
      { "type": "video", "artifactId": "278504",
        "expireAt": 1793040580,
        "url": "https://temp.hiapi.ai/.../clip.mp4" }
    ]
  },
  "message": "success"
}

Descarga data.output[0].url de inmediato. Es una URL firmada en temp.hiapi.ai con un expireAt (marca de tiempo Unix, aproximadamente una semana después) — persiste los bytes en tu propio almacenamiento en lugar de enlazarla en caliente. Ray 3.2 devuelve MP4 sin audio; no hay pista de audio que esperar.

Script completo de Python (enviar → sondear → descargar)

import time
import requests

API_BASE = "https://api.hiapi.ai/v1"
API_KEY = "sk-YOUR_KEY"
HEADERS = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}


def create_task(image_url: str, prompt: str, resolution: str = "540p") -> str:
    resp = requests.post(
        f"{API_BASE}/tasks",
        headers=HEADERS,
        json={
            "model": "ray-3.2/image-to-video",
            "input": {
                "prompt": prompt,
                "image_url": image_url,     # public URL of the first frame (required)
                "resolution": resolution,   # "540p" | "720p" | "1080p"
                # no "duration" and no "aspect_ratio" — both are rejected
            },
        },
        timeout=30,
    )
    resp.raise_for_status()
    return resp.json()["data"]["taskId"]


def wait_task(task_id: str, timeout_s: int = 900, poll_interval: int = 8) -> dict:
    """Poll until terminal. Tolerates transient network blips by re-polling
    the SAME saved task_id instead of aborting the whole job."""
    deadline = time.time() + timeout_s
    while time.time() < deadline:
        try:
            resp = requests.get(f"{API_BASE}/tasks/{task_id}", headers=HEADERS, timeout=30)
            resp.raise_for_status()
        except requests.RequestException:
            time.sleep(poll_interval)      # a dropped poll is not a failed task
            continue
        task = resp.json()["data"]
        if task["status"] == "success":
            return task
        if task["status"] == "fail":
            err = task.get("error") or {}
            raise RuntimeError(f"task failed: {err.get('code')}: {err.get('message')}")
        time.sleep(poll_interval)           # any non-terminal status = still running
    raise TimeoutError(f"task {task_id} not finished after {timeout_s}s")


task_id = create_task(
    image_url="https://your-cdn.example.com/first-frame.jpg",
    prompt="slow cinematic push-in, gentle parallax, soft drifting light",
)
print("task created:", task_id)

task = wait_task(task_id)
video_url = task["output"][0]["url"]

# Output URLs expire — download right away and store the bytes yourself.
video = requests.get(video_url, timeout=120).content
with open("ray32-i2v.mp4", "wb") as f:
    f.write(video)
print(f"saved ray32-i2v.mp4 ({len(video) / 1e6:.1f} MB)")

Ejecútalo con pip install requests, pon tu clave y una image_url real, y cuando la tarea termine tendrás un .mp4 en tu disco.

Cómo es una ejecución real

Para concretar los números, aquí tienes una ejecución real en 540p contra la API en producción el 2026-10-20. Primer fotograma: una imagen 1600×1600 (cuadrada); prompt: lento acercamiento cinematográfico.

  • La tarea pasó por handling → archiving → success, con el trabajo del servidor completado en unos 45 segundos (created a completed) — unos 60 segundos de tiempo real contando el bucle de sondeo de 8 segundos. Las tareas de video tardan decenas de segundos o minutos, no son instantáneas; deja un margen de tiempo holgado.
  • La salida fue un MP4 de 720×720, unos 1,3 MB, 5,04 segundos, sin audio.

Esta ejecución te dice dos cosas que la tabla de parámetros no explica:

  1. La relación de aspecto de salida coincide con la de entrada — entra cuadrado, sale cuadrado. Eso es por qué no hay campo aspect_ratio; encuadra la image_url según la composición del clip que quieras.
  2. resolution es un nivel de calidad, no una altura de píxeles literal. Pedir 540p para una entrada de 1600×1600 devolvió 720×720 — el nivel escala según la forma de entrada, no fuerza un tamaño fijo. Si necesitas dimensiones de salida exactas, redimensiona el clip entregado tú mismo.

Consideraciones de producción: callbacks, idempotencia, errores

Prefiere callbacks al sondeo

Para cualquier cosa más allá de un script, registra un webhook en la creación en lugar de sondear:

{
  "model": "ray-3.2/image-to-video",
  "input": {
    "prompt": "...",
    "image_url": "https://your-cdn.example.com/first-frame.jpg",
    "resolution": "720p"
  },
  "callback": { "url": "https://your-app.example.com/hooks/hiapi", "when": "final" }
}

Con "when": "final", hiapi llama a tu endpoint una vez cuando la tarea alcanza un estado terminal — sin tráfico de sondeo y sin perder finalizaciones entre intervalos. Tu handler hace entonces GET /v1/tasks/<id> para obtener output[0].url. El sondeo sigue estando bien para CLI y notebooks que no pueden exponer un endpoint HTTPS.

Haz los reintentos idempotentes

Persiste el taskId en el momento en que la llamada de creación lo devuelve. Si tu proceso se cae a mitad de la generación, reanuda sondeando el id guardado — no vuelvas a crear la tarea, o pagarás por un segundo clip. Recrea solo si la solicitud de creación original falló antes de devolver el id de tarea. El bucle de sondeo tolerante a fallos de arriba ya sigue esta regla: ante una caída de red, vuelve a sondear el mismo id en lugar de empezar de cero.

Errores que verás realmente

Cada fila de la tabla siguiente es una forma de respuesta verificada en producción:

Síntoma Respuesta Solución
Clave ausente/no válida 401, anidado {"error":{"code":"permission_denied","type":"hiapi_error","request_id":"...","message":"This API key is invalid..."}} Revisa la cabecera Authorization: Bearer sk-... y si la clave tiene acceso. Incluye el request_id si contactas con soporte.
Falta image_url 400 INVALID_REQUEST, message: "image_url: missing required field" Este modelo requiere image_url — no hay modo solo texto.
Enumeración incorrecta / campo sobrante 400 INVALID_REQUEST, plano {"code":400,"error_code":"INVALID_REQUEST","message":"..."} Lee el campo señalado en message. Para duration/aspect_ratio dirá additional properties ... not allowed — elimínalos.
Id de tarea incorrecto al sondear 404, plano {"code":404,"data":null,"message":"task not found"} (nota: no hay error_code en el 404) Id incorrecto, o estás sondeando con la clave de otra cuenta.
image_url inalcanzable La creación devuelve 200 + un taskId; la tarea termina después con data.status:"fail" y data.error:{"code":"INVALID_REQUEST","message":"Invalid request parameters for this model..."} Esta es la trampa. La llamada de creación no distingue entre un enlace muerto y uno vivo. Verifica que tu imagen es accesible públicamente y lee siempre el data.status terminal.

La última fila merece recordarse: incluso cuando la tarea falla, la envoltura HTTP del sondeo sigue siendo 200 — el fallo está en data.status y data.error, no en el código de estado HTTP. Las tareas fallidas se reembolsan, pero tu código aun así debe detectarlas. Ramifica según data.status == "fail", no confíes en el 200.

Recursos relacionados

Preguntas frecuentes

¿Cuál es el id del modelo y qué endpoint debo llamar? ray-3.2/image-to-video, enviado a POST https://api.hiapi.ai/v1/tasks, con los parámetros dentro de input. Es asíncrono — recibes un taskId y luego sondeas GET /v1/tasks/<id> (o registras un callback).

¿Qué exige la entrada? Exactamente dos campos obligatorios — prompt (movimiento) e image_url (URL pública del primer fotograma) — más un resolution opcional (540p/720p/1080p). Cualquier otra cosa se rechaza.

¿Por qué no puedo ajustar la duración ni la relación de aspecto? La duración del clip es fija (unos 5 segundos), así que no hay campo duration; la relación de aspecto de salida se hereda de tu imagen de entrada, así que no hay campo aspect_ratio. Enviarlos devuelve un 400. Si necesitas una duración seleccionable, usa mejor Ray 3.2 texto a video.

Mi llamada de creación devolvió 200, pero el video nunca llega. ¿Por qué? Lo más habitual es que la image_url sea inalcanzable. La llamada de creación solo valida la forma de la solicitud, así que un enlace muerto igualmente devuelve un taskId — y luego la tarea termina en fail. Sondea hasta el estado terminal y lee data.error. Asegúrate de que la imagen se pueda buscar públicamente (sin autenticación, ni ruta local, ni base64).

¿El clip tiene audio? No — Ray 3.2 imagen a video devuelve un MP4 sin audio. Añade sonido en posproducción o usa un modelo de audio aparte.

¿Cuánto cuesta un clip? Se factura por clip, y como la duración es fija, resolution es la única palanca — un clip en 1080p cuesta varias veces más que uno en 540p. Los precios cambian, así que consulta la página de precios en lugar de codificar cifras a fuego, y prototipa con 540p.

¿Cuánto debo esperar antes de rendirme? Lo normal es entre decenas de segundos y varios minutos. Tolera los data.status no terminales en el bucle de sondeo y usa un plazo de tiempo holgado (10–15 minutos), o registra un callback con "when": "final" para que hiapi te notifique en lugar de adivinar intervalos.

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