access_token válido (veja a
documentação de autenticação) e de um
número de WhatsApp Oficial já conectado na sua conta Atys. A conexão do número e a criação de
templates são feitas dentro da plataforma, em Configurações → Canais — não pela API.
Como funciona
O envio é feito com uma requisição a POST /api/api-send-message, informando
platform: ofc_whatsapp. O que você pode enviar depende das regras da
Meta: a conversa estar ou não dentro da janela de 24 horas determina se cabe texto
livre ou se é preciso usar um template.
Texto ou arquivo livre. Só é aceita dentro da janela de 24h.
Modelo aprovado pela Meta. Funciona sempre, dentro ou fora da janela.
A janela de 24 horas
É a regra central do WhatsApp Oficial, e a principal diferença em relação ao canal não oficial. A janela abre quando o contato envia uma mensagem para o seu número e dura 24 horas a partir dessa última mensagem dele.
| Situação | Texto/arquivo livre | Template |
|---|---|---|
| Contato escreveu nas últimas 24h | Permitido | Permitido |
| Contato nunca escreveu, ou faz mais de 24h | Recusado | Permitido |
409 com uma mensagem explicando o motivo — e nada é cobrado.
Você não recebe um erro genérico da Meta.
Para descobrir se a janela está aberta antes de tentar, consulte a conversa em
GET /api/contact-channels incluindo a relação da janela:
?with=["metaOpenedWindow"]. O campo expires_at indica até quando a
sessão é válida. Na dúvida, enviar um template é sempre seguro.
1) Listar templates aprovados
Devolve os templates aprovados pela Meta para o canal. Use o id
retornado como meta_waba_template_id no envio.
Headers obrigatórios
| Header | Valor | Descrição |
|---|---|---|
Authorization | Bearer SEU_ACCESS_TOKEN | Token de autenticação |
Accept | application/json | Tipo de resposta esperada |
Query params
| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
company_channel_id | integer | Sim | Id do canal oficial. Obtenha em GET /api/company-channels |
format dos components
(IMAGE, VIDEO ou DOCUMENT). Eles são enviados normalmente:
por padrão vai a mídia registrada no próprio template, e você pode trocá-la a cada disparo com
header_media_url. Veja Enviar template.
Exemplos de código
const response = await fetch(
'https://api.atys.pro/api/meta-waba-templates/for-channel?company_channel_id=700',
{
headers: {
'Authorization': 'Bearer SEU_ACCESS_TOKEN',
'Accept': 'application/json'
}
}
);
const { official, templates } = await response.json();
console.log(templates);
import requests
response = requests.get(
'https://api.atys.pro/api/meta-waba-templates/for-channel',
params={'company_channel_id': 700},
headers={
'Authorization': 'Bearer SEU_ACCESS_TOKEN',
'Accept': 'application/json'
}
)
data = response.json()
for template in data['templates']:
print(template['id'], template['name'])
<?php
$ch = curl_init('https://api.atys.pro/api/meta-waba-templates/for-channel?company_channel_id=700');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer SEU_ACCESS_TOKEN',
'Accept: application/json'
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
print_r($data['templates']);
<?php
// composer require guzzlehttp/guzzle
use GuzzleHttp\Client;
$client = new Client();
$response = $client->get('https://api.atys.pro/api/meta-waba-templates/for-channel', [
'headers' => [
'Authorization' => 'Bearer SEU_ACCESS_TOKEN',
'Accept' => 'application/json'
],
'query' => [
'company_channel_id' => 700
]
]);
$data = json_decode($response->getBody(), true);
foreach ($data['templates'] as $template) {
echo $template['id'], ' ', $template['name'], PHP_EOL;
}
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
)
func main() {
req, _ := http.NewRequest(
"GET",
"https://api.atys.pro/api/meta-waba-templates/for-channel?company_channel_id=700",
nil,
)
req.Header.Set("Authorization", "Bearer SEU_ACCESS_TOKEN")
req.Header.Set("Accept", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var data struct {
Official bool `json:"official"`
Templates []struct {
ID int `json:"id"`
Name string `json:"name"`
Language string `json:"language"`
} `json:"templates"`
}
json.Unmarshal(body, &data)
for _, t := range data.Templates {
fmt.Println(t.ID, t.Name, t.Language)
}
}
curl -X GET \
"https://api.atys.pro/api/meta-waba-templates/for-channel?company_channel_id=700" \
-H "Authorization: Bearer SEU_ACCESS_TOKEN" \
-H "Accept: application/json"
Resposta de sucesso
{
"official": true,
"templates": [
{
"id": 144,
"name": "address_update",
"language": "pt_BR",
"has_buttons": false,
"category": "UTILITY",
"value": {
"name": "address_update",
"language": "pt_BR",
"components": [
{ "type": "HEADER", "format": "TEXT", "text": "Atualização de endereço" },
{
"type": "BODY",
"text": "Olá {{first_name}}, seu endereço foi atualizado para {{address}}."
}
]
}
}
]
}
components para saber quais variáveis o template espera e em
que formato — é o que define como preencher o envio (veja
Variáveis do template).
O campo official confirma que o canal consultado é de WhatsApp Oficial. Vem
false, com a lista vazia e status 200, se o
company_channel_id for de outro tipo de canal — não é erro.
Erros possíveis
| Código | Mensagem | Causa |
|---|---|---|
400 | company_channel_id é obrigatório | Parâmetro ausente |
404 | Canal não encontrado | O canal não existe ou é de outra empresa |
403 | Sem permissão para acessar este recurso | O usuário não tem a permissão de visualizar templates |
2) Enviar mensagem de sessão
Texto ou arquivo livre, para conversas dentro da janela de 24h. É o caso típico de responder a quem acabou de escrever.
Headers obrigatórios
| Header | Valor | Descrição |
|---|---|---|
Authorization | Bearer SEU_ACCESS_TOKEN | Token de autenticação |
Content-Type | application/json ou multipart/form-data | multipart quando enviar arquivo |
Accept | application/json | Tipo de resposta esperada |
Parâmetros do body
| Parâmetro | Tipo | Obrigatório | Descrição | Exemplo |
|---|---|---|---|---|
platform | string | Sim | Use ofc_whatsapp para o canal oficial |
ofc_whatsapp |
sender_key | string | Sim | Número oficial da sua empresa, já conectado no Atys | 5521999999999 |
contact_key | string | Sim | Telefone do destinatário, com código do país | 5521988888888 |
text | string | Sim* | Texto da mensagem | Seu pedido saiu para entrega! |
file | file | Sim* | Arquivo (imagem, PDF, vídeo, áudio) | nota.pdf |
contact_is_group | boolean | Não | Marque true se o destino for um grupo. Default false |
false |
enqueue | boolean | Não | Default false (síncrono). Veja Resposta e status |
false |
text OU file. Fora da janela de 24h nenhum
dos dois é aceito — nesse caso use um template.
Exemplos de código
const response = await fetch('https://api.atys.pro/api/api-send-message', {
method: 'POST',
headers: {
'Authorization': 'Bearer SEU_ACCESS_TOKEN',
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
platform: 'ofc_whatsapp',
sender_key: '5521999999999',
contact_key: '5521988888888',
text: 'Seu pedido saiu para entrega!'
})
});
const data = await response.json();
// 409 = janela de 24h fechada: reenvie usando um template
if (response.status === 409) {
console.warn(data.message);
}
import requests
response = requests.post(
'https://api.atys.pro/api/api-send-message',
headers={
'Authorization': 'Bearer SEU_ACCESS_TOKEN',
'Accept': 'application/json'
},
json={
'platform': 'ofc_whatsapp',
'sender_key': '5521999999999',
'contact_key': '5521988888888',
'text': 'Seu pedido saiu para entrega!'
}
)
# 409 = janela de 24h fechada: reenvie usando um template
if response.status_code == 409:
print(response.json()['message'])
<?php
$ch = curl_init('https://api.atys.pro/api/api-send-message');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer SEU_ACCESS_TOKEN',
'Content-Type: application/json',
'Accept: application/json'
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'platform' => 'ofc_whatsapp',
'sender_key' => '5521999999999',
'contact_key' => '5521988888888',
'text' => 'Seu pedido saiu para entrega!'
]));
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// 409 = janela de 24h fechada: reenvie usando um template
print_r(json_decode($response, true));
<?php
// composer require guzzlehttp/guzzle
use GuzzleHttp\Client;
$client = new Client();
// http_errors => false para tratar o 409 sem lançar exceção
$response = $client->post('https://api.atys.pro/api/api-send-message', [
'headers' => [
'Authorization' => 'Bearer SEU_ACCESS_TOKEN',
'Accept' => 'application/json'
],
'json' => [
'platform' => 'ofc_whatsapp',
'sender_key' => '5521999999999',
'contact_key' => '5521988888888',
'text' => 'Seu pedido saiu para entrega!'
],
'http_errors' => false
]);
$data = json_decode($response->getBody(), true);
// 409 = janela de 24h fechada: reenvie usando um template
if ($response->getStatusCode() === 409) {
echo $data['message'];
}
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
func main() {
payload := map[string]any{
"platform": "ofc_whatsapp",
"sender_key": "5521999999999",
"contact_key": "5521988888888",
"text": "Seu pedido saiu para entrega!",
}
jsonData, _ := json.Marshal(payload)
req, _ := http.NewRequest(
"POST",
"https://api.atys.pro/api/api-send-message",
bytes.NewBuffer(jsonData),
)
req.Header.Set("Authorization", "Bearer SEU_ACCESS_TOKEN")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
// 409 = janela de 24h fechada: reenvie usando um template
if resp.StatusCode == http.StatusConflict {
var data struct {
Message string `json:"message"`
}
json.Unmarshal(body, &data)
fmt.Println(data.Message)
}
}
curl -X POST https://api.atys.pro/api/api-send-message \
-H "Authorization: Bearer SEU_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"platform": "ofc_whatsapp",
"sender_key": "5521999999999",
"contact_key": "5521988888888",
"text": "Seu pedido saiu para entrega!"
}'
3) Enviar template
Modelo previamente aprovado pela Meta. É a única forma de iniciar uma conversa ou de escrever fora da janela de 24h — e funciona dentro dela também.
Parâmetros adicionais
| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
meta_waba_template_id | integer | Sim | Id vindo de Listar templates. Substitui text |
variable_values | array | Não | Valores para variáveis posicionais ({{1}}, {{2}}), na ordem |
named_variable_values | object | Não | Valores para variáveis nomeadas ({{nome}}), por chave |
header_media_url | string | Não | Troca a mídia do cabeçalho neste envio. URL http ou https pública. Omitido, vale a mídia registrada no template |
header_media_filename | string | Não | Nome do arquivo mostrado ao destinatário. Só para cabeçalho DOCUMENT e só junto com header_media_url; sem ele, o nome é deduzido da URL |
Exemplos de código
// Template: "Olá, {{1}}, seu pedido {{2}} foi enviado."
await fetch('https://api.atys.pro/api/api-send-message', {
method: 'POST',
headers: {
'Authorization': 'Bearer SEU_ACCESS_TOKEN',
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
platform: 'ofc_whatsapp',
sender_key: '5521999999999',
contact_key: '5521988888888',
meta_waba_template_id: 143,
variable_values: ['Ana', 'A-1234']
})
});
# Template: "Olá {{first_name}}, seu endereço foi atualizado para {{address}}."
import requests
requests.post(
'https://api.atys.pro/api/api-send-message',
headers={
'Authorization': 'Bearer SEU_ACCESS_TOKEN',
'Accept': 'application/json'
},
json={
'platform': 'ofc_whatsapp',
'sender_key': '5521999999999',
'contact_key': '5521988888888',
'meta_waba_template_id': 144,
'named_variable_values': {
'first_name': 'Ana',
'address': 'Rua das Flores, 100'
}
}
)
<?php
$ch = curl_init('https://api.atys.pro/api/api-send-message');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer SEU_ACCESS_TOKEN',
'Content-Type: application/json',
'Accept: application/json'
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'platform' => 'ofc_whatsapp',
'sender_key' => '5521999999999',
'contact_key' => '5521988888888',
'meta_waba_template_id' => 143,
'variable_values' => ['Ana', 'A-1234'],
]));
print_r(json_decode(curl_exec($ch), true));
curl_close($ch);
<?php
// composer require guzzlehttp/guzzle
// Template: "Olá, {{1}}, seu pedido {{2}} foi enviado."
use GuzzleHttp\Client;
$client = new Client();
$response = $client->post('https://api.atys.pro/api/api-send-message', [
'headers' => [
'Authorization' => 'Bearer SEU_ACCESS_TOKEN',
'Accept' => 'application/json'
],
'json' => [
'platform' => 'ofc_whatsapp',
'sender_key' => '5521999999999',
'contact_key' => '5521988888888',
'meta_waba_template_id' => 143,
'variable_values' => ['Ana', 'A-1234'],
]
]);
print_r(json_decode($response->getBody(), true));
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
// Template: "Olá, {{1}}, seu pedido {{2}} foi enviado."
func main() {
payload := map[string]any{
"platform": "ofc_whatsapp",
"sender_key": "5521999999999",
"contact_key": "5521988888888",
"meta_waba_template_id": 143,
"variable_values": []string{"Ana", "A-1234"},
}
jsonData, _ := json.Marshal(payload)
req, _ := http.NewRequest(
"POST",
"https://api.atys.pro/api/api-send-message",
bytes.NewBuffer(jsonData),
)
req.Header.Set("Authorization", "Bearer SEU_ACCESS_TOKEN")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
}
curl -X POST https://api.atys.pro/api/api-send-message \
-H "Authorization: Bearer SEU_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"platform": "ofc_whatsapp",
"sender_key": "5521999999999",
"contact_key": "5521988888888",
"meta_waba_template_id": 143,
"variable_values": ["Ana", "A-1234"]
}'
Variáveis do template
Um template pode ter variáveis de dois formatos, e o formato é definido quando o template é
criado. Descubra qual usar lendo os components em
Listar templates.
| Formato | No template | Como enviar |
|---|---|---|
| Posicional | Olá, {{1}}, pedido {{2}} |
"variable_values": ["Ana", "A-1234"] — na ordem |
| Nomeado | Olá {{first_name}} |
"named_variable_values": {"first_name": "Ana"} |
_ são reservadas e ignoradas em
named_variable_values. Elas são o canal interno da mídia do cabeçalho — para
trocá-la, use o campo header_media_url, que é validado.
Mídia do cabeçalho
Templates com cabeçalho IMAGE, VIDEO ou DOCUMENT têm uma
mídia registrada na Meta quando o template é criado. Você tem duas opções:
| Situação | O que enviar | Resultado |
|---|---|---|
| Mídia fixa | Nada — omita header_media_url |
Vai a imagem/vídeo/documento registrado no template |
| Mídia por envio | "header_media_url": "https://..." |
Vai o arquivo que você indicou, só neste disparo |
A URL precisa ser http ou https e estar acessível publicamente — nós
baixamos o arquivo e o repassamos à Meta. Se você não tem onde hospedá-lo, use o endpoint de
upload abaixo e envie a URL que ele devolve.
curl -X POST https://back1.atys.pro/api/meta-waba-templates/upload-send-media \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-F "meta_waba_id=900" \
-F "file=@fatura.pdf"
# resposta
{ "data": {
"url": "https://...",
"filename": "fatura.pdf",
"expires_at": "2026-08-26T00:19:26-03:00"
} }
Aceita image/jpeg, image/png, image/webp,
video/mp4, video/3gpp e application/pdf, dentro dos
limites do WhatsApp: 5 MB para imagem, 16 MB para vídeo e
100 MB para documento. O tipo precisa combinar com o format do
cabeçalho do template.
expires_at da resposta traz o prazo. Suba o arquivo e dispare em
seguida — não guarde a URL para reusar depois. Depois de enviado, o arquivo deixa de ter prazo e
acompanha a mensagem normalmente.
header_media_filename define o nome que o
destinatário vê no WhatsApp, e só vale acompanhado de
header_media_url — quando a mídia é a fixa do template, quem manda é o nome
registrado nele. Sem header_media_filename, o nome é deduzido da URL.
Resposta e status
Com enqueue=false (o default) a requisição aguarda o envio real e devolve a resposta
da Meta:
{
"Status": true,
"Message": "OK",
"Data": {
"ExternalID": "wamid.HBgNNTUyMTk2OTAyMzYyMBUCABEYEjk1RjJGQkFG..."
}
}
A resposta traz ainda Data.raw com o retorno bruto da Meta. É material de
diagnóstico e não faz parte do contrato — não construa lógica em cima dele.
Use Data.ExternalID.
Guarde o ExternalID (o wamid): é o identificador da mensagem no WhatsApp e
o que permite correlacionar os recibos posteriores. A evolução do status é:
| Status | Significado |
|---|---|
| Enviada | A Meta aceitou o pedido de envio |
| Entregue | Chegou ao aparelho do destinatário |
| Lida | O destinatário abriu a conversa |
| Falha | A Meta recusou a entrega (número inválido, bloqueio, opt-out…) |
Com enqueue=true a resposta é imediata e confirma apenas que a mensagem foi aceita
para processamento — o envio acontece em segundo plano:
{
"message": "Whatsapp message enqueued successfully"
}
Erros comuns
409 Janela de 24 horas fechada
Causa: tentou enviar texto ou arquivo livre para uma conversa cujo contato não escreve há mais de 24 horas (ou nunca escreveu).
Solução: reenvie usando meta_waba_template_id. Nada foi cobrado nem enviado.
{
"status": false,
"message": "A janela de 24 horas desta conversa está fechada. Fora dela o
WhatsApp Oficial só aceita mensagens de template — envie
meta_waba_template_id."
}
404 Template não encontrado
Causa: o meta_waba_template_id não existe ou pertence a outra empresa.
Solução: use um id devolvido por Listar templates com o seu próprio token.
422 Template não aprovado ou mídia acima do limite
Causa: o template não foi aprovado pela Meta, ou a mídia do cabeçalho excede o limite do WhatsApp (5 MB para imagem, 16 MB para vídeo, 100 MB para documento).
Solução: confira o status do template em Listar templates — só os aprovados podem ser disparados. Se a causa for o tamanho, comprima o arquivo ou aponte header_media_url para uma versão menor.
400 sender_key not found
Causa: o número informado em sender_key não está cadastrado como canal de WhatsApp Oficial na sua conta.
Solução: confira o número em Configurações → Canais. Ele precisa estar conectado e verificado junto à Meta.
502 Failed to send message
Causa: a Meta recusou o envio. O campo details traz o motivo original.
Solução: causas frequentes são número sem WhatsApp, contato que bloqueou a empresa, ou limite de mensagens de marketing atingido para aquele contato.
400 Parâmetros obrigatórios
Causa: falta algum campo obrigatório. As mensagens possíveis são:
Platform is required—platformvazio ou ausenteSender key is required/Contact key is requiredText, file or meta_waba_template_id is required— nenhum conteúdo informadometa_waba_template_id só é aceito com platform: ofc_whatsapp— template enviado a um canal não oficial
429 Too Many Requests
Causa: o limite deste endpoint é de 600 requisições por minuto por empresa (não por token — todos os seus usuários API somam no mesmo limite).
Solução: espalhe os disparos ao longo do minuto. Para volumes grandes e programados, considere enqueue=true.
509 Cota de envios esgotada
Causa: a cota de envios do seu plano acabou.
Solução: fale com o atendimento Atys para ampliar o plano. A resposta é texto puro, não JSON.
401 Unauthenticated
Causa: token inválido, expirado ou ausente.
Solução: obtenha um novo token em Autenticação.
Limitações
- Fora da janela de 24h, só template.
- Templates precisam de aprovação prévia da Meta.
- Há limite diário de destinatários únicos, conforme o nível do seu número.
- A qualidade do número cai se houver muitos bloqueios ou denúncias.
- Conectar números e criar templates (feito na plataforma).
- Editar ou apagar mensagens já enviadas.
- Verificar se um número tem WhatsApp.