> ## Documentation Index
> Fetch the complete documentation index at: https://gccai.heqingsong.uk/llms.txt
> Use this file to discover all available pages before exploring further.

# Devolución de llamada al completar la tarea (Webhook)

> Incluye una URL de devolución de llamada al enviar una tarea de generación asíncrona y te enviaremos el resultado por POST cuando la tarea finalice (con éxito o error), sin necesidad de sondeo.

Al enviar tareas de generación asíncrona como video / imagen / audio, puedes incluir una URL de devolución de llamada. **Una vez que la tarea finaliza (con éxito o error)**, enviaremos activamente el resultado por POST a tu URL, para que no tengas que estar consultando constantemente.

## Inicio rápido

Al enviar una tarea, añade un campo `webhook` al cuerpo de la solicitud:

```bash theme={null}
curl -X POST https://tu-dominio-de-acceso/v1/images/generations \
  -H "Authorization: Bearer TU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-image-2",
    "prompt": "a red apple on a table",
    "size": "1024x1024",
    "webhook": "https://your-server.com"
  }'
```

Cuando la tarea termine, enviaremos una solicitud POST a **`tu URL + /callback`**.

<Note>
  Otros endpoints de tareas asíncronas (video, audio, etc.) funcionan igual: solo añade el campo `webhook` al cuerpo de la solicitud.
</Note>

## Reglas de la URL

El `webhook` que proporcionas es la **URL base**, a la que añadimos automáticamente `/callback`:

| Tu `webhook`                   | A dónde enviamos realmente el POST     |
| ------------------------------ | -------------------------------------- |
| `https://your-server.com`      | `https://your-server.com/callback`     |
| `https://your-server.com/api`  | `https://your-server.com/api/callback` |
| `https://your-server.com/api/` | `https://your-server.com/api/callback` |

Por lo tanto, tu servidor necesita un endpoint que acepte `POST .../callback`.

## Qué vas a recibir

El contenido enviado es **exactamente igual al que devuelve el endpoint «[Obtener estado de la tarea](/es/api-reference/tasks/status)»**: puedes procesarlo con la misma lógica de análisis.

<CodeGroup>
  ```json Éxito (status: completed) theme={null}
  {
    "id": "task_01KV7FXR8BEYS1BWHJCT3JMCJ5",
    "status": "completed",
    "progress": 100,
    "created": 1781589029,
    "completed": 1781589058,
    "actual_time": 29,
    "cost": 0.006,
    "credits_cost": 0.06,
    "result": {
      "images": [{ "url": ["https://.../result.png"], "expires_at": 1781675458 }]
    }
  }
  ```

  ```json Error (status: failed) theme={null}
  {
    "id": "task_xxx",
    "status": "failed",
    "progress": 100,
    "created": 1781589029,
    "completed": 1781589050,
    "error": { "message": "motivo del error...", "code": "" }
  }
  ```
</CodeGroup>

<Note>
  Para tareas de video el resultado está en `result.videos`, y para audio en `result.audios`.
</Note>

<Warning>
  Solo enviamos cuando una tarea alcanza un **estado final** (`completed` / `failed`); no enviamos mientras está en procesamiento.
</Warning>

## Reintentos y deduplicación (importante)

* **Reintentos**: Si tu servidor no devuelve `2xx` en unos 10 segundos, o devuelve `5xx`, reintentaremos automáticamente, hasta **3 veces**, con intervalos de aproximadamente **10 s, 30 s y 60 s**. Si las 3 fallan, desistimos (en unos 2 minutos).
* **Sin reintento**: Si tu endpoint devuelve `4xx` (se considera URL / solicitud incorrecta), desistimos de inmediato sin reintentar.
* **Deduplicación**: Normalmente una tarea se envía solo una vez. Pero en casos extremos (p. ej., un reinicio de nuestro lado tras el envío pero antes de la confirmación) **podrías recibir envíos duplicados**. Asegúrate de **deduplicar de forma idempotente por `id` (task\_id)** para evitar el procesamiento doble.

**Recomendaciones para tu endpoint receptor:**

<Steps>
  <Step title="Devuelve 2xx lo antes posible">
    Recibe y encola primero, luego procesa de forma asíncrona; no nos hagas esperar a que termines de procesar.
  </Step>

  <Step title="Deduplica por id">
    Usa `id` (task\_id) como clave de idempotencia para evitar el procesamiento doble.
  </Step>

  <Step title="Configura y verifica la firma">
    En producción, verifica el origen de las solicitudes de devolución de llamada y rechaza las falsificadas.
  </Step>
</Steps>

## Requisitos de la URL de devolución de llamada

Por seguridad, la URL de devolución de llamada debe cumplir:

| Requisito              | Descripción                                                                                               |
| ---------------------- | --------------------------------------------------------------------------------------------------------- |
| Accesible públicamente | **No puede** ser una dirección interna / local (p. ej. `127.0.0.1`, `10.x`, `192.168.x` serán rechazadas) |
| Protocolo              | `http` o `https` (`https` recomendado)                                                                    |
| Puerto                 | Usa puertos estándar (`80` / `443`); los puertos no estándar pueden bloquearse                            |
| Dominio                | No puede apuntar a nuestro propio dominio de servicio                                                     |

Las URL que no cumplan estos requisitos se descartan (sin envío ni reintento).

## Preguntas frecuentes

<AccordionGroup>
  <Accordion title="¿Envié una tarea con webhook pero no recibí ningún envío?">
    Revisa punto por punto:

    1. ¿La tarea **realmente finalizó**? Consulta los detalles de la tarea: ¿`status` es `completed` / `failed` (no se envía durante el procesamiento)?
    2. ¿Tu URL es **accesible públicamente**? ¿Podemos alcanzar tu `/callback`?
    3. ¿El puerto es estándar (80 / 443)? Los puertos no estándar pueden ser bloqueados por las políticas de seguridad.
    4. ¿Tu `/callback` **devolvió 2xx a tiempo**? Si devuelve 4xx, desistimos de inmediato.
    5. ¿Usas `https`? ¿El certificado es válido?
  </Accordion>

  <Accordion title="¿Por qué url en el resultado es un array?">
    Algunos modelos producen varias imágenes a la vez, por lo que `images[].url` puede ser un array; simplemente trátalo como un array.
  </Accordion>

  <Accordion title="¿Los enlaces de resultado caducan?">
    Si `result` incluye `expires_at` (marca de tiempo Unix), indica la hora de caducidad del enlace; transfiérelo/guárdalo a tiempo.
  </Accordion>

  <Accordion title="¿Se envía el estado 'en procesamiento'?">
    No. Solo enviamos una vez, cuando la tarea finalmente tiene éxito o falla.
  </Accordion>
</AccordionGroup>

## Ejemplo mínimo de receptor

```python Python theme={null}
from http.server import BaseHTTPRequestHandler, HTTPServer
import json

class H(BaseHTTPRequestHandler):
    def do_POST(self):
        n = int(self.headers.get("Content-Length") or 0)
        body = self.rfile.read(n)
        data = json.loads(body)
        print("Devolución de llamada de tarea recibida:", data["id"], data["status"])
        # TODO: deduplicar por id, verificar la firma y luego procesar
        self.send_response(200); self.end_headers()
        self.wfile.write(b'{"ok":true}')

HTTPServer(("0.0.0.0", 443), H).serve_forever()
```

Devuelve `200` lo antes posible y ejecuta tu lógica de procesamiento de forma asíncrona en segundo plano.
