GET /v1/messages/:id com o msg_… que o POST devolveu (um id numérico → 404). Um único vocabulário de estado: queued | sending | sent | delivered | read | failed | held. ⚠ Terminais: read, failed e held (uma mensagem retida nunca é reenviada por nós: se for preciso, reenvie você a partir de heldUntil). delivered NÃO é terminal (delivered→read é válido) — se você parar o polling em delivered, não vê a leitura.
Uma recusa do WhatsApp NÃO volta como erro HTTP. O POST já respondeu 202 (aceito, não enviado); se a Meta recusar, você fica sabendo aqui: a mensagem fica failed (ou held) com lastErrorCode, um código estável sobre o qual você pode fazer switch — WINDOW_24H_EXPIRED, RECIPIENT_OPTED_OUT, CONNECTION_TOKEN_REVOKED… O lastError que vem ao lado é ajuda em inglês para um humano, não o contrato. O catálogo completo está no módulo de erros.
E não é preciso perguntar: o webhook avisa. Se a Meta recusar o nosso envio (template inexistente, parâmetro inválido, número desativado…), chega um message.status com status: "failed", stage: "send", o mesmo failureCode que você vê aqui e messageId = o seu msg_…. Uma única vez por mensagem. Os recibos de ENTREGA do que você mandou pela API trazem connectMessageId = o seu msg_… (o messageId ali é o wamid da Meta); se a Meta recusar na entrega, stage: "delivery" + failureCode, e o mesmo código fica em lastErrorCode.
curl https://api.waiaconnect.com/v1/messages/msg_5d325780fe504d0f87eb45009d28ae4f \
-H "Authorization: Bearer wc_live_YOUR_API_KEY"
# → { "id": "msg_…", "status": "delivered", "attempts": 1, "createdAt": "…Z", … }Não guardamos conteúdo: esta resposta traz metadados (estado, tentativas, holdReason), nunca o texto da mensagem.
A recusa mais comum é WINDOW_24H_EXPIRED: você mandou texto livre para alguém que não te escreveu nas últimas 24 horas. Resolve-se com um template aprovado.