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

# Descripción general de Webhooks

> Recibe actualizaciones en tiempo real de Altur directamente en tu backend o CRM.

## Introducción a los Webhooks

Los webhooks te permiten recibir notificaciones en tiempo real sobre eventos en tu organización de Altur. Cuando ocurre un evento (por ejemplo, el fin de una llamada), Altur envía una solicitud HTTP POST con los datos del evento al endpoint que configures, manteniendo tus sistemas o CRM al día sin necesidad de hacer polling.

## Cómo Funcionan los Webhooks

1. **Configura tu Endpoint**\
   Configura la URL del webhook en la plataforma de Altur. Esta URL recibirá las solicitudes POST cuando se dispare un evento.

2. **Recibe Notificaciones**\
   Cuando ocurre un evento, Altur envía un POST con los detalles del evento en formato JSON a tu endpoint.

3. **Confirma la Recepción**\
   Tu endpoint debe responder con un código `200 OK` para confirmar la recepción. Altur espera hasta 5 segundos por la respuesta. Los timeouts, errores de conexión y respuestas `5xx`, `408` y `429` se reintentan con backoff exponencial (hasta 5 intentos en total). Cualquier otra respuesta `4xx` falla de inmediato sin reintentos, y los redirects no se siguen. Consulta la sección Política de Reintentos más abajo.

## Tipos de Evento

Los webhooks pueden disparar los siguientes tipos de evento:

