Templates
Template é uma mensagem pré-aprovada pela Meta. É o que permite iniciar conversa fora da janela de 24 horas. Dentro da janela, texto livre basta.
Quando você precisa de um template
- O contato nunca escreveu para o seu número, ou a última mensagem dele tem mais de 24 horas: só um template sai. Texto livre é recusado com
409. - Enviar um template reabre a conversa. Depois que ele é entregue, a mensagem livre volta a ser aceita.
- Para saber se a janela está aberta antes de tentar, leia
GET /v1/conversations. Veja Enviar mensagens.
Crie um template
POST /v1/templates cria o template na conta de WhatsApp Business do cliente e o submete à Meta para aprovação. Escopo templates:write. O wabaId é o identificador da conta na Meta, e vem de GET /v1/business-accounts (escopo numbers:read):
curl -X GET "https://api.lunahia.com.br/v1/business-accounts" \
-H "Authorization: Bearer $LUNA_API_KEY"const res = await fetch('https://api.lunahia.com.br/v1/business-accounts', {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.LUNA_API_KEY}`,
},
});
console.log(res.status, await res.json());import os
import requests
res = requests.get(
"https://api.lunahia.com.br/v1/business-accounts",
headers={
"Authorization": f"Bearer {os.environ['LUNA_API_KEY']}",
},
timeout=10,
)
print(res.status_code, res.json())curl -X POST "https://api.lunahia.com.br/v1/templates" \
-H "Authorization: Bearer $LUNA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"wabaId": "YOUR_WABA_ID",
"name": "confirmacao_de_pedido",
"language": "pt_BR",
"category": "UTILITY",
"bodyText": "Olá {{nome}}, recebemos o seu pedido e já estamos preparando.",
"examples": {
"nome": "Ana"
},
"footer": "Obrigado por comprar com a gente.",
"buttons": [
{
"type": "QUICK_REPLY",
"text": "Falar com atendente"
}
]
}'const res = await fetch('https://api.lunahia.com.br/v1/templates', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LUNA_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"wabaId": "YOUR_WABA_ID",
"name": "confirmacao_de_pedido",
"language": "pt_BR",
"category": "UTILITY",
"bodyText": "Olá {{nome}}, recebemos o seu pedido e já estamos preparando.",
"examples": {
"nome": "Ana"
},
"footer": "Obrigado por comprar com a gente.",
"buttons": [
{
"type": "QUICK_REPLY",
"text": "Falar com atendente"
}
]
}),
});
console.log(res.status, await res.json());import os
import requests
res = requests.post(
"https://api.lunahia.com.br/v1/templates",
headers={
"Authorization": f"Bearer {os.environ['LUNA_API_KEY']}",
},
json={
"wabaId": "YOUR_WABA_ID",
"name": "confirmacao_de_pedido",
"language": "pt_BR",
"category": "UTILITY",
"bodyText": "Olá {{nome}}, recebemos o seu pedido e já estamos preparando.",
"examples": {
"nome": "Ana",
},
"footer": "Obrigado por comprar com a gente.",
"buttons": [
{
"type": "QUICK_REPLY",
"text": "Falar com atendente",
},
],
},
timeout=10,
)
print(res.status_code, res.json())201{
"id": "ae60f288-7565-4315-a440-3663e9c231e1",
"metaTemplateId": "1259544702043801",
"name": "confirmacao_de_pedido",
"language": "pt_BR",
"category": "UTILITY",
"status": "PENDING",
"createdAt": "2026-10-06T14:00:00.000Z",
"updatedAt": "2026-10-06T14:00:00.000Z"
}| Campo | Regra |
|---|---|
name | Minúsculas, números e sublinhado: ^[a-z0-9_]{1,512}$. Não muda depois. |
language | Na grafia da Meta, com sublinhado e não hífen: pt_BR, pt_PT, en_US, es_MX, fr. Não muda depois. |
category | MARKETING ou UTILITY. Ausente vale UTILITY. Para código de verificação existe uma rota própria (veja abaixo). |
bodyText | Até 1024 caracteres. Aceita variáveis na forma {{nome_da_variavel}}, em minúsculas e sublinhado. |
examples | Um valor de amostra por variável do corpo, obrigatório quando há variável. A Meta recusa parâmetro sem exemplo, e a recusa custa uma unidade do orçamento de gestão da conta. |
header, footer | Texto de até 60 caracteres. O rodapé não aceita variável. Para cabeçalho de imagem, vídeo ou documento, use headerMedia. |
buttons | Até 10 no total, somando os tipos QUICK_REPLY, URL, PHONE_NUMBER e COPY_CODE. Se houver respostas rápidas junto de outros botões, os dois conjuntos precisam ficar agrupados. |
- A categoria que volta na resposta é a que a Meta atribuiu, e pode não ser a que você pediu, porque ela reclassifica na criação. A Luna guarda o que a Meta respondeu. É a categoria da criação: a Luna não a atualiza depois. Para a categoria corrente de um template antigo, consulte o painel da Meta.
- Cada idioma é um template separado para a Meta. Dez templates em nove idiomas são noventa templates, e o teto é de 250 por conta de WhatsApp Business. O teto é da Meta e é o mesmo para todos os clientes.
- Acima de três botões, apenas dois aparecem na mensagem entregue. A Meta aceita o template com mais e entrega dois, então decida o arranjo antes de desenhar o fluxo.
Acompanhe o estado
GET /v1/templates/{id} devolve o estado atual do template na Meta. Escopo templates:read:
curl -X GET "https://api.lunahia.com.br/v1/templates/YOUR_TEMPLATE_ID" \
-H "Authorization: Bearer $LUNA_API_KEY"const res = await fetch('https://api.lunahia.com.br/v1/templates/YOUR_TEMPLATE_ID', {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.LUNA_API_KEY}`,
},
});
console.log(res.status, await res.json());import os
import requests
res = requests.get(
"https://api.lunahia.com.br/v1/templates/YOUR_TEMPLATE_ID",
headers={
"Authorization": f"Bearer {os.environ['LUNA_API_KEY']}",
},
timeout=10,
)
print(res.status_code, res.json())- O
statusé texto livre, e não um conjunto fechado, de propósito: a Meta acrescenta valor novo sem aviso. Os comuns sãoAPPROVED,PENDING,REJECTED,PAUSEDeDISABLED. - A Luna não manda evento de webhook quando o estado de um template muda. Os quatro tipos de evento são de mensagem, de acumulado e de importação de histórico. Consulte o template, e não espere um evento.
GET /v1/templates lista os templates do cliente, do mais recente ao mais antigo. O status que volta na lista é o último que a Luna observou, e não uma leitura ao vivo na Meta. Use ?status= para filtrar. A comparação é literal e sensível a maiúsculas.
curl -X GET "https://api.lunahia.com.br/v1/templates?status=APPROVED" \
-H "Authorization: Bearer $LUNA_API_KEY"const res = await fetch('https://api.lunahia.com.br/v1/templates?status=APPROVED', {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.LUNA_API_KEY}`,
},
});
console.log(res.status, await res.json());import os
import requests
res = requests.get(
"https://api.lunahia.com.br/v1/templates?status=APPROVED",
headers={
"Authorization": f"Bearer {os.environ['LUNA_API_KEY']}",
},
timeout=10,
)
print(res.status_code, res.json())Aprovado não é liberado
Um template APPROVED não está necessariamente liberado para sair. A Meta aplica um ritmo próprio de liberação sobre os primeiros envios de um template recém-aprovado. Ela chama isso de template pacing, e o define como um processo que dá tempo aos usuários do WhatsApp de reagirem à mensagem: se as primeiras entregas forem mal recebidas, o template é pausado antes de alcançar muita gente.
- Escalone o primeiro disparo em vez de programá-lo para uma data.
- Comece por um volume pequeno e cresça conforme as entregas confirmarem.
- Acompanhe o estado do template: ele vai para
PAUSEDse a reação for negativa. Você pode filtrar a lista com?status=PAUSED.
Cabeçalho de mídia
Para um cabeçalho de imagem, vídeo ou documento, são dois passos. Primeiro suba o arquivo de exemplo em POST /v1/template-assets, em multipart/form-data, no campo file (escopo templates:write):
curl -X POST "https://api.lunahia.com.br/v1/template-assets" \
-H "Authorization: Bearer $LUNA_API_KEY" \
-F "file=@./exemplo.png;type=image/png"import { readFile } from 'node:fs/promises';
const form = new FormData();
form.append(
'file',
new Blob([await readFile('./exemplo.png')], { type: 'image/png' }),
'exemplo.png',
);
const res = await fetch('https://api.lunahia.com.br/v1/template-assets', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LUNA_API_KEY}`,
},
body: form,
});
console.log(res.status, await res.json());import os
import requests
res = requests.post(
"https://api.lunahia.com.br/v1/template-assets",
headers={
"Authorization": f"Bearer {os.environ['LUNA_API_KEY']}",
},
files={"file": ("exemplo.png", open("exemplo.png", "rb"), "image/png")},
timeout=10,
)
print(res.status_code, res.json())200{
"handle": "4:EXAMPLE_HANDLE_NOT_REAL:ARb0Xk:e:1790451084:0"
}Depois use o handle em headerMedia, com o format IMAGE, VIDEO ou DOCUMENT:
curl -X POST "https://api.lunahia.com.br/v1/templates" \
-H "Authorization: Bearer $LUNA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"wabaId": "YOUR_WABA_ID",
"name": "pedido_a_caminho",
"language": "pt_BR",
"category": "UTILITY",
"bodyText": "Olá {{nome}}, o seu pedido saiu para entrega.",
"examples": {
"nome": "Ana"
},
"headerMedia": {
"format": "IMAGE",
"handle": "YOUR_HANDLE_FROM_TEMPLATE_ASSETS"
}
}'const res = await fetch('https://api.lunahia.com.br/v1/templates', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LUNA_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"wabaId": "YOUR_WABA_ID",
"name": "pedido_a_caminho",
"language": "pt_BR",
"category": "UTILITY",
"bodyText": "Olá {{nome}}, o seu pedido saiu para entrega.",
"examples": {
"nome": "Ana"
},
"headerMedia": {
"format": "IMAGE",
"handle": "YOUR_HANDLE_FROM_TEMPLATE_ASSETS"
}
}),
});
console.log(res.status, await res.json());import os
import requests
res = requests.post(
"https://api.lunahia.com.br/v1/templates",
headers={
"Authorization": f"Bearer {os.environ['LUNA_API_KEY']}",
},
json={
"wabaId": "YOUR_WABA_ID",
"name": "pedido_a_caminho",
"language": "pt_BR",
"category": "UTILITY",
"bodyText": "Olá {{nome}}, o seu pedido saiu para entrega.",
"examples": {
"nome": "Ana",
},
"headerMedia": {
"format": "IMAGE",
"handle": "YOUR_HANDLE_FROM_TEMPLATE_ASSETS",
},
},
timeout=10,
)
print(res.status_code, res.json())- O `handle` não é o identificador de mídia que
POST /v1/numbers/{id}/mediadevolve. São dois endpoints da Meta com propósitos diferentes, e um identificador de mídia no lugar dohandlefaz a criação ser recusada. O identificador de mídia é um número decimal longo, e ohandlenão é numérico. - Os tipos aceitos no upload são
application/pdf,image/jpeg,image/jpg,image/pngevideo/mp4. Para documento a Meta aceita apenas PDF. Nem GIF nem localização são aceitos. - O conteúdo do arquivo é conferido contra o tipo declarado antes de qualquer ida à Meta. Um arquivo declarado como
image/pngcujos bytes são de outra coisa é recusado com400. Acima do teto,413. - A Meta não publica por quanto tempo o
handlevale, e a Luna não guarda o valor. Use-o na criação do template logo em seguida, e suba de novo se ele deixar de servir. header(texto) eheaderMediasão exclusivos: a Meta aceita um só cabeçalho por template.
Envie o template
Com o template APPROVED, envie por POST /v1/messages com type: "template". O language.code precisa bater com o idioma aprovado, e os components preenchem as variáveis. Para variável nomeada, use parameter_name:
curl -X POST "https://api.lunahia.com.br/v1/messages" \
-H "Authorization: Bearer $LUNA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "YOUR_PHONE_NUMBER_ID",
"to": "5511999990000",
"type": "template",
"template": {
"name": "confirmacao_de_pedido",
"language": {
"code": "pt_BR"
},
"components": [
{
"type": "body",
"parameters": [
{
"type": "text",
"parameter_name": "nome",
"text": "Ana"
}
]
}
]
}
}'const res = await fetch('https://api.lunahia.com.br/v1/messages', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LUNA_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"from": "YOUR_PHONE_NUMBER_ID",
"to": "5511999990000",
"type": "template",
"template": {
"name": "confirmacao_de_pedido",
"language": {
"code": "pt_BR"
},
"components": [
{
"type": "body",
"parameters": [
{
"type": "text",
"parameter_name": "nome",
"text": "Ana"
}
]
}
]
}
}),
});
console.log(res.status, await res.json());import os
import requests
res = requests.post(
"https://api.lunahia.com.br/v1/messages",
headers={
"Authorization": f"Bearer {os.environ['LUNA_API_KEY']}",
},
json={
"from": "YOUR_PHONE_NUMBER_ID",
"to": "5511999990000",
"type": "template",
"template": {
"name": "confirmacao_de_pedido",
"language": {
"code": "pt_BR",
},
"components": [
{
"type": "body",
"parameters": [
{
"type": "text",
"parameter_name": "nome",
"text": "Ana",
},
],
},
],
},
},
timeout=10,
)
print(res.status_code, res.json())Com cabeçalho de mídia, a mídia vai no componente header de cada envio. O parameters aceita os valores na forma que a Cloud API define, e aqui a imagem vai por link:
curl -X POST "https://api.lunahia.com.br/v1/messages" \
-H "Authorization: Bearer $LUNA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "YOUR_PHONE_NUMBER_ID",
"to": "5511999990000",
"type": "template",
"template": {
"name": "pedido_a_caminho",
"language": {
"code": "pt_BR"
},
"components": [
{
"type": "header",
"parameters": [
{
"type": "image",
"image": {
"link": "https://example.com/pedido.jpg"
}
}
]
},
{
"type": "body",
"parameters": [
{
"type": "text",
"parameter_name": "nome",
"text": "Ana"
}
]
}
]
}
}'const res = await fetch('https://api.lunahia.com.br/v1/messages', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LUNA_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"from": "YOUR_PHONE_NUMBER_ID",
"to": "5511999990000",
"type": "template",
"template": {
"name": "pedido_a_caminho",
"language": {
"code": "pt_BR"
},
"components": [
{
"type": "header",
"parameters": [
{
"type": "image",
"image": {
"link": "https://example.com/pedido.jpg"
}
}
]
},
{
"type": "body",
"parameters": [
{
"type": "text",
"parameter_name": "nome",
"text": "Ana"
}
]
}
]
}
}),
});
console.log(res.status, await res.json());import os
import requests
res = requests.post(
"https://api.lunahia.com.br/v1/messages",
headers={
"Authorization": f"Bearer {os.environ['LUNA_API_KEY']}",
},
json={
"from": "YOUR_PHONE_NUMBER_ID",
"to": "5511999990000",
"type": "template",
"template": {
"name": "pedido_a_caminho",
"language": {
"code": "pt_BR",
},
"components": [
{
"type": "header",
"parameters": [
{
"type": "image",
"image": {
"link": "https://example.com/pedido.jpg",
},
},
],
},
{
"type": "body",
"parameters": [
{
"type": "text",
"parameter_name": "nome",
"text": "Ana",
},
],
},
],
},
},
timeout=10,
)
print(res.status_code, res.json())O handle usado na criação não vira o conteúdo entregue, e não há nada a reaproveitar dele no envio. Para escolher entre link e identificador de mídia, vale a recomendação de Enviar mensagens.
Editar e remover
PATCH /v1/templates/{id} altera os componentes de um template existente. A atualização é parcial: o que você não informar permanece como está. Por isso o verbo é PATCH, e não PUT.
curl -X PATCH "https://api.lunahia.com.br/v1/templates/YOUR_TEMPLATE_ID" \
-H "Authorization: Bearer $LUNA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"bodyText": "Olá {{nome}}, recebemos o seu pedido e já estamos separando os itens.",
"examples": {
"nome": "Ana"
}
}'const res = await fetch('https://api.lunahia.com.br/v1/templates/YOUR_TEMPLATE_ID', {
method: 'PATCH',
headers: {
Authorization: `Bearer ${process.env.LUNA_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"bodyText": "Olá {{nome}}, recebemos o seu pedido e já estamos separando os itens.",
"examples": {
"nome": "Ana"
}
}),
});
console.log(res.status, await res.json());import os
import requests
res = requests.patch(
"https://api.lunahia.com.br/v1/templates/YOUR_TEMPLATE_ID",
headers={
"Authorization": f"Bearer {os.environ['LUNA_API_KEY']}",
},
json={
"bodyText": "Olá {{nome}}, recebemos o seu pedido e já estamos separando os itens.",
"examples": {
"nome": "Ana",
},
},
timeout=10,
)
print(res.status_code, res.json())- Nome, idioma e conta de negócio não podem ser alterados. Para um nome diferente, crie outro template.
- Se você informar qualquer componente, informe também
bodyText: a Meta recusa template sem corpo. - A edição volta a submeter o template à aprovação, e o estado muda. Consulte o estado depois.
DELETE /v1/templates/{id} responde 204.
curl -X DELETE "https://api.lunahia.com.br/v1/templates/YOUR_TEMPLATE_ID" \
-H "Authorization: Bearer $LUNA_API_KEY"const res = await fetch('https://api.lunahia.com.br/v1/templates/YOUR_TEMPLATE_ID', {
method: 'DELETE',
headers: {
Authorization: `Bearer ${process.env.LUNA_API_KEY}`,
},
});
console.log(res.status);import os
import requests
res = requests.delete(
"https://api.lunahia.com.br/v1/templates/YOUR_TEMPLATE_ID",
headers={
"Authorization": f"Bearer {os.environ['LUNA_API_KEY']}",
},
timeout=10,
)
print(res.status_code)404.Template de autenticação
Código de verificação tem uma rota de criação própria, POST /v1/templates/authentication (escopo templates:write), e não é uma categoria a mais em POST /v1/templates. O conteúdo não é seu: o corpo é fixo pela Meta, com o texto "{{1}} is your verification code." traduzido para o idioma do template. Por isso a rota não recebe corpo, exemplos, cabeçalho, rodapé nem botões.
curl -X POST "https://api.lunahia.com.br/v1/templates/authentication" \
-H "Authorization: Bearer $LUNA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"wabaId": "YOUR_WABA_ID",
"name": "codigo_de_verificacao",
"language": "pt_BR",
"addSecurityRecommendation": true,
"codeExpirationMinutes": 10
}'const res = await fetch('https://api.lunahia.com.br/v1/templates/authentication', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LUNA_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"wabaId": "YOUR_WABA_ID",
"name": "codigo_de_verificacao",
"language": "pt_BR",
"addSecurityRecommendation": true,
"codeExpirationMinutes": 10
}),
});
console.log(res.status, await res.json());import os
import requests
res = requests.post(
"https://api.lunahia.com.br/v1/templates/authentication",
headers={
"Authorization": f"Bearer {os.environ['LUNA_API_KEY']}",
},
json={
"wabaId": "YOUR_WABA_ID",
"name": "codigo_de_verificacao",
"language": "pt_BR",
"addSecurityRecommendation": True,
"codeExpirationMinutes": 10,
},
timeout=10,
)
print(res.status_code, res.json())- Você escolhe o
name, olanguage, se o aviso de segurança aparece (addSecurityRecommendation), em quantos minutos o código expira (codeExpirationMinutes, obrigatório) e, se quiser, obuttonTexte omessageSendTtlSeconds. - Omitir
buttonTexté o recomendado: sem ele a Meta usa o rótulo padrão do idioma, que já vem traduzido. - No envio, o código de uso único vai duas vezes no corpo da mensagem: uma no componente de corpo e outra no componente de botão. É o formato, e não redundância a ser otimizada. A senha aceita no máximo 15 caracteres.