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

# Erros e limites

> Formato de erro, catálogo completo de códigos e rate limit de 120 req/min.

## Formato de erro

Toda resposta de erro é um objeto **plano** — não há um sub-objeto `details`, os campos extras vêm soltos no mesmo nível de `error` e `code`:

```json theme={null}
{
  "error": "ERR_TOKEN_INVALID",
  "code": "ERR_TOKEN_INVALID"
}
```

Alguns erros trazem campos extras direto no corpo, conforme o caso:

```json theme={null}
{ "error": "Limite de requisições excedido", "code": "ERR_RATE_LIMITED", "retryAfter": 4 }
```

```json theme={null}
{ "error": "Token válido, mas sem o escopo exigido", "code": "ERR_SCOPE_MISSING", "requiredScope": "contacts:write" }
```

```json theme={null}
{ "error": "ERR_TICKET_IN_HUMAN_SERVICE", "code": "ERR_TICKET_IN_HUMAN_SERVICE", "ticketId": 933 }
```

```json theme={null}
{ "error": "ERR_INVALID_DATE_FILTER", "code": "ERR_INVALID_DATE_FILTER", "param": "createdAtAfter" }
```

* `error` — texto pra exibição/log. Pode mudar de uma versão pra outra. Para cerca de 22 códigos ele repete o próprio `code` (como `ERR_TOKEN_INVALID` acima); para os demais é um texto legível em português (como `ERR_RATE_LIMITED` acima). Não dá pra prever qual caso é qual sem consultar o catálogo — por isso a regra abaixo.
* `code` — identificador estável, sempre em maiúsculas com prefixo `ERR_`.
* `retryAfter`, `requiredScope`, `ticketId`, `param` — extras que aparecem só em alguns códigos específicos (429, 403, 409 de flows, `ERR_INVALID_DATE_FILTER`).

<Warning>Não faça match em `error` — nem para saber o tipo do erro, nem por igualdade de texto. Use sempre `code`: é o único campo estável entre versões.</Warning>

## Catálogo de códigos

