Tutorial
Como usar a API de imagem para vídeo do Ray 3.2: curl, Python e uma requisição que funciona
O Ray 3.2 imagem para vídeo anima um quadro estático: você envia uma imagem mais um prompt descrevendo como ela deve se mover, e ele devolve um vídeo curto que começa a partir da sua imagem. No hiapi, o id do modelo é ray-3.2/image-to-video, chamado pela mesma API assíncrona de tarefas unificada de todos os outros modelos de vídeo — POST /v1/tasks, depois polling ou callback.
É um endpoint diferente do Ray 3.2 texto para vídeo, não apenas um prompt diferente. Seu schema de entrada é menor e mais rígido, e dois controles que você talvez espere do modelo de texto não estão lá. Este guia entrega uma requisição que funciona de primeira: o schema exato (verificado contra a API em produção em 2026-10-20), curl pronto para copiar, um script Python completo de envio/polling/download e detalhes de produção — callbacks, idempotência e respostas de erro reais, incluindo aquele modo de falha que não aparece onde você espera.
O que você vai construir e o que precisa ter
Objetivo: enviar uma imagem de primeiro quadro + um prompt de movimento, fazer polling da tarefa e baixar um .mp4 sem áudio que anima sua imagem.
Pré-requisitos:
- Uma chave de API do hiapi — pegue uma no seu painel. A chave se parece com
sk-...e vai no cabeçalhoAuthorization: Bearer. Novo na API de tarefas? Leia primeiro o quickstart. - Uma URL de imagem publicamente acessível para a imagem do primeiro quadro (detalhes abaixo). Qualquer cliente HTTP — vamos mostrar
curle Python (requests).
O Ray 3.2 imagem para vídeo é cobrado por clipe de vídeo. Como a duração do clipe é fixa (veja abaixo), resolution é a única alavanca de custo que você tem — um clipe em 1080p custa várias vezes mais que um em 540p. Consulte os preços atuais e prototipe com 540p enquanto itera nos prompts.
O schema de entrada exato (dois campos obrigatórios, um opcional)
Tudo fica dentro do objeto input. O schema é rígido — ele rejeita qualquer campo não reconhecido. Verificado campo a campo contra a API em produção:
| Campo | Tipo | Obrigatório | Valores permitidos | Padrão |
|---|---|---|---|---|
prompt |
string | Sim | Como a imagem deve se mover (câmera, movimento do sujeito) | — |
image_url |
string | Sim | URL pública do primeiro quadro | — |
resolution |
string | Não | 540p, 720p, 1080p |
540p |
Três armadilhas para quem vem do modelo de texto para vídeo:
- Sem
duration. A duração do clipe é fixa (cerca de 5 segundos). Enviardurationnão é truncado — é rejeitado de imediato. É o oposto do Ray 3.2 texto para vídeo, ondedurationé um inteiro selecionável (5 ou 10). - Sem
aspect_ratio. O formato da saída segue sua imagem de entrada, então não há nada para definir. No modelo de texto você escolhe a proporção; aqui a imagem decide. (Confirmado abaixo com uma execução real: entrada quadrada gerou clipe quadrado.) - O schema é fechado.
seed,negative_prompt,prompt_optimizere qualquer outro campo retornam 400 comadditional properties '<field>' not allowed. Não porte um corpo de requisição de outro modelo supondo que chaves extras serão ignoradas.
O que a validação realmente retorna
Estas são respostas reais do endpoint em produção — enviando valores inválidos de propósito, sem criar tarefa e sem cobrança:
// 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"}
O message aponta o campo problemático, então a própria API é sua referência de schema quando você esquecer.
Preparando o primeiro quadro
image_url precisa ser uma URL que o worker do hiapi consiga buscar — um link público, não um caminho local nem um blob base64 data:. A imagem determina a proporção do clipe e a composição inicial, então parta do quadro exato em que você quer que o movimento comece.
Uma armadilha que vale antecipar, porque é fácil de ignorar: um image_url ruim não é capturado na criação da tarefa. A chamada de criação valida apenas a forma da requisição (é uma string?), não se a URL resolve. Uma URL de imagem inacessível ou 404 retorna um 200 perfeitamente normal mais um taskId — e então a tarefa vira fail alguns segundos depois. Veremos o payload exato de falha na seção de solução de problemas; a lição é sempre ramificar pelo status terminal da tarefa, nunca assumir que um 200 na criação significa sucesso.
Exemplo mínimo que funciona: curl
Crie a tarefa:
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"
}
}'
A resposta de criação traz um id de tarefa:
{ "code": 200, "data": { "taskId": "tk-hiapi-01M5..." }, "message": "success" }
Faça polling até chegar a um estado terminal:
curl -s https://api.hiapi.ai/v1/tasks/tk-hiapi-01M5... \
-H "Authorization: Bearer sk-YOUR_KEY"
Enquanto a tarefa roda, data.status passa por vários estados em andamento (você verá valores como handling, depois archiving). Os dois estados terminais são success e fail. Em caso de sucesso:
{
"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"
}
Baixe data.output[0].url imediatamente. É uma URL assinada em temp.hiapi.ai com expireAt (timestamp Unix, cerca de uma semana depois) — persista os bytes no seu próprio armazenamento em vez de fazer hotlink. O Ray 3.2 retorna MP4 sem áudio; não há faixa de áudio para esperar.
Script Python completo (enviar → polling → baixar)
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)")
Execute com pip install requests, use sua chave e um image_url real, e você terá um .mp4 no disco quando a tarefa terminar.
Como é uma execução real
Para tornar os números concretos, aqui está uma execução real em 540p contra a API em produção em 2026-10-20. Primeiro quadro: uma imagem 1600×1600 (quadrada); prompt: push-in cinematográfico lento.
- A tarefa passou por
handling → archiving → success, com o trabalho do servidor concluído em cerca de 45 segundos (createdcriação,completedconclusão) — cerca de 60 segundos de tempo de relógio contando o laço de polling de 8 segundos. Tarefas de vídeo levam dezenas de segundos a minutos, não são instantâneas; reserve prazos generosos. - A saída foi um MP4 720×720, cerca de 1,3 MB, 5,04 segundos, sem áudio.
Essa execução revela duas coisas que a tabela de parâmetros não diz:
- A proporção da saída corresponde à da entrada — quadrado entra, quadrado sai. É por isso que não há campo
aspect_ratio; enquadre oimage_urlna composição que você quer no clipe. resolutioné um nível de qualidade, não uma altura literal em pixels. Pedir540ppara uma entrada 1600×1600 retornou 720×720 — o nível escala pela forma da entrada em vez de forçar um tamanho fixo. Se você precisa de dimensões exatas de saída, redimensione o clipe entregue.
Notas de produção: callbacks, idempotência, erros
Prefira callbacks ao polling
Para qualquer coisa além de um script, registre um webhook na criação em vez de fazer polling:
{
"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" }
}
Com "when": "final", o hiapi chama seu endpoint uma vez quando a tarefa atinge um estado terminal — sem tráfego de polling e sem perder a conclusão entre intervalos. Seu handler então faz GET /v1/tasks/<id> para buscar output[0].url. Para CLIs e notebooks que não conseguem expor um endpoint HTTPS, o polling continua bom.
Faça os retries serem idempotentes
Persista o taskId no momento em que a chamada de criação retorna. Se seu processo cair no meio da geração, retome fazendo polling do id armazenado — não recrie a tarefa, ou você pagará por um segundo clipe. Só recrie se a requisição de criação original falhou antes de retornar um id de tarefa. O laço de polling tolerante acima já segue essa regra: em uma requisição de rede interrompida, refaça o polling do mesmo id em vez de recomeçar do zero.
Erros que você realmente verá
Cada linha da tabela abaixo é uma forma de resposta verificada em produção:
| Sintoma | Resposta | Correção |
|---|---|---|
| Chave ausente/inválida | 401, aninhado {"error":{"code":"permission_denied","type":"hiapi_error","request_id":"...","message":"This API key is invalid..."}} |
Verifique o cabeçalho Authorization: Bearer sk-... e se a chave tem acesso. Informe o request_id ao contatar o suporte. |
image_url ausente |
400 INVALID_REQUEST, message: "image_url: missing required field" |
Este modelo exige image_url — não há modo somente texto. |
| Enum errado / campo extra | 400 INVALID_REQUEST, plano {"code":400,"error_code":"INVALID_REQUEST","message":"..."} |
Leia o campo apontado no message. Para duration/aspect_ratio, ele dirá additional properties ... not allowed — remova-os. |
| Id de tarefa errado no polling | 404, plano {"code":404,"data":null,"message":"task not found"} (atenção: não há error_code no 404) |
Id errado, ou você está fazendo polling com a chave de uma conta diferente. |
image_url inacessível |
A criação retorna 200 + um taskId; a tarefa depois termina com data.status:"fail" e data.error:{"code":"INVALID_REQUEST","message":"Invalid request parameters for this model..."} |
É a armadilha. A chamada de criação não consegue distinguir link morto de link vivo. Verifique se sua imagem é publicamente acessível e sempre leia o data.status terminal. |
A última linha merece ser memorizada: mesmo quando a tarefa falha, o envelope HTTP do polling continua 200 — a falha está em data.status e data.error, não no código de status HTTP. Tarefas com falha são reembolsadas, mas seu código ainda precisa notá-las. Ramifique por data.status == "fail", não confie no 200.
Recursos relacionados
- Página do modelo Ray 3.2 imagem para vídeo — playground, status ao vivo e tabela completa de preços por resolução
- Página do modelo Ray 3.2 texto para vídeo — o endpoint irmão; começa do texto em vez da imagem (ele adiciona
durationeaspect_ratioe removeimage_url) - Como usar a API de texto para vídeo do Hailuo 2.3 — outro tutorial de imagem para vídeo na mesma API de tarefas, útil como comparação de schema
- Como usar a API de imagem para vídeo do grok-imagine — um terceiro modelo i2v para comparação
- Preços do hiapi — preço por clipe de vídeo por resolução
Perguntas frequentes
Qual é o id do modelo e qual endpoint devo chamar?
ray-3.2/image-to-video, enviado para POST https://api.hiapi.ai/v1/tasks, com os parâmetros sob input. É assíncrono — você recebe um taskId e depois faz polling em GET /v1/tasks/<id> (ou registra um callback).
O que a entrada exige?
Exatamente dois campos obrigatórios — prompt (movimento) e image_url (URL pública do primeiro quadro) — mais o opcional resolution (540p/720p/1080p). Qualquer outra coisa é rejeitada.
Por que não posso definir duração ou proporção?
A duração do clipe é fixa (cerca de 5 segundos), então não há campo duration; a proporção da saída é herdada da sua imagem de entrada, então não há campo aspect_ratio. Enviá-los retorna 400. Se você precisa de duração selecionável, use o Ray 3.2 texto para vídeo.
Minha chamada de criação retornou 200, mas o vídeo nunca chegou. Por quê?
O mais comum é image_url inacessível. A chamada de criação valida apenas a forma da requisição, então um link morto ainda retorna um taskId — e a tarefa termina em fail. Faça polling até o estado terminal e leia data.error. Garanta que a imagem é publicamente acessível (sem autenticação, não é caminho local, não é base64).
O clipe tem áudio? Não — o Ray 3.2 imagem para vídeo retorna MP4 sem áudio. Adicione som no pós-processamento ou use um modelo de áudio separado.
Quanto custa um clipe?
A cobrança é por clipe e, como a duração é fixa, resolution é a única alavanca — um clipe em 1080p custa várias vezes mais que um em 540p. Os preços mudam, então consulte a página de preços em vez de codificar números fixos, e prototipe com 540p.
Quanto tempo devo esperar antes de desistir?
Dezenas de segundos a alguns minutos é normal. Tolere data.status não terminais no laço de polling e use um prazo generoso (10–15 minutos), ou registre um callback com "when": "final" para o hiapi avisar você em vez de você adivinhar intervalos.
Artigos relacionados
Ver todos os artigos →-
Tutorial
Como chamar hailuo-2.3-fast/image-to-video pela API HiAPI: curl e Python na prática
A primeira requisição executável do nível rápido e econômico de geração de vídeo a partir de imagem — do ID do modelo ao download do MP4
-
Tutorial
Como chamar hailuo-2.3/text-to-video pela API HiAPI: curl, Python e exemplos de requisição executáveis
-
Tutorial
Usar Kandinsky 6.0 Pro text-to-video na HiAPI: tutorial de curl e Python
Demonstração prática com foco na requisição: montar a chamada de prompt para clipe de vídeo, enviar a requisição, consultar o status de conclusão e baixar o resultado