Conecte números do WhatsApp Business oficial (Meta Cloud API), envie e receba mensagens, dispare campanhas por template e cadastre webhooks para receber os eventos em tempo real na sua plataforma.
Gere um token em Configurações → Tokens de API. Envie-o em todas as
chamadas (exceto /health) no header Authorization. A URL base é
https://SEU_DOMINIO/api/v1.
curl https://SEU_DOMINIO/api/v1/health
curl https://SEU_DOMINIO/api/v1/channels \
-H "Authorization: Bearer noz_seu_token_aqui"
Para o WhatsApp oficial não há QR: você provisiona o número com as credenciais
da Meta (Graph API). O Noz guarda o access_token e o app_secret
criptografados e valida a credencial na hora. Use o verify_token e a
webhook.url retornados para configurar o webhook no App da Meta.
POST /channels/official — cria e configura o número oficial
GET /channels/{id}/official — config + dados do webhook
PATCH /channels/{id}/official — atualiza credenciais
POST /channels/{id}/verify — re-testa a credencial
GET /channels/{id}/templates — templates aprovados do número
# cria o número oficial (Cloud API)
curl -X POST https://SEU_DOMINIO/api/v1/channels/official \
-H "Authorization: Bearer noz_seu_token" -H "Content-Type: application/json" \
-d '{
"name":"Vendas oficial",
"phone_number_id":"123456789012345",
"waba_id":"098765432109876",
"access_token":"EAAG...",
"app_secret":"a1b2c3...",
"verify_token":"meu-verify-token"
}'
# => { "data": { "id": 12, "official": true, "status": "WORKING", ... },
# "verification": { "ok": true, "display_phone_number": "+55 11 9...", "verified_name": "..." } }
# consulta a config + como apontar o webhook na Meta
curl https://SEU_DOMINIO/api/v1/channels/12/official \
-H "Authorization: Bearer noz_seu_token"
# => { "data": { ..., "webhook": { "url": "https://SEU_DOMINIO/webhooks/whatsapp", "verify_token": "meu-verify-token", "fields": ["messages"] } } }
Depois de conectado, envie mensagens 1-a-1 com POST /messages (informe o
channel_id do número) ou dispare em massa por template com as
campanhas oficiais. Os verify_token e
app_secret são por número — a assinatura do webhook (X-Hub-Signature-256)
é validada por canal automaticamente.
O jeito mais rápido de conectar um número oficial é o Embedded Signup: dentro da
plataforma, em Canais → Conectar oficial (Meta), o usuário clica, um popup da Meta
abre, ele seleciona/cria a conta e o número, e o número já entra conectado — sem
colar credenciais. Nos bastidores a plataforma troca o code por um token permanente,
assina os webhooks da WABA e registra o número.
Para isso funcionar, o App da Meta precisa de um webhook único de app (diferente do webhook por número). Configure em App → WhatsApp → Configuração → Webhook:
| Callback URL | https://SEU_DOMINIO/webhooks/whatsapp |
| Verify token | o valor de META_VERIFY_TOKEN no servidor |
| Campos (subscribe) | messages, message_template_status_update, phone_number_quality_update, account_update |
O que a plataforma faz com cada evento recebido da Meta:
| Evento | Ação no Noz |
|---|---|
messages | Registra a mensagem recebida / status de entrega (ack) — Inbox e campanhas. |
phone_number_quality_update | Atualiza a qualidade do número (GREEN/YELLOW/RED) e o tier. RED pausa as campanhas daquele número (circuit breaker anti-spam) até recuperar. |
message_template_status_update | Sincroniza o status de aprovação do template na biblioteca de campanhas. |
account_update | Ban/restrição da WABA → marca os números para atenção. |
Variáveis de ambiente no servidor (o app_id e o config_id já têm default):
# App ABNR BM2 (Tech Provider)
META_APP_SECRET= # obrigatório: troca o code + valida assinatura
META_VERIFY_TOKEN= # obrigatório: verificação do webhook do app
META_API_VERSION=v25.0 # recomendado (default do código: v21)
META_APP_ID= # obrigatório: vai no front (Embedded Signup)
META_CONFIG_ID= # obrigatório: vai no front (FB.login)
No painel do App também é preciso liberar o domínio da plataforma (App Domains + domínios do
Facebook Login JS SDK), senão o popup do Embedded Signup não abre. Alternativa headless (sem popup):
provisionar por API com POST /channels/official.
POST /messages. Informe to (número)
ou chat_id. O channel_id é opcional (usa o primeiro número conectado).
Além de text, aceita template (HSM — inicia conversa fora da
janela de 24h) e mídia por URL. A resposta traz wa_message_id
(wamid da Meta) — é por ele que os message.ack chegam.
# texto livre (dentro da janela de 24h)
curl -X POST https://SEU_DOMINIO/api/v1/messages \
-H "Authorization: Bearer noz_seu_token" \
-H "Content-Type: application/json" \
-d '{"to":"5511999999999","text":"Olá!","channel_id":7}'
# template aprovado (params posicionais do corpo + sufixo do botão de URL dinâmica)
curl -X POST https://SEU_DOMINIO/api/v1/messages \
-H "Authorization: Bearer noz_seu_token" \
-H "Content-Type: application/json" \
-d '{
"to":"5511999999999","channel_id":7,
"template":{"name":"minha_notificacao","language":"pt_BR",
"params":["João","Sua fatura venceu"],"button_param":"cliente/faturas"},
"preview_text":"Olá, João. Sua fatura venceu."
}'
# mídia por URL pública (o Noz baixa e sobe pra Meta; caption opcional)
curl -X POST https://SEU_DOMINIO/api/v1/messages \
-H "Authorization: Bearer noz_seu_token" \
-H "Content-Type: application/json" \
-d '{"to":"5511999999999","channel_id":7,
"media":{"type":"image","url":"https://exemplo.com/foto.jpg"},"caption":"Segue a foto"}'
Cadastre a URL da sua plataforma por número. O Noz fará POST
no seu endpoint a cada evento assinado. O secret retornado é mostrado
uma única vez — guarde-o para validar as assinaturas.
POST /channels/{id}/webhooks — cadastra
GET /channels/{id}/webhooks — lista
POST /webhooks/{id}/test — dispara um "ping"
GET /webhooks/{id}/deliveries — histórico de entregas
curl -X POST https://SEU_DOMINIO/api/v1/channels/7/webhooks \
-H "Authorization: Bearer noz_seu_token" \
-H "Content-Type: application/json" \
-d '{
"url": "https://minha-plataforma.com/webhooks/noz",
"events": ["message.received","message.ack","session.status"]
}'
# => { "data": { "id": 3, "secret": "whsec_...", ... } } (secret só aparece aqui)
A entrega é assíncrona e com retentativas (backoff). Responda 2xx rapidamente;
qualquer outro status agenda uma nova tentativa até esgotar.
Cada requisição traz X-Noz-Signature: sha256=<hex>, um HMAC-SHA256 do
corpo cru usando o seu secret. Compare em tempo constante.
// Node.js (Express)
const crypto = require('crypto');
app.post('/webhooks/noz', express.raw({ type: '*/*' }), (req, res) => {
const signature = req.header('X-Noz-Signature') || '';
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.NOZ_WEBHOOK_SECRET)
.update(req.body) // Buffer cru
.digest('hex');
const ok = signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
if (!ok) return res.sendStatus(401);
const event = JSON.parse(req.body.toString());
// ... trate event.event / event.data
res.sendStatus(200);
});
// PHP
$raw = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $raw, getenv('NOZ_WEBHOOK_SECRET'));
if (!hash_equals($expected, $_SERVER['HTTP_X_NOZ_SIGNATURE'] ?? '')) {
http_response_code(401); exit;
}
$event = json_decode($raw, true);
Envelope comum a todos os eventos:
{
"event": "message.received",
"delivery_id": 987,
"created_at": "2026-08-02T12:00:00+00:00",
"channel": { "id": 7, "name": "Vendas", "phone": "5511999999999", "meta_phone_number_id": "1234567890" },
"data": { ... } // específico do evento
}
| Evento | Quando | data |
|---|---|---|
message.received | Mensagem recebida de um contato | conversation_id, contact, message |
message.sent | Mensagem enviada (API/bot/atendente) | conversation_id, contact, message |
message.ack | Status de entrega/leitura mudou | message_id, waha_message_id, ack, ack_label |
template.status | A Meta aprovou/rejeitou um template do canal | name, event, status, reason |
session.status | Número conectou/desconectou | status, previous_status, phone |
Mídia recebida vem em message.media_src — baixe com
GET /api/v1/messages/{id}/media usando seu token.
Gerencie páginas da sua wiki por API — inclusive por um agente de IA. Cada página tem
categoria, visibilidade (privada/pública) e pode ser marcada
como conhecimento do bot. Páginas públicas ganham uma URL amigável
/w/{conta}/{página}. O corpo é Markdown; envie JSON em
POST/PUT.
GET /wiki — lista as páginas
GET /wiki/categories — lista as categorias
GET /wiki/{id} — detalha (com o Markdown)
POST /wiki — cria uma página
PUT /wiki/{id} — atualiza (parcial)
DELETE /wiki/{id} — remove
curl -X POST https://SEU_DOMINIO/api/v1/wiki \
-H "Authorization: Bearer noz_xxx" \
-H "Content-Type: application/json" \
-d '{"title":"Política de trocas","content":"## Trocas\n\nVocê tem **30 dias**.","visibility":"public","bot_knowledge":true}'
Disparo em massa por template aprovado (HSM) em canais oficiais. Suporta marketing e utility, variáveis por coluna da planilha e distribuição por vários números. Guardrails por canal (cap diário de campanha, cadência e aquecimento) são aplicados automaticamente — ao atingir o limite, o canal para até o dia seguinte (salvo autorização de um owner/admin).
POST /templates — cria e (opcional) submete à Meta
POST /templates/{id}/sync — sincroniza o status de aprovação
POST /campaigns — cria a campanha (por número via channels)
POST /campaigns/{id}/recipients — adiciona destinatários (com vars)
POST /campaigns/{id}/start — enfileira e dispara
GET /campaigns/{id} — progresso (entregues/lidas/falhas)
# 1) cria um template oficial: {{1}} = coluna "nome" da planilha
curl -X POST https://SEU_DOMINIO/api/v1/templates \
-H "Authorization: Bearer noz_xxx" -H "Content-Type: application/json" \
-d '{
"name":"boas_vindas","kind":"official","category":"marketing","language":"pt_BR",
"body":"Oi {{1}}! Sua compra {{2}} foi confirmada.",
"var_map":{"1":{"field":"nome","default":"cliente"},"2":{"field":"pedido","default":""}},
"submit_channels":[7]
}'
# 2) cria a campanha por template, em 2 números (peso distribui a carga)
# var_map liga cada variável a uma COLUNA da audiência: "1".."n" no corpo e
# "btn:0" no sufixo do botão de URL (sem ele o link sai com o exemplo do layout)
curl -X POST https://SEU_DOMINIO/api/v1/campaigns \
-H "Authorization: Bearer noz_xxx" -H "Content-Type: application/json" \
-d '{"name":"Julho","category":"marketing","template_id":10,
"var_map":{"1":{"field":"nome"},"btn:0":{"field":"link"}},
"channels":[{"channel_id":7,"weight":2},{"channel_id":8,"weight":1}]}'
# 3) adiciona destinatários com as variáveis por linha
curl -X POST https://SEU_DOMINIO/api/v1/campaigns/15/recipients \
-H "Authorization: Bearer noz_xxx" -H "Content-Type: application/json" \
-d '{"recipients":[{"phone":"5511999999999","vars":{"nome":"Ana","pedido":"1234"}}]}'
# 4) inicia o disparo
curl -X POST https://SEU_DOMINIO/api/v1/campaigns/15/start -H "Authorization: Bearer noz_xxx"
O acompanhamento de entrega/leitura chega pelos mesmos webhooks (message.ack) e é
refletido em GET /campaigns/{id}.stats. Falhas podem ser reenviadas — inclusive
trocando o canal — com POST /campaigns/{id}/resend (target_channel_id).
Um grupo é um pool de números (ex.: "PicPay"). A integração escolhe o grupo em vez de número por número: lista os layouts aprovados em todos os números do grupo, cria a campanha com os números dele (e dispara na hora) ou envia uma mensagem avulsa pelo número com mais folga. O grupo pode ter um limite próprio em 24h (soma dos números) — ex.: 3 números de 250 limitados em 200 —, respeitado nos disparos e na API (HTTP 429 quando esgota).
GET /groups — grupos, limite, enviado em 24h, disponível agora e layouts em comum
GET /groups/{id} — o grupo com os números (saúde e folga de cada um)
GET /groups/{id}/templates — layouts aprovados em TODOS os números (com nº de variáveis)
POST /groups/{id}/campaigns — cria a campanha com os números do grupo e dispara (start, padrão true)
POST /groups/{id}/messages — mensagem avulsa pelo melhor número do grupo
# 1) quais layouts dá para usar no grupo 3 (aprovados em todos os números)
curl https://SEU_DOMINIO/api/v1/groups/3/templates -H "Authorization: Bearer noz_xxx"
# 2) campanha pelo grupo, disparando agora
# mode: "all" (todos os números juntos, padrão) | "sequential" (um por vez, avança sozinho)
curl -X POST https://SEU_DOMINIO/api/v1/groups/3/campaigns \
-H "Authorization: Bearer noz_xxx" -H "Content-Type: application/json" \
-d '{"name":"Recuperação outubro","template_id":10,"mode":"all",
"var_map":{"1":{"field":"nome"}},
"recipients":[{"phone":"5511999999999","vars":{"nome":"Ana"}}]}'
# agendar: "scheduled_at":"2026-10-05T09:00:00" · só criar: "start":false
# 3) mensagem avulsa (transacional) pelo número com mais folga do grupo
curl -X POST https://SEU_DOMINIO/api/v1/groups/3/messages \
-H "Authorization: Bearer noz_xxx" -H "Content-Type: application/json" \
-d '{"to":"5511999999999","template_id":10,"params":["Ana"]}'
Layout que falta em algum número devolve 422 com a lista missing (replique em Grupos → Replicar).
A campanha criada é uma campanha comum: acompanhe por GET /campaigns/{id} e pelos webhooks.
Dois níveis, com os mesmos filtros: o resumo (uma linha por campanha) e o
detalhe (uma linha por mensagem, com destinatário, número usado, carimbos de envio/entrega/leitura
e o erro que a Meta devolveu). Em qualquer um deles, ?format=csv baixa o arquivo
em vez do JSON — e o CSV do detalhe traz o filtro inteiro, não só a página.
GET /reports/campaigns — resumo por campanha + totais
GET /reports/campaign-messages — detalhe mensagem a mensagem
Filtros: de, ate (YYYY-MM-DD), campanha, canal,
template, audiencia, status
(queued, sending, sent, delivered,
read, failed, skipped) e q (telefone ou nome).
No detalhe também limit (máx. 1000) e offset.
# resumo do mês, todas as campanhas
curl "https://SEU_DOMINIO/api/v1/reports/campaigns?de=2026-09-01&ate=2026-09-30" \
-H "Authorization: Bearer noz_xxx"
# só as falhas de uma campanha, detalhe por mensagem
curl "https://SEU_DOMINIO/api/v1/reports/campaign-messages?campanha=15&status=failed&limit=500" \
-H "Authorization: Bearer noz_xxx"
# o mesmo em CSV (abre no Excel: separador ";" e BOM)
curl -L -o falhas.csv \
"https://SEU_DOMINIO/api/v1/reports/campaign-messages?campanha=15&status=failed&format=csv" \
-H "Authorization: Bearer noz_xxx"
delivered e read não são status guardados: são carimbos de hora que a
Meta manda depois, e o filtro trata os dois assim. Por isso "entregues" pode ser maior que o
número de linhas com status = sent.