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

# Erros

> Formato de erro da API, códigos de recusa e o que fazer em cada um.

A chamada foi recusada (4xx/5xx na hora)? Está aqui. A chamada foi aceita (`202`) e a mensagem falhou depois (`FAILED`)? Veja [Erros de envio](/erros-de-envio).

## Formato do erro

```json theme={null}
{
  "error": {
    "code": "PLAN_FEATURE_LOCKED",
    "message": "Essa feature está liberada a partir do plano VOO.",
    "details": {
      "feature": "CONTACTS",
      "currentPlan": "ESSENTIAL",
      "upgradeTo": "VOO"
    }
  }
}
```

| Campo | Tipo | Descrição |
| - | - | - |
| `error.code` | string | Código estável. Use em `if`/`switch`. |
| `error.message` | string | Texto em português para exibir ou logar. Pode mudar. |
| `error.details` | object | Contexto do código (limites, plano, campo). Sempre presente, às vezes vazio. |

Os identificadores de plano em `currentPlan` e `upgradeTo` são `VOO` (Voo) e `VOO_ASSISTIDO` (Primeira Classe), além de `EMBARQUE` e `ESSENTIAL` em contas de planos anteriores. Contas antigas podem trazer `FREE`, `POC`, `DECOLAGEM`, `VOO_SOLO`, `PRO` ou `ENTERPRISE`.

## Respostas fora do formato

Trate primeiro pelo status HTTP. Só leia `error.code` quando `error` for um objeto.

