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
errore pelo status, nunca pelo texto domessage. - Alguns erros acrescentam campos que dizem o que fazer:
campos,fechadaEm,acaoRecomendada,estado,faltando,tentativasRestantes,janelaVira,reenfileiradoEmecredentialStatus. 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. 404vale 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 |
|---|---|---|
| 400 | invalid_request | O 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. |
| 401 | unauthorized | A 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. |
| 403 | forbidden | A 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. |
| 403 | connection_not_released | A 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. |
| 404 | not_found | O 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. |
| 409 | window_closed | A 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. |
| 409 | media_not_ready | A 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. |
| 409 | no_webhook_endpoint | Você pediu o reenvio de um webhook e não tem endpoint cadastrado. Cadastre um em POST /v1/webhooks/endpoints e peça de novo. |
| 409 | already_requeued | Este item da fila de rejeitados já foi re-enfileirado. reenfileiradoEm diz quando. Nada a fazer. |
| 409 | pin_not_applicable | O número não está aguardando o PIN. O state diz em que ponto ele está. Veja Conectar um número. |
| 409 | register_budget_reserved | As 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. |
| 409 | reconnect_not_applicable | A autorização deste número não admite reconexão. credentialStatus diz o estado. |
| 413 | file_too_large | O 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. |
| 429 | rate_limited | Você passou do limite de requisições da chave. Espere o tempo do Retry-After. Veja a próxima seção. |
| 429 | queue_full | A fila de envio deste número está cheia. Espere o tempo do Retry-After e tente de novo. |
| 429 | number_rate_limited | Este número está no limite de chamadas por segundo. Espere o tempo do Retry-After. |
| 429 | management_busy | As 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. |
| 500 | internal | Falha 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. |
| 503 | unavailable | A 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çalho | O que traz |
|---|---|
X-RateLimit-Limit | O teto da janela. |
X-RateLimit-Remaining | Quantas requisições ainda cabem na janela. |
X-RateLimit-Reset | Em quantos segundos a janela recomeça. |
Retry-After | Em respostas 429: quantos segundos esperar antes de tentar de novo. |
- Os três
X-RateLimit-*acompanham as respostas bem-sucedidas. LeiaX-RateLimit-Remaininge desacelere antes de chegar a zero. - Num
429, espere oRetry-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 é |
|---|---|
codigo | O 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. |
categoria | A 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. |
mensagem | O que aconteceu, em português. É texto da Helsen, e não a tradução da mensagem da Meta. |
acaoRecomendada | O que fazer, em uma frase acionável. |
original | O 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 ocodigoe ooriginal.errosreú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: aacaoRecomendadaé 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_iddentro deoriginalé o que faz um chamado avançar.