> ## 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

> Receive woku events on your system in real time, with HMAC signature to verify authenticity and automatic retries

**Webhooks** let you receive HTTP notifications on your own
server every time a relevant event happens in woku: a new
**woku** review, an **NPS** response, or a **form**
submission. woku sends a `POST` with a JSON body to the URL you
configure.

## Configuration

Webhooks are managed from the admin application, not through the
public API.

<Steps>
  <Step title="Open the integrations settings">
    In the panel, go to **Company → Integrations → Webhooks**.
  </Step>

  <Step title="Create a webhook">
    Press **New webhook** and enter a **name**, the **URL** of your
    endpoint, and the **events** you subscribe to. Optionally
    you can add **custom headers** (for example, a token
    of your own) and enable or disable the webhook at any time.
  </Step>

  <Step title="Save the secret">
    When you create the webhook, woku shows the **secret** just once. Copy it and
    store it securely: you will need it to verify the signature of
    each event.
  </Step>
</Steps>

<Warning>
  The secret is shown **only once** when you create or rotate the webhook. woku
  stores it encrypted and cannot show it to you again. If you lose it, rotate it
  from the panel to generate a new one.
</Warning>

## Available events

woku emits these events:

| Event                     | When it is emitted                            |
| ------------------------- | --------------------------------------------- |
| `qualification.created`   | A new woku review was recorded (rating 1-5).  |
| `nps_submission.created`  | A new NPS response was recorded (score 0-10). |
| `form_submission.created` | A form response was completed.                |

### Common structure

All payloads share this envelope:

| Field       | Description                             |
| ----------- | --------------------------------------- |
| `event`     | Event name (see the table above).       |
| `timestamp` | Emission date and time (ISO 8601, UTC). |
| `companyId` | Company the event belongs to.           |
| `data`      | Event-specific data.                    |

### Payload examples

<Note>
  Payloads do not include customer contact data.
</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>

## Signature verification (HMAC)

Each delivery includes the `X-Woku-Signature` header with an
**HMAC-SHA256** signature of the raw JSON body, using your secret as key:

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

Besides the signature, each delivery carries two informational headers:

| Header            | Content                                                              |
| ----------------- | -------------------------------------------------------------------- |
| `X-Woku-Event`    | Name of the delivered event (for example `nps_submission.created`).  |
| `X-Woku-Delivery` | Unique identifier of the delivery, useful for discarding duplicates. |

<Warning>
  Compute the signature over the **raw body** of the request (the exact
  bytes received), **before** parsing it as JSON. Re-serializing the
  object may change the order of the keys or the spacing and produce a
  different signature.
</Warning>

Always compare with a **constant-time** function to avoid
timing attacks. If the signature does not match, discard the request.

<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);
  }

  // Example with Express. Use the raw body, not the parsed JSON.
  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'));
      // ... process the event
      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)


  # Example with Flask. request.data is the raw body.
  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()
      # ... process the event
      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);
  // ... process the event

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

## Expected response from your endpoint

Your endpoint must respond with an **HTTP 2xx** code (ideally `200`)
as soon as possible. woku considers the delivery successful on any 2xx.

<Tip>
  Process the event **asynchronously**: respond `200` immediately and
  queue the heavy work. woku uses a timeout of **15 seconds** per
  attempt; if your endpoint takes longer, the delivery is counted as failed.
</Tip>

## Retries and dead-letter

If your endpoint does not respond with 2xx (or does not respond within the timeout),
woku **retries with exponential backoff**:

| Attempt | Wait before the attempt |
| ------- | ----------------------- |
| 1       | immediate               |
| 2       | \~1 s                   |
| 3       | \~2 s                   |

After exhausting the **3 attempts**, the delivery moves to **dead-letter** state
and is not attempted again. In the webhook detail, under **Company →
Integrations → Webhooks**, you can review the **delivery history**
filtered by status (Successful, Failed, Pending, Dead-letter). From
there you can also edit the webhook, rotate its key, or delete it.

<Note>
  Since an event may be delivered more than once (from a retry on a
  delivery that did arrive but responded late), design your endpoint to be
  **idempotent**: use the `X-Woku-Delivery` header or the resource
  identifier in `data` to discard duplicates.
</Note>

## Customer support tickets

Support tickets generated by woku's AI are not delivered by
webhook: they are sent to your support platform through **direct
destinations** configured in the Support module. See the guide on
[customer support tickets](/docs/en/guias/tickets-sac).
