LunaDocumentaçãoEntrar

Erros

Todo erro da API devolve um corpo JSON com error e message. O error é um código estável, pensado para o seu código decidir o que fazer. O message é texto para uma pessoa ler, em português.

O formato do erro

400
{
  "error": "invalid_request",
  "message": "Requisição inválida.",
  "campos": [{ "campo": "text.body" }]
}
  • Decida pelo error e pelo status, nunca pelo texto do message.
  • Alguns erros acrescentam campos que dizem o que fazer: campos, fechadaEm, acaoRecomendada, estado, faltando, tentativasRestantes, janelaVira, reenfileiradoEm e credentialStatus. Eles estão na tabela abaixo.
  • Nenhum erro repete de volta o valor que você mandou. Num 400, o que sai é o caminho do campo recusado, e nunca o conteúdo.
  • 404 vale também para um recurso que existe e não é seu. A API não confirma a existência de recurso de outro cliente, nem pelo status.

Os códigos principais

Status`error`O que quer dizer, e o que fazer
400invalid_requestO corpo ou a query não bate com o contrato. O campo campos lista o caminho de cada campo recusado. Corrija e envie de novo. Repetir o mesmo pedido repete a recusa.
401unauthorizedA chave está ausente, malformada, inexistente, expirada ou revogada. O corpo é sempre o mesmo, sem dizer qual. Confira o cabeçalho Authorization: Bearer hlsn_... e se a chave não foi revogada.
403forbiddenA chave não tem o escopo da rota. Crie uma chave com o escopo certo, e dê a cada chave só o que ela precisa. A resposta fala do verbo, e nunca do recurso.
403connection_not_releasedA conta ainda não pode abrir conexão de número. O campo faltando lista as condições (email_confirmado, conexao_liberada). Confirme o e-mail da conta.
404not_foundO recurso não existe, ou não é seu. As duas respostas são idênticas, byte a byte. Confira o identificador, e se ele é o da Luna ou o da Meta para esta rota.
409window_closedA janela de 24 horas está fechada. fechadaEm diz quando fechou, e acaoRecomendada diz o caminho: enviar um template. Não há Retry-After, e repetir não adianta. Veja Enviar mensagens.
409media_not_readyA mídia ainda não foi obtida. O estado diz onde ela está: pendente e falhou voltam sozinhos, expirado é definitivo. Tente de novo mais tarde.
409no_webhook_endpointVocê pediu o reenvio de um webhook e não tem endpoint cadastrado. Cadastre um em POST /v1/webhooks/endpoints e peça de novo.
409already_requeuedEste item da fila de rejeitados já foi re-enfileirado. reenfileiradoEm diz quando. Nada a fazer.
409pin_not_applicableO número não está aguardando o PIN. O state diz em que ponto ele está. Veja Conectar um número.
409register_budget_reservedAs tentativas de registro que a Meta concede ao número em 72 horas estão perto do fim. tentativasRestantes e janelaVira dizem quantas sobram e quando a janela vira. Confirme o PIN com o negócio final antes de tentar de novo.
409reconnect_not_applicableA autorização deste número não admite reconexão. credentialStatus diz o estado.
413file_too_largeO arquivo passa do teto de upload, que é o mesmo para todos os clientes. A recusa acontece antes de qualquer ida à Meta. Mande um arquivo menor.
429rate_limitedVocê passou do limite de requisições da chave. Espere o tempo do Retry-After. Veja a próxima seção.
429queue_fullA fila de envio deste número está cheia. Espere o tempo do Retry-After e tente de novo.
429number_rate_limitedEste número está no limite de chamadas por segundo. Espere o tempo do Retry-After.
429management_busyAs operações de gestão da conexão (criar e editar template, por exemplo) atingiram o limite momentâneo. Espere o tempo do Retry-After.
500internalFalha inesperada do lado da Luna. O corpo não traz detalhe, e o detalhe fica no log interno. Tente de novo, e se persistir, peça ajuda com o horário e o identificador da mensagem.
503unavailableA plataforma não pode atender agora. Tente de novo com espera crescente. Este mesmo corpo também aparece, de propósito, quando você tenta conectar uma conta ou número que já pertence a outro cliente: a API não confirma a existência de recurso alheio.

Os quatro códigos 429 têm origem diferente, e a resposta traz o Retry-After, em segundos. Se ele faltar, espere com recuo crescente, como o código abaixo faz. O 429 da Luna nunca é o limite de ritmo da Meta devolvido a você: esse limite é resolvido por fila, e por isso o envio responde 202.

O limite de requisições

Cada chave de API pode fazer 600 requisições por minuto. O limite é técnico e idêntico para todos os clientes. Nenhum limite desta plataforma varia por preço. Requisição sem chave é contada pelo endereço de origem.

CabeçalhoO que traz
X-RateLimit-LimitO teto da janela.
X-RateLimit-RemainingQuantas requisições ainda cabem na janela.
X-RateLimit-ResetEm quantos segundos a janela recomeça.
Retry-AfterEm respostas 429: quantos segundos esperar antes de tentar de novo.
  • Os três X-RateLimit-* acompanham as respostas bem-sucedidas. Leia X-RateLimit-Remaining e desacelere antes de chegar a zero.
  • Num 429, espere o Retry-After. Retentar na hora só é recusado de novo.
  • O cadastro de conta tem um teto próprio: 5 por hora por endereço de origem.

