WABA Templates
Templates aprovados pela Meta para envio de mensagens proativas (marketing, utility, authentication). Sem template, você só pode mandar mensagem dentro da janela de 24h após uma inbound do usuário.
Base path: /v1/templates
Apenas WABA. Baileys não tem conceito de template.
Lifecycle
POST /v1/templates
│
▼
DRAFT ────► (gateway envia pra Meta sync)
│
▼
PENDING ──► (Meta revisa, ~minutos a ~24h)
│
├──► APPROVED ──► usável em POST /v1/messages type=template
├──► REJECTED ──► verifique rejectionReason
└──► PAUSED ──► (Meta pausou por baixo engagement)
Após APPROVED, você pode editar (volta pra PENDING).
Componentes suportados
Cada template é composto por componentes (HEADER, BODY, FOOTER, BUTTONS, CAROUSEL, LIMITED_TIME_OFFER). O schema completo:
HEADER
{ "type": "HEADER", "format": "TEXT", "text": "Olá {{1}}", "example": { "header_text": ["João"] } }
Formats: TEXT, IMAGE, VIDEO, DOCUMENT, LOCATION.
Para mídia, use example com header_handle (de POST /v1/media/upload/resumable).
BODY
{ "type": "BODY", "text": "Sua consulta {{1}} foi confirmada para {{2}}", "example": { "body_text": [["Cardiologia", "20/05 às 14h"]] } }
Body é obrigatório. Suporta variáveis {{1}}, {{2}}, ... que viram variables em POST /v1/messages.
FOOTER
{ "type": "FOOTER", "text": "Acme Saúde" }
Máx 60 chars, sem variáveis.
BUTTONS
Lista de até 10 botões. Tipos suportados:
| Tipo | Descrição |
|---|---|
QUICK_REPLY | Botão de resposta rápida (texto curto) |
URL | Abre URL (suporta variáveis em URL: https://acme.com/{{1}}) |
PHONE_NUMBER | Disca número |
COPY_CODE | Copia código (cupom, PIX, etc) |
OTP | Authentication template (COPY_CODE/ONE_TAP/ZERO_TAP) |
CATALOG | Abre catálogo da WABA |
MPM | Multi-Product Message |
SPM | Single-Product Message |
FLOW | Abre WhatsApp Flow |
VOICE_CALL | Inicia voice call WABA |
Exemplo URL com variável:
{
"type": "BUTTONS",
"buttons": [
{ "type": "URL", "text": "Ver pedido", "url": "https://acme.com/pedido/{{1}}", "example": ["12345"] }
]
}
CAROUSEL
Marketing template com até 10 cards (cada card = mini-template com HEADER mídia + BODY + BUTTONS).
{
"type": "CAROUSEL",
"cards": [
{
"components": [
{ "type": "HEADER", "format": "IMAGE", "example": { "header_handle": ["h:..."] } },
{ "type": "BODY", "text": "Produto A — R$ {{1}}", "example": { "body_text": [["99,90"]] } },
{ "type": "BUTTONS", "buttons": [{ "type": "URL", "text": "Comprar", "url": "https://acme.com/a" }] }
]
}
]
}
LIMITED_TIME_OFFER
Template com countdown:
{
"type": "LIMITED_TIME_OFFER",
"limited_time_offer": { "text": "Oferta termina em", "has_expiration": true }
}
POST /v1/templates
Cria template e submete à Meta de forma síncrona (a request espera a chamada Graph API).
Body
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
phoneId | UUID | sim | Phone WABA dono |
name | string | sim | 1-120, [a-z0-9_]+ (lowercase + underscore) |
language | string | sim | pt_BR, en_US, etc. |
category | enum | sim | MARKETING, UTILITY, AUTHENTICATION |
components | array | sim | 1+ componentes |
allowCategoryChange | boolean | não | Default true. Meta pode reclassificar |
messageSendTtlSeconds | int | não | 30-43200 (12h). Auto-fail se não entregue |
curl -X POST https://wpp.ogmma.com.br/v1/templates \
-H "X-API-Key: ak_live_..." \
-H "Content-Type: application/json" \
-d '{
"phoneId": "5f7b2e1c-...",
"name": "boas_vindas_v2",
"language": "pt_BR",
"category": "UTILITY",
"components": [
{ "type": "BODY", "text": "Olá {{1}}, bem-vindo!", "example": { "body_text": [["João"]] } },
{ "type": "FOOTER", "text": "Acme Saúde" },
{
"type": "BUTTONS",
"buttons": [{ "type": "QUICK_REPLY", "text": "Saiba mais" }]
}
]
}'
Response 201
{
"id": "tpl-1234-...",
"accountId": "...",
"phoneId": "5f7b2e1c-...",
"name": "boas_vindas_v2",
"language": "pt_BR",
"category": "UTILITY",
"status": "PENDING",
"components": [ ... ],
"metaTemplateId": "987654321",
"createdAt": "2026-05-21T14:00:00.000Z"
}
Errors
| HTTP | Code | Quando |
|---|---|---|
| 400 | VALIDATION_ERROR | Schema inválido |
| 400 | WABA_CREDS_INCOMPLETE | Phone sem wabaPhoneNumberId/accessToken |
| 404 | PHONE_NOT_FOUND | Phone não é WABA ou não pertence à account |
| 409 | ALREADY_EXISTS | Já existe template aprovado/submetido com o mesmo (phoneId, name, language) |
Quando a Meta recusa na hora (variável sem exemplo, ratio, nome repetido), a linha
local fica em REJECTED sem metaTemplateId. Reenviar com o mesmo nome depois de
corrigir funciona: o gateway reaproveita a mesma linha (mesmo id, status volta
para DRAFT/PENDING) em vez de responder 409. O 409 ALREADY_EXISTS fica só para
template que existe de verdade na Meta — ou para uma submissão do mesmo nome ainda
em voo (linha em DRAFT), caso em que basta repetir depois da primeira terminar.
O corpo do erro de recusa carrega details.metaError inteiro — code,
error_subcode (ex.: 2388293 variáveis demais, 2388299 não pode começar/terminar
com variável, 2388024 nome já existe) e error_user_msg — para o cliente traduzir
por código em vez de por substring.
GET /v1/templates
Lista templates da account.
curl https://wpp.ogmma.com.br/v1/templates \
-H "X-API-Key: ak_live_..."
Response 200 — array.
GET /v1/templates/:id
Detalhe.
curl https://wpp.ogmma.com.br/v1/templates/tpl-1234-... \
-H "X-API-Key: ak_live_..."
Errors
| HTTP | Code | Quando |
|---|---|---|
| 404 | TEMPLATE_NOT_FOUND | — |
PATCH /v1/templates/:id
Edita um template já sincronizado com Meta. Mantém name/language; permite mudar components, category, messageSendTtlSeconds. Após edit, Meta re-revisa (status volta para PENDING).
Body (todos opcionais)
| Campo | Tipo | Obrigatório se quiser editar |
|---|---|---|
components | array | — |
category | enum | — |
messageSendTtlSeconds | int | — |
curl -X PATCH https://wpp.ogmma.com.br/v1/templates/tpl-1234-... \
-H "X-API-Key: ak_live_..." \
-H "Content-Type: application/json" \
-d '{
"components": [
{ "type": "BODY", "text": "Versão atualizada {{1}}", "example": { "body_text": [["X"]] } }
]
}'
Template REJECTED sem metaTemplateId (a Meta recusou na criação): o PATCH
re-submete como criação, reaproveitando a mesma linha — não há o que editar na Meta.
components e category omitidos mantêm os valores atuais da linha, e
messageSendTtlSeconds é recusado com 400 nesse caminho (a criação não repassa TTL).
Template em DRAFT — submissão ainda em voo — continua respondendo 409
TEMPLATE_NOT_SUBMITTED.
Errors
| HTTP | Code | Quando |
|---|---|---|
| 400 | VALIDATION_ERROR | Body inválido; messageSendTtlSeconds na re-submissão; ou components gravados que não validam no schema atual |
| 400 | META_BAD_REQUEST | Meta recusou a edição ou a re-submissão (details.metaError) |
| 404 | TEMPLATE_NOT_FOUND | Template não pertence à account |
| 409 | TEMPLATE_NOT_SUBMITTED | Template em DRAFT (submissão em voo) |
| 500 | WABA_CREDS_MISSING | Phone sem credenciais WABA |
DELETE /v1/templates/:id
Deleta na Meta + remove local. Se Meta falhar (ex.: template já em uso), remove localmente mesmo assim (log de warn).
curl -X DELETE https://wpp.ogmma.com.br/v1/templates/tpl-1234-... \
-H "X-API-Key: ak_live_..."
Response 204
POST /v1/templates/sync
Re-importa todos os templates de uma WABA direto da Meta (útil quando o time criou templates pelo Business Manager).
Body
| Campo | Tipo | Obrigatório |
|---|---|---|
phoneId | UUID | sim |
curl -X POST https://wpp.ogmma.com.br/v1/templates/sync \
-H "X-API-Key: ak_live_..." \
-H "Content-Type: application/json" \
-d '{ "phoneId": "5f7b2e1c-..." }'
Response 200 — objeto com counts de criados/atualizados.
Notificação de aprovação
Quando a Meta aprova/rejeita um template, o webhook template:status é publicado (assumindo que você subscreve esse evento). O DB local é atualizado automaticamente pelo handler meta-webhook. Você pode também fazer polling em GET /v1/templates/:id.
Boas práticas
- Nomes únicos por phone+language:
boas_vindas_v2+pt_BRé único — versione o nome se mudar muito o body. - Categorias corretas:
MARKETINGé cobrado mais caro queUTILITY. Meta auto-reclassifica se identificar abuse. - Examples obrigatórios: Meta exige
exampleem componentes com variáveis para entender o uso real. - Header media via handle: para template com IMAGE/VIDEO header, não use
url— suba viaPOST /v1/media/upload/resumablee use ohandlenoexample.header_handle.