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

# Webhooks

> Recibe eventos de woku en tu sistema en tiempo real, con firma HMAC para verificar la autenticidad y reintentos automáticos

Los **webhooks** te permiten recibir notificaciones HTTP en tu propio
servidor cada vez que ocurre un evento relevante en woku: una nueva
reseña **woku**, una respuesta **NPS** o la respuesta de un
**formulario**. woku hace un `POST` con un cuerpo JSON a la URL que
configures.

## Configuración

Los webhooks se administran desde la aplicación administrativa, no por
API pública.

<Steps>
  <Step title="Abre la configuración de integraciones">
    En el panel, entra a **Empresa → Integraciones → Webhooks**.
  </Step>

  <Step title="Crea un webhook">
    Presiona **Nuevo webhook** e ingresa un **nombre**, la **URL** de tu
    endpoint y los **eventos** a los que te suscribes. Opcionalmente
    puedes agregar **headers personalizados** (por ejemplo, un token
    propio) y activar o desactivar el webhook en cualquier momento.
  </Step>

  <Step title="Guarda el secret">
    Al crear el webhook, woku muestra el **secret** una sola vez. Cópialo y
    guárdalo de forma segura: lo necesitarás para verificar la firma de
    cada evento.
  </Step>
</Steps>

<Warning>
  El secret se muestra **una única vez** al crear o rotar el webhook. woku
  lo guarda cifrado y no puede volver a mostrártelo. Si lo pierdes, rótalo
  desde el panel para generar uno nuevo.
</Warning>

## Eventos disponibles

woku emite estos eventos:

| Evento                    | Cuándo se emite                                       |
| ------------------------- | ----------------------------------------------------- |
| `qualification.created`   | Se registró una nueva reseña woku (calificación 1-5). |
| `nps_submission.created`  | Se registró una nueva respuesta NPS (puntaje 0-10).   |
| `form_submission.created` | Se completó una respuesta de formulario.              |

### Estructura común

Todos los payloads comparten esta envoltura:

| Campo       | Descripción                              |
| ----------- | ---------------------------------------- |
| `event`     | Nombre del evento (ver tabla anterior).  |
| `timestamp` | Fecha y hora de emisión (ISO 8601, UTC). |
| `companyId` | Empresa a la que pertenece el evento.    |
| `data`      | Datos específicos del evento.            |

### Ejemplos de payload

<Note>
  Los payloads no incluyen datos de contacto del cliente.
</Note>

<CodeGroup>
  ```json qualification.created theme={null}
  {
    "event": "qualification.created",
    "timestamp": "2026-05-26T15:30:00Z",
    "companyId": "64a1b2c3d4e5f6a7b8c9d0e1",
    "data": {
      "clientId": "64f1a2b3c4d5e6f7a8b9c0d1",
      "wokuId": "64d4e5f6a7b8c9d0e1f2a3b4",
      "qualification": 5,
      "responseChannel": "review-app"
    }
  }
  ```

  ```json nps_submission.created theme={null}
  {
    "event": "nps_submission.created",
    "timestamp": "2026-05-26T15:30:00Z",
    "companyId": "64a1b2c3d4e5f6a7b8c9d0e1",
    "data": {
      "npsId": "64b2c3d4e5f6a7b8c9d0e1f2",
      "npsToolId": "64c3d4e5f6a7b8c9d0e1f2a3",
      "responseChannel": "whatsapp"
    }
  }
  ```

  ```json form_submission.created theme={null}
  {
    "event": "form_submission.created",
    "timestamp": "2026-05-26T15:30:00Z",
    "companyId": "64a1b2c3d4e5f6a7b8c9d0e1",
    "data": {
      "formResponseId": "64b2c3d4e5f6a7b8c9d0e1f2",
      "formId": "64c3d4e5f6a7b8c9d0e1f2a3",
      "responseChannel": "api"
    }
  }
  ```
</CodeGroup>

## Verificación de firma (HMAC)

Cada entrega incluye el header `X-Woku-Signature` con una firma
**HMAC-SHA256** del cuerpo JSON crudo, usando tu secret como llave:

```
X-Woku-Signature: sha256=<hex>
```

Además de la firma, cada entrega lleva dos headers informativos:

| Header            | Contenido                                                           |
| ----------------- | ------------------------------------------------------------------- |
| `X-Woku-Event`    | Nombre del evento entregado (por ejemplo `nps_submission.created`). |
| `X-Woku-Delivery` | Identificador único de la entrega, útil para descartar duplicados.  |

<Warning>
  Calcula la firma sobre el **cuerpo crudo** de la solicitud (los bytes
  exactos recibidos), **antes** de parsearlo como JSON. Re-serializar el
  objeto puede cambiar el orden de las llaves o el espaciado y producir una
  firma distinta.
