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

> ## Agent Instructions
> Wabox é uma API não-oficial de WhatsApp (aparelho vinculado). Tudo é snake_case; a base é https://api.wabox.me/instances/{instance_id}/token/{token}.
> Envios respondem { id, message_id, wabox_id, status: "queued" } na hora; o resultado real chega no webhook delivery. Envios não são idempotentes: confira GET /queue antes de repetir.
> Sempre verifique X-Wabox-Signature (HMAC-SHA256 de "<t>.<corpo cru>") nos webhooks e deduplique por event_id.
> Botões, listas, carrossel e catálogo são best effort e não renderizam no WhatsApp Web/Desktop. Não existem: chamadas, listas de transmissão, histórico de mensagens, instância mobile.
> Não invente endpoints ou campos: use o OpenAPI em https://api.wabox.me/openapi.json.

# Assinatura dos webhooks

> Verifique com HMAC-SHA256 que cada entrega veio do Wabox e não é uma repetição.

Toda entrega de webhook leva o header `X-Wabox-Signature`, calculado com o **segredo da instância** sobre o corpo cru da requisição:

```text theme={"system"}
X-Wabox-Signature: t=1786968300,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
```

* `t` — timestamp Unix (segundos) de quando o Wabox assinou.
* `v1` — `HMAC-SHA256(secret, "<t>.<corpo cru>")` em hexadecimal.

O segredo (`whsec_…`) aparece no painel em **Webhooks e configurações gerais** e em `GET /webhooks` (campo `secret`). Cada instância tem o seu.

## Verificação

<Steps>
  <Step title="Leia o corpo cru">
    A assinatura é sobre os bytes exatos que chegaram. Se o seu framework já converteu o JSON em objeto, re-serializar não reproduz o mesmo texto. Capture o corpo antes do parser (`express.raw`, `request.body()` no FastAPI, `php://input`).
  </Step>

  <Step title="Recalcule e compare em tempo constante">
    Monte `"<t>.<corpo>"`, calcule o HMAC-SHA256 com o segredo e compare com `v1` usando comparação de tempo constante.
  </Step>

  <Step title="Rejeite timestamps antigos">
    Descarte entregas com `t` a mais de 5 minutos do relógio atual. Isso limita ataques de repetição com um corpo capturado.
  </Step>
</Steps>

<CodeGroup>
  ```javascript Node.js theme={"system"}
  import { createHmac, timingSafeEqual } from "node:crypto";

  export function verifyWabox(secret, header, rawBody, toleranceSec = 300) {
    const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
    const t = Number(parts.t);
    if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
    const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
    const given = Buffer.from(parts.v1 ?? "", "hex");
    return given.length === expected.length && timingSafeEqual(given, expected);
  }

  // Express: use o corpo cru na rota do webhook
  app.post("/wabox", express.raw({ type: "application/json" }), (req, res) => {
    if (!verifyWabox(process.env.WABOX_WEBHOOK_SECRET, req.get("X-Wabox-Signature") ?? "", req.body.toString("utf8"))) {
      return res.status(401).end();
    }
    const event = JSON.parse(req.body);
    res.status(200).end(); // responda antes de processar
    handle(event);
  });
  ```

  ```python Python theme={"system"}
  import hashlib, hmac, time

  def verify_wabox(secret: str, header: str, raw_body: bytes, tolerance: int = 300) -> bool:
      parts = dict(p.split("=", 1) for p in header.split(","))
      try:
          t = int(parts["t"])
      except (KeyError, ValueError):
          return False
      if abs(time.time() - t) > tolerance:
          return False
      expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
      return hmac.compare_digest(expected, parts.get("v1", ""))

  # FastAPI
  @app.post("/wabox")
  async def wabox(request: Request):
      raw = await request.body()
      if not verify_wabox(SECRET, request.headers.get("X-Wabox-Signature", ""), raw):
          raise HTTPException(401)
      event = json.loads(raw)
      background.add_task(handle, event)
      return {"ok": True}
  ```

  ```php PHP theme={"system"}
  function verifyWabox(string $secret, string $header, string $rawBody, int $tolerance = 300): bool {
      $parts = [];
      foreach (explode(',', $header) as $p) {
          [$k, $v] = array_pad(explode('=', $p, 2), 2, '');
          $parts[trim($k)] = trim($v);
      }
      $t = (int) ($parts['t'] ?? 0);
      if ($t === 0 || abs(time() - $t) > $tolerance) return false;
      $expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
      return hash_equals($expected, $parts['v1'] ?? '');
  }

  $raw = file_get_contents('php://input');
  if (!verifyWabox(getenv('WABOX_WEBHOOK_SECRET'), $_SERVER['HTTP_X_WABOX_SIGNATURE'] ?? '', $raw)) {
      http_response_code(401);
      exit;
  }
  http_response_code(200);
  $event = json_decode($raw, true);
  ```
</CodeGroup>

## Rotação do segredo

`POST /webhooks/secret` gera um segredo novo e devolve o valor. Entregas **já enfileiradas** continuam assinadas com o segredo anterior até esgotarem as tentativas; aceite os dois por algumas horas durante a troca. Hoje o header traz um único `v1=`; escreva o verificador aceitando vários, separados por vírgula, para não quebrar se isso mudar.

## Quando não há assinatura

Toda instância nasce com um segredo. Se `GET /webhooks` devolver `secret: null` (instâncias antigas), as entregas chegam **sem** o header: gere um segredo com `POST /webhooks/secret` e passe a exigir a assinatura no seu endpoint.

## Outras defesas

* Use HTTPS na URL do webhook. HTTP é aceito para testes, mas expõe o conteúdo das mensagens no caminho.
* Trate `event_id` como chave de idempotência: entregas podem repetir.
* Não confie em `instance_id` do corpo para autorizar: use a assinatura da instância correspondente.
