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

# Integração HTTP genérica

> Receba webhooks, verifique a assinatura, envie mensagens e trate os erros da API em qualquer linguagem.

Esta página cobre o ciclo completo sem depender de ferramenta: receber o webhook com o corpo cru, verificar `X-Wabox-Signature`, responder `200` rápido, enviar mensagens, tratar `429`/`402`/`409` e acompanhar o resultado no webhook `delivery`.

<Note>
  Base: `https://api.wabox.me/instances/{instance_id}/token/{token}`. O token vai na URL — trate-o como senha ([Autenticação](/security/authentication)). O header `Client-Token` é obrigatório apenas se estiver ativado no workspace ([Client-Token](/security/client-token)).
</Note>

## Receber webhooks

Toda entrega é um `POST` com `Content-Type: application/json` e os headers `User-Agent: Wabox-Webhook/1.0`, `X-Wabox-Event` (tipo), `X-Wabox-Event-Id`, `X-Wabox-Instance-Id` e `X-Wabox-Signature`. O Wabox espera `2xx` em até 10 s; senão, reenvia em 10 s, 1 min, 10 min, 1 h e 6 h, com o mesmo `event_id`. Detalhes em [Visão geral dos webhooks](/webhooks/overview).

Três regras para o endpoint:

1. **Leia o corpo cru** antes de qualquer parser de JSON — a assinatura é calculada sobre os bytes originais.
2. **Verifique a assinatura** e a janela de tempo antes de confiar no conteúdo.
3. **Responda `200` imediatamente** e processe depois (fila, worker, `setImmediate`, tarefa em background). Não chame a API do Wabox nem serviços lentos dentro do handler.

A assinatura tem o formato `t=<unix>,v1=<hex>`, com `v1 = HMAC-SHA256(secret, t + "." + corpo_cru)`. O `secret` (`whsec_…`) é por instância e aparece em `GET /webhooks` e no dashboard. Rejeite `t` fora de uma janela de \~5 minutos. Mais em [Assinatura de webhooks](/security/webhook-signature).

<CodeGroup>
  ```javascript Node (Express) theme={"system"}
  import express from "express";
  import crypto from "node:crypto";

  const app = express();
  const SECRET = process.env.WABOX_WEBHOOK_SECRET; // whsec_...

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

  // express.raw mantém o corpo como Buffer, sem parse.
  app.post("/webhooks/wabox", express.raw({ type: "application/json" }), (req, res) => {
    const raw = req.body.toString("utf8");
    if (!verify(raw, req.get("X-Wabox-Signature"))) return res.status(401).end();

    res.status(200).end(); // responde antes de processar

    const event = JSON.parse(raw);
    setImmediate(() => handleEvent(event)); // ou publique em uma fila
  });

  async function handleEvent(event) {
    switch (event.type) {
      case "received":
        if (event.from_me || !event.text) return;
        // event.phone, event.text.message, event.message_id, event.event_id
        break;
      case "delivery":
        // event.message_id, event.wabox_id, event.error?, event.error_code?
        break;
    }
  }
  ```

  ```python Python (FastAPI) theme={"system"}
  import hmac, hashlib, time, json
  from fastapi import FastAPI, Request, Response, BackgroundTasks
  import os

  app = FastAPI()
  SECRET = os.environ["WABOX_WEBHOOK_SECRET"].encode()  # whsec_...

  def verify(raw: bytes, header: str | None) -> bool:
      parts = dict(p.split("=", 1) for p in (header or "").split(",") if "=" in p)
      t, v1 = parts.get("t"), parts.get("v1")
      if not t or not v1:
          return False
      if abs(time.time() - int(t)) > 300:
          return False
      expected = hmac.new(SECRET, f"{t}.".encode() + raw, hashlib.sha256).hexdigest()
      return hmac.compare_digest(expected, v1)

  @app.post("/webhooks/wabox")
  async def wabox_webhook(request: Request, background: BackgroundTasks):
      raw = await request.body()  # corpo cru, antes de qualquer parse
      if not verify(raw, request.headers.get("x-wabox-signature")):
          return Response(status_code=401)
      event = json.loads(raw)
      background.add_task(handle_event, event)  # processa depois de responder
      return Response(status_code=200)

  def handle_event(event: dict):
      if event["type"] == "received":
          if event.get("from_me") or "text" not in event:
              return
          # event["phone"], event["text"]["message"], event["message_id"]
      elif event["type"] == "delivery":
          # event["message_id"], event.get("error"), event.get("error_code")
          pass
  ```

  ```php PHP puro theme={"system"}
  <?php
  $secret = getenv('WABOX_WEBHOOK_SECRET'); // whsec_...

  $raw = file_get_contents('php://input'); // corpo cru
  $header = $_SERVER['HTTP_X_WABOX_SIGNATURE'] ?? '';

  $parts = [];
  foreach (explode(',', $header) as $p) {
      [$k, $v] = array_pad(explode('=', $p, 2), 2, null);
      $parts[$k] = $v;
  }

  $valid = isset($parts['t'], $parts['v1'])
      && abs(time() - (int) $parts['t']) <= 300
      && hash_equals(hash_hmac('sha256', $parts['t'] . '.' . $raw, $secret), $parts['v1']);

  if (!$valid) {
      http_response_code(401);
      exit;
  }

  // Responde 200 e libera a conexão antes de processar.
  http_response_code(200);
  if (function_exists('fastcgi_finish_request')) {
      fastcgi_finish_request();
  }

  $event = json_decode($raw, true);
  if ($event['type'] === 'received' && empty($event['from_me']) && isset($event['text'])) {
      // $event['phone'], $event['text']['message'], $event['message_id']
  }
  ```
