x-zap Documentação

Documentação

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.

Como funciona

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.

Seus projetosloja, CRM, ERP…
→
Gatewayfila, retry, opt-out, status
→
EvolutionWhatsApp

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 e instância

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.

Autenticação

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.

A chave aparece uma única vez, quando o app é criado. Não fica guardada em lugar nenhum — só o hash. Se perdeu, gere outra em Chaves de API: a anterior para de funcionar na hora.

Enviar texto

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.

Campos

toobrigatório. DDI + DDD + número, só dígitos. 5511999990000
textobrigatório. Até 4096 caracteres.
instanceOpcional quando o app tem uma instância só.
reply_toOpcional. 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.

Não enviar duplicado

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.

Imagem, vídeo e documento

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"
  }'
typeimage, video ou document
urlEndereço público do arquivo. Prefira este caminho.
base64Alternativa à URL, para arquivo gerado em memória. Teto de 5 MB.
captionLegenda, até 1024 caracteres. Não vale para documento.
file_nameObrigatório para documento — é o nome que o cliente vê.
O tipo do arquivo é verificado pelo conteúdo, não pela extensão que você declara. Um PNG enviado como video é recusado com 400.

Enviar áudio

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.

"Digitando..."

Uma resposta que chega em dois segundos sem indicador parece robô. Com ele, parece atendimento.

Junto com o envio

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"
  }'
truecalcula pelo tamanho do texto: de 1,2s a 8s
2500fixa em milissegundos

Separado, quando o processamento demora

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.

Use onde faz sentido. Em agente de IA e atendimento humano, sim — é onde mais rende. Em confirmação automática, boleto ou código de rastreio, não: ninguém espera que o sistema "digite" um número de rastreio, e o indicador só atrasa a entrega.

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.

Status e histórico

curl https://x-zap.com/v1/messages/MESSAGE_ID \
  -H "X-Api-Key: SUA_CHAVE"
StatusSignifica
queuedNa fila, ainda não saiu.
sentSaiu do gateway e o WhatsApp aceitou.
deliveredChegou no aparelho do destinatário.
readFoi aberta.
retryingFalhou por algo passageiro; vai tentar de novo.
failedDesistiu. O motivo está no campo error.
Não construa fluxo que dependa de 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.

Histórico de um número

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âmetroO que faz
since=2026-10-01Só mensagens a partir desse dia. Aceita também data/hora ISO (2026-10-01T14:00:00-03:00).
before=MESSAGE_IDPró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-01Também aceita data ou data/hora: só o que veio antes dela.

Cada mensagem traz origem, que diz quem escreveu:

origemQuem escreveu
clienteO contato mandou para a loja.
apiO seu sistema enviou pelo gateway.
celularAlguém da loja escreveu direto no WhatsApp do celular.

Com quem a instância conversou

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.

Clientes esperando resposta

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.

Baixar a mídia que chegou

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ódigoQuando
404A mensagem não tem mídia (ou é anterior a outubro/2026 e saiu da retenção de 7 dias).
410O WhatsApp não guarda mais o arquivo (mídia antiga, algumas semanas). Baixe e guarde do seu lado quando a mensagem chegar.
413Arquivo maior que 32 MB.
502O servidor do WhatsApp não respondeu agora. Tente de novo.

Anúncios que trouxeram contatos

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.

Receber mensagens

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.

Recuperar o que perdeu

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.

Processe em segundo plano. Se o seu webhook demorar 20 segundos para responder porque está chamando uma IA, o gateway vai considerar timeout e reenviar — e você processa duas vezes.

Verificar a assinatura

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

Formato dos eventos

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"
}
eventQuando acontece
message.receivedAlguém mandou mensagem para o seu número.
message.sentA 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.statusUma mensagem sua avançou para delivered ou read.
message.deletedUma mensagem foi apagada para todos.
instance.connectionO número conectou ou caiu.
call.receivedAlguém ligou.

Mensagem recebida

{
  "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
  }
}
Use o campo 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.

Campos extras

CampoQuando vem
timestamp_unixSempre, 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.
referralSó 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.reactionReação a uma mensagem: {emoji, target_message_id, removed}. O type continua unsupported.
content.editedMensagem editada: {target_message_id, text}.
content.revokedMensagem apagada para todos: {target_message_id}.
content.contactsContato compartilhado: [{name, phones}].

target_message_id é o id do WhatsApp da mensagem original (o mesmo provider_msg_id do histórico).

Mensagem enviada pelo celular da loja

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.

No histórico (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.

Opt-out

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.

Erros

{
  "error": {
    "code": "opted_out",
    "message": "Número na lista de opt-out"
  }
}
HTTPcodeO que fazer
400bad_requestCorrija o corpo. A mensagem diz o campo.
401unauthorizedChave errada ou ausente.
403opted_outNúmero bloqueado. Não insista.
404not_foundInstância ou mensagem inexistente.
413payload_too_largeBase64 acima de 5 MB. Use URL.
429rate_limitedTeto por hora atingido. Veja Retry-After.

Erros 5xx e falha de rede valem retentativa. 4xx não — corrija a chamada.

Dúvidas comuns

Enviei e o status ficou em 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.

Por que o número que recebo é diferente do que enviei?

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.

Mandei duas vezes por engano. Dá para cancelar?

Se ainda estiver queued, dá para marcar como cancelada no banco. Depois de sent, não — já saiu.

Meu servidor caiu de madrugada. Perdi mensagens?

Não. O gateway tenta quatro vezes e depois guarda o evento. Quando voltar, busque tudo em GET /v1/events?after_id=….

Quantas mensagens posso enviar?

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

Uma mensagem não chegou no meu sistema. Onde olho?

Busque em GET /v1/events?delivered=false: lá estão os avisos que o seu endpoint ainda não confirmou, com o motivo.