Os exemplos usam
nomes de exemplo — crie um app e uma instância para vê-los com seus dados.
Substitua SUA_CHAVE pela chave do app.
O gateway fica entre os seus projetos e o WhatsApp. Eles nunca falam com a Evolution direto — falam com o gateway, e ele resolve o resto.
Isso existe por um motivo prático: se um dia a Evolution parar de servir, você troca a peça de dentro do gateway e nenhum projeto seu precisa mudar uma linha.
| App | Um projeto seu. Cada um tem sua chave. Se a chave de um vazar, você troca só a dele. |
|---|---|
| Instância | Um número de WhatsApp. Um app pode ter vários — vendas e suporte, por exemplo. |
Um app só enxerga as próprias instâncias. Se o CRM tentar enviar pela instância da loja, recebe 404.
Toda chamada leva a chave do app no cabeçalho:
X-Api-Key: SUA_CHAVE
Também aceita Authorization: Bearer SUA_CHAVE, que é o
padrão da maioria das bibliotecas HTTP.
curl -X POST https://x-zap.com/v1/messages/text \
-H "X-Api-Key: SUA_CHAVE" \
-H "Content-Type: application/json" \
-d '{
"to": "5511999990000",
"text": "Seu pedido saiu para entrega",
"instance": "c1-vendas-a1b2"
}'
A resposta é 202: o gateway aceitou e colocou na fila. Quem envia é o worker, em cerca de um segundo.
{
"message_id": "019fecb2-48cf-78cf-a720-9d6f5d11e763",
"status": "queued",
"instance": "c1-vendas-a1b2",
"to": "5511999990000",
"type": "text"
}
Guarde o message_id: é por ele que você consulta o status
depois, e é ele que volta nos eventos de webhook.
to | obrigatório. DDI + DDD + número, só dígitos. 5511999990000 |
|---|---|
text | obrigatório. Até 4096 caracteres. |
instance | Opcional quando o app tem uma instância só. |
reply_to | Opcional. Responde citando uma mensagem: no WhatsApp a resposta aparece "respondendo" a ela. Aceita o message_id do gateway ou o id do WhatsApp (provider_msg_id). A mensagem citada tem que ser da mesma conversa. Vale também para imagem, vídeo, documento e áudio. |
Se a rede cair depois do envio e antes da resposta, seu projeto pode tentar de novo — e o cliente receberia a mesma mensagem duas vezes. Para evitar, mande um identificador seu:
-H "Idempotency-Key: pedido-8421-confirmacao"
Na segunda vez com a mesma chave, o gateway devolve 200
com "duplicate": true e a mensagem original — sem enviar de novo.
curl -X POST https://x-zap.com/v1/messages/media \
-H "X-Api-Key: SUA_CHAVE" \
-H "Content-Type: application/json" \
-d '{
"to": "5511999990000",
"type": "document",
"url": "https://seusite.com.br/boletos/8421.pdf",
"file_name": "boleto-8421.pdf",
"instance": "c1-vendas-a1b2"
}'
type | image, video ou document |
|---|---|
url | Endereço público do arquivo. Prefira este caminho. |
base64 | Alternativa à URL, para arquivo gerado em memória. Teto de 5 MB. |
caption | Legenda, até 1024 caracteres. Não vale para documento. |
file_name | Obrigatório para documento — é o nome que o cliente vê. |
video é recusado com 400.
curl -X POST https://x-zap.com/v1/messages/audio \
-H "X-Api-Key: SUA_CHAVE" \
-H "Content-Type: application/json" \
-d '{
"to": "5511999990000",
"url": "https://seusite.com.br/audio/resposta.ogg",
"instance": "c1-vendas-a1b2"
}'
Chega como nota de voz, não como arquivo anexado.
Use audio/ogg com codec opus para melhor compatibilidade —
é o formato que o próprio WhatsApp usa.
Uma resposta que chega em dois segundos sem indicador parece robô. Com ele, parece atendimento.
O gateway mostra o indicador e só então entrega:
curl -X POST https://x-zap.com/v1/messages/text \
-H "X-Api-Key: SUA_CHAVE" \
-d '{
"to": "5511999990000",
"text": "Claro! Seu pedido sai hoje até as 18h.",
"typing": true,
"instance": "c1-vendas-a1b2"
}'
true | calcula pelo tamanho do texto: de 1,2s a 8s |
|---|---|
2500 | fixa em milissegundos |
curl -X POST https://x-zap.com/v1/presence \
-H "X-Api-Key: SUA_CHAVE" \
-d '{
"to": "5511999990000",
"state": "composing",
"duration_ms": 5000,
"instance": "c1-vendas-a1b2"
}'
state aceita composing (digitando),
recording (gravando áudio) e paused
(encerra o indicador).
O WhatsApp encerra sozinho em cerca de 25 segundos. Para processos mais longos, chame de novo.
Quem está na lista de opt-out não vê o indicador — a rota respeita o bloqueio igual ao envio. E falha aqui nunca impede a mensagem de sair: presença é enfeite, não conteúdo.
curl https://x-zap.com/v1/messages/MESSAGE_ID \ -H "X-Api-Key: SUA_CHAVE"
| Status | Significa |
|---|---|
| queued | Na fila, ainda não saiu. |
| sent | Saiu do gateway e o WhatsApp aceitou. |
| delivered | Chegou no aparelho do destinatário. |
| read | Foi aberta. |
| Falhou por algo passageiro; vai tentar de novo. | |
| failed | Desistiu. O motivo está no campo error. |
read.
Muita gente desliga a confirmação de leitura no WhatsApp, e nesse caso
o status nunca passa de delivered. Se o seu processo espera
"o cliente leu", ele trava para sempre nesses casos.
curl "https://x-zap.com/v1/messages?number=5511999990000&instance=c1-vendas-a1b2" \ -H "X-Api-Key: SUA_CHAVE"
Vem da mais nova para a mais antiga, no máximo 200 por vez
(limit, padrão 50). Parâmetros opcionais:
| Parâmetro | O que faz |
|---|---|
| since=2026-10-01 | Só mensagens a partir desse dia. Aceita também data/hora ISO (2026-10-01T14:00:00-03:00). |
| before=MESSAGE_ID | Próxima página: o que veio antes dessa mensagem. A resposta já traz o valor pronto em next_before (null quando acabou). |
| before=2026-10-01 | Também aceita data ou data/hora: só o que veio antes dela. |
Cada mensagem traz origem, que diz quem escreveu:
| origem | Quem escreveu |
|---|---|
| cliente | O contato mandou para a loja. |
| api | O seu sistema enviou pelo gateway. |
| celular | Alguém da loja escreveu direto no WhatsApp do celular. |
curl "https://x-zap.com/v1/messages/numbers?since=2026-10-01&instance=c1-vendas-a1b2" \ -H "X-Api-Key: SUA_CHAVE"
Devolve um número por contato, do mais recente para o mais antigo
(limit até 500, padrão 100): o número com as
variants, o total de mensagens no período, a
última mensagem (last_message, com origem, tipo, status e um
trecho do texto) e referral, o anúncio que trouxe o contato
(null se não veio de anúncio).
Filtros de anúncio: ad=ID_DO_ANUNCIO traz só quem veio
daquele anúncio; from_ads=1, quem veio de qualquer anúncio.
curl "https://x-zap.com/v1/messages/unanswered?minutes=30&instance=c1-vendas-a1b2" \ -H "X-Api-Key: SUA_CHAVE"
Quem escreveu e não teve resposta da loja — nem pelo sistema, nem pelo
celular — há mais de minutes (padrão 30). Olha os últimos
days dias (padrão 7, até 90). Cada item traz
waiting_since, waiting_minutes,
pending (quantas mensagens sem resposta), a última mensagem
e o referral. Quem esperou mais vem primeiro. Reação,
edição e apagada do cliente não contam como pergunta.
curl "https://x-zap.com/v1/messages/MESSAGE_ID/media" \ -H "X-Api-Key: SUA_CHAVE" -o arquivo
Devolve a foto, o vídeo, o áudio ou o documento que o cliente (ou a
loja, pelo celular) mandou, já aberto e com o Content-Type
certo. Com ?format=base64 vem em JSON
(mimetype, file_name, size,
base64).
| Código | Quando |
|---|---|
| 404 | A mensagem não tem mídia (ou é anterior a outubro/2026 e saiu da retenção de 7 dias). |
| 410 | O WhatsApp não guarda mais o arquivo (mídia antiga, algumas semanas). Baixe e guarde do seu lado quando a mensagem chegar. |
| 413 | Arquivo maior que 32 MB. |
| 502 | O servidor do WhatsApp não respondeu agora. Tente de novo. |
curl "https://x-zap.com/v1/ads?since=2026-10-01&instance=c1-vendas-a1b2" \ -H "X-Api-Key: SUA_CHAVE"
Um item por anúncio (click-to-WhatsApp do Instagram/Facebook):
source_id, headline, source_url,
contacts (quantos contatos diferentes ele trouxe),
clicks, first_at e last_at. No
histórico, a mensagem que veio do anúncio traz o campo
referral.
O x-zap entrega os eventos no endereço que você configurar para cada
número — em Números, abra o número e preencha
Webhook. O endereço precisa ser https://.
Seu endpoint deve responder 2xx rápido. Se responder
erro, o gateway tenta de novo — 1 minuto, 5, 15 e 1 hora. Depois disso o
evento fica guardado esperando você
reprocessar, e pode ser buscado em GET /v1/events. Nada é perdido.
Escolher os eventos: na página do número, em Avisar quando, desmarque o que o seu sistema não usa. As mensagens continuam guardadas no histórico; só o aviso deixa de ir. Sem mexer, recebe tudo.
Se o seu sistema ficou fora do ar, busque os eventos em ordem, no mesmo formato do webhook:
curl "https://x-zap.com/v1/events?since=2026-10-09&instance=c1-vendas-a1b2&limit=100" \ -H "X-Api-Key: SUA_CHAVE"
Leia de página em página passando after_id = o
next_after_id da resposta anterior, até vir
null. Filtros opcionais: type=message.received,message.sent
e delivered=false (só o que ainda não foi entregue). Use o
event_id para não processar duas vezes o que também chegou
pelo webhook.
Todo webhook chega com o cabeçalho X-Signature. Verifique
sempre — sem isso, quem descobrir a URL do seu endpoint consegue injetar
"mensagem recebida" e fazer seu sistema responder a um cliente que nunca
escreveu.
O segredo aparece quando a instância é criada. Para vê-lo de novo, consulte o banco ou recrie a instância.
<?php
function verificarWebhook(string $corpo, string $header, string $segredo): bool
{
$partes = [];
foreach (explode(',', $header) as $par) {
[$k, $v] = array_pad(explode('=', trim($par), 2), 2, '');
$partes[$k] = $v;
}
$ts = (int) ($partes['t'] ?? 0);
// Rejeita repetição: webhook capturado não vale depois de 5 minutos
if ($ts <= 0 || abs(time() - $ts) > 300) {
return false;
}
// hash_equals: comparação em tempo constante
return hash_equals(
hash_hmac('sha256', "{$ts}.{$corpo}", $segredo),
(string) ($partes['v1'] ?? '')
);
}
$corpo = file_get_contents('php://input');
$assinatura = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
if (!verificarWebhook($corpo, $assinatura, WEBHOOK_SEGREDO)) {
http_response_code(401);
exit;
}
$evento = json_decode($corpo, true);
// ... processe em segundo plano e responda 200
http_response_code(200);
Todo evento tem o mesmo envelope:
{
"event": "message.received",
"event_id": 4,
"message_id": "019feca9-4016-75ca-ab5d-bc427d07ccd0",
"instance": "c1-vendas-a1b2",
"data": { },
"timestamp": "2026-08-10T14:12:22-03:00"
}
| event | Quando acontece |
|---|---|
| message.received | Alguém mandou mensagem para o seu número. |
| message.sent | A loja escreveu pelo celular (não pela API). Só chega para as instâncias com a caixa Encaminhar mensagens do celular marcada — veja abaixo. |
| message.status | Uma mensagem sua avançou para delivered ou read. |
| message.deleted | Uma mensagem foi apagada para todos. |
| instance.connection | O número conectou ou caiu. |
| call.received | Alguém ligou. |
{
"event": "message.received",
"instance": "c1-vendas-a1b2",
"data": {
"from": {
"phone": "551199990000",
"variants": ["551199990000", "5511999990000"],
"name": "Guthem"
},
"from_me": false,
"type": "audio",
"content": {
"mimetype": "audio/ogg; codecs=opus",
"size": 10043,
"duration": 4,
"voice_note": true,
"media": { "url": "https://..." }
},
"timestamp": "2026-08-10T12:31:34-03:00",
"timestamp_unix": 1786375894
}
}
variants para casar com o seu banco.
Números brasileiros antigos vêm sem o nono dígito: você envia para
5511999990000 e recebe de 551199990000. As duas
formas vêm em variants — procure o cliente por qualquer uma.
Para áudio, voice_note: true significa nota de voz gravada
na hora — é o que você quer transcrever. false é arquivo de
áudio anexado.
| Campo | Quando vem |
|---|---|
| timestamp_unix | Sempre, em todo evento de mensagem. É a mesma hora do timestamp, em segundos (número inteiro). Prefira este: não depende de ler texto de data. |
| referral | Só quando o cliente chegou clicando num anúncio do Instagram/Facebook ("Enviar mensagem"). Traz source_id (id do anúncio), source_url, ctwa_clid, headline, body e source_type. |
| content.reaction | Reação a uma mensagem: {emoji, target_message_id, removed}. O type continua unsupported. |
| content.edited | Mensagem editada: {target_message_id, text}. |
| content.revoked | Mensagem apagada para todos: {target_message_id}. |
| content.contacts | Contato compartilhado: [{name, phones}]. |
target_message_id é o id do WhatsApp da mensagem original
(o mesmo provider_msg_id do histórico).
Quando alguém da loja responde direto no WhatsApp do celular, o seu
sistema não sabe dessa mensagem. Com a instância ligada, ela chega como
message.sent. O formato é o mesmo da recebida, com
from_me: true. Repare que from e to
trazem os dois o número do cliente, porque é por ele que
a conversa é agrupada:
{
"event": "message.sent",
"instance": "c1-vendas-a1b2",
"data": {
"from": { "phone": "551199990000", "variants": ["551199990000", "5511999990000"], "name": "Nome da loja" },
"to": { "phone": "551199990000", "variants": ["551199990000", "5511999990000"] },
"from_me": true,
"type": "text",
"content": { "text": "Oi! Seu pedido já saiu." },
"timestamp": "2026-10-09T09:10:00-03:00",
"timestamp_unix": 1791547800
}
}
Mensagem que o seu sistema enviou pela API nunca
chega como message.sent: você já sabe dela. Reação,
edição ou apagada feitas no celular também chegam como
message.sent, com type: "unsupported" e os
campos extras descritos abaixo.
Para ligar: em Números, abra o número e marque Mensagens digitadas no celular. Vale na hora.
GET /v1/messages) a mensagem do
celular aparece em todas as instâncias, ligadas ou não, com
direction: "out" e status: "sent". Antes de
outubro/2026 ela aparecia como "in" / "received",
como se o cliente tivesse escrito.
Quando alguém pede para parar de receber, registre. O gateway passa a recusar envios para esse número antes de entrar na fila, devolvendo 403.
curl -X POST https://x-zap.com/v1/contacts/5511999990000/opt-out \
-H "X-Api-Key: SUA_CHAVE" \
-H "Content-Type: application/json" \
-d '{"reason": "pediu para parar"}'
Para liberar: /opt-in. Para consultar:
GET /v1/contacts/NUMERO/status.
O bloqueio vale por app. Se o cliente pede para a loja parar, isso não impede o CRM de falar com ele — são relações diferentes. E cobre as duas formas do número automaticamente.
{
"error": {
"code": "opted_out",
"message": "Número na lista de opt-out"
}
}
| HTTP | code | O que fazer |
|---|---|---|
| 400 | bad_request | Corrija o corpo. A mensagem diz o campo. |
| 401 | unauthorized | Chave errada ou ausente. |
| 403 | opted_out | Número bloqueado. Não insista. |
| 404 | not_found | Instância ou mensagem inexistente. |
| 413 | payload_too_large | Base64 acima de 5 MB. Use URL. |
| 429 | rate_limited | Teto por hora atingido. Veja Retry-After. |
Erros 5xx e falha de rede valem retentativa. 4xx não — corrija a chamada.
sent. Chegou?Saiu do gateway e o WhatsApp aceitou. delivered confirma
que chegou no aparelho, e costuma vir em segundos. Se ficar preso em
sent por minutos, o destinatário provavelmente está sem
internet.
O nono dígito. Linhas registradas antes da mudança da Anatel mantêm o
formato antigo no WhatsApp. Envie com o 9; para casar no seu banco, use
o campo variants.
Se ainda estiver queued, dá para marcar como cancelada no
banco. Depois de sent, não — já saiu.
Não. O gateway tenta quatro vezes e depois guarda o evento. Quando
voltar, busque tudo em GET /v1/events?after_id=….
Cada número tem o limite diário do seu plano (veja em Assinatura). Além disso, o x-zap espaça os envios de 3 a 8 segundos, com teto de 20 por destinatário por dia. Isso não garante nada com o WhatsApp — nenhuma cadência garante. Evita apenas o comportamento que claramente parece robô.
Busque em GET /v1/events?delivered=false: lá estão os
avisos que o seu endpoint ainda não confirmou, com o motivo.