# Trabajos asíncronos

Cómo enviar trabajos de imagen y vídeo, consultarlos o recibirlos por webhook, descargar el resultado, entender cada estado y campo, y cómo funcionan los reembolsos.

> https://nezhagate.com/es/docs/guide/async-jobs

## Qué endpoints son asíncronos

La generación de imágenes y vídeo funciona con trabajos asíncronos: la llamada devuelve al momento un id de trabajo y la generación continúa en segundo plano. El chat es síncrono y nunca usa trabajos.

| Método | Ruta | Qué hace |
| --- | --- | --- |
| POST | `/v1/images/generations` | Texto a imagen. Devuelve al momento un id de trabajo (HTTP 202). |
| POST | `/v1/images/edits` | Imagen a imagen. Pasar `image` a la llamada anterior hace lo mismo. |
| GET | `/v1/images/jobs/{id}` | Consulta un trabajo de imagen: estado y resultado. |
| POST | `/v1/videos/generations` | Genera un vídeo. Devuelve al momento un id de trabajo (HTTP 202). |
| GET | `/v1/videos/jobs/{id}` | Consulta un trabajo de vídeo: estado y resultado. |

## Cómo funciona

1. Envío: llama a un endpoint de generación y recibirás al momento HTTP 202 con el `id` del trabajo (tiene la forma `img_...`; los trabajos de vídeo usan el mismo prefijo).

2. Espera: consulta el endpoint del trabajo con ese id, o añade un `callback_url` al enviar para que el resultado te llegue automáticamente.

3. Fin: `status` pasa a `succeeded` y el enlace del resultado está en `data[0].url`; si pasa a `failed`, el motivo está en `error` y los créditos retenidos para el trabajo vuelven a tu saldo.

4. Descarga: los enlaces de los resultados se guardan 60 días; copia en tu propio almacenamiento todo lo que necesites conservar.

```
curl https://nezhagate.com/v1/videos/generations \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "seedance-2.0", "prompt": "a paper boat drifting down a rainy street", "duration": 5, "size": "16:9"}'

# HTTP 202
{"id": "img_9c1e...7a", "object": "video.generation.job", "status": "queued", "created": 1791199400, "model": "seedance-2.0"}
```

```
curl https://nezhagate.com/v1/videos/jobs/img_9c1e...7a -H "Authorization: Bearer YOUR_API_KEY"
```

## Estados del trabajo

| Estado | Significado |
| --- | --- |
| `queued` | En cola. Incluye `queue_position` (trabajos por delante del tuyo) y `eta_seconds` (espera estimada). |
| `processing` | Generándose. |
| `succeeded` | Terminado. El resultado está en `data[0].url` y el detalle del cobro en `usage`. |
| `failed` | Falló. El motivo está en `error`; los créditos reservados se devuelven íntegros. |

`succeeded` y `failed` son estados finales; después, el trabajo ya no cambia.

## Campos de la respuesta

| Campo | Significado |
| --- | --- |
| `id` | El id del trabajo; se usa para consultarlo y en los webhooks. |
| `object` | `image.generation.job` para imágenes, `video.generation.job` para vídeo. |
| `status` | `queued` / `processing` / `succeeded` / `failed`. |
| `created` | Cuándo se envió el trabajo, en segundos Unix. |
| `model` | El ID del modelo con el que se envió. |
| `queue_position` | Solo mientras está en `queued`: cuántos trabajos hay por delante del tuyo. |
| `eta_seconds` | Solo mientras está en `queued`: cuántos segundos faltan, aproximadamente, para que empiece. |
| `data[].url` | Solo con `succeeded`: el enlace al resultado (una imagen o un mp4), que se guarda 60 días. |
| `usage` | Solo con `succeeded`: lo que se cobró. En imágenes indica `images` (imágenes entregadas) y `resolution`; en vídeo, `clips`. |
| `error` | Solo con `failed`: `message`, `type`, `code` y `param`; decide según `code`. |

```
{
  "id": "img_9c1e...7a",
  "object": "video.generation.job",
  "status": "succeeded",
  "created": 1791199400,
  "model": "seedance-2.0",
  "data": [{"url": "https://img.nezhagate.com/i/5d41402abc....mp4"}],
  "usage": {"clips": 1, "model": "seedance-2.0"}
}
```

```
{
  "id": "img_9c1e...7a",
  "object": "video.generation.job",
  "status": "failed",
  "created": 1791199400,
  "model": "seedance-2.0",
  "error": {"message": "...", "type": "invalid_request_error", "code": "content_policy", "param": null}
}
```

## Consejos para consultar

- Consulta las imágenes cada 2–3 segundos y los vídeos cada 5–10 segundos. Las consultas son gratis.

- Deja de consultar en cuanto veas `succeeded` o `failed`.

- Ponle a tu programa un tiempo máximo de espera total: las imágenes suelen tardar desde decenas de segundos hasta unos minutos, y los vídeos desde uno o dos minutos hasta más de una hora (los clips más largos y las colas tardan más); la documentación de cada modelo indica los tiempos habituales. Cuando se agote ese tiempo, no vuelvas a enviar el trabajo: sigue ahí; vuelve a consultarlo más tarde.

- Un trabajo pertenece a la cuenta que lo envió: cualquier clave de esa cuenta puede consultarlo, y cualquier otra cuenta recibe 404 `job_not_found`. Los registros de trabajos se guardan 3 días; después, ese id también devuelve 404, aunque el enlace del resultado sigue funcionando durante sus 60 días.