| Código | HTTP | Quando acontece |
| - | - | - |
| `ERR_TOKEN_MISSING` | 401 | Header `Authorization` ausente. Envie `Authorization: Bearer wtx_live_…`. |
| `ERR_TOKEN_INVALID` | 401 | O token não existe ou está mal formado. Gere um novo na tela API, aba Tokens. |
| `ERR_TOKEN_REVOKED` | 401 | O token foi revogado no painel. Crie outro. |
| `ERR_TOKEN_EXPIRED` | 401 | Passou da validade definida na criação. Crie outro ou rotacione antes de expirar. |
| `ERR_SCOPE_MISSING` | 403 | O token é válido mas não tem o escopo exigido pelo endpoint. Um escopo `x:write` satisfaz `x:read`. |
| `ERR_RATE_LIMITED` | 429 | Mais de 120 requisições por minuto com o mesmo token. Aguarde os segundos de `Retry-After`. |
| `ERR_MISSING_CONTENT` | 400 | O envio precisa de pelo menos um entre `body`, `medias` (multipart) ou `templateData`. |
| `ERR_NUMBER_REQUIRED` | 400 | O campo `number` é obrigatório. |
| `ERR_NUMBER_INVALID` | 400 | O número não tem dígitos válidos após normalização. Use E.164 sem `+` (ex.: 5534999990000). |
| `ERR_CONNECTION_NOT_FOUND` | 404 | `whatsappId`/`whatsappName` não corresponde a uma conexão desta conta. |
| `ERR_NO_CONNECTION_AVAILABLE` | 404 | A conta não tem nenhuma conexão `whatsapp`/`waba`. Informe `whatsappId`/`whatsappName` ou conecte um canal no painel. |
| `ERR_UNSUPPORTED_CHANNEL` | 400 no envio (`POST /v1/messages/send`), 422 no disparo de flow (`POST /v1/flows/trigger`) | A conexão escolhida não é `whatsapp`/`waba` — só esses dois canais enviam pela API. |
| `ERR_NO_TICKET_FOUND` | 404 | Conversa (ticket) inexistente ou de outra conta. |
| `ERR_MESSAGE_NOT_FOUND` | 404 | Mensagem inexistente ou de outra conta. |
| `ERR_MISSING_NUMBER` | 400 | `number` é obrigatório no upsert de contato. |
| `ERR_NUMBER_NOT_ON_WHATSAPP` | 422 | Com `validateNumber=true`, o número não existe no WhatsApp. |
| `ERR_WHATSAPP_NOT_FOUND` | 404 | `whatsappId` informado não é uma conexão desta conta. |
| `ERR_NO_CONTACT_FOUND` | 404 | Contato inexistente ou de outra conta. |
| `ERR_NUMBER_IMMUTABLE` | 400 | `PUT /v1/contacts/{contactId}` não aceita `number`. Corrija o número pelo painel. |
| `ERR_INVALID_TAG_IDS` | 400 | `tagIds` ausente, não é array, ou contém tag de outra conta. |
| `ERR_INVALID_DATE_FILTER` | 400 | Filtro de data sem offset de fuso (use `2026-09-01T00:00:00-03:00`). Vem com `param` indicando qual parâmetro falhou. |
| `ERR_NO_CARD_FOUND` | 404 | Card inexistente ou de outra conta. |
| `ERR_CONTACT_IDENTIFIER_REQUIRED` | 400 | Informe exatamente um entre `contactId` e `phone` ao criar card. |
| `ERR_PIPELINE_NOT_FOUND` | 404 | `pipelineId` inexistente ou de outra conta. |
| `ERR_STAGE_REQUIRED` | 400 | `stageId` é obrigatório ao mover card. |
| `ERR_STAGE_NOT_FOUND` | 404 | `stageId` inexistente ou de outra conta. |
| `ERR_STAGE_NOT_IN_PIPELINE` | 422 | A etapa existe, mas pertence a outro pipeline. Mover entre pipelines não é permitido. |
| `ERR_SOURCE_NOT_FOUND` | 404 | `sourceId` (origem do card) inexistente ou de outra conta. |
| `ERR_FLOW_ID_AND_WHATSAPP_ID_REQUIRED` | 400 | `flowId` e `whatsappId` são obrigatórios. |
| `ERR_VARIABLES_MUST_BE_OBJECT` | 400 | `variables` precisa ser um objeto JSON (chave → valor). |
| `ERR_FLOW_NOT_FOUND` | 404 | Flow inexistente ou de outra conta. |
| `ERR_FLOW_NOT_EXECUTABLE` | 422 | Flow inativo, despublicado ou sem nós. Publique-o no FlowBuilder. |
| `ERR_TICKET_IN_HUMAN_SERVICE` | 409 | O contato está em atendimento humano ativo; o flow não é disparado pra não atropelar o atendente. |

## Rate limit

Cada token pode fazer até **120 requisições por minuto**. Toda resposta — inclusive as de erro — traz os headers:

* `X-RateLimit-Limit` — limite por minuto (120).
* `X-RateLimit-Remaining` — quantas ainda restam na janela atual.
* `X-RateLimit-Reset` — epoch (segundos) de quando a janela reseta.

Ao exceder, a API responde `429` com `code: ERR_RATE_LIMITED` e o header `Retry-After` (segundos até poder tentar de novo).

### Backoff em Node

```js theme={null}
async function callApi(url, options, attempt = 1) {
  const r = await fetch(url, options);
  if (r.status === 429 && attempt < 3) {
    const retryAfter = Number(r.headers.get("Retry-After") ?? "1");
    await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
    return callApi(url, options, attempt + 1);
  }
  return r;
}
```

<Note>Limite as tentativas (aqui, 3) em vez de retentar indefinidamente — se o 429 persistir depois disso, o problema provavelmente não é uma rajada passageira, e vale investigar o padrão de chamadas da sua integração antes de continuar insistindo.</Note>

<Note>Se sua integração faz muitas chamadas em rajada, prefira paginar com calma ou distribuir as chamadas no tempo em vez de confiar só no retry — o limite é por token, então rajadas grandes derrubam o `Remaining` rápido.</Note>