</CodeGroup>

<Tip>
  Guarde os `event_id` processados (chave única no banco ou `SET NX` no Redis com expiração de alguns dias) e descarte repetidos. Reentregas trazem o mesmo `event_id`, e uma resposta ao cliente enviada duas vezes é o erro mais comum de webhook.
</Tip>

## Enviar mensagens

`POST .../send-text` com `phone` (só dígitos, DDI + DDD + número) e `message`. Outros campos, como `reply_to_message_id` e `mentioned`, estão na [API Reference](/api-reference/introduction).

<CodeGroup>
  ```bash curl theme={"system"}
  curl -X POST "https://api.wabox.me/instances/{instance_id}/token/{token}/send-text" \
    -H "Content-Type: application/json" \
    -d '{ "phone": "5511988887777", "message": "Olá! Recebemos seu pedido." }'
  ```

  ```javascript Node (fetch) theme={"system"}
  const BASE = `https://api.wabox.me/instances/${process.env.WABOX_INSTANCE_ID}/token/${process.env.WABOX_TOKEN}`;

  async function sendText(phone, message) {
    const res = await fetch(`${BASE}/send-text`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        // "Client-Token": process.env.WABOX_CLIENT_TOKEN, // só se ativado
      },
      body: JSON.stringify({ phone, message }),
    });
    const body = await res.json();
    if (!res.ok) throw Object.assign(new Error(body.error.message), { status: res.status, code: body.error.code, res });
    return body; // { id, message_id, wabox_id, status: "queued" }
  }
  ```

  ```python Python (requests) theme={"system"}
  import os, requests

  BASE = f"https://api.wabox.me/instances/{os.environ['WABOX_INSTANCE_ID']}/token/{os.environ['WABOX_TOKEN']}"

  def send_text(phone: str, message: str) -> dict:
      r = requests.post(
          f"{BASE}/send-text",
          json={"phone": phone, "message": message},
          # headers={"Client-Token": os.environ["WABOX_CLIENT_TOKEN"]},  # só se ativado
          timeout=15,
      )
      body = r.json()
      if not r.ok:
          raise RuntimeError(f"{r.status_code} {body['error']['code']}: {body['error']['message']}")
      return body  # { id, message_id, wabox_id, status: "queued" }
  ```
</CodeGroup>

A resposta chega na hora, com a mensagem já na fila:

```json theme={"system"}
{ "id": "3EB0A9C6D2F1E4B5A7D0", "message_id": "3EB0A9C6D2F1E4B5A7D0", "wabox_id": "wbx_01J5Q8ZK3M4N5P6Q7R8S9T0M01", "status": "queued" }
```

`message_id` já é o id definitivo da mensagem no WhatsApp (`id` é um alias). Guarde-o: é a chave para cruzar com os webhooks `delivery` e `message_status`.

## Tratar erros

Todo erro vem como `{ "error": { "code": "...", "message": "..." } }`. Os que sua integração precisa tratar de forma diferente:

<Accordion title="429 rate_limited — limite de requisições por instância">
  O limite é por instância (token bucket). A resposta traz `Retry-After` em segundos e os headers `X-RateLimit-Limit` e `X-RateLimit-Remaining`. Espere o tempo indicado e repita a requisição. Não faça retry imediato em loop: cada tentativa consome o mesmo balde.

  Um `429` também pode vir com `code: "queue_full"` quando a fila de saída da instância está no limite — nesse caso, reduza o ritmo de envio em vez de repetir.
</Accordion>

<Accordion title="402 subscription_required — workspace sem assinatura ativa">
  A instância existe, mas o workspace não tem plano ativo (trial encerrado ou pagamento pendente). Repetir não resolve; alerte quem administra a conta. Trate como erro permanente até o plano ser regularizado.
</Accordion>

<Accordion title="409 instance_not_connected — número desconectado">
  A instância não está com o WhatsApp conectado (QR não lido, sessão caiu ou foi deslogada). Rotas imediatas — contatos, grupos, perfil — falham com esse código. O envio de mensagens, por padrão, entra na fila e sai quando o número reconectar; se a instância estiver configurada para recusar envios enquanto desconectada, a resposta é `409` com `code: "queue_disabled_while_disconnected"`.

  Para reagir, consulte `GET .../status` ou assine os webhooks `connected`/`disconnected` ([Conexão](/webhooks/connection)) e pause os envios enquanto o número estiver fora.
</Accordion>

A lista completa de códigos está em [Erros](/api-reference/errors), e a estratégia de retry em [Rate limit e erros](/guides/rate-limits-and-errors).

Um esqueleto de retry que respeita esses três casos:

```javascript theme={"system"}
async function sendWithRetry(phone, message, attempt = 0) {
  try {
    return await sendText(phone, message);
  } catch (err) {
    if (err.status === 429 && attempt < 3) {
      const wait = Number(err.res.headers.get("Retry-After") ?? 1) * 1000;
      await new Promise((r) => setTimeout(r, wait));
      return sendWithRetry(phone, message, attempt + 1);
    }
    if (err.status === 402) throw err;                 // permanente: avise o administrador
    if (err.status === 409) throw err;                 // aguarde o webhook `connected`
    throw err;
  }
}
```

## Acompanhar a entrega

`status: "queued"` não é confirmação de envio. O resultado chega no webhook [delivery](/webhooks/delivery), na `delivery_url` da instância, com o mesmo `message_id` e `wabox_id` da resposta:

```json theme={"system"}
{
  "type": "delivery",
  "event_id": "01J5Q8ZK3M4N5P6Q7R8S9T0V1X",
  "instance_id": "8f2a3c1e-6b7d-4e5f-9a0b-1c2d3e4f5a6b",
  "momment": 1786968420000,
  "wabox_id": "wbx_01J5Q8ZK3M4N5P6Q7R8S9T0M01",
  "message_id": "3EB0A9C6D2F1E4B5A7D0",
  "phone": "5511988887777"
}
```

Sem `error`, a mensagem saiu. Quando o envio falha, o mesmo evento traz `error` (texto) e `error_code` (por exemplo, número sem WhatsApp ou mídia inválida) — persista isso ao lado do `message_id` que você guardou na resposta. Os recibos (enviado, recebido, lido) vêm depois, no webhook [message\_status](/webhooks/message-status), com `ids[]` contendo o mesmo `message_id`.

Enquanto a mensagem ainda não saiu, `GET .../queue` lista a fila da instância e `DELETE .../queue/{wabox_id}` remove um item — veja [Fila e reenvio](/guides/queue-and-retries).
