Mensagens de Template
Envie um WhatsApp com um template aprovado e consulte em que estado ficou.
Quando você precisa escrever primeiro para um cliente — recuperar um carrinho abandonado, avisar que um pedido está pronto, entregar um PIN de retirada — a mensagem tem que sair como um template aprovado pela Meta. Estes endpoints são a porta pública para pedir esse envio e para consultar onde ele parou.
Sua conta precisa estar habilitada antes da primeira chamada. O contrato é publicado com antecedência para que você possa construir; a habilitação é por conta e nós a combinamos com você junto com o template e o número remetente.
Enquanto sua conta não estiver habilitada, cada chamada responde 422 com
"type": "account_not_eligible" — sempre, e não de forma intermitente. Não é problema
da sua requisição e nenhuma retentativa muda isso: é configuração do nosso lado, e a
resposta diz isso com essas palavras em vez de esconder atrás de um erro genérico. Fale
conosco para habilitá-la. Qualquer mudança no contrato é anunciada no
Changelog.
O que faz e o que não faz
| Faz | Não faz |
|---|---|
| Valida o telefone, o template e suas variáveis | Não cria nem atualiza o contato, e hoje nenhuma chamada pública faz isso: a API de Contatos é somente leitura (GET /v1/contacts). Se o registro da pessoa faz parte do seu fluxo, converse conosco antes de integrar |
| Descarta um aviso vencido em vez de enviá-lo atrasado | Não define quando um carrinho está abandonado: essa lógica é sua |
É idempotente pelo seu event_id |
Não abre conversa livre. O outbound é sempre template; a conversa começa se o cliente responder — regra da Meta, não nossa |
Devolve um conversation_id que você pode citar para nós |
Não mostra o template enviado em GET /v1/conversations/{id}/messages. Hoje o envio e essa lista de mensagens são servidos por armazenamentos diferentes: reconcilie pelo seu próprio event_id, não pela lista |
Pré-requisitos
| Requisito | Detalhe |
|---|---|
| Template aprovado na Meta | Deve existir como template aprovado no mesmo portfólio de WhatsApp (WABA) que o seu número remetente. Se estiver aprovado em outro portfólio, não pode ser enviado a partir desse número: é preciso replicá-lo, e isso reabre a janela de aprovação da Meta. |
| Número remetente ativo | Um número de WhatsApp Business conectado à YaVendio. Sem ele, cada envio responde 422 com "type": "no_sender_number". |
| API key com os escopos | outbound:send para enviar, outbound:read para consultar o estado — veja Autenticação. |
| Base de consentimento | O texto de aceite do seu checkout e a data em que passou a valer. |
Para saber quais templates você pode enviar, consulte o catálogo.
GET /v1/templates lista os que
você tem aprovados, com o nome e o idioma exatos que esta chamada espera. Uma lista
vazia significa que nenhum foi aprovado ainda, e até que haja um cada envio é recusado.
O campo sendable de cada template diz se esta API consegue enviá-lo: é false quando o texto mistura variáveis por posição e por nome, numera com buracos ou tem variável no cabeçalho (esta API preenche só o corpo). Um template com sendable: false responde 422 ao enviar, e o ajuste é na Meta, não no seu request.
Enviar uma mensagem
curl -X POST https://api.ya.onl/v1/template-messages \
-H "X-API-Key: yv_live_v1_sua_api_key" \
-H "Content-Type: application/json" \
-d @- <<'JSON'
{
"event_id": "CART-8F3A21",
"phone": "+51987654321",
"template": { "name": "cart_recovery", "language": "es" },
"variables": {
"1": "Juan",
"2": "S/ 1.250",
"3": "https://sua-loja.com/cart/8F3A21"
},
"occurred_at": "2026-08-11T13:22:05Z",
"metadata": { "use_case": "cart_abandonment" }
}
JSONCampos do request
| Campo | Obrigatório | Formato | Descrição |
|---|---|---|---|
event_id |
Sim | ^[A-Za-z0-9._-]{8,128}$ |
Seu identificador do evento. É a chave de idempotência e a chave de consulta do estado. Precisa ser estável entre retentativas — nunca gere um por tentativa. O charset é limitado porque este valor viaja na URL do GET de estado. |
phone |
Sim | E.164 estrito: ^\+[1-9]\d{7,14}$ |
Telefone de destino. Um número fora do formato é rejeitado com 422 — nunca completamos o código do país por você, porque um código adivinhado pode produzir o número válido de outra pessoa. |
template.name |
Sim | texto, 1–512 | Nome do template exatamente como está aprovado na Meta. |
template.language |
Sim | texto, 2–15 | Código de idioma do template. es e es_PE não são o mesmo template. |
variables |
Conforme o template | objeto {"1": "…", "2": "…"} ou {"name": "…"} |
Valores das variáveis, com exatamente as chaves que required_variables publica em GET /v1/templates: por posição ("1" preenche {{1}}) ou por nome ("name" preenche {{name}}), conforme o template foi criado. Uma chave faltando ou sobrando é um 422. |
occurred_at |
Não | ISO 8601 com offset | Quando ocorreu o evento que origina a mensagem. Controla a frescura — veja Janelas de tempo. Um valor sem offset é lido como UTC, então envie Z ou o seu offset explícito. Se você omitir, o aviso nunca é considerado vencido. |
metadata.use_case |
Não | texto, ≤120 | Rótulo livre para atribuir o envio a um caso de uso, por exemplo cart_abandonment. Não afeta o envio. |
O identificador da sua empresa não viaja no request: ele é derivado da sua API key (Autenticação). O template viaja sim, por nome e idioma, e é validado contra os que você tem aprovados.
Respostas
Envio aceito — 202
Validamos, reservamos o event_id e entregamos ao serviço de envio. A resposta chega
assim que isso acontece e não traz o desfecho:
{
"event_id": "CART-8F3A21",
"status": "accepted",
"status_detail": null
}accepted significa "a caminho", não "deu certo". O envio segue seu curso depois da
resposta. Não leia isso como sucesso — consulte o endpoint de estado, que é onde ele
se resolve contra a mensagem real. É o único caminho: os webhooks de /v1/webhooks
emitem apenas eventos de pedido (order.created, order.status_changed), nenhum de
entrega.
Configure o timeout do seu cliente em 35 segundos ou mais. O POST aguarda a chamada
ao serviço de envio, que tem o seu próprio teto de 30 segundos. No caminho normal ele
responde em menos de um segundo, mas esse é o caso típico e não uma garantia: um cliente
com timeout de 5 ou 10 segundos abandona requests saudáveis que ainda estão em voo, e depois reporta
falhas que nunca aconteceram.
Sem envio novo — 200
Um 200 significa que esta requisição não produziu um envio. São dois casos:
- Um
event_idrepetido: devolve o estado guardado da primeira requisição, como descrito na seção de idempotência abaixo. - Um envio inviável de saída — o aviso está vencido, a conversa do cliente está em
pausa, ou o comerciante desligou o envio pelo painel: responde
"status": "discarded"com o motivo emstatus_detail(stale,pausedoudisabled). A requisição funcionou; o envio está desligado de propósito.
Idempotência
Mesmo event_id = mesmo envio. Reenviá-lo devolve o estado da primeira requisição e
nunca gera um segundo WhatsApp. Por isso é seguro repetir depois de um timeout de rede.
A idempotência roda antes das validações de telefone, template e frescura: um reenvio
devolve o estado guardado sem julgar esses campos de novo. O que roda primeiro é a
forma do corpo — o charset do event_id, os tamanhos de template.name e
template.language, o parse de occurred_at. Um reenvio malformado recebe 422 e nunca
chega à idempotência.
Há um caso em que um event_id deixa de servir: se do lado do envio ele já ficou atrelado a
outra mensagem, ou saiu da janela de 30 dias, a resposta é 409 com
"type": "idempotency_conflict". Mande esse evento com um event_id novo — repetir o mesmo
devolve o mesmo 409. De propósito não guardamos estado sob a chave recusada: se
guardássemos, a próxima tentativa receberia um reenvio silencioso em vez do 409 que diz o
que corrigir.
| Repetir | Não repetir |
|---|---|
429, 500 e timeouts de rede — use backoff exponencial (Rate Limiting) |
401, 403 e os 422 de dado — são erros do dado ou da credencial; repetir devolve o mesmo erro e consome sua cota |
502, mas limitando as tentativas — veja a nota em Erros |
422 com "type": "account_not_eligible" ou "type": "no_sender_number" — são de configuração de conta, não do corpo; nenhuma retentativa muda isso |
409 (idempotency_conflict), mas com um event_id novo — o mesmo colide de novo |
409 com o mesmo event_id — a resposta vai ser idêntica |
402, mas depois de recarregar — com o mesmo event_id, que continua livre |
402 sem recarregar — a resposta vai ser idêntica |
Um 422 num reenvio merece uma olhada antes de descartar o evento. Se a primeira
tentativa pode já ter saído, o evento não está perdido: corrija a forma do corpo e
reenvie com o mesmo event_id, que continua seguro e devolve o estado real. Quando o
422 foi de conta, esse reenvio responde 200 com "status": "failed" — é o estado
guardado da primeira tentativa, não um envio novo.
O event_id não é um identificador de rastreio. Se o seu sistema gera um id novo a
cada tentativa HTTP, esse valor não serve como event_id: cada retentativa apareceria
como um evento distinto e o cliente receberia o WhatsApp mais de uma vez. Para
rastrear tentativas use o header X-Request-ID — veja Request IDs.
Consultar o estado
"Enviado" não é "entregue": a Meta reporta a entrega ao telefone minutos ou horas depois —
ou nunca, se o telefone estiver desligado. Esse ciclo é consultado pelo seu event_id:
curl https://api.ya.onl/v1/template-messages/CART-8F3A21 \
-H "X-API-Key: yv_live_v1_sua_api_key"{
"event_id": "CART-8F3A21",
"status": "accepted",
"status_detail": null,
"updated_at": "2026-08-11T13:24:40Z"
}Um event_id que nunca foi enviado responde 404; um que não respeita o charset responde
422.
| Estado | Significa | Alcançável hoje? |
|---|---|---|
accepted |
Recebido e entregue ao serviço de envio. É o que o 202 devolve. |
Sim |
in_progress |
Herdado. Nenhum envio novo chega a este estado; só os envios anteriores a esta mudança que ainda estejam dentro da janela de retenção o devolvem. Trate-o como accepted. |
Só envios anteriores |
discarded |
Descartado por regra antes do envio. status_detail traz o motivo: stale (aviso vencido), paused (conversa em pausa) ou disabled (o comerciante desligou o envio pelo painel). |
Sim |
sent |
A Meta aceitou a mensagem. | Sim |
delivered |
Entregue ao telefone do cliente. | Sim |
read |
Lido pelo cliente. | Sim |
failed |
Rejeição definitiva. status_detail traz o motivo — veja abaixo. |
Sim |
status_detail em um failed não é uma lista fechada e pode estar ausente. Pode ser
uma recusa de conta (account_not_eligible, no_sender_number), um envio que não chegou a
se completar (send_failed) ou que foi cancelado antes de ser tentado
(send_cancelled), ou, quando o provedor recusa a entrega com um código,
provider_error_<código> — por exemplo provider_error_131049. O código é o da Meta e o
catálogo deles muda sem nos avisar, então não ramifique de forma exaustiva sobre este
campo: decida pelo status, registre status_detail e trate qualquer valor
desconhecido — ou ausente — como uma falha genérica.
Um accepted também pode trazer delivery_not_observable: o envio desse comerciante
vive em uma instância própria que este deployment não consegue ler, então o status não
vai se resolver a cada consulta — não é uma falha.
Dois detalhes úteis na integração:
updated_até quando o registro mudou pela última vez, não quando você consultou. Consultar não o move, mas uma escrita de entrega que deixa ostatusigual move, então não o use como marca d'água: reconcilie peloevent_id.- Só
failedediscardedsão definitivos.sent,deliveredereadsão resolvidos de novo a cada consulta, porque ainda pode chegar um recibo posterior — inclusive um de falha depois de umread.
Janelas de tempo
| Regra | Valor | Detalhe |
|---|---|---|
| Frescura do aviso | 6 horas desde occurred_at |
Um evento mais antigo que a janela é descartado (discarded, motivo stale) em vez de enviado. Uma mensagem de recuperação que chega no dia seguinte não recupera nada, só incomoda. Se você omitir occurred_at, esta regra não se aplica. |
| Retenção de idempotência | 30 dias | Janela durante a qual um event_id repetido é reconhecido como duplicado e seu estado continua consultável. |
| Limite de requisições | Veja Rate Limiting | Compartilhado com o resto da API, não reservado para estes endpoints, e cada resposta traz o valor vigente no header X-RateLimit-Limit. Vale considerar ao dimensionar um lote, já que cada POST pode ocupar até 30 segundos. |
Envie o offset em occurred_at. Um valor sem fuso horário é lido como UTC. Se o seu
sistema envia hora local do Peru (UTC-5), o evento envelhece 5 horas a mais contra uma
janela de 6: um carrinho abandonado há duas horas computa como de sete horas e volta
discarded com motivo stale — respondendo 200, sem erro e sem mensagem enviada.
Erros
Os erros seguem o formato padrão da API:
| Status | Quando |
|---|---|
401 |
API key ausente, inválida, revogada ou expirada. |
402 |
(apenas no POST) A conta não pode pagar a mensagem e nada foi enviado. "type" diz qual: insufficient_funds (sem saldo), wallet_sends_paused (os envios foram cortados quando o saldo acabou) ou wallet_currency_mismatch. Os dois primeiros se resolvem recarregando na página de pagamentos do WhatsApp, e o envio volta sozinho; o terceiro não e precisa falar com a gente. |
403 |
A key não tem o escopo necessário: outbound:send para enviar, outbound:read para consultar. |
404 |
(apenas no GET) Não existe mensagem com esse event_id. |
409 |
(apenas no POST) O event_id já está atrelado a outra mensagem, ou saiu da janela de idempotência. "type": "idempotency_conflict". Mande o evento com um event_id novo. |
422 |
Dado inválido: telefone fora do E.164, template não aprovado para a sua conta nesse idioma, ou uma variável obrigatória faltando. A mensagem nomeia o campo. |
422 |
(apenas no POST) Recusa de conta: "type": "account_not_eligible" se a sua conta não está habilitada para enviar, "type": "no_sender_number" se ela não tem um número de WhatsApp conectado para enviar. Nenhum dos dois se corrige pelo corpo da requisição. |
422 |
(no GET) O event_id da URL não respeita o charset. |
429 |
Limite de requisições excedido — veja Rate Limiting. |
500 |
Falha inesperada do nosso lado. Repita com backoff — é seguro por event_id. |
502 |
(apenas no POST) O serviço interno de envio falhou ou está indisponível. Leia a nota abaixo antes de repetir. |
Os dois 422 de conta se distinguem por detail[0].type, não pelo campo: eles chegam
com "loc": ["body"] porque não há um valor a corrigir. É o mesmo formato de array de
qualquer outro 422 — veja Tratamento de Erros. Já o 409 usa o formato
padrão e traz o seu type na raiz.
Um 502 não significa mais "pode ser qualquer coisa". As recusas que dependem da sua
conta ou do seu evento têm resposta própria: conta não habilitada e falta de número
remetente são 422, uma conversa em pausa é 200 com "status": "discarded" e
"status_detail": "paused", e uma chave repetida é 409. O que sobra no 502 é uma
falha real do serviço interno de envio. Repita com backoff algumas poucas vezes; se
persistir, pare de repetir e fale conosco.
Nem um aviso vencido, nem uma conversa em pausa, nem o envio desligado são erros: os três
respondem 200 com "status": "discarded" e "status_detail" em "stale", "paused"
ou "disabled". Uma pausa é posta por um agente humano que assumiu a conversa, ou por um
corte de faturamento; o desligamento é posto pelo comerciante no painel de carrinho
abandonado. Nenhum dos dois é levantado pela requisição.
Receita: Shopify (Flow)
O Shopify Flow faz esse mesmo POST sem código e sem um servidor seu no meio: é o exemplo
guiado do contrato acima, com o disparo do abandono já resolvido.
Usa Shopify? Siga a receita guiada →
Testar a integração
Não existe um ambiente sandbox. O dashboard sempre cria keys do tipo live e não há
um ambiente isolado atrás delas — veja Autenticação. O ambiente de
teste desta integração se diferencia pelo telefone de destino, não pela credencial.
- Fase de teste — envie para telefones da sua própria equipe, em uma janela combinada. A mensagem chega de verdade, com o template real: essa é exatamente a prova.
- Fase produtiva — mesma key, mesmo contrato; só muda que os telefones são de clientes reais.
A lista de telefones de destino é, portanto, o único controle real entre uma fase e a outra: combine-a por escrito antes do primeiro teste e trate-a como parte da configuração da integração.
