Docs
Integraciones

Webhooks

Recibe notificaciones en tiempo real sobre eventos de llamadas, transcripciones y acciones del agente en tu propio servidor.

¿Cómo funcionan los webhooks?

Cuando ocurre un evento relevante en VoiceAgent (una llamada que comienza, termina, o una transcripción que se completa), VoiceAgent envía un HTTP POST a la URL que hayas configurado en tu dashboard. El payload es un objeto JSON con toda la información del evento.

Los webhooks son la forma recomendada de mantener tu sistema sincronizado con VoiceAgent sin necesidad de hacer polling a la API. Son instantáneos, fiables y seguros gracias al sistema de verificación de firma HMAC.

Configurar tu URL de webhook

Ve a Configuración → Webhooks → Añadir endpoint en el dashboard. Introduce la URL de tu servidor y selecciona los eventos que deseas recibir. También puedes configurarlo vía API:

curl -X POST https://usvoiceagent.com/api/webhooks/endpoints \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://tu-servidor.com/webhooks/voiceagent",
    "events": [
      "call.started",
      "call.ended",
      "call.transcribed",
      "appointment.booked"
    ],
    "description": "Webhook de producción"
  }'

Eventos disponibles

call.started

Se emite cuando una llamada (entrante o saliente) comienza y el agente está listo para hablar. Para llamadas entrantes, esto ocurre cuando el llamante descuelga. Para salientes, cuando el destinatario contesta.

{
  "event": "call.started",
  "id": "evt_01HWXYZ123",
  "createdAt": "2025-05-21T10:30:05.123Z",
  "data": {
    "callId": "call_abc123def456",
    "agentId": "agent_xxxxxxxxxxxx",
    "direction": "inbound",
    "from": "+34600123456",
    "to": "+12125551234",
    "startedAt": "2025-05-21T10:30:05Z",
    "twilioCallSid": "CA1234567890abcdef1234567890abcdef"
  }
}

call.ended

Se emite cuando una llamada finaliza por cualquier motivo: el usuario cuelga, el agente cierra la llamada, timeout por inactividad, o error de red.

{
  "event": "call.ended",
  "id": "evt_01HWXYZ124",
  "createdAt": "2025-05-21T10:35:47.891Z",
  "data": {
    "callId": "call_abc123def456",
    "agentId": "agent_xxxxxxxxxxxx",
    "direction": "inbound",
    "from": "+34600123456",
    "to": "+12125551234",
    "startedAt": "2025-05-21T10:30:05Z",
    "endedAt": "2025-05-21T10:35:47Z",
    "durationSeconds": 342,
    "endReason": "user_hangup",
    "twilioCallSid": "CA1234567890abcdef1234567890abcdef"
  }
}

Valores posibles de endReason: user_hangup, agent_hangup, timeout,transferred, error, no_answer, busy.

call.transcribed

Se emite cuando la transcripción completa de la llamada está disponible. Esto ocurre típicamente entre 5 y 30 segundos después de que la llamada finaliza, dependiendo de la duración de la misma.

{
  "event": "call.transcribed",
  "id": "evt_01HWXYZ125",
  "createdAt": "2025-05-21T10:36:12.400Z",
  "data": {
    "callId": "call_abc123def456",
    "agentId": "agent_xxxxxxxxxxxx",
    "durationSeconds": 342,
    "transcript": [
      {
        "role": "agent",
        "text": "Hola, bienvenido a TuEmpresa. ¿En qué puedo ayudarte?",
        "timestamp": "2025-05-21T10:30:05Z"
      },
      {
        "role": "user",
        "text": "Quiero saber el horario de atención al público.",
        "timestamp": "2025-05-21T10:30:09Z",
        "confidence": 0.97
      },
      {
        "role": "agent",
        "text": "Nuestro horario es de lunes a viernes de 9 a 18 horas, y sábados de 10 a 14 horas.",
        "timestamp": "2025-05-21T10:30:11Z"
      }
    ],
    "summary": "El cliente preguntó por el horario de atención. Se le proporcionó la información correcta.",
    "sentiment": "positive",
    "intentDetected": "horario_consulta",
    "recordingUrl": "https://usvoiceagent.com/recordings/call_abc123def456.mp3"
  }
}

