API e desenvolvedores

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" }
}
JSON

Campos 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_id repetido: 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 em status_detail (stale, paused ou disabled). 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 o status igual move, então não o use como marca d'água: reconcilie pelo event_id.
  • Só failed e discarded são definitivos. sent, delivered e read são resolvidos de novo a cada consulta, porque ainda pode chegar um recibo posterior — inclusive um de falha depois de um read.

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.

Precisa de ajuda?