Documentação
ZAP-API
API REST para enviar e receber mensagens WhatsApp. Autentique com Bearer token, gerencie instâncias e configure webhooks em minutos.
https://api.zap-api.tech/v1Referência completa da API — 250+ endpoints
Esta página é o início rápido. A referência interativa traz todos os endpoints — 18 tipos de mensagem, grupos, contatos, chats, perfil, status, catálogo business, comunidades, newsletters, chamadas e fila. É gerada do código, então nunca fica desatualizada.
Em 3 passos você já está enviando mensagens WhatsApp via API.
Criar uma instância
No dashboard, acesse Instâncias → Nova Instância e dê um nome.
Conectar via QR Code
Clique em Conectar e escaneie o QR Code com seu WhatsApp.
Copiar o token
Acesse Credenciais na instância e copie o Bearer token.
curl -X POST https://api.zap-api.tech/v1/instances/inst_xxx/send \
-H "Authorization: Bearer tk_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"phone":"5511999999999","type":"text","body":"Olá! API funcionando ✅"}'Cada instância possui um Bearer token único. Inclua-o em todas as requisições no header Authorization.
Header obrigatório
Authorization: Bearer tk_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
Node.js (axios)
const axios = require('axios');
const api = axios.create({
baseURL: 'https://api.zap-api.tech/v1',
headers: {
'Authorization': 'Bearer tk_xxxxxxxxxxxxxxxxxxxxxxxxxxxx',
'Content-Type': 'application/json',
},
});
const response = await api.post('/instances/inst_xxx/send', {
phone: '5511999999999',
type: 'text',
body: 'Olá! Mensagem via ZAP API.',
});
console.log(response.data);Python (requests)
import requests
headers = {
"Authorization": "Bearer tk_xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"Content-Type": "application/json",
}
resp = requests.post(
"https://api.zap-api.tech/v1/instances/inst_xxx/send",
json={"phone": "5511999999999", "type": "text", "body": "Olá via Python!"},
headers=headers,
)
print(resp.json())Cada instância representa uma conexão WhatsApp independente com seu próprio token.
/instances/:id/instances/:id/status/instances/:id/qrcode/instances/:id/connect/instances/:id/pairing/instances/:id/disconnectGET /instances — listar
curl https://api.zap-api.tech/v1/instances \ -H "Authorization: Bearer tk_xxxxxxxxxxxx"
{
"instances": [
{
"id": "inst_abc123",
"name": "Suporte",
"status": "connected",
"phone": "+5511999999999",
"plan": "pro",
"createdAt": "2026-01-15T10:00:00Z"
}
]
}POST /instances/:id/connect — conectar
Inicia (ou reinicia) a conexão da instância. Em seguida, busque o QR Code em /qrcode e escaneie pelo WhatsApp.
curl -X POST https://api.zap-api.tech/v1/instances/inst_abc123/connect \ -H "Authorization: Bearer tk_xxxxxxxxxxxx"
GET /instances/:id/qrcode — QR Code
QRCODE, chame POST /connect primeiro e tente de novo.curl https://api.zap-api.tech/v1/instances/inst_abc123/qrcode \ -H "Authorization: Bearer tk_xxxxxxxxxxxx"
{
"instanceId": "inst_abc123",
"qrCode": "data:image/png;base64,iVBORw0KGgo...",
"status": "qr_required"
}GET /instances/:id/status — status de conexão
{
"instanceId": "inst_abc123",
"waStatus": "CONNECTED",
"connected": true,
"number": "5511999999999",
"name": "Suporte",
"profilePicUrl": null,
"qrcode": null
}Principais valores de waStatus:
CONNECTEDDISCONNECTEDQRCODE/instances/:id/send/instances/:id/send/batch/instances/:id/send/scheduletype: text, image, audio, video, document, sticker, gif, location, contact, link, reaction, poll, buttons, list, product, order, pix, event.buttons e list são aceitos e entregues — mas o WhatsApp só exibe botão clicável em conta oficial. Em conta conectada por QR Code ele descartaria a mensagem interativa, e ela sumiria. Por isso convertemos em texto com as opções numeradas antes de enviar: garante a entrega. A resposta vem com messageId normalmente, então não dá para perceber pelo retorno.Para interação clicável de verdade, use
poll (enquete) — é nativa. Atenção: o voto da enquete não é enviado ao seu webhook. Se o seu fluxo precisa ler a escolha por código, continue no texto numerado e leia a resposta do cliente pelo evento message.received.POST /instances/:id/send — texto
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
phone | string | sim | Número com DDI, só dígitos, sem + (ex: 5511999999999) ou JID de grupo (@g.us) |
type | string | sim | Tipo da mensagem — use "text" |
body | string | sim | Conteúdo do texto (1–4096 caracteres) |
previewUrl | boolean | opcional | Gera preview do link no texto |
quotedMessageId | string | opcional | ID de uma mensagem para responder/citar |
curl -X POST https://api.zap-api.tech/v1/instances/inst_abc123/send \
-H "Authorization: Bearer tk_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"phone": "5511999999999",
"type": "text",
"body": "Seu pedido #1234 foi confirmado. ✅"
}'body — nunca text ou message. Um 200 significa que a mensagem foi aceita na fila — acompanhe a entrega real em /messages/status/:msgId ou pelo webhook message.status.POST /instances/:id/send — mídia (imagem, vídeo, documento…)
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
phone | string | sim | Número com DDI, só dígitos, sem + |
type | string | sim | image, video, gif, audio, document, sticker… |
mediaUrl | string | sim | URL pública do arquivo (jpg, png, pdf, mp4…) |
caption | string | opcional | Legenda (image / video / gif / document) |
fileName | string | opcional | Nome do arquivo (obrigatório para document) |
curl -X POST https://api.zap-api.tech/v1/instances/inst_abc123/send \
-H "Authorization: Bearer tk_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"phone": "5511999999999",
"type": "document",
"mediaUrl": "https://exemplo.com/boleto.pdf",
"fileName": "boleto-fev-2026.pdf",
"caption": "Seu boleto de fevereiro"
}'POST /instances/:id/send — enquete (poll)
Interação clicável nativa: diferente de buttons e list, a enquete não é convertida em texto — ela chega como enquete de verdade.
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
phone | string | sim | Número com DDI, só dígitos, sem + (ex: 5511999999999) ou JID de grupo (@g.us) |
type | string | sim | Tipo da mensagem — use "poll" |
pollQuestion | string | sim | Pergunta da enquete (1–255 caracteres) |
pollOptions | string[] | sim | Opções: mínimo 2, máximo 12, cada uma de 1 a 100 caracteres |
pollMultiSelect | boolean | opcional | Permite marcar mais de uma opção (padrão: false) |
quotedMessageId | string | opcional | ID de uma mensagem para responder/citar |
curl -X POST https://api.zap-api.tech/v1/instances/inst_abc123/send \
-H "Authorization: Bearer tk_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"phone": "5511999999999",
"type": "poll",
"pollQuestion": "Qual horário fica melhor para a entrega?",
"pollOptions": ["Manhã (8h–12h)", "Tarde (12h–18h)", "Não estarei em casa"],
"pollMultiSelect": false
}'message.received.Configure uma URL no seu servidor para receber eventos em tempo real: mensagens recebidas, mudanças de status e confirmações de entrega.
Configurar webhook
Configure via API com PUT /instances/:id/webhook ou pelo Dashboard em Instâncias → Webhook. A URL deve ser HTTPS e pública (não localhost).
curl -X PUT https://api.zap-api.tech/v1/instances/inst_abc123/webhook \
-H "Authorization: Bearer tk_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"url": "https://seu-servidor.com/webhook",
"events": ["message.received", "message.status"],
"secret": "sua_chave_secreta"
}'POST /webhook/test.Eventos disponíveis
message.receivedmessage.sentmessage.statusmessage.readinstance.connectedinstance.disconnectedSão 21 eventos no total (também grupos, contatos, presença, chamadas, status e newsletter). Inscreva só os que precisar no campo events.
Payload — message.received
{
"event": "message.received",
"instanceId": "inst_abc123",
"timestamp": "2026-02-15T14:30:00Z",
"data": {
"messageId": "msg_xyz789",
"phone": "5511999999999",
"name": "João Silva",
"type": "text",
"body": "Olá, quero saber sobre o produto",
"fromMe": false,
"timestamp": 1771166400
}
}Dependendo do tipo da mensagem, o data traz também: mediaUrl, mimeType, fileName e caption (mídia), latitude e longitude (localização), buttonReply (clique em botão/lista) e lid (contatos com identificador oculto). Campos ausentes simplesmente não vêm no JSON — trate sempre com valor padrão.
Localização e cliques em botão
Quando o contato compartilha localização, as coordenadas vêm no próprio evento. E quando ele clica num botão ou item de lista, você recebe o texto em body e o identificador da opção em buttonReply — útil para roteirizar o funil sem depender do texto digitado.
{
"event": "message.received",
"data": {
"type": "location",
"latitude": -23.55052,
"longitude": -46.633308
}
}
{
"event": "message.received",
"data": {
"type": "text",
"body": "Quero falar com vendas",
"buttonReply": { "id": "opt_vendas", "text": "Quero falar com vendas" }
}
}Mídia recebida — áudio, imagem, vídeo e documento
Quando o contato envia mídia, o payload traz mediaUrl — um link temporário e assinado para baixar o arquivo — além do mimeType e, em documentos, do fileName. A legenda, quando existir, vem em caption.
{
"event": "message.received",
"data": {
"messageId": "msg_xyz789",
"phone": "5511999999999",
"type": "audio",
"mediaUrl": "https://.../zap-inbound-media/...?X-Amz-Signature=...",
"mimeType": "audio/ogg; codecs=opus"
}
}GET /instances/:id/messages/:msgId/media — gerar novo link
O link do webhook expira em 7 dias. Se você processa os eventos de forma assíncrona (fila, reprocessamento, agente de IA que roda depois), use este endpoint com o messageId que veio no payload para gerar um link novo enquanto o arquivo estiver dentro do período de retenção.
curl https://api.zap-api.tech/v1/instances/inst_abc123/messages/msg_xyz789/media \ -H "Authorization: Bearer tk_xxxxxxxxxxxx"
{
"messageId": "msg_xyz789",
"type": "audio",
"mediaUrl": "https://.../zap-inbound-media/...?X-Amz-Signature=...",
"mimeType": "audio/ogg; codecs=opus",
"bytes": 14238,
"expiresIn": 604800,
"expiresAt": "2026-08-25T14:30:00.000Z"
}200Link novo gerado — use mediaUrl até expiresAt404A mensagem existe, mas não tem mídia (texto, ou recebida antes de 18/08/2026)410O arquivo passou do período de retenção — não há como recuperar503Indisponibilidade nossa — tente novamente mais tardeinstanceId que recebeu a mensagem. Chamar /v1/messages/:msgId/media (sem a instância) devolve 404 com o caminho correto.Contatos com LID — envie sempre para o phone
Alguns contatos chegam com um lid (Linked ID) — o WhatsApp oculta o número real por privacidade. Nesses casos o payload traz os dois campos: o número real em phone e o identificador em lid.
{
"event": "message.received",
"data": {
"phone": "5535997022148", // número real — USE este para enviar
"lid": "143435449282736@lid", // identificador — NÃO use para enviar
"name": "Contato",
"type": "text",
"body": "..."
}
}phone — nunca o lid. Enviar para um …@lid abre um “chat fantasma” (o WhatsApp trata os dígitos do LID como um número solto, às vezes até estrangeiro) e a mensagem não chega — mesmo a API respondendo 200 / sent, porque o WhatsApp aceitou o envio. Pode mandar o phone exatamente como veio no webhook: o gateway completa o 9º dígito do número brasileiro automaticamente.Verificação HMAC-SHA256
Configure um Webhook Secret na instância. Cada entrega traz o header X-ZapAPI-Signature-256 no formato sha256=<hmac-hex> (padrão GitHub). O header legado X-Zap-Signature é mantido por compatibilidade.
const crypto = require('crypto');
app.post('/webhook', (req, res) => {
const payload = JSON.stringify(req.body);
const header = req.headers['x-zapapi-signature-256'] || '';
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(payload)
.digest('hex');
// comparação em tempo constante (evita timing attack)
const ok = header.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(header), Buffer.from(expected));
if (!ok) {
return res.status(401).json({ error: 'Unauthorized' });
}
const { event, data } = req.body;
console.log('Evento recebido:', event, data);
res.json({ ok: true });
});Limite de envio por instância, por minuto. Ao atingir o teto a API retorna HTTP 429 com o header Retry-After. Não há limite mensal de envio — a cobrança é apenas por instância ativa.
| Plano | Envio/min | Preço por instância | Retenção de logs |
|---|---|---|---|
| Trial | 30 | Grátis por 7 dias | — |
| Pago | 120 | R$ 49 (1ª e 2ª) · R$ 29 (3ª+) | 30 dias |
Todos os erros seguem o formato: { "error": "ERROR_CODE", "message": "texto explicativo" }. Em um 404 de rota não-escopada, a resposta inclui o campo hint com o caminho correto.
| HTTP | Significado | Causa comum |
|---|---|---|
200 | OK | Aceito (no envio: enfileirado, não necessariamente entregue) |
400 | VALIDATION_ERROR | Campo obrigatório ausente/inválido (ex: phone, type, body) |
401 | Unauthorized | Bearer token inválido ou ausente |
404 | ROUTE_NOT_SCOPED | Rota chamada sem /instances/:id — use o campo hint da resposta |
409 | NOT_CONNECTED | Instância não está conectada (waStatus ≠ CONNECTED) |
422 | SELF_SEND_BLOCKED | Tentativa de enviar para o próprio número da instância |
429 | Too Many Requests | Rate limit por minuto atingido — veja Retry-After |
503 | CIRCUIT_OPEN | Circuit breaker aberto após falhas — aguarde ~60s |
Pronto para integrar?
7 dias grátis · sem cartão · instância funcionando em minutos.