Antes de abrir o editor, vale gastar dez minutos no terminal. Um curl que devolve 200 prova três coisas de uma vez: o token está certo, o número está conectado e o corpo tem o formato que a API espera. Quando o mesmo envio falha depois dentro do seu código, você já sabe que o problema não é a API — é a serialização, o header ou a variável de ambiente que não chegou.
Esta página é o roteiro dessa checagem, e serve também como referência de HTTP puro para quem integra de uma linguagem sem tutorial aqui. Todo comando é colável, e o que muda entre sistemas está marcado. A base é https://api.zapon.dev e a autenticação é o header token.
O que você precisa antes do primeiro comando
O que preciso para testar a API do zapon no terminal?
O curl, que já vem instalado em Linux, macOS e Windows 10 ou superior, uma conta no zapon e um número conectado. Da conexão sai o token, que vai no header de toda chamada — um token por número. O jq é opcional, mas facilita muito: ele monta JSON com escape correto e lê a resposta sem você contar chaves.
- curl. Confira com
curl --version. Qualquer versão dos últimos anos serve; a flag --fail-with-body, citada mais adiante, existe do 7.76 em diante. - Conexão e token. No painel, crie a conexão e leia o QR Code com o WhatsApp do número (Aparelhos conectados → Conectar aparelho). Copie o
token: ele autentica as chamadas daquele número e só daquele. - jq (opcional). Está nos gerenciadores de pacote das distribuições e do macOS, e resolve o problema mais chato do terminal: montar JSON com acento e aspas sem quebrar nada.
Como não deixar o token gravado no histórico do shell?
Colar o token direto no comando o deixa no ~/.bash_history para sempre, e histórico vaza em captura de tela, sessão compartilhada e backup. Exporte a variável com um espaço antes do comando, ou leia de um arquivo com permissão restrita:
token fora do histórico
# o espaço inicial evita o histórico (com HISTCONTROL=ignorespace no bash)
export ZAPON_TOKEN='cole_aqui_o_token_da_conexao'
# ou, melhor, num arquivo só seu
printf '%s' 'cole_aqui_o_token_da_conexao' > ~/.zapon-token
chmod 600 ~/.zapon-token
export ZAPON_TOKEN="$(cat ~/.zapon-token)"
A autenticação é um header só, de nome token. Não é Authorization, não é Bearer e não há OAuth. Por isso a flag -u do curl não tem uso aqui: o token vai em -H "token: ...", e mais nada.
Primeiro envio de texto
Como enviar uma mensagem de WhatsApp com curl?
Um POST para https://api.zapon.dev/chat/send/text com o header token, Content-Type: application/json e um corpo com dois campos: Phone, o número só com dígitos incluindo país e DDD, e Body, o texto. A resposta traz data.Id, o identificador da mensagem no WhatsApp.
primeiro envio
curl -sS -X POST https://api.zapon.dev/chat/send/text \
-H "token: $ZAPON_TOKEN" \
-H "Content-Type: application/json" \
--connect-timeout 10 --max-time 25 \
-d '{
"Phone": "5511999999999",
"Body": "Pedido 4821 confirmado. Assim que o pacote sair, mandamos o rastreio por aqui."
}'
O par -sS vale a explicação: -s cala a barra de progresso, que polui a saída em comandos encadeados, e -S devolve a mensagem de erro que o -s teria escondido — usar só -s é desligar o alarme de incêndio. Os dois prazos também são deliberados: --connect-timeout limita a conexão e --max-time limita a chamada inteira, então nenhum comando fica pendurado dentro de um script.
A resposta de sucesso tem sempre este formato:
resposta da API
{
"code": 200,
"success": true,
"data": {
"Details": "Sent",
"Id": "90B2F8B13FAC8A9CF6B06E99C7834DC5",
"Timestamp": "2026-09-03T09:12:08-03:00"
}
}
Por que a mensagem com apóstrofo quebra o comando?
Porque o corpo depois de -d está entre aspas simples, e um apóstrofo no texto fecha a string no meio: d'água derruba o comando com erro de sintaxe do shell, não da API. A mesma armadilha aparece com $, que dentro de aspas duplas vira expansão de variável. A saída limpa é não montar JSON à mão — o jq escapa tudo, inclusive quebra de linha e emoji.
montando o corpo com jq, sem medo de aspas
TEXTO="Seu pedido d'água mineral saiu para entrega. Custo: R\$ 0,00 no frete."
jq -n --arg p "5511999999999" --arg b "$TEXTO" \
'{Phone: $p, Body: $b}' \
| curl -sS -X POST https://api.zapon.dev/chat/send/text \
-H "token: $ZAPON_TOKEN" \
-H "Content-Type: application/json" \
--data-binary @-
O --data-binary @- lê o corpo da entrada padrão. Prefira-o a -d @-: o -d remove quebras de linha, o que é inofensivo em JSON compacto mas destrói uma mensagem de várias linhas carregada de um arquivo. E guardar o corpo em disco resolve de vez o problema de aspas:
corpo em arquivo — funciona igual em qualquer sistema
cat > corpo.json <<'JSON'
{
"Phone": "5511999999999",
"Body": "Bom dia! Sua consulta está confirmada para amanhã às 14h."
}
JSON
curl -sS -X POST https://api.zapon.dev/chat/send/text \
-H "token: $ZAPON_TOKEN" \
-H "Content-Type: application/json" \
--data-binary @corpo.json
As aspas simples no delimitador <<'JSON' impedem que o shell expanda qualquer $ dentro do bloco — detalhe que evita ver R$ 89,90 chegar como R no WhatsApp. Salve o arquivo em UTF-8: um corpo.json em ISO-8859-1 manda acento quebrado, e a API não tem como adivinhar.
Como ver o código HTTP e o corpo ao mesmo tempo?
Por padrão o curl imprime só o corpo e devolve 0 como código de saída mesmo em 401 — o que faz um script achar que deu tudo certo. A flag -w resolve, e é a que mais vale a pena decorar:
corpo e status na mesma saída
curl -sS -X POST https://api.zapon.dev/chat/send/text \
-H "token: $ZAPON_TOKEN" -H "Content-Type: application/json" \
-d '{"Phone":"5511999999999","Body":"teste"}' \
-w '\n--- HTTP %{http_code} em %{time_total}s\n'
Para diagnosticar a fundo, troque por -i (headers da resposta) ou -v (a conversa inteira, inclusive o header token — nunca cole a saída de um -v num chamado de suporte sem apagar essa linha). E há a dupla --fail e --fail-with-body: as duas fazem o curl sair com código diferente de zero em 4xx e 5xx, mas só a segunda imprime a explicação. Em script, use sempre a segunda.
Imagem e botões
Como enviar uma imagem pela API do zapon com curl?
POST /chat/send/image com Phone, Image e, opcionalmente, Caption. O campo Image é o arquivo em base64 no formato data URI — data:image/jpeg;base64,.... No terminal, isso é o comando base64 mais um jq para montar o JSON, porque a string codificada é grande demais para caber numa linha de comando confortável.
enviando uma imagem
# Linux: -w0 evita quebra de linha a cada 76 caracteres
B64=$(base64 -w0 etiqueta-4821.jpg)
# macOS e BSD: a flag -w não existe; lá o padrão já é linha única
# B64=$(base64 -i etiqueta-4821.jpg)
jq -n --arg img "data:image/jpeg;base64,$B64" \
'{Phone: "5511999999999", Image: $img, Caption: "Etiqueta do pedido 4821."}' \
| curl -sS -X POST https://api.zapon.dev/chat/send/image \
-H "token: $ZAPON_TOKEN" -H "Content-Type: application/json" \
--data-binary @- -w '\nHTTP %{http_code}\n'
A diferença entre o base64 do GNU e o do BSD é a pegadinha número um deste envio: no Linux, sem -w0, a saída vem quebrada em várias linhas e o data URI fica inválido; no macOS, a flag -w0 nem existe. E lembre que base64 cresce o arquivo em cerca de um terço — uma foto de 5 MB vira quase 7 MB de corpo, então redimensione antes de testar com a imagem original da câmera.
Como mandar botões de resposta rápida?
O POST /chat/send/buttons vai com quantos botões você precisar — três ou quatro é o que se lê num toque —, cada um com um id definido por você e um text de no máximo 20 caracteres — acima disso o WhatsApp trunca o rótulo. O id é o que volta inteiro no webhook, então carimbe nele a chave do seu registro. Além da resposta rápida, o mesmo endpoint monta botão que abre um link ("type": "cta_url"), botão que abre o discador ("type": "cta_call") e botão que copia um código ("type": "copy") — e os tipos convivem na mesma mensagem. O toque em resposta rápida volta no webhook; os outros três agem no aparelho de quem recebe.
botoes.json — corpo pronto para colar
cat > botoes.json <<'JSON'
{
"Phone": "5511999999999",
"Body": "Pedido 4821 sai para entrega hoje. Tem alguém para receber à tarde?",
"Footer": "Loja Exemplo",
"Buttons": [
{ "id": "ped4821|hoje", "text": "Pode entregar" },
{ "id": "ped4821|amanha", "text": "Prefiro amanhã" },
{ "id": "ped4821|humano", "text": "Falar com alguém" }
]
}
JSON
curl -sS -X POST https://api.zapon.dev/chat/send/buttons \
-H "token: $ZAPON_TOKEN" -H "Content-Type: application/json" \
--data-binary @botoes.json -w '\nHTTP %{http_code}\n'
Antes de acusar a API de um 400, valide o arquivo com jq . botoes.json: vírgula sobrando no fim de um array é o erro mais comum em corpo escrito à mão, e o jq aponta a linha exata. Sem o carimbo no id, a resposta chega só com o telefone; com ele, o seu código separa por | e vai direto ao registro. Para mais de três opções, o caminho é o /chat/send/list.
Verificar o número antes de enviar
Como saber se um número tem WhatsApp antes de disparar?
POST /user/check recebe um array em Phone e devolve, para cada número, o campo IsInWhatsapp. No terminal, isso vira um comando só, e o jq filtra a saída direto para um arquivo de números válidos.
verificando e separando a lista
curl -sS -X POST https://api.zapon.dev/user/check \
-H "token: $ZAPON_TOKEN" -H "Content-Type: application/json" \
-d '{"Phone":["5511999999999","5511888888888","551133334444"]}' \
| tee resultado.json \
| jq -r '.data.Users[] | select(.IsInWhatsapp) | .Query' > validos.txt
# quem ficou de fora, para você corrigir o cadastro
jq -r '.data.Users[] | select(.IsInWhatsapp | not) | .Query' resultado.json > sem-whatsapp.txt
O tee guarda a resposta bruta enquanto o jq filtra, o que evita repetir a chamada quando você percebe que precisava da outra metade da lista. Rodar essa verificação antes de qualquer disparo evita gastar chamada com telefone fixo e cadastro velho — e uma sequência de envios para números inexistentes é justamente o padrão que aumenta o risco do seu número.
Receber mensagens: testando o webhook
Como testar o webhook do WhatsApp sem publicar nada?
Suba um receptor na sua máquina, exponha a porta com um túnel HTTP — qualquer serviço que dê uma URL pública apontando para a sua porta local serve — e cadastre essa URL na conexão, escolhendo os eventos. Cada evento vira um POST com a mensagem inteira no corpo, e você vê tudo chegando no terminal.
Para só olhar o que chega, um receptor de três linhas em Python resolve. Ele responde 200 na hora e imprime o corpo, que é exatamente o que você quer nessa fase:
receptor mínimo para inspecionar eventos
python3 - <<'PY'
from http.server import BaseHTTPRequestHandler, HTTPServer
class H(BaseHTTPRequestHandler):
def do_POST(self):
n = int(self.headers.get('Content-Length', 0))
corpo = self.rfile.read(n)
self.send_response(200); self.end_headers() # responda primeiro
print(self.headers.get('User-Agent'), corpo.decode('utf-8', 'replace'), flush=True)
HTTPServer(('0.0.0.0', 8080), H).serve_forever()
PY
Com o servidor no ar, abra o túnel para a porta 8080, copie a URL pública e cadastre-a na conexão, no painel. Marque só os eventos que interessam — começar por Message já cobre um fluxo de atendimento. Mande uma mensagem do seu celular para o número conectado e o evento aparece no terminal em segundos.
Qual é o formato do evento de mensagem recebida?
É o que o receptor acima imprime: o tipo do evento e a mensagem inteira. Uma resposta escrita chega assim:
payload do evento Message
{
"type": "Message",
"event": {
"Info": {
"ID": "3EB0C767D26A1B5F7C83",
"Chat": "[email protected]",
"Sender": "[email protected]",
"IsFromMe": false,
"IsGroup": false,
"PushName": "Ana Souza",
"Timestamp": "2026-09-03T18:04:22-03:00"
},
"Message": { "conversation": "pode entregar sim" }
}
}
// quando ele toca num botão, muda só o bloco Message:
"Message": { "buttonsResponseMessage": {
"selectedButtonID": "ped4821|hoje",
"Response": { "SelectedDisplayText": "Pode entregar" }
} }
Como simular um evento para testar o meu endpoint?
Salvando o payload num arquivo e mandando um POST para o seu próprio serviço. Isso separa dois problemas que costumam ser depurados juntos: se o seu endpoint trata o evento e se o túnel está funcionando. Teste o primeiro sem depender do segundo:
simulando a entrega do evento
# salve o payload acima como evento.json e dispare contra o seu endpoint
curl -sS -X POST http://localhost:8080/whatsapp/eventos \
-H "Content-Type: application/json" \
-H "User-Agent: zapon-webhook/1" \
--data-binary @evento.json -w '\nHTTP %{http_code} em %{time_total}s\n'
# o corpo também pode chegar como formulário, com o evento dentro de jsonData
curl -sS -X POST http://localhost:8080/whatsapp/eventos \
--data-urlencode "[email protected]" -w '\nHTTP %{http_code}\n'
Os dois comandos precisam devolver 200, e rápido. Se o segundo falhar, o seu código está lendo só JSON e vai perder eventos entregues como formulário. Repare no %{time_total}: a entrega tem janela curta, então um endpoint que demora segundos precisa passar a devolver 200 antes de processar, e não depois. As entregas reais chegam com o cabeçalho User-Agent: zapon-webhook/1, o que ajuda a separar tráfego legítimo de varredura na sua porta.
Tratamento de erro por código HTTP
O que fazer em cada erro da API?
400 é corpo inválido e você conserta no comando, não repetindo. 401 é token errado ou de outra conexão. 404 é caminho digitado errado. 500 costuma ser conexão fora do ar, e é o único caso em que repetir faz sentido — com espera crescente. Repetir um 400 ou um 401 só gasta chamada.
| Código | Significa | O que fazer no terminal |
|---|
200 | Requisição aceita | Guarde data.Id: jq -r '.data.Id' na saída |
400 | Corpo inválido | Valide o arquivo com jq . corpo.json e confira o formato do Phone: só dígitos, com país e DDD |
401 | Token ausente ou inválido | Teste echo "$ZAPON_TOKEN" — variável vazia manda o header em branco e o erro é idêntico ao de token errado |
404 | Endpoint inexistente | Confira a rota; um /chat/send/txt cai aqui |
500 | Erro ao processar | Repita com espera crescente e, se persistir, veja o estado da conexão no painel |
Em script, a decisão fica clara quando você separa status e corpo:
enviar.sh — decidindo pelo código
#!/usr/bin/env bash
set -euo pipefail
enviar() {
local corpo="$1" resposta status
resposta=$(curl -sS -X POST https://api.zapon.dev/chat/send/text \
-H "token: $ZAPON_TOKEN" -H "Content-Type: application/json" \
--connect-timeout 10 --max-time 25 \
--data-binary "$corpo" -w '\n%{http_code}')
status=$(printf '%s' "$resposta" | tail -n1)
corpo=$(printf '%s' "$resposta" | sed '$d')
case "$status" in
200) printf '%s' "$corpo" | jq -r '.data.Id'; return 0 ;;
400) echo "corpo inválido: $corpo" >&2; return 1 ;; # não repita
401) echo "token inválido" >&2; return 1 ;; # não repita
404) echo "rota inexistente" >&2; return 1 ;; # não repita
*) echo "temporário ($status)" >&2; return 2 ;; # vale tentar de novo
esac
}
# repetição só do que é temporário: 1s, 2s, 4s
for tentativa in 1 2 3; do
enviar '{"Phone":"5511999999999","Body":"Pedido 4821 confirmado."}' && break
[ $? -eq 1 ] && break
sleep $((2 ** (tentativa - 1)))
done
As flags --retry e --retry-all-errors do próprio curl fazem repetição automática, mas não distinguem 400 de 500: com elas você repete um corpo inválido três vezes sem ganho nenhum. Para envio de verdade, o case acima é mais honesto.
Ritmo de envio e preservação do número
Um laço de shell sobre um arquivo de telefones é a forma mais rápida de disparar em rajada sem perceber. Coloque intervalo, com variação, e trate cada linha como um envio que pode falhar sozinho:
um por vez, com intervalo
while IFS=';' read -r telefone nome; do
jq -n --arg p "$telefone" --arg b "Olá, $nome! Sua entrega sai hoje." \
'{Phone: $p, Body: $b}' \
| curl -sS -X POST https://api.zapon.dev/chat/send/text \
-H "token: $ZAPON_TOKEN" -H "Content-Type: application/json" \
--data-binary @- -o /dev/null -w "$telefone HTTP %{http_code}\n" \
| tee -a envios.log
sleep "$(shuf -i 3-7 -n 1)" # 3s a 7s, com variação
done < clientes.csv
- Fale com quem espera. Mensagem sobre um pedido que a pessoa fez é esperada; lista comprada é denúncia esperando acontecer, e denúncia é o que pesa.
- Intervalo com variação. Um
sleep 2 exato mil vezes é tão artificial quanto o disparo em rajada; o shuf acima custa nada. - Aqueça número novo. Comece com poucos envios por dia e aumente ao longo de semanas.
- Guarde o log. O
envios.log do exemplo é o que permite reprocessar só as linhas que falharam, em vez de repetir o arquivo inteiro e mandar mensagem dobrada.
Sobre bloqueio de número, sem promessa mágica. Nenhuma API — nem a oficial da Meta — impede que o WhatsApp aja contra um número denunciado. O risco não se elimina, se administra: falar com quem espera, manter volume e ritmo compatíveis com o seu negócio, responder quem responde e parar de enviar para quem pediu para sair. Quem promete que o número nunca será bloqueado não está sendo honesto.
Um aviso sobre o laço acima: shell é ótimo para testar e ruim para campanha — não há controle de reentrada, o descadastro fica de fora e um Ctrl+C no meio deixa você sem saber onde parou. Depois de validar o formato no terminal, mova a rotina para uma linguagem com banco de dados: os tutoriais de Node.js e Python mostram o mesmo fluxo já com registro de envio e respeito ao opt-out.
Sobre custo, o cálculo é o mesmo em qualquer ferramenta: R$ 27 por mês por número conectado, sem cobrança por mensagem, dentro da cota de 300 mensagens por dia por número — cerca de 9.000 por mês, com HTTP 429 quando estoura. O teste é de 14 dias sem cartão e só começa a contar na primeira conexão — dá para explorar a API inteira no terminal antes de gastar o primeiro dia.
Perguntas frequentes
Os comandos funcionam no Windows?
Funcionam, com um ajuste de aspas. O curl.exe vem no Windows 10 e 11, mas o prompt de comando não entende aspas simples: guarde o corpo num arquivo e use --data-binary @corpo.json, que funciona igual nos três sistemas. No PowerShell há uma armadilha extra: curl é apelido de Invoke-WebRequest, então chame curl.exe pelo nome completo.
Preciso mesmo do jq?
Não para o primeiro teste, mas ele evita duas classes inteiras de erro: JSON malformado quando você monta o corpo à mão e falha de escape quando o texto tem aspas, acento ou emoji. Se não puder instalá-lo, use o arquivo com heredoc — é a alternativa mais segura no terminal puro.
Posso testar num cliente gráfico de API em vez do terminal?
Pode, e o mapeamento é direto: método POST, a URL do endpoint, um header token com o valor da conexão e o corpo em JSON cru. Um cuidado vale para qualquer ferramenta dessas: não sincronize na nuvem nem compartilhe com o time uma coleção com o token dentro — use variável de ambiente local.
Como envio para um grupo?
Use o identificador do grupo no lugar do telefone, no mesmo campo Phone dos endpoints de envio. A lista de grupos do número vem em GET /group/list, que no terminal é curl -sS -H "token: $ZAPON_TOKEN" https://api.zapon.dev/group/list | jq. São 18 endpoints de grupo no total, cobrindo criação, participantes, link de convite e configurações.
Dá para ver o schema de cada endpoint?
Dá: a referência interativa lista os 49 endpoints publicados com o schema de cada corpo e cada resposta, com os campos obrigatórios marcados. É o lugar certo para conferir antes de escrever um corpo novo à mão — e evita boa parte dos 400 que parecem inexplicáveis.
Um token serve para vários números?
Não: um token é um número. Se você opera vários, exporte uma variável por conexão (ZAPON_TOKEN_LOJA, ZAPON_TOKEN_SUPORTE) e troque no header conforme o número que deve enviar. Cada número conectado custa R$ 27 por mês, sem cobrança por mensagem, e tem cota própria de 300 mensagens por dia — cerca de 9.000 por mês — que protege aquele número contra bloqueio.
Preciso da API oficial da Meta ou de aprovação de template?
Não. O número conecta por QR Code ou código de pareamento, sem fila de aprovação e sem mensagem cadastrada antes. A contrapartida é que a conduta do número é de quem integra — motivo pelo qual esta página gasta uma seção inteira com ritmo de envio.