Retentar com segurança

Repita só 429 e 503, e espere o Retry-After. Para POST /v1/messages, mande sempre o Idempotency-Key, e o mesmo em todas as tentativas: sem ele, uma retentativa depois de um tempo esgotado pode enviar a mensagem duas vezes. Não repita 400, 401, 403, 404 nem 409: a resposta não muda.

const BASE = 'https://api.lunahia.com.br';

async function lunaFetch(path, options = {}, attempts = 5) {
  for (let attempt = 1; ; attempt++) {
    const res = await fetch(`${BASE}${path}`, {
      ...options,
      headers: {
        Authorization: `Bearer ${process.env.LUNA_API_KEY}`,
        'Content-Type': 'application/json',
        ...options.headers,
      },
    });

    const retryable = res.status === 429 || res.status === 503;
    if (!retryable || attempt === attempts) return res;

    // Retry-After is in seconds. Without it, back off 2, 4, 8... seconds.
    const wait = Number(res.headers.get('retry-after')) || 2 ** attempt;
    await new Promise((resolve) => setTimeout(resolve, wait * 1000));
  }
}

// One Idempotency-Key for all attempts, so a retry never sends twice.
const res = await lunaFetch('/v1/messages', {
  method: 'POST',
  headers: { 'Idempotency-Key': crypto.randomUUID() },
  body: JSON.stringify({
    from: 'YOUR_PHONE_NUMBER_ID',
    to: '5511999990000',
    text: { body: 'Olá!' },
  }),
});

const body = await res.json();
if (!res.ok) {
  console.error(res.status, body.error, body.message);
} else {
  console.log(res.status, body);
}
import os
import time
import uuid

import requests

BASE = "https://api.lunahia.com.br"


def luna_request(method, path, attempts=5, **kwargs):
    headers = {
        "Authorization": f"Bearer {os.environ['LUNA_API_KEY']}",
        **kwargs.pop("headers", {}),
    }
    for attempt in range(1, attempts + 1):
        res = requests.request(
            method, BASE + path, headers=headers, timeout=10, **kwargs
        )
        if res.status_code not in (429, 503) or attempt == attempts:
            return res
        # Retry-After is in seconds. Without it, back off 2, 4, 8... seconds.
        time.sleep(int(res.headers.get("Retry-After", 2**attempt)))


# One Idempotency-Key for all attempts, so a retry never sends twice.
res = luna_request(
    "POST",
    "/v1/messages",
    headers={"Idempotency-Key": str(uuid.uuid4())},
    json={
        "from": "YOUR_PHONE_NUMBER_ID",
        "to": "5511999990000",
        "text": {"body": "Olá!"},
    },
)

body = res.json()
if not res.ok:
    print(res.status_code, body["error"], body["message"])
else:
    print(res.status_code, body)

Quando a mensagem é aceita e depois falha

O 202 do envio quer dizer que a Luna aceitou a mensagem, e não que ela foi entregue. A falha pode vir depois, da Meta. Você a vê de duas formas: no webhook, com um message.status de status: "failed" e o error_code, e em GET /v1/messages/{id}, no campo erros.

{
  "codigo": 131026,
  "categoria": "nao_entregavel",
  "mensagem": "O destinatário não pode receber esta mensagem: o número não tem WhatsApp, ou não existe.",
  "acaoRecomendada": "Confira o número do destinatário e reenvie para o número correto. Repetir o envio para o mesmo número não muda o resultado.",
  "original": {
    "error": {
      "message": "Message undeliverable",
      "type": "OAuthException",
      "code": 131026,
      "fbtrace_id": "EXAMPLE_TRACE_ID"
    }
  }
}
Campo de `erros`O que é
codigoO código de erro da Meta, íntegro. Nulo quando não houve resposta da Meta: um tempo esgotado, por exemplo, não tem código.
categoriaA natureza do erro, no vocabulário da Luna. Os valores são parametro_invalido, reauth_required, recurso_inacessivel, orcamento_management, throughput_excedido, estado_de_produto, janela_de_atendimento, midia_indisponivel, nao_entregavel, template_invalido, ritmo_por_destinatario, terminal, transporte, contrato e desconhecido.
mensagemO que aconteceu, em português. É texto da Helsen, e não a tradução da mensagem da Meta.
acaoRecomendadaO que fazer, em uma frase acionável.
originalO corpo de erro da Meta, sem reescrita e sem filtro. É o que o suporte da Meta reconhece.
  • desconhecido é literal e deliberado: a Meta acrescenta códigos sem aviso, e um código ainda não mapeado chega com essa categoria em vez de ser escondido. Leia o codigo e o original.
  • erros reúne o que a Meta recusou na hora do envio e o que ela informou depois, por evento de status. Você não precisa saber de qual dos dois veio cada item: a acaoRecomendada é a que resolve.
  • Cuidado ao repassar `original`. Ele pode conter o telefone do consumidor final e trechos da conversa. Trate-o com o cuidado do restante do conteúdo das mensagens antes de mandá-lo a terceiros.
  • Ao pedir ajuda à Meta, o fbtrace_id dentro de original é o que faz um chamado avançar.
A referência da API traz o contrato de cada rota e os exemplos de corpo, na referência da API.