Documentação

API do GO-API

Tudo o que você precisa para integrar seu sistema: autenticação, todos os endpoints, botões interativos, webhooks e exemplos prontos para copiar.

Visão geral

O GO-API expõe uma API REST em JSON. Neste documento, o endereço da API aparece como https://api.suaempresa.com: troque pelo seu (o mesmo valor de PUBLIC_API_URL no .env; sem domínio, http://IP_DO_SERVIDOR:4000).

Existem dois tipos de rota:

  • Rotas de integração (/api/*): enviam mensagens e consultam a instância. Autenticadas pela API key da instância. São as que você usará no dia a dia.
  • Rotas de gerenciamento (/instances, /webhooks, /auth…): criam instâncias, geram QR Code e alteram configurações. Autenticadas por token JWT do login no painel.

Todas as requisições com corpo devem enviar Content-Type: application/json.

Início rápido

  1. No painel, em Instâncias, crie uma instância e clique em Conectar.
  2. No celular: WhatsApp › Aparelhos conectados › Conectar aparelho e escaneie o QR Code.
  3. Copie a API key da instância (começa com goapikey_).
  4. Envie sua primeira mensagem:
cURL
curl -X POST https://api.suaempresa.com/api/send/text \
  -H "Content-Type: application/json" \
  -H "x-api-key: goapikey_SUA_CHAVE" \
  -d '{"to": "5511999999999", "text": "Olá! Primeira mensagem pela API 🚀"}'

Se a resposta tiver "success": true, a mensagem foi entregue ao WhatsApp.

Autenticação

API key (rotas /api/*)

Cada instância tem a sua própria chave, e é ela que define de qual número a mensagem sai: não existe campo de instância no corpo. Envie a chave em um destes headers (todos equivalentes):

Headers aceitos
x-api-key: goapikey_SUA_CHAVE
apikey: goapikey_SUA_CHAVE
Authorization: Bearer goapikey_SUA_CHAVE

O formato é sempre goapikey_ seguido de 48 caracteres hexadecimais. Chave ausente, com formato errado ou de instância removida retorna 401.

A API key dá acesso total ao número. Guarde-a só no servidor (nunca em JavaScript do navegador ou em apps públicos). Se vazar, gere outra no painel (Gerar nova chave) ou via rotate-key: a antiga para de funcionar na hora.

Token JWT (rotas de gerenciamento)

Faça login com e-mail e senha do painel e use o token retornado no header Authorization: Bearer <token>. Detalhes em Login (JWT).

Destinatário (campo to)

O campo to aceita vários formatos:

FormatoExemploObservação
Número5511999999999DDI + DDD + número. Símbolos são ignorados: +55 (11) 99999-9999 também funciona.
Grupo120363012345678901@g.usPegue o ID em Contatos e grupos ou no campo chatJid do webhook.
JID de contato5511999999999@s.whatsapp.netFormato interno do WhatsApp.
LID123456789@lidIdentificador anônimo que o WhatsApp usa em alguns chats. Pode ser respondido diretamente.

Para números, a API confere no WhatsApp se o número existe antes de enviar (com cache de 24h) e resolve sozinha o nono dígito de celulares brasileiros: tanto 5511999999999 quanto 551199999999 chegam ao contato certo. Se o número não tiver WhatsApp, a resposta é 400 com "O número informado não possui WhatsApp".

Respostas e erros

Sucesso no envio

Todos os endpoints de envio respondem 201 com o mesmo formato:

Resposta 201
{
  "id": "4b8f2c1e-6a1d-4f5e-9d3a-2c7b8e9f0a1b",
  "messageId": "3EB0A1B2C3D4E5F6A7B8",
  "to": "5511999999999",
  "type": "TEXT",
  "status": "SENT",
  "createdAt": "2026-10-16T13:45:12.000Z",
  "success": true,
  "jid": "5511999999999@s.whatsapp.net"
}
  • messageId: ID da mensagem no WhatsApp. Guarde-o para casar com os eventos de status (MESSAGES_UPDATE) no webhook.
  • jid: destino já resolvido (útil para saber qual variação do nono dígito foi usada).
  • type: TEXT, IMAGE, VIDEO, AUDIO, DOCUMENT, CONTACT, LOCATION, POLL, BUTTONS, CATALOG ou PRODUCT.
A requisição só responde depois que a mensagem saiu de fato (ela passa pela fila da instância). Por isso, use um timeout generoso no seu cliente HTTP (60 s ou mais) e não reenvie automaticamente em caso de timeout, para não duplicar mensagens.

Erros

Erros seguem o formato abaixo. Em erros de validação, message é uma lista com o campo problemático (ex: "Botão 2: Informe o texto do botão").

Resposta de erro
{
  "statusCode": 400,
  "message": ["Botão 2: Botão de link requer uma URL http(s) válida"],
  "error": "Bad Request"
}
CódigoQuando acontece
400Campo inválido, número sem WhatsApp, mídia que não pôde ser baixada, instância desconectada.
401API key ou token ausente/inválido.
403O token JWT não tem acesso à instância informada.
404Recurso não encontrado (instância, webhook, produto).
429Muitas requisições por IP, ou fila de envio da instância cheia. Aguarde e tente de novo.
504O WhatsApp não respondeu a uma consulta (ex: catálogo).

Fila e limites (anti-bloqueio)

Para proteger o número contra bloqueios, cada instância tem uma fila que envia uma mensagem por vez, com comportamento parecido com o de uma pessoa. Os valores padrão podem ser ajustados no .env da API:

VariávelPadrãoO que faz
SEND_MIN_INTERVAL_MS900Intervalo mínimo entre mensagens da mesma instância.
SEND_JITTER_MS600Variação aleatória somada ao intervalo.
SEND_MAX_PER_MINUTE40Máximo de mensagens por minuto por instância (as excedentes aguardam).
SEND_QUEUE_LIMIT500Tamanho máximo da fila. Acima disso, a API responde 429.
TYPING_SIMULATIONtrueMostra "digitando..." (ou "gravando..." para áudio) antes de enviar.
THROTTLE_LIMIT600Requisições HTTP por minuto por IP.
MEDIA_MAX_MB32Tamanho máximo de mídia baixada. Imagens de cabeçalho e de produto: até 5 MB.
BODY_LIMIT25mbTamanho máximo do corpo JSON (relevante ao enviar mídia em base64).

Para envios em massa, dispare as requisições uma de cada vez (aguardando a resposta) em vez de centenas em paralelo. Acompanhe o tamanho da fila em GET /api/instance.

Status da instância

GET/api/instanceAPI key

Retorna os dados da instância dona da API key. Use antes de enviar para conferir se está conectada.

curl https://api.suaempresa.com/api/instance -H "x-api-key: goapikey_SUA_CHAVE"
Resposta
{
  "id": "4b8f2c1e-6a1d-4f5e-9d3a-2c7b8e9f0a1b",
  "name": "Atendimento",
  "status": "CONNECTED",
  "phone": "5511999999999",
  "profileName": "Minha Loja",
  "queue": 0
}

status pode ser CONNECTED, CONNECTING, WAITING_QR ou DISCONNECTED. queue é a quantidade de mensagens aguardando na fila.

Verificar números

POST/api/check-numbersAPI key

Confere quais números têm WhatsApp, até 50 por chamada. Ideal para limpar listas antes de uma campanha.

CampoTipoObrigatórioDescrição
numbersstring[]simDe 1 a 50 números.
Corpo
{ "numbers": ["5511999999999", "5521988887777"] }
Resposta 200
[
  { "number": "5511999999999", "exists": true, "jid": "5511999999999@s.whatsapp.net" },
  { "number": "5521988887777", "exists": false, "jid": null }
]

Enviar texto

POST/api/send/textAPI key
CampoTipoObrigatórioDescrição
tostringsimDestino: número com DDI+DDD (5511999999999), grupo (...@g.us) ou JID. Veja Destinatário.
textstringsimTexto da mensagem (até 65.536 caracteres). Aceita a formatação do WhatsApp: *negrito*, _itálico_, ~riscado~, ```mono``` e quebras de linha \n.
delaynúmeronãoTempo em ms (0 a 25000) exibindo "digitando..." antes do envio. Se omitido, é calculado pelo tamanho do texto.
Corpo
{
  "to": "5511999999999",
  "text": "Olá, *Maria*! 👋\nSeu pedido #1234 foi enviado.",
  "delay": 1500
}

Enviar imagem

POST/api/send/imageAPI key
CampoTipoObrigatórioDescrição
tostringsimDestino: número com DDI+DDD (5511999999999), grupo (...@g.us) ou JID. Veja Destinatário.
imageUrlstringsimURL pública http(s) da imagem ou data URI base64 (data:image/png;base64,...).
captionstringnãoLegenda (até 4.096 caracteres).
delaynúmeronãoTempo em ms (0 a 25000) exibindo "digitando..." antes do envio. Se omitido, é calculado pelo tamanho do texto.
Corpo
{
  "to": "5511999999999",
  "imageUrl": "https://suaempresa.com/promo.jpg",
  "caption": "Promoção da semana 🔥"
}
A API baixa a mídia a partir da URL. Ela precisa ser pública (sem login) e responder rápido. Links de "visualização" do Google Drive e Dropbox não funcionam: use o link de download direto.

Enviar vídeo

POST/api/send/videoAPI key
CampoTipoObrigatórioDescrição
tostringsimDestino: número com DDI+DDD (5511999999999), grupo (...@g.us) ou JID. Veja Destinatário.
videoUrlstringsimURL pública ou data URI. Prefira MP4 (H.264).
captionstringnãoLegenda (até 4.096 caracteres).
delaynúmeronãoTempo em ms (0 a 25000) exibindo "digitando..." antes do envio. Se omitido, é calculado pelo tamanho do texto.
Corpo
{ "to": "5511999999999", "videoUrl": "https://suaempresa.com/tutorial.mp4", "caption": "Veja como usar" }

Enviar áudio

POST/api/send/audioAPI key
CampoTipoObrigatórioDescrição
tostringsimDestino: número com DDI+DDD (5511999999999), grupo (...@g.us) ou JID. Veja Destinatário.
audioUrlstringsimURL pública ou data URI (MP3, OGG, M4A, WAV…).
pttbooleannãotrue (padrão) envia como mensagem de voz (com a onda sonora, como se gravado na hora). false envia como arquivo de áudio.
delaynúmeronãoTempo em ms (0 a 25000) exibindo "digitando..." antes do envio. Se omitido, é calculado pelo tamanho do texto.
Corpo
{ "to": "5511999999999", "audioUrl": "https://suaempresa.com/boas-vindas.mp3", "ptt": true }

Como mensagem de voz, o áudio é convertido automaticamente para OGG/Opus, e o contato vê "gravando áudio..." antes de receber.

Enviar documento

POST/api/send/documentAPI key
CampoTipoObrigatórioDescrição
tostringsimDestino: número com DDI+DDD (5511999999999), grupo (...@g.us) ou JID. Veja Destinatário.
documentUrlstringsimURL pública ou data URI (PDF, planilhas, ZIP…).
filenamestringnãoNome exibido para o arquivo, com extensão (ex: boleto.pdf). Se omitido, usa o nome da URL.
captionstringnãoLegenda.
delaynúmeronãoTempo em ms (0 a 25000) exibindo "digitando..." antes do envio. Se omitido, é calculado pelo tamanho do texto.
Corpo
{
  "to": "5511999999999",
  "documentUrl": "https://suaempresa.com/boletos/1234.pdf",
  "filename": "boleto-outubro.pdf",
  "caption": "Segue seu boleto 📄"
}

Enviar contato

POST/api/send/contactAPI key
CampoTipoObrigatórioDescrição
tostringsimDestino: número com DDI+DDD (5511999999999), grupo (...@g.us) ou JID. Veja Destinatário.
contactNamestringsimNome do contato.
contactPhonestringsimTelefone com DDI (ex: 5511988887777).
organizationstringnãoEmpresa.
delaynúmeronãoTempo em ms (0 a 25000) exibindo "digitando..." antes do envio. Se omitido, é calculado pelo tamanho do texto.
Corpo
{ "to": "5511999999999", "contactName": "Suporte Loja", "contactPhone": "5511988887777", "organization": "Minha Loja" }

Enviar localização

POST/api/send/locationAPI key
CampoTipoObrigatórioDescrição
tostringsimDestino: número com DDI+DDD (5511999999999), grupo (...@g.us) ou JID. Veja Destinatário.
latitudenúmerosimLatitude (-90 a 90).
longitudenúmerosimLongitude (-180 a 180).
namestringnãoNome do local.
addressstringnãoEndereço.
delaynúmeronãoTempo em ms (0 a 25000) exibindo "digitando..." antes do envio. Se omitido, é calculado pelo tamanho do texto.
Corpo
{
  "to": "5511999999999",
  "latitude": -23.561684,
  "longitude": -46.655981,
  "name": "Loja Paulista",
  "address": "Av. Paulista, 1000 - São Paulo"
}

Enviar enquete

POST/api/send/pollAPI key
CampoTipoObrigatórioDescrição
tostringsimDestino: número com DDI+DDD (5511999999999), grupo (...@g.us) ou JID. Veja Destinatário.
questionstringsimPergunta (até 255 caracteres).
optionsstring[]simDe 2 a 12 opções (repetidas são removidas).
selectableCountnúmeronãoQuantas opções o contato pode marcar. Padrão: 1.
delaynúmeronãoTempo em ms (0 a 25000) exibindo "digitando..." antes do envio. Se omitido, é calculado pelo tamanho do texto.
Corpo
{
  "to": "5511999999999",
  "question": "Qual horário prefere para a entrega?",
  "options": ["Manhã", "Tarde", "Noite"],
  "selectableCount": 1
}

Os votos chegam pelo webhook no evento MESSAGES_UPDATE.

Enviar botões

POST/api/send/buttonsAPI key

Envia uma mensagem com botões interativos (formato native flow do WhatsApp). Há quatro tipos de botão, que podem ser combinados na mesma mensagem:

typeO que faz ao tocarCampos extras
replyEnvia uma resposta rápida de volta. Você recebe o id do botão no webhook.id (opcional)
urlAbre um link.url (obrigatório, http/https)
callLiga para um telefone.phoneNumber (obrigatório)
copyCopia um código (cupom, chave Pix, código de rastreio).copyCode (obrigatório), id (opcional)

Campos da mensagem

CampoTipoObrigatórioDescrição
tostringsimDestino: número com DDI+DDD (5511999999999), grupo (...@g.us) ou JID. Veja Destinatário.
textstringsimCorpo da mensagem (até 1.024 caracteres).
titlestringnãoTítulo no cabeçalho (até 60). Ignorado quando há imageUrl.
footerstringnãoRodapé em cinza (até 60).
imageUrlstringnãoImagem no cabeçalho: URL pública ou data URI, até 5 MB.
buttonsarraysimDe 1 a 10 botões (campos abaixo).
delaynúmeronãoTempo em ms (0 a 25000) exibindo "digitando..." antes do envio. Se omitido, é calculado pelo tamanho do texto.

Campos de cada botão

CampoTipoObrigatórioDescrição
typestringsimreply, url, call ou copy.
textstringsimTexto exibido no botão (até 25 caracteres).
idstringnãoIdentificador do botão (até 256). Vale para reply e copy. Se omitido, é gerado como reply_1, reply_2… pela posição. Não pode repetir na mesma mensagem.
urlstringnãoObrigatório no tipo url. Deve começar com http:// ou https://.
phoneNumberstringnãoObrigatório no tipo call. Com DDI, 8 a 15 dígitos (ex: +5511999999999).
copyCodestringnãoObrigatório no tipo copy. Texto copiado (até 512).

Exemplo 1: respostas rápidas (menu de atendimento)

Corpo
{
  "to": "5511999999999",
  "title": "Atendimento Minha Loja",
  "text": "Olá! Como podemos ajudar? Escolha uma opção:",
  "footer": "Responda tocando em um botão",
  "buttons": [
    { "type": "reply", "text": "📦 Meu pedido", "id": "menu_pedido" },
    { "type": "reply", "text": "💳 Pagamento", "id": "menu_pagamento" },
    { "type": "reply", "text": "👤 Falar com atendente", "id": "menu_humano" }
  ]
}

Exemplo 2: cobrança com Pix copia e cola + link

Corpo
{
  "to": "5511999999999",
  "imageUrl": "https://suaempresa.com/img/fatura.png",
  "text": "Sua fatura de *R$ 89,90* vence amanhã.\nCopie o código Pix abaixo ou pague pelo link.",
  "footer": "Minha Loja · Financeiro",
  "buttons": [
    { "type": "copy", "text": "Copiar código Pix", "copyCode": "00020126580014br.gov.bcb.pix0136...", "id": "pix_1234" },
    { "type": "url", "text": "Pagar pelo site", "url": "https://suaempresa.com/fatura/1234" },
    { "type": "call", "text": "Ligar para o suporte", "phoneNumber": "+551140028922" }
  ]
}

Exemplo 3: confirmação de agendamento

Corpo
{
  "to": "5511999999999",
  "text": "Olá, João! Você tem consulta *amanhã às 14h*. Confirma presença?",
  "buttons": [
    { "type": "reply", "text": "✅ Confirmo", "id": "agenda_987_sim" },
    { "type": "reply", "text": "❌ Preciso remarcar", "id": "agenda_987_nao" }
  ]
}
Dica: coloque no id tudo o que você precisa para agir quando a resposta chegar (ex: agenda_987_sim = agendamento 987, resposta "sim"). Assim seu sistema não precisa guardar estado de conversa.

Como receber o clique nos botões

Quando o contato toca em um botão reply, chega uma mensagem no seu webhook com o evento MESSAGES_UPSERT. O campo data.buttonId traz o id do botão e data.text traz o texto exibido:

Webhook recebido
{
  "event": "MESSAGES_UPSERT",
  "instance": { "id": "4b8f2c1e-...", "name": "Atendimento" },
  "data": {
    "key": { "remoteJid": "5511999999999@s.whatsapp.net", "fromMe": false, "id": "3EB0C5D6E7F8A9B0" },
    "pushName": "João",
    "chatJid": "5511999999999@s.whatsapp.net",
    "messageType": "interactiveResponseMessage",
    "text": "✅ Confirmo",
    "buttonId": "agenda_987_sim",
    "message": { "...": "conteúdo bruto do WhatsApp" },
    "messageTimestamp": 1760622312
  },
  "date_time": "2026-10-16T13:45:12.000Z"
}

Botões dos tipos url, call e copy executam a ação no próprio aparelho do contato (abrir link, ligar, copiar) e não enviam mensagem de volta.

Erros comuns ao enviar botões

  • Botão 1: O texto do botão deve ter no máximo 25 caracteres: encurte o texto (emojis contam).
  • Botão 2: Botão de link requer uma URL http(s) válida: a URL precisa ter https://.
  • Botão 3: Botão de copiar requer o código: faltou copyCode.
  • ID de botão duplicado: dois botões com o mesmo id.
  • Máximo de 10 botões / Informe pelo menos 1 botão.

Mensagens interativas dependem do WhatsApp do destinatário. Teste sempre no aparelho (celular) com o app atualizado antes de disparar para a sua base.

Enviar catálogo

POST/api/send/catalogAPI key

Envia um cartão com o botão Ver catálogo, que abre o catálogo dentro do WhatsApp. O número dono do catálogo precisa ser uma conta WhatsApp Business com catálogo ativo.

CampoTipoObrigatórioDescrição
tostringsimDestino: número com DDI+DDD (5511999999999), grupo (...@g.us) ou JID. Veja Destinatário.
textstringnãoTexto (padrão: "Confira nosso catálogo 🛍️").
titlestringnãoTítulo do cabeçalho (até 60).
footerstringnãoRodapé (até 60).
buttonTextstringnãoTexto do botão (padrão: "Ver catálogo", até 25).
imageUrlstringnãoImagem do cabeçalho.
catalogPhonestringnãoNúmero dono do catálogo, só dígitos. Padrão: o número da própria instância.
delaynúmeronãoTempo em ms (0 a 25000) exibindo "digitando..." antes do envio. Se omitido, é calculado pelo tamanho do texto.
Corpo
{
  "to": "5511999999999",
  "title": "Coleção de Verão",
  "text": "Chegaram as novidades! Toque abaixo para ver todos os produtos.",
  "imageUrl": "https://suaempresa.com/img/colecao.jpg",
  "buttonText": "Ver produtos"
}

Enviar produto

POST/api/send/productAPI key

Envia o cartão nativo de um produto (foto, nome, preço e botão "Ver"). A instância precisa ser WhatsApp Business.

CampoTipoObrigatórioDescrição
tostringsimDestino: número com DDI+DDD (5511999999999), grupo (...@g.us) ou JID. Veja Destinatário.
productIdstringsimID numérico do produto no catálogo: o número em wa.me/p/ID/telefone (copie o link do produto no app).
titlestringnãoNome do produto (até 120).
pricenúmeronãoPreço em reais, ex: 59.9.
imageUrlstringnãoFoto do produto (até 5 MB).
currencystringnãoMoeda ISO, padrão BRL.
descriptionstringnãoDescrição (até 500).
urlstringnãoLink do produto no seu site.
textstringnãoTexto que acompanha o cartão.
footerstringnãoRodapé.
delaynúmeronãoTempo em ms (0 a 25000) exibindo "digitando..." antes do envio. Se omitido, é calculado pelo tamanho do texto.
Sempre envie title, price e imageUrl. Atualmente o WhatsApp não responde às consultas de catálogo feitas por aparelhos conectados. Com esses três campos, a API monta o cartão sem depender da consulta, e o envio funciona normalmente.
Corpo
{
  "to": "5511999999999",
  "productId": "7012345678901234",
  "title": "Camiseta Básica Preta",
  "price": 59.9,
  "imageUrl": "https://suaempresa.com/produtos/camiseta.jpg",
  "description": "100% algodão, tamanhos P ao GG",
  "text": "Separamos este item para você 😉"
}

Listar produtos do catálogo

GET/api/catalogAPI key

Parâmetros de query (todos opcionais): phone (outro número Business), limit (1 a 100, padrão 30), cursor (próxima página, vindo de nextCursor) e refresh=true (ignora o cache de 3 minutos).

curl "https://api.suaempresa.com/api/catalog?limit=20" -H "x-api-key: goapikey_SUA_CHAVE"
Resposta
{
  "owner": "5511999999999",
  "catalogUrl": "https://wa.me/c/5511999999999",
  "products": [
    {
      "id": "7012345678901234",
      "name": "Camiseta Básica Preta",
      "description": "100% algodão",
      "price": 59.9,
      "priceFormatted": "R$ 59,90",
      "currency": "BRL",
      "imageUrl": "https://...",
      "url": null,
      "retailerId": "SKU-001",
      "available": true
    }
  ],
  "nextCursor": null
}

Pela limitação do WhatsApp citada acima, esta consulta pode responder 504. O envio do catálogo e do produto (com os dados informados) não depende dela.

Webhooks

Webhooks avisam seu sistema, em tempo real, sobre mensagens recebidas, status de entrega, conexão e mais. A API faz um POST em JSON para a sua URL a cada evento.

Como configurar

  • Por instância: no painel, abra a instância › Configurações › Webhook, ative, informe a URL, um segredo e os eventos. (Via API: PUT /instances/:id/settings.)
  • Global: no painel, página Webhooks, para receber eventos de todas as suas instâncias (ou de uma só) na mesma URL. (Via API: /webhooks.)

Deixar a lista de eventos vazia na configuração da instância significa receber todos.

Formato da entrega

Requisição enviada ao seu servidor
POST https://seusistema.com/webhook/whatsapp
Content-Type: application/json
X-Event: MESSAGES_UPSERT
X-Delivery-Id: 1f0c6a2e-9b3d-4c1a-8e7f-5d4c3b2a1f0e
X-Timestamp: 1760622312
X-Signature: sha256=9f2c...   (somente se houver segredo)

{
  "event": "MESSAGES_UPSERT",
  "instance": { "id": "4b8f2c1e-...", "name": "Atendimento" },
  "data": { ... },
  "date_time": "2026-10-16T13:45:12.000Z"
}
  • Responda rápido com HTTP 200 (timeout de 10 s). Processe o que for demorado em segundo plano.
  • Respostas 5xx, 429 ou falhas de rede geram 3 novas tentativas (após 2 s, 10 s e 30 s). Use o X-Delivery-Id e o data.key.id para ignorar duplicados.
  • Respostas 4xx não são repetidas.

Mensagem recebida (MESSAGES_UPSERT)

Payload
{
  "event": "MESSAGES_UPSERT",
  "instance": { "id": "4b8f2c1e-...", "name": "Atendimento" },
  "data": {
    "key": { "remoteJid": "5511999999999@s.whatsapp.net", "fromMe": false, "id": "3EB0A9F1C2D3E4F5" },
    "pushName": "Maria",
    "chatJid": "5511999999999@s.whatsapp.net",
    "messageType": "conversation",
    "text": "Oi, quero saber do meu pedido",
    "buttonId": null,
    "message": { "conversation": "Oi, quero saber do meu pedido" },
    "messageTimestamp": 1760622312
  },
  "date_time": "2026-10-16T13:45:12.000Z"
}
  • chatJid: use este campo como destino (to) para responder. Ele já resolve o número real mesmo quando o WhatsApp usa endereçamento @lid.
  • text: texto normalizado (mensagem, legenda de mídia ou texto do botão tocado).
  • buttonId: id do botão tocado, ou null.
  • messageType: tipo bruto (conversation, extendedTextMessage, imageMessage, audioMessage, interactiveResponseMessage…).
  • key.fromMe: true quando a mensagem foi enviada pelo próprio celular conectado. Ignore essas mensagens no seu bot para não criar loops.
  • Grupos: chatJid termina em @g.us e key.participant indica quem enviou.

Mensagens enviadas pela API não chegam como MESSAGES_UPSERT: elas geram o evento SEND_MESSAGE.

Outros payloads úteis

Campo data de cada evento
// CONNECTION_UPDATE
{ "state": "open", "status": "CONNECTED", "phone": "5511999999999", "reason": null }

// QRCODE_UPDATED (base64 é uma imagem PNG pronta para <img src="...">)
{ "qrcode": { "code": "2@AbCd...", "base64": "data:image/png;base64,iVBOR..." }, "attempt": 1 }

// SEND_MESSAGE (source: api, panel, bot ou chatwoot)
{ "key": { "remoteJid": "5511999999999@s.whatsapp.net", "fromMe": true, "id": "3EB0..." }, "message": { ... }, "source": "api" }

Validar a assinatura do webhook

Se você configurou um segredo, cada entrega traz o header X-Signature: sha256=<hex>, calculado como HMAC-SHA256(segredo, X-Timestamp + "." + corpo_bruto). Valide-o para ter certeza de que o evento veio da sua API:

PHP
<?php
$secret    = 'SEU_SEGREDO';
$body      = file_get_contents('php://input');            // corpo BRUTO, antes de decodificar
$timestamp = $_SERVER['HTTP_X_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';

$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, $secret);

if (!hash_equals($expected, $signature) || abs(time() - (int) $timestamp) > 300) {
    http_response_code(401);
    exit;
}

$event = json_decode($body, true);
http_response_code(200);
// ... trate $event['event'] e $event['data']
Node.js
// Node.js + Express
const crypto = require('crypto');
const express = require('express');
const app = express();

app.post('/webhook/whatsapp', express.raw({ type: 'application/json' }), (req, res) => {
  const timestamp = req.get('X-Timestamp') || '';
  const expected = 'sha256=' + crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET)
    .update(`${timestamp}.${req.body}`)
    .digest('hex');
  const received = req.get('X-Signature') || '';

  const valid = received.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
  if (!valid) return res.sendStatus(401);

  res.sendStatus(200);
  const event = JSON.parse(req.body);
  // ... trate event.event e event.data
});
Calcule a assinatura sobre o corpo bruto recebido. Se você decodificar e recodificar o JSON antes, a assinatura não vai bater.

Lista de eventos

EventoQuando dispara
MESSAGES_UPSERTMensagem nova recebida (inclui respostas de botões).
MESSAGES_UPDATEStatus de mensagens (entregue, lida) e votos de enquete.
MESSAGES_DELETEMensagem apagada.
MESSAGES_SETSincronização de histórico concluída (apenas contagem).
SEND_MESSAGEMensagem enviada pela API, painel, bot ou Chatwoot.
CONNECTION_UPDATEInstância conectou, desconectou ou está conectando.
QRCODE_UPDATEDNovo QR Code gerado.
LOGOUT_INSTANCESessão encerrada (desconectada pelo celular ou pelo painel).
REMOVE_INSTANCEInstância removida.
CALLChamada recebida.
PRESENCE_UPDATEContato digitando, gravando, online.
CONTACTS_UPSERT / CONTACTS_UPDATE / CONTACTS_SETContatos adicionados, alterados ou sincronizados.
CHATS_UPSERT / CHATS_UPDATE / CHATS_DELETE / CHATS_SETConversas criadas, alteradas, apagadas ou sincronizadas.
GROUPS_UPSERT / GROUP_UPDATEGrupo criado/entrou, ou dados do grupo alterados.
GROUP_PARTICIPANTS_UPDATEParticipantes adicionados, removidos, promovidos.
LABELS_EDIT / LABELS_ASSOCIATIONEtiquetas (WhatsApp Business) editadas ou aplicadas.
TYPEBOT_START / TYPEBOT_CHANGE_STATUSEventos da integração Typebot.
APPLICATION_STARTUPAPI iniciada.

Também são aceitos os apelidos message_received (= MESSAGES_UPSERT), message_sent (= SEND_MESSAGE), instance_status (= CONNECTION_UPDATE) e qr_code_updated (= QRCODE_UPDATED). A lista sempre atualizada está em GET /webhooks/events.

WebSocket (tempo real)

Como alternativa ao webhook, conecte-se via Socket.IO no caminho /ws. Os mesmos eventos chegam com o mesmo payload. Primeiro, ative o WebSocket na instância (Configurações › WebSocket).

JavaScript
// npm install socket.io-client
import { io } from 'socket.io-client';

const socket = io('https://api.suaempresa.com', {
  path: '/ws',
  auth: { apikey: 'goapikey_SUA_CHAVE' }, // uma instância
  // auth: { token: 'JWT' },              // todas as instâncias do usuário
});

socket.on('MESSAGES_UPSERT', (evt) => {
  console.log(evt.data.chatJid, evt.data.text, evt.data.buttonId);
});
socket.on('CONNECTION_UPDATE', (evt) => console.log('Status:', evt.data.status));
socket.on('error', (err) => console.error(err.message));

Login e token JWT

As rotas de gerenciamento usam o usuário do painel. Faça login e envie o token em Authorization: Bearer <token>. O token vale 7 dias por padrão (JWT_EXPIRES_IN) e é invalidado ao trocar a senha.

POST/auth/loginPúblico
curl -X POST https://api.suaempresa.com/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "admin@suaempresa.com", "password": "SUA_SENHA"}'
Resposta 200
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "user": { "id": "...", "email": "admin@suaempresa.com", "name": "admin", "role": "ADMIN" }
}
RotaDescrição
GET /auth/meDados do usuário logado.
POST /auth/change-password{ "currentPassword", "newPassword" } (mínimo 8 caracteres).
POST /auth/register{ "email", "password", "name"? }. Aberto ao público só com ALLOW_REGISTRATION=true; o administrador logado sempre pode cadastrar usuários.
GET /auth/optionsPúblico. Informa se o cadastro está aberto.

Login: 10 tentativas por minuto. Cadastro e troca de senha: 5 por minuto.

Instâncias

Uma instância é um número de WhatsApp conectado. Todas as rotas abaixo usam JWT; :id é o ID da instância.

RotaDescrição
GET /instancesLista as instâncias (com apiKey, status, phone).
POST /instancesCria: { "name": "Atendimento" }. A resposta já traz a apiKey.
GET /instances/:idDetalhes de uma instância.
PUT /instances/:idRenomeia: { "name": "Vendas" }.
DELETE /instances/:idDesconecta e remove.
POST /instances/:id/connectInicia a conexão. Retorna { status, qr, qrCode }, onde qrCode é uma imagem base64.
GET /instances/:id/qrQR Code atual. status: QR_READY, WAITING, CONNECTED ou DISCONNECTED.
POST /instances/:id/disconnectEncerra a sessão (logout do aparelho).
POST /instances/:id/reconnectReconecta (pode gerar novo QR).
POST /instances/:id/restartReinicia a conexão sem deslogar.
POST /instances/:id/rotate-keyGera nova API key: { "apiKey": "goapikey_..." }. A antiga deixa de funcionar.
POST /auth/shared-tokenLink de QR para o cliente escanear sem acessar o painel: { "instanceId", "expiresInHours": 24 } (1 a 168 h). Retorna { token, expiresAt }.
GET /instances/shared/:tokenPúblico. QR Code da instância a partir do token compartilhado.

Fluxo para conectar um número por código

Bash
# 1. Login
TOKEN=$(curl -s -X POST https://api.suaempresa.com/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"admin@suaempresa.com","password":"SUA_SENHA"}' | jq -r .token)

# 2. Criar instância (guarde o id e a apiKey)
curl -s -X POST https://api.suaempresa.com/instances \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"name":"Cliente 01"}'

# 3. Iniciar conexão e exibir o qrCode (imagem base64) para o usuário escanear
curl -s -X POST https://api.suaempresa.com/instances/ID_DA_INSTANCIA/connect \
  -H "Authorization: Bearer $TOKEN"

# 4. Consultar até status = CONNECTED (ou ouvir o evento CONNECTION_UPDATE)
curl -s https://api.suaempresa.com/instances/ID_DA_INSTANCIA/qr \
  -H "Authorization: Bearer $TOKEN"

O QR Code é renovado a cada ~20 s. Depois de várias tentativas sem leitura, a conexão pausa; chame connect de novo.

Configurações da instância

GET/instances/:id/settingsJWT
PUT/instances/:id/settingsJWT

Envie apenas os campos que deseja alterar.

CampoDescrição
rejectCalls / rejectCallMessageRejeita chamadas e responde com a mensagem informada.
ignoreGroupsNão processa mensagens de grupos.
alwaysOnlineMantém o número como "online".
readMessagesMarca mensagens recebidas como lidas.
readStatusVisualiza os status dos contatos.
syncFullHistorySincroniza o histórico completo ao conectar.
webhook{ enabled, url, secret, events[] }
websocket{ enabled, events[] }
rabbitmq{ enabled, uri, exchange, events[] }
sqs{ enabled, accessKeyId, secretAccessKey, region, queueUrl, events[] }
proxy{ enabled, protocol (http|https|socks4|socks5), host, port, username, password }
typebot / n8n / chatwootIntegrações nativas (mais fácil configurar pelo painel).
Exemplo: ativar webhook
{
  "rejectCalls": true,
  "rejectCallMessage": "Não atendemos ligações. Mande uma mensagem 🙂",
  "webhook": {
    "enabled": true,
    "url": "https://seusistema.com/webhook/whatsapp",
    "secret": "um-segredo-forte",
    "events": ["MESSAGES_UPSERT", "MESSAGES_UPDATE", "CONNECTION_UPDATE"]
  }
}

Contatos e grupos

Rota (JWT)Descrição
GET /instances/:id/contacts/groupsGrupos do número: { groups: [{ id, name, participants, creation, owner }] }. O id (...@g.us) é usado no to.
GET /instances/:id/contacts/groups/:groupId/participantsParticipantes: { groupName, groupId, participants: [{ id, phone, admin, name }] }.
GET /instances/:id/contacts/all?search=mariaContatos conhecidos (até 5.000), com busca por nome ou número.

Webhooks globais

Rota (JWT)Descrição
GET /webhooks/eventsLista de eventos disponíveis.
GET /webhooksSeus webhooks (o segredo vem mascarado).
POST /webhooks{ "url", "events": [...], "secret"?, "instanceId"? }. Sem instanceId, recebe de todas as instâncias.
PUT /webhooks/:idAltera url, events, active, secret, instanceId (null remove).
POST /webhooks/:id/testEnvia um evento TEST e informa o HTTP retornado.
DELETE /webhooks/:idRemove o webhook.

Envio pelo painel (JWT)

Todos os envios também existem em /messages/* com token JWT. O corpo é o mesmo das rotas /api/send/*, mais o campo instanceId. Útil quando um único sistema gerencia várias instâncias com um só login.

API keyEquivalente JWT
POST /api/send/textPOST /messages/text
POST /api/send/image · video · audio · documentPOST /messages/image · video · audio · document
POST /api/send/contact · location · pollPOST /messages/contact · location · poll
POST /api/send/buttons · catalog · productPOST /messages/buttons · catalog · product
GET /api/catalogGET /messages/catalog?instanceId=...
POST /api/check-numbersPOST /messages/check-numbers
-GET /messages/stats: { today, total } mensagens enviadas.
POST /messages/text
{ "instanceId": "4b8f2c1e-6a1d-4f5e-9d3a-2c7b8e9f0a1b", "to": "5511999999999", "text": "Olá!" }

Saúde da API

Rota (pública)Descrição
GET /statusVerificação simples: { "status": "ok", "timestamp": "..." }.
GET /healthInclui o banco: { status, database, uptimeSeconds, connectedInstances, version }. Responde 503 se o banco estiver fora. Ideal para monitoramento (UptimeRobot, Kuma).

Exemplo completo: bot com botões

Um webhook em PHP que responde "oi" com um menu de botões e trata cada opção. Coloque em um servidor com HTTPS e cadastre a URL no webhook da instância com o evento MESSAGES_UPSERT.

PHP
<?php
// webhook.php
const API_URL = 'https://api.suaempresa.com';
const API_KEY = 'goapikey_SUA_CHAVE';
const SECRET  = 'SEU_SEGREDO';

function go_api(string $path, array $body): array
{
    $ch = curl_init(API_URL . $path);
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 60,
        CURLOPT_HTTPHEADER     => ['Content-Type: application/json', 'x-api-key: ' . API_KEY],
        CURLOPT_POSTFIELDS     => json_encode($body, JSON_UNESCAPED_UNICODE),
    ]);
    $response = curl_exec($ch);
    curl_close($ch);
    return json_decode((string) $response, true) ?? [];
}

// 1. Valida a assinatura
$raw = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', ($_SERVER['HTTP_X_TIMESTAMP'] ?? '') . '.' . $raw, SECRET);
if (!hash_equals($expected, $_SERVER['HTTP_X_SIGNATURE'] ?? '')) {
    http_response_code(401);
    exit;
}

// 2. Responde 200 imediatamente
http_response_code(200);
if (function_exists('fastcgi_finish_request')) fastcgi_finish_request();

$event = json_decode($raw, true);
if (($event['event'] ?? '') !== 'MESSAGES_UPSERT') exit;

$data = $event['data'];
if (!empty($data['key']['fromMe'])) exit;                   // ignora mensagens próprias
if (str_ends_with($data['chatJid'], '@g.us')) exit;          // ignora grupos

$to       = $data['chatJid'];
$text     = mb_strtolower(trim($data['text'] ?? ''));
$buttonId = $data['buttonId'] ?? null;

// 3. Clique em botão
switch ($buttonId) {
    case 'menu_pedido':
        go_api('/api/send/text', ['to' => $to, 'text' => 'Informe o número do seu pedido 📦']);
        exit;
    case 'menu_pagamento':
        go_api('/api/send/buttons', [
            'to'      => $to,
            'text'    => 'Pague com Pix copiando o código abaixo:',
            'buttons' => [
                ['type' => 'copy', 'text' => 'Copiar código Pix', 'copyCode' => '00020126580014br.gov.bcb.pix...'],
                ['type' => 'url', 'text' => 'Ver fatura', 'url' => 'https://suaempresa.com/fatura'],
            ],
        ]);
        exit;
    case 'menu_humano':
        go_api('/api/send/text', ['to' => $to, 'text' => 'Certo! Um atendente vai falar com você em instantes 👤']);
        exit;
}

// 4. Texto livre: mostra o menu
if (in_array($text, ['oi', 'olá', 'ola', 'menu'], true)) {
    go_api('/api/send/buttons', [
        'to'      => $to,
        'title'   => 'Minha Loja',
        'text'    => 'Olá, ' . ($data['pushName'] ?? '') . '! Como podemos ajudar?',
        'footer'  => 'Atendimento automático',
        'buttons' => [
            ['type' => 'reply', 'text' => '📦 Meu pedido', 'id' => 'menu_pedido'],
            ['type' => 'reply', 'text' => '💳 Pagamento', 'id' => 'menu_pagamento'],
            ['type' => 'reply', 'text' => '👤 Atendente', 'id' => 'menu_humano'],
        ],
    ]);
}

Envio em Node.js

Node.js 18+
const API_URL = 'https://api.suaempresa.com';
const API_KEY = process.env.GOAPI_KEY;

async function goApi(path, body) {
  const res = await fetch(API_URL + path, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'x-api-key': API_KEY },
    body: JSON.stringify(body),
    signal: AbortSignal.timeout(60_000),
  });
  const data = await res.json();
  if (!res.ok) throw new Error([].concat(data.message).join('; '));
  return data;
}

await goApi('/api/send/buttons', {
  to: '5511999999999',
  text: 'Seu pedido saiu para entrega 🚚',
  buttons: [
    { type: 'url', text: 'Rastrear pedido', url: 'https://suaempresa.com/rastreio/1234' },
    { type: 'copy', text: 'Copiar rastreio', copyCode: 'BR123456789BR' },
  ],
});

Envio em Python

Python
import requests

API_URL = "https://api.suaempresa.com"
HEADERS = {"x-api-key": "goapikey_SUA_CHAVE"}

r = requests.post(f"{API_URL}/api/send/buttons", headers=HEADERS, timeout=60, json={
    "to": "5511999999999",
    "text": "Confirma sua consulta amanhã às 14h?",
    "buttons": [
        {"type": "reply", "text": "✅ Confirmo", "id": "agenda_987_sim"},
        {"type": "reply", "text": "❌ Remarcar", "id": "agenda_987_nao"},
    ],
})
r.raise_for_status()
print(r.json()["messageId"])

Problemas comuns

401 "API key não informada"

O header não chegou. Confira o nome (x-api-key) e, atrás de proxy, se ele não está sendo removido. Como alternativa, use apikey.

400 "Instância não está conectada ao WhatsApp"

Consulte GET /api/instance. Se o status não for CONNECTED, reconecte pelo painel (o celular precisa estar com internet na primeira conexão).

400 "Não foi possível baixar a mídia"

A URL não é pública, exige login, redireciona para uma página HTML ou é muito lenta. Teste a URL em uma aba anônima: ela deve baixar o arquivo direto. Outra opção é enviar em base64 (data:...;base64,...).

Os botões não aparecem para o contato

Peça para o contato atualizar o WhatsApp e confira no celular (não apenas no WhatsApp Web). Revise também os limites: texto do botão com até 25 caracteres e no máximo 10 botões.

Não recebo os cliques dos botões

Confirme que o webhook está ativo com o evento MESSAGES_UPSERT (ou lista vazia), use o botão Testar do painel e verifique se seu endpoint responde 200 em menos de 10 s. Lembre-se: só botões reply geram mensagem de volta.

429 Too Many Requests

Você ultrapassou o limite de requisições por IP ou a fila da instância encheu. Envie de forma sequencial, aguardando cada resposta.

Catálogo/produto: "não é uma conta WhatsApp Business"

Catálogo e produto só funcionam em números WhatsApp Business. No produto, envie sempre title, price e imageUrl.

Boas práticas: envie mensagens só para quem pediu para recebê-las, ofereça uma forma de sair da lista e evite disparos em massa para números novos. Isso reduz muito o risco de bloqueio do número pelo WhatsApp.