</Warning>

Compara siempre con una función de **tiempo constante** para evitar
ataques de temporización. Si la firma no coincide, descarta la solicitud.

<CodeGroup>
  ```js Node.js theme={null}
  import crypto from 'node:crypto';

  function verifyWokuSignature(rawBody, signatureHeader, secret) {
    // signatureHeader: "sha256=<hex>"
    const expected =
      'sha256=' +
      crypto.createHmac('sha256', secret).update(rawBody).digest('hex');

    const a = Buffer.from(signatureHeader);
    const b = Buffer.from(expected);
    return a.length === b.length && crypto.timingSafeEqual(a, b);
  }

  // Ejemplo con Express. Usa el cuerpo crudo, no el JSON parseado.
  import express from 'express';
  const app = express();

  app.post(
    '/webhooks/woku',
    express.raw({ type: 'application/json' }),
    (req, res) => {
      const signature = req.header('X-Woku-Signature') ?? '';
      if (!verifyWokuSignature(req.body, signature, process.env.WOKU_WEBHOOK_SECRET)) {
        return res.status(401).send('Invalid signature');
      }
      const event = JSON.parse(req.body.toString('utf8'));
      // ... procesa el evento
      res.status(200).json({ received: true });
    },
  );
  ```

  ```python Python theme={null}
  import hashlib
  import hmac

  def verify_woku_signature(raw_body: bytes, signature_header: str, secret: str) -> bool:
      # signature_header: "sha256=<hex>"
      expected = "sha256=" + hmac.new(
          secret.encode("utf-8"), raw_body, hashlib.sha256
      ).hexdigest()
      return hmac.compare_digest(expected, signature_header)


  # Ejemplo con Flask. request.data es el cuerpo crudo.
  from flask import Flask, request, abort, jsonify
  import os

  app = Flask(__name__)

  @app.post("/webhooks/woku")
  def woku_webhook():
      signature = request.headers.get("X-Woku-Signature", "")
      if not verify_woku_signature(request.data, signature, os.environ["WOKU_WEBHOOK_SECRET"]):
          abort(401)
      event = request.get_json()
      # ... procesa el evento
      return jsonify(received=True), 200
  ```

  ```php PHP theme={null}
  <?php
  function verify_woku_signature(string $rawBody, string $signatureHeader, string $secret): bool
  {
      // $signatureHeader: "sha256=<hex>"
      $expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
      return hash_equals($expected, $signatureHeader);
  }

  $rawBody = file_get_contents('php://input');
  $signature = $_SERVER['HTTP_X_WOKU_SIGNATURE'] ?? '';
  $secret = getenv('WOKU_WEBHOOK_SECRET');

  if (!verify_woku_signature($rawBody, $signature, $secret)) {
      http_response_code(401);
      echo 'Invalid signature';
      exit;
  }

  $event = json_decode($rawBody, true);
  // ... procesa el evento

  http_response_code(200);
  header('Content-Type: application/json');
  echo json_encode(['received' => true]);
  ```
</CodeGroup>

## Respuesta esperada de tu endpoint

Tu endpoint debe responder con un código **HTTP 2xx** (idealmente `200`)
lo antes posible. woku considera exitosa la entrega ante cualquier 2xx.

<Tip>
  Procesa el evento de forma **asíncrona**: responde `200` de inmediato y
  encola el trabajo pesado. woku usa un timeout de **15 segundos** por
  intento; si tu endpoint tarda más, la entrega se cuenta como fallida.
</Tip>

## Reintentos y dead-letter

Si tu endpoint no responde con 2xx (o no responde dentro del timeout),
woku **reintenta con backoff exponencial**:

| Intento | Espera antes del intento |
| ------- | ------------------------ |
| 1       | inmediato                |
| 2       | \~1 s                    |
| 3       | \~2 s                    |

Tras agotar los **3 intentos**, la entrega pasa a estado **dead-letter**
y no se vuelve a intentar. En el detalle del webhook, en **Empresa →
Integraciones → Webhooks**, puedes revisar el **historial de entregas**
con filtro por estado (Exitoso, Fallido, Pendiente, Dead-letter). Desde
ahí también puedes editar el webhook, rotar su clave o eliminarlo.

<Note>
  Como un evento puede entregarse más de una vez (por un reintento sobre una
  entrega que sí llegó pero respondió tarde), diseña tu endpoint para que
  sea **idempotente**: usa el header `X-Woku-Delivery` o el identificador
  del recurso en `data` para descartar duplicados.
</Note>

## Tickets a SAC

Los tickets de soporte generados por la IA de woku no se entregan por
webhook: se envían a tu plataforma de soporte mediante **destinos
directos** configurados en el módulo Soporte. Consulta la guía de
[tickets a SAC](/docs/guides/support-tickets).