* [`on_call_end`](https://docs.altur.io/api-reference/webhooks/on-call-end): Se dispara al final de una llamada. Envía información detallada de la llamada junto con datos básicos del agente de IA y del usuario final.
* [`transfer_started`](https://docs.altur.io/es/api-reference/webhooks/transfer-started): Se dispara cuando inicia una transferencia, para preparar contexto en tiempo real para tus agentes humanos.
* [`transfer_completed`](https://docs.altur.io/es/api-reference/webhooks/transfer-completed): Se dispara cuando conecta la transferencia con el agente humano.
* [`campaign.status_changed`](https://docs.altur.io/es/api-reference/webhooks/campaign-status-changed): Se dispara cuando una Campaña cambia de estado. Incluye el snapshot de analíticas cuando la campaña llega a `finished`.
* [`campaign.cycle_completed`](https://docs.altur.io/es/api-reference/webhooks/campaign-cycle-completed): Se dispara cuando una iteración del ciclo de una Campaña termina, con el número de iteración completada y el timestamp de la siguiente iteración.

<Note>
  Los webhooks de campaña usan un envelope versionado (`event_id`, `event_type`,
  `occurred_at`, `api_version`, `project_id`, `data`). Usa `event_id` para
  procesamiento idempotente. Los eventos de llamada (`on_call_end`,
  `transfer_started`, `transfer_completed`) son anteriores a este envelope,
  mantienen su forma plana en el nivel superior y no tienen `event_id`:
  deduplícalos usando `id` (el ID de la llamada) junto con `event_type`.
</Note>

## Asegurando tus Webhooks

Para garantizar comunicaciones seguras y verificar la autenticidad de las solicitudes, Altur incluye una firma HMAC en el header `X-Altur-Signature` de cada solicitud.

### Cómo Funciona

* **Secreto Compartido**: Cada integración de webhook se configura con una llave secreta única codificada en base64.
* **Generación del HMAC**: Altur genera un hash HMAC SHA-256 usando el secreto compartido (decodificado de base64) y el payload serializado como JSON compacto (sin espacios después de `:` ni `,`, con los caracteres no ASCII escapados como `\uXXXX`, p. ej. `Sofía` → `Sof\u00eda`). El hash se codifica en base64 y se incluye en el header `X-Altur-Signature`.
* **Validación**: Tu endpoint debe parsear el body y volver a serializarlo en ese mismo formato compacto antes de calcular el HMAC.

<Warning>
  El body que envía Altur **no** es exactamente el string que firma: el body
  incluye espacios después de `:` y `,`. Calcular el HMAC sobre el body crudo
  fallará. Siempre parsea el JSON y vuelve a serializarlo como se describe arriba.
</Warning>

### Función de Validación de Ejemplo

<CodeGroup>
  ```python Python theme={null}
  import base64
  import hashlib
  import hmac
  import json


  def is_valid_signature(secret: str, payload: dict, signature: str) -> bool:
      """
      Valida la firma de un webhook de Altur.

      Args:
          secret: Llave secreta compartida codificada en base64
          payload: Body de la solicitud ya parseado (no el body crudo)
          signature: Firma codificada en base64 del header X-Altur-Signature
      """
      # Volver a serializar en formato compacto. json.dumps escapa los
      # caracteres no ASCII como \uXXXX por defecto, igual que Altur.
      payload_bytes = json.dumps(payload, separators=(",", ":")).encode("utf-8")

      # Decodificar el secreto base64
      secret_bytes = base64.b64decode(secret)

      # Generar la firma esperada
      expected_signature = base64.b64encode(
          hmac.new(secret_bytes, payload_bytes, hashlib.sha256).digest()
      ).decode()

      return hmac.compare_digest(signature, expected_signature)
  ```

  ```javascript JavaScript theme={null}
  const crypto = require("crypto");

  function isValidSignature(secret, payload, signature) {
    /**
     * Valida la firma de un webhook de Altur.
     *
     * @param {string} secret - Llave secreta compartida codificada en base64
     * @param {object} payload - Body de la solicitud ya parseado (no el body crudo)
     * @param {string} signature - Firma codificada en base64 del header X-Altur-Signature
     */

    // Volver a serializar en formato compacto y escapar los caracteres no ASCII
    // como \uXXXX (JSON.stringify no los escapa, Altur sí)
    const payloadString = JSON.stringify(payload).replace(
      /[\u0080-\uffff]/g,
      (c) => "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0")
    );

    // Decodificar el secreto base64
    const secretBuffer = Buffer.from(secret, "base64");

    // Generar la firma esperada
    const expected = crypto
      .createHmac("sha256", secretBuffer)
      .update(payloadString)
      .digest();
    const received = Buffer.from(signature || "", "base64");

    // timingSafeEqual lanza un error si las longitudes difieren, así que se comparan primero
    return (
      received.length === expected.length &&
      crypto.timingSafeEqual(received, expected)
    );
  }

  // Ejemplo de uso (con express.json())
  const signature = req.headers["x-altur-signature"];
  const isValid = isValidSignature(yourSecret, req.body, signature);
  ```

  ```php PHP theme={null}
  function isValidSignature($secret, $payload, $signature) {
      /*
       * Valida la firma de un webhook de Altur.
       *
       * @param string $secret Llave secreta compartida codificada en base64
       * @param object $payload Body decodificado con json_decode($raw), SIN `true`
       * @param string $signature Firma codificada en base64 del header X-Altur-Signature
       */

      // Volver a serializar en formato compacto. json_encode escapa los
      // caracteres no ASCII como \uXXXX por defecto, igual que Altur.
      $payloadString = json_encode($payload, JSON_UNESCAPED_SLASHES);

      // Decodificar el secreto base64
      $secretBytes = base64_decode($secret);

      // Generar la firma esperada
      $expectedSignature = base64_encode(
          hash_hmac('sha256', $payloadString, $secretBytes, true)
      );

      // Comparar firmas con comparación timing-safe
      return hash_equals($expectedSignature, (string) $signature);
  }

  // Ejemplo de uso
  $signature = $_SERVER['HTTP_X_ALTUR_SIGNATURE'] ?? '';
  // Decodifica como objetos: json_decode(..., true) convierte `{}` (p. ej. un
  // extracted_data vacío) en `[]`, que se re-codifica distinto y rompe la firma.
  $payload = json_decode(file_get_contents('php://input'));
  $isValid = isValidSignature($yourSecret, $payload, $signature);
  ```
</CodeGroup>

### Ejemplo de Uso

```python theme={null}
request_body = request.get_json()  # JSON parseado por tu framework web
signature = request.headers.get("X-Altur-Signature", "")
shared_secret = "tu-secreto-codificado-en-base64"

if is_valid_signature(shared_secret, request_body, signature):
    print("Webhook válido!")
else:
    print("Webhook inválido!")
```

### Headers Enviados con el Webhook

* `Content-Type`: `application/json`
* `X-Altur-Signature`: hash HMAC SHA-256 codificado en base64 del payload JSON compacto (ver arriba; no es el body crudo)

### Por Qué Importa

Este mecanismo garantiza que:

* La solicitud proviene de Altur.
* El payload no fue modificado en tránsito.

## Política de Reintentos

Cualquier respuesta distinta de `2xx` cuenta como fallo. Que Altur reintente o no depende del tipo de fallo:

| Fallo | Comportamiento |
| :- | :- |
| Timeout (sin respuesta en 5 segundos) | Se reintenta |
| Error de conexión (DNS, TLS, conexión rechazada, etc.) | Se reintenta |
| `5xx` | Se reintenta |
| `408 Request Timeout`, `429 Too Many Requests` | Se reintenta |
| Cualquier otro `4xx` (p. ej. `400`, `401`, `403`, `404`) | **Terminal**: falla de inmediato, sin reintentos |
| Redirect `3xx` | **Terminal**: los redirects no se siguen y cuentan como fallo |

Los reintentos usan:

* **Intentos máximos**: 5 (entrega inicial más 4 reintentos).
* **Backoff**: 60s después del primer fallo, duplicándose en cada intento (60s, 2m, 4m, 8m).

Si todos los intentos fallan, o el fallo es terminal, el evento se marca como fallido y no vuelve a entregarse.

<Tip>
  Si tu endpoint no puede procesar un evento temporalmente, responde con `5xx`
  o `429` para que Altur reintente. Un `4xx` como `400` le indica a Altur que el
  evento nunca será aceptado. Configura directamente la URL final: un redirect
  (por ejemplo `http` → `https`, o una barra final faltante) hace que todas las
  entregas fallen.
</Tip>

## Ejemplo de Flujo

Un flujo típico para manejar webhooks:

1. **Configura tu Endpoint**

   Crea un endpoint en tu backend, por ejemplo `/webhooks/altur/on-call-end`. Debe ser accesible públicamente y aceptar solicitudes POST con `Content-Type: application/json`.

2. **Parsea el Payload**

   Extrae y procesa el payload JSON enviado por Altur. Maneja casos como payloads malformados o campos faltantes para evitar errores en runtime.

3. **Valida la Autenticidad**

   Usa el header `X-Altur-Signature` para verificar la autenticidad. Esto implica:

   * Recomputar la firma HMAC usando el secreto compartido y el payload parseado, re-serializado como JSON compacto.
   * Compararla con la firma del header.

4. **Responde Rápido**

   Devuelve `200 OK` para confirmar la recepción. Altur hace timeout a los 5 segundos, así que responde mucho antes de ese límite para evitar reintentos innecesarios.

5. **Registra los Eventos**

   Guarda logs de las solicitudes y del procesamiento para tener trazabilidad. Es especialmente útil al depurar problemas con payloads o reintentos.

6. **Maneja los Reintentos**

   Diseña tu sistema para tolerar reintentos. Asegúrate de que tu endpoint sea idempotente: procesar el mismo evento varias veces no debe duplicar acciones (por ejemplo, inserts en BD o llamadas a APIs).

## Buenas Prácticas

1. **Asegura tu Endpoint**

   * **Autentica las Solicitudes**: Verifica la firma HMAC en cada request. Usa un secreto único y seguro por integración.
   * **Restringe el Acceso**: Usa whitelisting de IP o firewall para permitir solicitudes únicamente desde los servidores de Altur.
   * **Cifra la Comunicación**: Usa HTTPS para que los datos viajen cifrados.

2. **Registra los Eventos**

   * **Registra Todo**: Guarda timestamps, headers, payloads y respuestas de cada solicitud entrante.
   * **Monitorea Fallos**: Configura alertas para fallos repetidos o solicitudes inválidas.
   * **Mantén Logs**: Retén los logs por un periodo razonable para auditoría o debugging.

3. **Prueba Bien**

   * **Simula Eventos**: Usa el dashboard de Altur para simular eventos y confirmar que tu endpoint los maneja correctamente.
   * **Prueba Casos Borde**: Payloads malformados, payloads grandes, campos faltantes.
   * **Usa Entornos de Staging**: Mantén un endpoint dedicado para pruebas, separado de producción.

4. **Optimiza el Rendimiento**

   * **Responde Rápido**: Altur hace timeout a los 5 segundos. Si el procesamiento es más largo, responde `200 OK` de inmediato y procesa en background.
   * **Minimiza el Procesamiento**: Haz solo validación ligera y encolado en el endpoint; deja el trabajo pesado a workers.
   * **Usa Caching**: Cuando aplique, cachea respuestas para solicitudes redundantes.

5. **Sé Idempotente**

   * **Evita Acciones Duplicadas**: Diseña el sistema para que los reintentos no causen operaciones duplicadas (inserts en BD, llamadas a APIs). En los eventos de campaña, deduplica con `event_id`. Los eventos de llamada (`on_call_end`, `transfer_started`, `transfer_completed`) no tienen `event_id`, así que deduplica con `id` junto con `event_type`.

6. **Comunica los Fallos**
   * **Devuelve Status Codes Correctos**: Usa `400` para solicitudes inválidas, `500` para errores del servidor, etc. Recuerda que las respuestas `4xx` distintas de `408` y `429` no se reintentan.
   * **Loguea y Notifica**: Registra los fallos y considera notificar al equipo si los reintentos críticos se agotan.

## Solución de Problemas con la Validación de Firma

Si tienes problemas validando la firma, revisa estos puntos comunes:

### 1. Formato de Serialización JSON

Usa el mismo formato JSON que Altur:

* **Parsea y vuelve a serializar**: No calcules el HMAC sobre el body crudo. Tiene espacios y no coincidirá.
* **Formato compacto**: Sin espacios después de comas ni de dos puntos.
* **No ASCII escapado**: Caracteres como `í` o `ñ` deben escaparse como `\uXXXX` (`Sofía` → `Sof\u00eda`). `JSON.stringify` no lo hace; consulta el ejemplo de JavaScript.
* **Orden consistente**: Conserva las llaves en el orden en que se recibieron.
* **PHP**: Usa `json_decode($raw)`, no `json_decode($raw, true)`. Los arrays asociativos convierten `{}` en `[]`.

```python theme={null}
# ✅ Correcto - JSON compacto
json.dumps(data, separators=(',', ':'))

# ❌ Incorrecto - incluye espacios
json.dumps(data)  # Resultado: {"key": "value"}

# ❌ Incorrecto - no escapa caracteres no ASCII
json.dumps(data, separators=(',', ':'), ensure_ascii=False)
```

### 2. Formato de la Llave Secreta

* Tu secreto debe estar **codificado en base64**.
* Debe ser el mismo configurado en tu integración de webhook en Altur.

### 3. Nombre del Header

* El header es `X-Altur-Signature` (algunos frameworks son case-sensitive).
* Confirma que estás leyendo el header correcto.

### 4. Timing Attacks

Usa siempre funciones de comparación timing-safe:

* Python: `hmac.compare_digest()`
* Node.js: `crypto.timingSafeEqual()`
* PHP: `hash_equals()`

### 5. Probar tu Implementación

Puedes probar tu validación con este ejemplo:

```python theme={null}
# Datos de prueba
test_payload = {"test": "data"}
test_secret = "dGVzdC1zZWNyZXQ="  # base64 de "test-secret"
expected_signature = "6RQRnEGhLZoG7UMG+fPBzjLuZhjLsG9U8AooC6LceYU="

# Debe devolver True
result = is_valid_signature(test_secret, test_payload, expected_signature)
print(f"Resultado de validación: {result}")
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.