> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ya.works/llms.txt
> Use this file to discover all available pages before exploring further.

# Mensagens de Template

> Envie um WhatsApp com um template aprovado e consulte em que estado ficou.

# Mensagens de Template

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.

<Warning>
  **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](/pt/changelog).
</Warning>

## 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](/pt/authentication). |
| **Base de consentimento** | O texto de aceite do seu checkout e a data em que passou a valer. |

<Note>
  **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.
</Note>

## Enviar uma mensagem

```bash theme={null}
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. |

<Note>
  **O identificador da sua empresa não viaja no request:** ele é derivado da sua API key
  ([Autenticação](/pt/authentication)). O template viaja sim, por nome e idioma, e é
  validado contra os que você tem aprovados.
</Note>

## 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**:

```json theme={null}
{
  "event_id": "CART-8F3A21",
  "status": "accepted",
  "status_detail": null
}
```

<Warning>
  **`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.
</Warning>

<Warning>
  **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.
</Warning>

### 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](/pt/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 |

<Warning>
  **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.
</Warning>

<Warning>
  **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](/pt/request-ids).
</Warning>

## 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`:

```bash theme={null}
curl https://api.ya.onl/v1/template-messages/CART-8F3A21 \
  -H "X-API-Key: yv_live_v1_sua_api_key"
```

```json theme={null}
{
  "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 |

<Warning>
  **`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.
</Warning>

<Note>
  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`.
</Note>

## 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](/pt/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. |

<Warning>
  **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.
</Warning>

## Erros

Os erros seguem o [formato padrão da API](/pt/errors):

| 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](/pt/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. |

<Note>
  **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](/pt/errors). Já o `409` usa o formato
  padrão e traz o seu `type` na raiz.
</Note>

<Warning>
  **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.
</Warning>

<Note>
  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.
</Note>

## 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 →](/pt/outbound/shopify-flow)

## Testar a integração

<Warning>
  **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](/pt/authentication). O ambiente de
  teste desta integração se diferencia pelo **telefone de destino**, não pela credencial.
</Warning>

* **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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.