appointment.booked

Se emite cuando el agente usa la herramienta book_appointment para crear una cita durante una llamada. Requiere tener la integración de calendario configurada.

{
  "event": "appointment.booked",
  "id": "evt_01HWXYZ126",
  "createdAt": "2025-05-21T10:33:20.100Z",
  "data": {
    "callId": "call_abc123def456",
    "agentId": "agent_xxxxxxxxxxxx",
    "appointment": {
      "id": "appt_789xyz",
      "title": "Consulta médica - María García",
      "startTime": "2025-06-15T10:00:00Z",
      "endTime": "2025-06-15T10:30:00Z",
      "attendeePhone": "+34600123456",
      "attendeeName": "María García",
      "calendarId": "cal_primary",
      "confirmed": true
    }
  }
}

call.bridged

Se emite cuando una triangulación se establece con éxito y las dos partes quedan conectadas.

{
  "event": "call.bridged",
  "id": "evt_01HWXYZ127",
  "createdAt": "2025-05-21T10:34:00.000Z",
  "data": {
    "callId": "call_abc123def456",
    "bridgedTo": "+34912345678",
    "bridgedAt": "2025-05-21T10:34:00Z",
    "qualificationSummary": "DNI verificado: 12345678Z. Motivo: consulta factura.",
    "agentRole": "silent_listener"
  }
}

Seguridad: verificación de firma

Cada webhook que VoiceAgent envía incluye la cabecera X-VoiceAgent-Signature. Esta firma HMAC-SHA256 te permite verificar que el webhook proviene realmente de VoiceAgent y no de un actor malintencionado.

Cómo verificar la firma

La firma se calcula concatenando el timestamp de la cabecera X-VoiceAgent-Timestamp con un punto y el cuerpo raw del request, y firmando con tu VOICE_ENGINE_SECRET.

// Node.js / TypeScript
import crypto from 'crypto'

export function verifyWebhookSignature(
  payload: string,
  timestamp: string,
  signature: string,
  secret: string
): boolean {
  const signedContent = `${timestamp}.${payload}`
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(signedContent)
    .digest('hex')

  // Usar timingSafeEqual para prevenir timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  )
}

// Uso en tu endpoint de webhook (Next.js App Router)
export async function POST(request: Request) {
  const payload = await request.text()
  const signature = request.headers.get('X-VoiceAgent-Signature') ?? ''
  const timestamp = request.headers.get('X-VoiceAgent-Timestamp') ?? ''

  const isValid = verifyWebhookSignature(
    payload,
    timestamp,
    signature,
    process.env.VOICE_ENGINE_SECRET!
  )

  if (!isValid) {
    return new Response('Unauthorized', { status: 401 })
  }

  const event = JSON.parse(payload)
  // Procesar el evento...
  return new Response('OK', { status: 200 })
}

Validación del timestamp

También debes verificar que el timestamp no tenga más de 5 minutos de antigüedad para protegerte de ataques de replay. Rechaza webhooks con timestamps fuera de este rango aunque la firma sea válida.

Política de reintentos

Si tu servidor no responde con un código HTTP 2xx en un plazo de 10 segundos, VoiceAgent considera el intento fallido y lo reintenta automáticamente con backoff exponencial:

IntentoEspera antes del reintentoTiempo acumulado
1 (inicial)0 min
230 segundos0,5 min
32 minutos2,5 min
410 minutos12,5 min
5 (último)30 minutos42,5 min

Después de 5 intentos fallidos, el evento se marca como fallido y no se vuelve a reintentar. Puedes ver los eventos fallidos en el dashboard en Configuración → Webhooks → [Endpoint] → Eventos fallidos y reenviarlos manualmente si es necesario.

Idempotencia

Cada webhook incluye un id de evento único (ej. evt_01HWXYZ123). Usa este ID para implementar idempotencia en tu handler y evitar procesar el mismo evento dos veces en caso de reintentos.