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

# Webhooks

> Payload de entrega, calendario de reintentos y cómo escribir un handler seguro.

Indica `webhook.url` en una tarea y enviaremos el resultado por POST cuando la tarea llegue a un estado final:
`COMPLETED` o `FAILED`.

```json theme={null}
{
  "taskType": "PERPLEXITY",
  "payload": { "prompt": "best wireless earbuds 2026" },
  "webhook": { "url": "https://your-server.example.com/hook" }
}
```

## Payload de entrega

Enviamos por POST `Content-Type: application/json` con este cuerpo:

<ResponseField name="task" type="object" required>
  Los mismos metadatos de tarea que recibiste al enviar, ahora con un `status` final y tu `idempotencyKey` de vuelta.
</ResponseField>

<ResponseField name="credits" type="object" required>
  El coste en créditos del motor: `2` para Perplexity, `3` para ChatGPT, etc. (1–3 créditos según el motor).
  `creditsCharged` es `0` si la tarea falló. A diferencia de las respuestas de envío y sondeo, que siempre informan
  cero, este campo no es cero.
</ResponseField>

<ResponseField name="response" type="object" required>
  El resultado específico del motor. Consulta [Motores](/es/engines/overview). En una tarea fallida es
  `{ "error": "<reason>" }` en lugar del resultado del motor.
</ResponseField>

```json theme={null}
{
  "task": {
    "id": "8f2c1e40-...",
    "taskType": "PERPLEXITY",
    "status": "COMPLETED",
    "priority": 1,
    "createdAt": "2026-07-09T04:12:00.000Z",
    "idempotencyKey": "job-42:prompt-7:perplexity"
  },
  "credits": { "creditsToCharge": 3, "creditsCharged": 3 },
  "response": {
    "text": "The best wireless earbuds in 2026 are ...",
    "sources": [{ "position": 1, "url": "https://...", "label": "..." }]
  }
}
```

Una tarea fallida también se entrega:

```json theme={null}
{
  "task": { "id": "8f2c1e40-...", "status": "FAILED", "...": "..." },
  "credits": { "creditsToCharge": 3, "creditsCharged": 0 },
  "response": { "error": "ENGINE_TIMEOUT: The engine did not answer before the task deadline." }
}
```

## Reintentos

Devuelve cualquier `2xx` para confirmar. Cualquier otra cosa, incluido un timeout, cuenta como fallo y
reintentamos con backoff exponencial:

| Intento | Se envía |
| - | - |
| 1 | inmediatamente |
| 2 | a los 2 minutos |
| 3 | a los 4 minutos |
| 4 | a los 8 minutos |
| 5 | a los 16 minutos |

Tras el quinto intento la entrega se abandona y se registra. No hay cola de mensajes fallidos que puedas leer.
Para obtener el resultado mediante la API de tareas, sondea dentro de la ventana pública de 24 horas. Esa ventana
no define la vida útil del registro subyacente.

Cada intento tiene un timeout de **30 segundos**.

<Warning>
  Desde el punto de vista de la tarea, la entrega es de tipo «enviar y olvidar». Una tarea completada cuyo webhook
  nunca se entrega sigue mostrando `COMPLETED` en `GET /v1/async/task/{id}`: el estado describe el trabajo del
  motor, no la notificación.
</Warning>

## Escribir un handler seguro

<Steps>
  <Step title="Confirma rápido">
    Devuelve `200` en cuanto hayas encolado el payload de forma persistente. Haz el parseo y las escrituras en base
    de datos después. Un handler lento agota el timeout de 30 segundos y provoca un reintento que no querías.
  </Step>

  <Step title="Deduplica por idempotencyKey">
    Por los reintentos, tu handler puede recibir legítimamente la misma tarea más de una vez. Compara por
    `task.idempotencyKey` (o `task.id`) y haz la escritura idempotente.
  </Step>

  <Step title="Ramifica por estado, no por presencia">
    Comprueba `task.status === "FAILED"` de forma explícita. Una tarea fallida también entrega un webhook.
  </Step>
</Steps>

<Note>
  Las solicitudes de webhook no llevan firma ni secreto compartido. Si tu endpoint es público, trata el payload como
  no confiable y usa una ruta de URL imposible de adivinar, o coloca el handler detrás de restricciones de red.
</Note>
