Skip to content

Erros ​

Erros usam o mesmo envelope das respostas de sucesso, com success: false:

json
{
  "success": false,
  "error": {
    "code": "DUPLICATE_INVITE",
    "message": "This contact already has an invite for this form",
    "details": null
  }
}

Programe contra o code

error.code é estável e faz parte do contrato. error.message é texto para humano e pode mudar a qualquer momento — inclusive de idioma. Nunca compare mensagens.

error.details aparece quando há contexto útil a mais: os erros por campo numa validação, o consumo atual numa quota estourada.

Autenticação ​

CódigoHTTPO que fazer
UNAUTHORIZED401Chave ausente, inválida, revogada ou vencida. Não repita — verifique a configuração
FORBIDDEN403A chave é válida mas não tem o escopo da rota. Confira GET /v1/me

Ver Autenticação para distinguir os casos de 401.

Requisição ​

CódigoHTTPO que fazer
VALIDATION_ERROR400Corpo inválido. details traz os erros por campo
INVALID_CURSOR400O cursor não é legível. Recomece a varredura sem cursor
PAGE_NOT_SUPPORTED400Você mandou ?page= numa rota de cursor. Ver Paginação
NOT_FOUND404O recurso não existe, é de outra empresa ou foi excluído
PAYLOAD_TOO_LARGE413Corpo grande demais

NOT_FOUND não distingue "não existe" de "não é seu"

Recurso inexistente, de outra empresa e excluído dão a mesma resposta. Isso é deliberado — distinguir permitiria descobrir ids válidos de terceiros por tentativa e erro.

Se você tem certeza de que o id existe, verifique se a chave pertence à empresa certa (GET /v1/me).

Relatórios ​

CódigoHTTPO que fazer
ANONYMITY_SUPPRESSED409A instituição definiu um piso de anonimato e esta pesquisa tem menos possíveis autores (pessoas convidadas ou alunos da turma) do que ele. Não há o que repetir: os números só existem num recorte maior

A promessa de anonimato é da instituição para quem responde, e o piso é o que a sustenta: com poucos possíveis autores, um resultado agregado ainda aponta para quem respondeu. details traz o piso vigente (floor) e a base medida (authors).

Convites ​

CódigoHTTPO que fazer
DUPLICATE_INVITE409Esse contato já tem convite neste formulário. Busque o existente em GET /forms/{id}/invites e reuse a url
ALREADY_RESPONDED409O convite já virou resposta; revogar não teria efeito
QUOTA_EXCEEDED403A quota mensal de envio da empresa acabou. details traz o consumo
CHANNEL_NOT_CONFIGURED403O canal WhatsApp não está configurado para esta empresa
TEMPLATE_REQUIRED400O envio por WhatsApp Cloud API exige um template aprovado

DUPLICATE_INVITE é o mais comum numa integração nova, e quase sempre é comportamento esperado, não bug: a regra é um contato, um convite por formulário, para a mesma pessoa não receber dois links e as duas respostas contarem.

js
// Padrão idempotente: cria, e se já existe, recupera
async function garantirConvite(formId, contato) {
  const res = await api(`/forms/${formId}/invites`, { method: 'POST', body: contato });
  if (res.status === 409) {
    const { data } = await api(`/forms/${formId}/invites?limit=100`).then((r) => r.json());
    return data.find((i) => i.email === contato.email || i.phone === contato.phone);
  }
  return (await res.json()).data;
}

Limite de requisições ​

CódigoHTTPO que fazer
RATE_LIMITED429Espere o que diz o header retry-after (segundos) e repita

É o único erro em que repetir é a resposta certa — desde que você respeite o retry-after.

Servidor ​

CódigoHTTPO que fazer
INTERNAL_ERROR500Falha nossa. Repita com backoff; se persistir, fale com o suporte

Como tratar ​

Um esqueleto que cobre os casos acima:

js
async function chamar(caminho, opts = {}, tentativa = 0) {
  const res = await fetch(`https://api.edusati.com/v1${caminho}`, {
    ...opts,
    headers: {
      Authorization: `Bearer ${process.env.EDUSATI_KEY}`,
      'content-type': 'application/json',
      ...opts.headers,
    },
  });

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

  const { error } = await res.json();

  switch (error.code) {
    // Repetir resolve
    case 'RATE_LIMITED': {
      const espera = Number(res.headers.get('retry-after') ?? 60);
      await new Promise((r) => setTimeout(r, espera * 1000));
      return chamar(caminho, opts, tentativa);       // 429 não conta como tentativa
    }
    case 'INTERNAL_ERROR':
      if (tentativa >= 3) throw new Error('Edusati indisponível');
      await new Promise((r) => setTimeout(r, 2 ** tentativa * 1000));
      return chamar(caminho, opts, tentativa + 1);

    // Repetir nunca resolve — pare e avise alguém
    case 'UNAUTHORIZED':
    case 'FORBIDDEN':
    case 'VALIDATION_ERROR':
    case 'NOT_FOUND':
      throw new Error(`${error.code}: ${error.message}`);

    // Esperado no fluxo — trate onde faz sentido
    default:
      throw Object.assign(new Error(error.message), { code: error.code, details: error.details });
  }
}

A distinção que mais importa: 401 e 403 não são transitórios. Uma integração que repete indefinidamente com chave revogada só gasta o seu limite e esconde o problema real.

Sucesso que não é sucesso ​

Dois casos respondem 200 sem ter feito o que você esperava. Vale checar explicitamente:

Envio de convites — queued: 0 com skipped[] cheio significa que ninguém recebeu:

js
const { queued, skipped } = await enviar(...);
if (queued === 0) console.warn('Nada enviado:', skipped.map((s) => s.reason));

Estatísticas — truncated: true significa que a varredura bateu no teto e os números são parciais. Recorte o período com from/to para obter um resultado completo.

Documentação da API pública do Edusati