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

# Webhooks de finalización

> Recibe un aviso cuando un run termina, con entregas firmadas y verificables

En lugar de consultar en bucle, deja que Leme te llame. Pasa un `callbackUrl` al [crear un run](/es/api/runs#crear-un-run) y Leme envía un `POST` firmado a esa URL cuando el run llega a un estado final — o se pausa para una aprobación.

```json theme={null}
{
  "input": { "message": "Califica este lead." },
  "callbackUrl": "https://example.com/hooks/leme"
}
```

La URL debe ser HTTPS pública en el puerto por defecto. Las URLs que apuntan a redes privadas se rechazan con `422 callback_url_invalid`.

## Eventos

| Evento                 | Se envía cuando                                 |
| ---------------------- | ----------------------------------------------- |
| `run.completed`        | El run terminó con éxito                        |
| `run.failed`           | El run falló                                    |
| `run.canceled`         | El run fue cancelado                            |
| `run.waiting_approval` | El run se pausó esperando una aprobación humana |

El payload es deliberadamente ligero — identificadores y estado, nunca la respuesta del agente:

```json theme={null}
{
  "type": "run.completed",
  "timestamp": "2026-07-07T18:00:00Z",
  "data": {
    "runId": "run_id",
    "agentId": "agent_id",
    "sessionId": "session_id",
    "status": "completed",
    "origin": "api",
    "metadata": { "leadId": "4821" }
  }
}
```

Al recibirlo, busca el resultado con [`GET /api/v1/runs/:id`](/es/api/runs#seguir-el-run). Esto mantiene el contenido sensible fuera de tu endpoint de webhook y garantiza que siempre leas el estado más reciente.

## Verificar la firma

Cada entrega se firma siguiendo la especificación [Standard Webhooks](https://www.standardwebhooks.com), el mismo esquema que usan OpenAI y Svix. Tres headers acompañan la solicitud:

| Header              | Contenido                                             |
| ------------------- | ----------------------------------------------------- |
| `webhook-id`        | ID único de la entrega, estable entre reintentos      |
| `webhook-timestamp` | Timestamp Unix (segundos) del envío                   |
| `webhook-signature` | Una o más firmas `v1,<base64>`, separadas por espacio |

Primero, obtén el secreto de firma de tu proyecto (requiere un token `read_write`):

```bash theme={null}
curl https://app.leme.ai/api/v1/project/webhook-signing-secret \
  -H "Authorization: Bearer leme_pat_v1_..."
```

Luego verifica con cualquier biblioteca Standard Webhooks:

<CodeGroup>
  ```js Node theme={null}
  import { Webhook } from "standardwebhooks"

  const webhook = new Webhook(secret) // "whsec_..."

  // Lanza un error si la firma es inválida o demasiado antigua
  webhook.verify(rawBody, {
    "webhook-id": req.headers["webhook-id"],
    "webhook-timestamp": req.headers["webhook-timestamp"],
    "webhook-signature": req.headers["webhook-signature"],
  })
  ```

  ```python Python theme={null}
  from standardwebhooks import Webhook

  webhook = Webhook(secret)  # "whsec_..."

  # Lanza un error si la firma es inválida o demasiado antigua
  webhook.verify(raw_body, {
      "webhook-id": headers["webhook-id"],
      "webhook-timestamp": headers["webhook-timestamp"],
      "webhook-signature": headers["webhook-signature"],
  })
  ```
</CodeGroup>

<Warning>
  Verifica siempre contra el **cuerpo crudo de la solicitud**, antes de
  cualquier parseo de JSON. Rechaza las entregas cuya firma no coincida.
</Warning>

## Rotar el secreto

Si el secreto se filtra — o en tu ciclo normal de rotación — genera uno nuevo:

```bash theme={null}
curl -X POST https://app.leme.ai/api/v1/project/webhook-signing-secret \
  -H "Authorization: Bearer leme_pat_v1_..."
```

El secreto anterior sigue firmando durante 24 horas, para que hagas la transición sin perder entregas. Durante esa ventana, `webhook-signature` lleva dos firmas — la entrega es válida si **cualquiera** coincide, algo que toda biblioteca Standard Webhooks maneja por ti.

## Reintentos y confiabilidad

Las entregas son **at least once**. Si tu endpoint no responde `2xx` en 15 segundos, Leme reintenta: tras 1 minuto, 5 minutos, 30 minutos y 2 horas. Después de cinco intentos fallidos, la entrega se marca como agotada.

Como los reintentos pueden solaparse con tu procesamiento, haz tu handler idempotente — deduplica por `webhook-id`.

Puedes inspeccionar el estado de la entrega en cualquier momento en el recurso del run:

```json theme={null}
{
  "callback": {
    "url": "https://example.com/hooks/leme",
    "lastStatus": "delivered",
    "attempts": 1
  }
}
```

## Buenas prácticas

* **Responde rápido.** Confirma con `2xx` de inmediato y procesa de forma asíncrona; el timeout de 15 segundos incluye tu handler.
* **Deduplica por `webhook-id`.** Los reintentos reutilizan el mismo ID.
* **No confíes solo en el payload.** Verifica la firma y busca el run vía API para obtener el estado autoritativo.
* **Vigila las entregas agotadas.** Si se agotaron, tu endpoint estuvo caído durante horas — consulta los runs en curso para ponerte al día.