| Situação | Corpo | O que fazer |
| - | - | - |
| Falha de autenticação: `401`, `403` e `503` do filtro de chave de API | JSON padrão do Spring: `{ "timestamp", "status", "error", "path" }`. Aqui `error` é uma string. | Decida pelo status. Veja [Autenticação](/api-reference/authentication#erros-de-autenticação). |
| `GET /v1/messages/{id}`: `404` e `403` (mensagem de outro usuário) | Vazio. | Confira o `id` e a chave usada. |
| Alguns `404` de conversas, teste A/B e smart link | Vazio. | Confira o id na URL. |
| Algumas validações de conversas, upload de mídia e histórico | `{ "error": "mensagem" }`, com `error` como string. | Leia a string e corrija a chamada. |

## Autenticação e permissão

| HTTP | Código | Quando | O que fazer |
| - | - | - | - |
| `401` | sem `code` | Chave inexistente, revogada, expirada, que não começa com `ara_live_`, ou IP fora da allowlist. | Use uma chave válida. Confira expiração e allowlist no painel. |
| `403` | sem `code` | Header `Authorization` ausente, Bearer que não começa com `ara_`, chave sem permissão para a rota, ou rota que não aceita chave de API. | Envie o header. Confira as [permissões](/api-reference/authentication#permissões) da chave. |
| `503` | sem `code` | Autenticação indisponível no momento. Traz `Retry-After: 5`. | Espere e repita. |
| `403` | `RESOURCE_FORBIDDEN` | Recurso de outra organização, ou operação que só o painel faz (ex.: gerir chaves). | Confira o id. Faça a operação pelo painel. |
| `403` | `FORBIDDEN` | Acesso negado por regra de segurança. | Leia `message`. Não repita sem corrigir. |
| `401` | `API_KEY_REQUIRED` | Rota que só aceita chave de API foi chamada com sessão de usuário. | Chame com chave de API. |
| `403` | `VIEWER_READ_ONLY` | A chave pertence a um usuário com papel Leitor, que só acessa rotas de acompanhamento. | Use a chave de um usuário com permissão de escrita. |

## Plano, conta e carteira

| HTTP | Código | `details` | Quando | O que fazer |
| - | - | - | - | - |
| `403` | `ACCOUNT_NOT_ACTIVATED` | `reason`, `blockedAction` | `reason` = `FIRST_SUBSCRIPTION_REQUIRED`: a organização ainda não assinou um plano e tentou enviar. | Assine um plano em [Faturamento](https://app.ararahq.com/dashboard/faturamento). |
| `403` | `ACCOUNT_NOT_ACTIVATED` | `currentStatus` | O usuário está suspenso, rejeitado ou revogado. | Fale com o suporte. |
| `403` | `PLAN_FEATURE_LOCKED` | `feature`, `currentPlan`, `upgradeTo` | Funcionalidade fora do plano. | Mude para o plano em `upgradeTo`. |
| `403` | `PLAN_LIMIT_REACHED` | `feature`, `current`, `max`, `currentPlan`, `upgradeTo` | Teto do plano (mensagens no mês, templates, automações). | Mude de plano ou espere a virada do período. |
| `402` | `PLAN_REQUIRED` | `feature` | Exige assinatura paga. | Assine um plano. |
| `402` | `INSUFFICIENT_FUNDS` | `diagnostic` | Saldo insuficiente. | Recarregue a carteira ou ligue a recarga automática. |
| `403` | `ACCOUNT_BANNED` | vazio | Conta banida. | Fale com o suporte. |
| `403` | `ORGANIZATION_SUSPENDED` | vazio | Organização suspensa (decisão administrativa ou taxa de falha alta). | Pare os envios e fale com o suporte. |
| `403` | `NO_DEDICATED_NUMBER` | vazio | A organização não tem número conectado, ou a operação exige número dedicado. | Conecte um número em [Números](https://app.ararahq.com/dashboard/numeros). |
| `403` | `IP_BLOCKED` | vazio | IP bloqueado. | Fale com o suporte. |
| `502` | `PAYMENT_PROVIDER_UNAVAILABLE` | vazio | Provedor de pagamento fora do ar. | Tente de novo em alguns minutos. |

Os planos atuais não têm limite de templates. Contas no plano Free legado têm teto de 3 templates aprovados e recebem `403 PLAN_LIMIT_REACHED` com `details.feature` = `TEMPLATES`.

## Envio

Recusas antes do envio, em `POST /v1/messages`, `POST /v1/messages/batch` e campanhas:

| HTTP | Código | `details` | Quando | O que fazer |
| - | - | - | - | - |
| `422` | `INVALID_RECIPIENT` | `receiver`, `reason` | Telefone inválido. | Use `+5511999999999`, com código do país e DDD. |
| `422` | `RECIPIENT_OPTED_OUT` | `receiver` | O destinatário pediu para não receber. | Remova o contato da lista. Não reenvie. |
| `422` | `CONVERSATION_WINDOW_CLOSED` | `receiver` | Conteúdo livre com a janela de 24h fechada. | Envie um template. Veja [Janela de 24h](/concepts/24h-window). |
| `422` | `TEMPLATE_PAUSED` | `templateName`, `providerStatus` | Template pausado pela Meta. | Pare a campanha e revise o conteúdo. |
| `422` | `TEMPLATE_NOT_SENDABLE` | `templateName`, `providerStatus` | Template pendente, reprovado ou desativado. | Aguarde a aprovação ou corrija o template. |
| `422` | `TEMPLATE_VARIABLES_MISMATCH` | `templateName`, `expected`, `received` | Quantidade de variáveis diferente do template. | Envie `expected` valores em `templateVariables`. |
| `422` | `SENDER_NOT_ALLOWED` | `requestedSender`, `reason` | `sender` não pertence à organização ou não pode ser usado. | Use um número listado em `GET /v1/organizations/me/numbers`. |
| `422` | `UNPROCESSABLE_ENTITY` | vazio | Regra de negócio sem código próprio. Exemplos: telefone inválido ou saldo insuficiente em `/v1/messages/batch`. | Leia `message`. |
| `403` | `NUMBER_BLOCKED` | vazio | O destinatário falhou repetidamente (inválido ou sem WhatsApp). | Remova o contato da base. |
| `403` | `FRAUD_SUSPECTED` | `reason` | Padrão de envio com sinal de abuso. | Pare e fale com o suporte. |
| `409` | `CONFLICT` | vazio | Template não encontrado na organização. | Confira `templateName`. |
| `409` | `IDEMPOTENCY_CONFLICT` | vazio | `Idempotency-Key` reutilizada com outro telefone ou outras variáveis, só nas rotas de envio por webhook. `POST /v1/messages` não compara o corpo e devolve a mensagem original. | Use uma chave por intenção de envio. |
| `409` | `PROVIDER_NOT_RESOLVED` | `sender` | O número ainda não está pronto para enviar. | Reconecte em [Números](https://app.ararahq.com/dashboard/numeros). |
| `429` | `SEND_RATE_LIMITED` | `scope`, `limit` | Teto de envio (inclui o limite diário da chave). Traz `Retry-After`. | Espere o `Retry-After` e repita. |
| `429` | `BATCH_BUSY` | vazio | Há um lote em processamento. Traz `Retry-After`. | Espere o `Retry-After` e repita. |
| `429` | `MARKETING_FREQUENCY_EXCEEDED` | `receiver`, `limit`, `windowHours` | O contato já recebeu marketing demais na janela. Não traz `Retry-After`. | Não repita. Tire o contato desta campanha. |
| `503` | `SEND_TEMPORARILY_UNAVAILABLE` | vazio | Envio indisponível no momento. Traz `Retry-After`. | Espere o `Retry-After` e repita com a mesma `Idempotency-Key`. |
| `400` | `CAMPAIGN_SCHEDULE_TOO_SOON`, `CAMPAIGN_SCHEDULE_TOO_FAR` | `scheduledAt` | Agendamento de campanha fora de 5 minutos a 30 dias. | Ajuste `scheduledAt`. |

Quando a recusa vem de uma falha reconhecida do provedor, `error.code` é um código `ararahq-*` e `error.details.diagnostic` traz `whatHappened` e `howToAct`. A lista desses códigos está em [Erros de envio](/erros-de-envio#códigos-ararahq).

## Recursos

| HTTP | Código | Quando | O que fazer |
| - | - | - | - |
| `404` | `TEMPLATE_NOT_FOUND` | Template não existe. | Confira o id. |
| `404` | `CAMPAIGN_NOT_FOUND` | Campanha não existe. | Confira o id. |
| `404` | `LINK_NOT_FOUND` | Link não existe. | Confira o id. |
| `404` | `NOT_FOUND` | Rota inexistente. | Confira o caminho e o método. |
| `409` | `CONFLICT` | Em `POST /v1/templates`: nome duplicado na mesma conta WhatsApp Business, ou conteúdo bloqueado pela moderação. | Troque o nome ou revise o conteúdo. |
| `400` | `INVALID_PHONE` | Telefone inválido em opt-out ou LGPD. | Use `+5511999999999`. |
| `400` | `INVALID_AB_TEST`, `INVALID_WINNER` | Configuração de A/B inválida. | Leia `message` e corrija. |
| `400` | `INVALID_VERTICAL` | Vertical do perfil comercial fora da lista da Meta. | Use um valor da lista. |
| `422` | `GUPSHUP_APP_NOT_READY` | Operação de template sem número conectado. | Conecte um número antes. |

## Genéricos

| HTTP | Código | Quando | O que fazer |
| - | - | - | - |
| `400` | `VALIDATION_ERROR` | Campo obrigatório ausente ou regra de validação do corpo. `message` traz a primeira violação. | Corrija o campo citado. |
| `400` | `INVALID_JSON` | JSON malformado, encoding errado ou campo com tipo divergente. | Valide o JSON e os tipos. |
| `400` | `INVALID_PATH_PARAM` | Id na URL em formato errado (ex.: nome no lugar de UUID). `details.param` diz qual. | Corrija o id. |
| `400` | `INVALID_IDEMPOTENCY_KEY` | `Idempotency-Key` vazia, acima de 255 caracteres ou com caractere de controle. | Corrija a chave. |
| `405` | `METHOD_NOT_ALLOWED` | Método HTTP errado na rota. | Confira o método. |
| `409` | `CONFLICT` | Conflito de estado. | Leia `message`. |
| `409` | `CONCURRENT_MODIFICATION` | Operação simultânea no mesmo recurso. | Tente de novo. |
| `422` | `UNPROCESSABLE_ENTITY` | Regra de negócio sem código próprio. | Leia `message`. |
| `429` | `RATE_LIMIT_EXCEEDED` | Rate limit geral. Traz `Retry-After` e `X-RateLimit-Remaining`. | Espere o `Retry-After` e repita. |
| `500` | `INTERNAL_SERVER_ERROR` | Erro interno. | Repita com a mesma `Idempotency-Key`. Se persistir, abra chamado. |
| `500` | `MISSING_RATE_CARD` | Falha ao precificar. | Tente de novo. |

<Note>
  Hoje, header ou parâmetro obrigatório ausente (`Idempotency-Key` em campanha, `count` na estimativa, `batchId`) responde `500 INTERNAL_SERVER_ERROR`, não `400`.
</Note>

## Recuperação

| HTTP | Código | Quando | O que fazer |
| - | - | - | - |
| `404` | `RECOVERY_ENDPOINT_NOT_FOUND` | Endpoint ou ingestão não encontrado. | Confira a URL do endpoint. |
| `403` | `RECOVERY_ENDPOINT_INACTIVE` | Endpoint de conversão desativado. O endpoint de recuperação desligado não responde isso: ele devolve `REJECTED` com `NO_ACTIVE_AUTOMATION` quando nenhuma automação ativa escuta o evento. | Reative o endpoint no painel. |
| `400` | `RECOVERY_EVENT_INVALID` | Tipo de evento ou período inválido. | Corrija o evento. |
| `401` | `RECOVERY_HMAC_INVALID` | Assinatura HMAC não confere. | Confira o segredo e o corpo assinado. |
| `429` | `RECOVERY_RATE_LIMITED` | Excesso de eventos. Traz `Retry-After: 60`. | Espere e repita. |
| `404` | `RECOVERY_CONFIG_NOT_FOUND` | Sem configuração para o evento. | Configure a recuperação no painel. |
| `412` | `RECOVERY_TEMPLATE_MISSING` | O template vinculado foi removido. | Vincule outro template. |
| `402` | `RECOVERY_FREE_CAP_REACHED` | Cota gratuita de recuperação atingida. | Assine um plano. |

## Quando repetir a chamada

| Resposta | Repetir? |
| - | - |
| `429` com `Retry-After` | Sim, depois do tempo indicado. |
| `429 MARKETING_FREQUENCY_EXCEEDED` | Não. Não traz `Retry-After` e esperar alguns segundos não muda o resultado. |
| `500`, `502`, `503` | Sim, com espera crescente. Respeite o `Retry-After` quando vier. |
| Outros `4xx` | Não. Corrija a causa. |

Em toda repetição de envio, mande a mesma `Idempotency-Key` da primeira tentativa. Sem ela a mensagem pode sair duas vezes.

Depois do `202`, a Arara repete sozinha as falhas temporárias do provedor. Você não precisa reenviar nesses casos.

## Tratamento no cliente

Arquivo completo para Node 18 ou superior, sem dependências. Ele trata corpo vazio, corpo fora do formato e `Retry-After`.

```javascript theme={null}
// arara.js
const API_URL = 'https://api.ararahq.com/v1/messages';
const MAX_ATTEMPTS = 4;

class AraraError extends Error {
  constructor(status, code, message, details) {
    super(message);
    this.name = 'AraraError';
    this.status = status;
    this.code = code; // undefined quando a resposta não segue o formato padrão
    this.details = details;
  }
}

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function readError(res) {
  const text = await res.text();
  let payload = null;
  try {
    payload = text ? JSON.parse(text) : null;
  } catch {
    payload = null;
  }

  const error = payload && payload.error;
  if (error && typeof error === 'object') {
    return new AraraError(res.status, error.code, error.message || `HTTP ${res.status}`, error.details || {});
  }
  const message = typeof error === 'string' ? error : `HTTP ${res.status}`;
  return new AraraError(res.status, undefined, message, {});
}

function shouldRetry(res, error) {
  if (error.code === 'MARKETING_FREQUENCY_EXCEEDED') return false;
  return res.status === 429 || res.status >= 500;
}

function retryDelayMs(res, attempt) {
  const retryAfter = Number(res.headers.get('Retry-After'));
  if (Number.isFinite(retryAfter) && retryAfter > 0) return retryAfter * 1000;
  return 500 * 2 ** attempt;
}

// idempotencyKey: derive de um id do seu sistema, ex.: `pedido-${order.id}-confirmacao`.
async function sendMessage(body, idempotencyKey) {
  for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt += 1) {
    const res = await fetch(API_URL, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.ARARA_API_KEY}`,
        'Content-Type': 'application/json',
        'Idempotency-Key': idempotencyKey,
      },
      body: JSON.stringify(body),
    });

    if (res.ok) return res.json();

    const error = await readError(res);
    const lastAttempt = attempt === MAX_ATTEMPTS - 1;
    if (!shouldRetry(res, error) || lastAttempt) throw error;

    await sleep(retryDelayMs(res, attempt));
  }
  throw new Error('unreachable');
}

module.exports = { sendMessage, AraraError };
```

Quem chama decide o que fazer com cada código:

```javascript theme={null}
// confirmar-pedido.js
const { sendMessage, AraraError } = require('./arara');

async function confirmarPedido(order) {
  try {
    return await sendMessage(
      {
        receiver: order.phone, // +5511999999999
        templateName: 'pedido_confirmado',
        templateVariables: [order.customerName, String(order.id)],
      },
      `pedido-${order.id}-confirmacao`,
    );
  } catch (err) {
    if (!(err instanceof AraraError)) throw err;

    if (err.status === 401) throw new Error('Chave de API inválida, revogada ou expirada');
    if (err.status === 403 && !err.code) throw new Error('Chave de API sem permissão para esta rota');

    switch (err.code) {
      case 'RECIPIENT_OPTED_OUT':
      case 'NUMBER_BLOCKED':
      case 'INVALID_RECIPIENT':
        console.warn(`Remova ${order.phone} da lista: ${err.code}`);
        return null;
      case 'INSUFFICIENT_FUNDS':
      case 'ACCOUNT_NOT_ACTIVATED':
        throw new Error(`Envios parados: ${err.message}`);
      default:
        throw err;
    }
  }
}

module.exports = { confirmarPedido };
```

## Próximos passos

<CardGroup cols={2}>
  <Card title="Erros de envio" icon="triangle-exclamation" href="/erros-de-envio">
    A mensagem foi aceita e falhou depois: causa e solução.
  </Card>

  <Card title="Eventos de webhook" icon="bell" href="/webhooks/events">
    Receba a falha de envio em tempo real.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.