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

# Webhooks

> Recibe eventos firmados con HMAC cuando algo pasa en tus agentes.

Cuando tus agentes terminan llamadas (o fallan), Ryvo te manda un evento a una URL que tú configuras. Útil para:

* Actualizar el deal en tu CRM con el resultado de la llamada.
* Disparar un follow-up automatizado si la llamada falló.
* Loggear el resumen de cada conversación en tu data warehouse.

## Configurar tu webhook

1. Entra a [app.ryvo.so/developers](https://app.ryvo.so/developers).
2. En la sección **Webhook**, agrega tu URL pública (HTTPS obligatorio).
3. Copia el **signing secret** que generamos. **Solo se muestra una vez**: guárdalo en un secret manager. Lo necesitas para verificar la firma de cada evento.

<Warning>
  La URL del webhook debe ser pública y aceptar peticiones `POST`. Si está detrás de un firewall o requiere auth, los eventos no van a llegar.
</Warning>

## Eventos disponibles

| Evento                 | Cuándo se dispara                                                             | Carril      |
| ---------------------- | ----------------------------------------------------------------------------- | ----------- |
| `conversation.started` | Cuando disparas una llamada vía `POST /v1/calls`.                             | ElevenLabs  |
| `conversation.ended`   | Cuando termina la llamada, con o sin éxito. Trae duración, resumen y motivo.  | ElevenLabs  |
| `analysis.ready`       | Cuando termina el análisis posterior (criterios de éxito y campos extraídos). | Ryvo Engine |
| `call.initiated`       | Alias de `conversation.started`.                                              | ElevenLabs  |
| `call.completed`       | Alias de `conversation.ended` cuando la llamada terminó bien.                 | ElevenLabs  |
| `call.failed`          | Alias de `conversation.ended` cuando la llamada falló o no contestó.          | ElevenLabs  |

Por cada ocurrencia recibes **un solo** evento: el canónico (`conversation.*`) si te suscribiste a él, o su alias (`call.*`) si no. Nunca los dos.

<Note>
  Las suscripciones creadas con el comodín `*` reciben únicamente la familia `call.*`. Para recibir `conversation.*` o `analysis.ready` hay que nombrarlos explícitamente en la lista de eventos del webhook. Desde el portal hoy sólo se eligen los `call.*`; si necesitas los otros, escríbenos a [team@ryvo.so](mailto:team@ryvo.so) y los activamos en tu webhook.
</Note>

<Note>
  Los agentes construidos con el **Constructor** (Ryvo Engine) emiten por ahora únicamente `analysis.ready`. Los eventos de inicio y fin de llamada del Engine llegarán cuando se abra el marcado por API.
</Note>

## Shape de un evento

Todos los eventos comparten esta estructura:

```json theme={null}
{
  "id": "<uuid>",
  "type": "conversation.ended",
  "created_at": "2026-04-30T18:35:42.123Z",
  "data": {
    /* campos específicos por tipo de evento */
  }
}
```

Dentro de `data`, dos identificadores distintos:

* `call_id`: el handle `call_…` que te devolvió `POST /v1/calls`. Úsalo para casar el evento con la llamada que tú disparaste.
* `id`: el identificador de la conversación en Ryvo. Es `null` en `conversation.started` porque en ese momento la conversación todavía no existe.

### `conversation.started` / `call.initiated`

```json theme={null}
{
  "id": "evt_01abc...",
  "type": "conversation.started",
  "created_at": "2026-04-30T18:32:11.123Z",
  "data": {
    "id": null,
    "call_id": "call_conv_01abcdef",
    "agent_id": "agent_8801kpabc123",
    "to": "+5215555550100",
    "metadata": {
      "lead_name": "Juan García"
    }
  }
}
```

### `conversation.ended` / `call.completed`

```json theme={null}
{
  "id": "evt_02def...",
  "type": "conversation.ended",
  "created_at": "2026-04-30T18:35:44.987Z",
  "data": {
    "id": "7f3c2a1e-9b0d-4c6e-8a52-1d2e3f4a5b6c",
    "call_id": "call_conv_01abcdef",
    "agent_id": "agent_8801kpabc123",
    "status": "completed",
    "duration_seconds": 211,
    "transcript_summary": "El lead aceptó agendar una demo para el 5 de mayo a las 11am.",
    "termination_reason": "user_hung_up",
    "started_at": "2026-04-30T18:32:13Z",
    "ended_at": "2026-04-30T18:35:44Z"
  }
}
```

`status` puede ser `completed`, `failure` o `unknown` según el resultado del análisis de la llamada. El resumen nunca incluye la transcripción completa.

### `conversation.ended` / `call.failed` (no se pudo iniciar)

```json theme={null}
{
  "id": "evt_03ghi...",
  "type": "call.failed",
  "created_at": "2026-04-30T18:32:25.456Z",
  "data": {
    "id": null,
    "call_id": "call_conv_01abcdef",
    "agent_id": "agent_8801kpabc123",
    "status": "failed",
    "reason": "no_answer"
  }
}
```

### `analysis.ready`

```json theme={null}
{
  "id": "evt_04jkl...",
  "type": "analysis.ready",
  "created_at": "2026-09-21T18:41:02.000Z",
  "data": {
    "conversation_id": "7f3c2a1e-9b0d-4c6e-8a52-1d2e3f4a5b6c",
    "summary": "La clienta pidió cotización de instalación y quedó en llamar el lunes.",
    "evaluation_results": {
      "agendo_cita": { "criteria_id": "agendo_cita", "result": "failure", "rationale": "No se fijó fecha." }
    },
    "data_collection": {
      "nombre": { "data_collection_id": "nombre", "value": "Mariana López", "rationale": "Se presentó al inicio.", "json_schema": { "type": "string" } },
      "presupuesto": { "data_collection_id": "presupuesto", "value": 12000, "rationale": "Mencionó doce mil pesos.", "json_schema": { "type": "number" } }
    }
  }
}
```

Las llaves de `evaluation_results` y `data_collection` son los identificadores que definiste en el Constructor ([Análisis después de cada conversación](/es/guides/agents/builder#analisis-despues-de-cada-conversacion)).

## Headers de cada delivery

| Header             | Valor                                           |
| ------------------ | ----------------------------------------------- |
| `Content-Type`     | `application/json`                              |
| `User-Agent`       | `Ryvo-Webhook/1.0`                              |
| `X-Ryvo-Signature` | `t=<unix_secs>,v1=<hex_sha256>`. Verifica esto. |
| `X-Ryvo-Event`     | El tipo de evento (ej. `call.completed`).       |
| `X-Ryvo-Event-Id`  | UUID del evento. Úsalo para idempotencia.       |
| `X-Ryvo-Delivery`  | UUID único de este intento de entrega.          |

## Reintentos automáticos

Si tu endpoint responde con un código que no es `2xx` o se cuelga más de 10 segundos, **reintentamos automáticamente** con backoff exponencial:

| Intento | Cuándo                             |
| ------- | ---------------------------------- |
| 1       | Inmediato cuando ocurre el evento. |
| 2       | 5 minutos después.                 |
| 3       | 30 minutos después.                |
| 4       | 2 horas después.                   |

Después del 4to intento fallido, marcamos la entrega como `exhausted` y no volvemos a intentar. Puedes ver el historial en `app.ryvo.so/developers`.

## Idempotencia del lado del receptor

Aunque hagamos retry solo en errores, **un mismo evento puede llegarte 2 veces** (ej. tu endpoint procesó OK pero la respuesta `200` se perdió en la red). Por eso te mandamos `X-Ryvo-Event-Id`: guarda los IDs procesados y descarta duplicados.

```javascript theme={null}
const processed = new Set() // en producción, usa Redis o tu DB

app.post("/webhook/ryvo", async (req, res) => {
  const eventId = req.headers["x-ryvo-event-id"]
  if (processed.has(eventId)) return res.sendStatus(200)
  processed.add(eventId)

  // ... tu lógica
  res.sendStatus(200)
})
```

## Responde rápido

Tu endpoint debe responder `200` en menos de 10 segundos. Si necesitas hacer trabajo lento (LLM call, sync a una API externa), responde `200` inmediato y procesa async (cola, background job).

## Próximos pasos

<CardGroup cols={2}>
  <Card title="Verificar firmas" icon="signature" href="/es/webhooks/signature-verification">
    Cómo validar que el evento viene de Ryvo y no de un atacante.
  </Card>

  <Card title="Probar tu webhook" icon="flask" href="https://app.ryvo.so/developers">
    Desde el portal puedes mandarte un evento de prueba.
  </Card>
</CardGroup>