- Si el envío agota el tiempo de espera en tu lado antes de que recibas el id del trabajo, revisa en el registro de llamadas de la consola si el trabajo se creó antes de volver a enviarlo, para no pagar dos veces.

## Webhooks en lugar de consultas

Añade `callback_url` al enviar y, en cuanto termine el trabajo, con éxito o con error, la pasarela enviará a esa URL exactamente lo mismo que devuelve el endpoint de consulta, firmado y con reintentos si la entrega falla. También puedes configurar una dirección para toda la cuenta en Configuración → Webhook, en la consola.

```
{"model": "seedance-2.0", "prompt": "...", "duration": 5, "callback_url": "https://example.com/webhooks/nezhagate"}
```

[Documentación de webhooks →](https://nezhagate.com/es/docs/webhooks)

## Facturación

- Al enviar, se retiene de tu saldo el precio del trabajo; si el saldo no alcanza, recibes 402 y no se crea ningún trabajo.

- Si sale bien, se liquida lo entregado: las imágenes según el número de imágenes entregadas y el vídeo según la duración o el clip que pediste (lo mismo que se retuvo); lo retenido de más se devuelve automáticamente. Si falla, se devuelve toda la retención.

- Las consultas y los envíos de webhooks son gratis.

## Cuánto tiempo se guardan los resultados

`data[0].url` apunta a nuestro almacenamiento y se borra automáticamente a los 60 días. Para seguir usando un resultado, descárgalo a tu propio almacenamiento o CDN.

## Por qué falla un trabajo

Un trabajo fallido siempre se reembolsa íntegro. Estos son los códigos que verás con más frecuencia en un trabajo fallido:

| code | Cuándo aparece | Qué hacer |
| --- | --- | --- |
| `content_policy` | Vídeo: el prompt, una referencia, o la imagen o el sonido generados no superaron la revisión de contenido (`param` es `audio` cuando lo que se marcó fue el sonido). | Reescribe el prompt o cambia la referencia antes de volver a enviarlo; la misma entrada se rechaza otra vez. |
| `moderation_blocked` | Imagen: el sistema de seguridad del proveedor del modelo rechazó el prompt o una imagen de referencia (el código también puede ser `content_policy_violation`). | Reescribe el prompt o cambia la referencia; si se reenvía sin cambios, normalmente se vuelve a rechazar. |
| `render_failed` | Vídeo: esta vez no salió ningún clip; suele ser algo puntual. | Volver a enviar la misma petición suele funcionar. |
| `invalid_material` | Vídeo: el proveedor encontró un problema en la entrada. Es un error genérico y a menudo puntual. | Primero reenvíalo tal cual; si se repite, revisa las referencias y la longitud del prompt. |
| `render_timeout` | Vídeo: la cola o el render tardaron demasiado y el proveedor abandonó el trabajo. | Vuelve a enviarlo. |
| `wait_timeout` | Vídeo: no llegó ningún clip dentro del tiempo máximo de espera que permite la línea. | Vuelve a enviarlo. |
| `upstream_unstable` | Vídeo: la conexión se cortó durante el render. | Vuelve a enviarlo. |
| `result_fetch_failed` | El vídeo se generó, pero su descarga o almacenamiento falló una y otra vez de nuestro lado. | Vuelve a enviarlo. |
| `invalid_request` | Vídeo: la línea no acepta esta combinación de parámetros (por ejemplo, duración, proporción y resolución). | Ajusta los parámetros según indica `message` y vuelve a enviarlo. |
| `upstream_error` | Cualquier otro fallo: se probaron todas las líneas, el trabajo esperó 15 minutos sin empezar, el proveedor lo rechazó sin dar motivo, etc.; `message` indica cuál. | Volver a enviarlo suele funcionar. |

[Lista completa de códigos de error →](https://nezhagate.com/es/docs/guide/errors)

## Ejemplo completo

Envío, consulta y descarga de una sola vez, con un tiempo máximo total y manejo de errores. Sustituye YOUR_API_KEY y ejecútalo.

```
import time
import requests

BASE = "https://nezhagate.com/v1"
HEADERS = {"Authorization": "Bearer YOUR_API_KEY"}

def run_job(kind, payload, every=5, give_up=5400):
    """kind is "images" or "videos". Returns the finished job (succeeded or failed)."""
    job = requests.post(f"{BASE}/{kind}/generations", headers=HEADERS, json=payload, timeout=120)
    job.raise_for_status()                       # 4xx here: fix the request; nothing was billed
    job_id = job.json()["id"]
    deadline = time.time() + give_up
    while time.time() < deadline:
        time.sleep(every)
        r = requests.get(f"{BASE}/{kind}/jobs/{job_id}", headers=HEADERS, timeout=30).json()
        if r["status"] in ("succeeded", "failed"):
            return r
    raise TimeoutError(f"{job_id} still running; poll it again later, it is not lost")

job = run_job("videos", {"model": "seedance-2.0", "prompt": "a paper boat drifting down a rainy street",
                         "duration": 5, "size": "16:9"})
if job["status"] == "succeeded":
    url = job["data"][0]["url"]
    with open("clip.mp4", "wb") as f:            # keep your own copy: links expire after 60 days
        f.write(requests.get(url, timeout=300).content)
else:
    print(job["error"]["code"], job["error"]["message"])
```
