N
Noz API

Integre sua plataforma ao WhatsApp

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.

1. Autenticação

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"

2. Conectar um número (WhatsApp Cloud API)

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.

2b. Embedded Signup + webhook do App

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 URLhttps://SEU_DOMINIO/webhooks/whatsapp
Verify tokeno 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:

EventoAção no Noz
messagesRegistra a mensagem recebida / status de entrega (ack) — Inbox e campanhas.
phone_number_quality_updateAtualiza 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_updateSincroniza o status de aprovação do template na biblioteca de campanhas.
account_updateBan/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.

3. Enviar mensagens

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"}'

4. Receber mensagens (webhooks)

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.

5. Validar a assinatura

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);

6. Eventos & payloads

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
}
EventoQuandodata
message.receivedMensagem recebida de um contatoconversation_id, contact, message
message.sentMensagem enviada (API/bot/atendente)conversation_id, contact, message
message.ackStatus de entrega/leitura mudoumessage_id, waha_message_id, ack, ack_label
template.statusA Meta aprovou/rejeitou um template do canalname, event, status, reason
session.statusNúmero conectou/desconectoustatus, previous_status, phone

Mídia recebida vem em message.media_src — baixe com GET /api/v1/messages/{id}/media usando seu token.

7. Wiki (base de conhecimento)

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}'

8. Campanhas (template oficial)

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

8.1 Grupos de envio (disparo pelo grupo)

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.

9. Relatórios de campanha

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.