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
- No painel, em Instâncias, crie uma instância e clique em Conectar.
- No celular: WhatsApp › Aparelhos conectados › Conectar aparelho e escaneie o QR Code.
- Copie a API key da instância (começa com
goapikey_). - Envie sua primeira mensagem:
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):
x-api-key: goapikey_SUA_CHAVE
apikey: goapikey_SUA_CHAVE
Authorization: Bearer goapikey_SUA_CHAVEO formato é sempre goapikey_ seguido de 48 caracteres hexadecimais. Chave ausente, com formato errado ou de instância removida retorna 401.
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:
| Formato | Exemplo | Observação |
|---|---|---|
| Número | 5511999999999 | DDI + DDD + número. Símbolos são ignorados: +55 (11) 99999-9999 também funciona. |
| Grupo | 120363012345678901@g.us | Pegue o ID em Contatos e grupos ou no campo chatJid do webhook. |
| JID de contato | 5511999999999@s.whatsapp.net | Formato interno do WhatsApp. |
| LID | 123456789@lid | Identificador 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:
{
"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,CATALOGouPRODUCT.
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").
{
"statusCode": 400,
"message": ["Botão 2: Botão de link requer uma URL http(s) válida"],
"error": "Bad Request"
}| Código | Quando acontece |
|---|---|
| 400 | Campo inválido, número sem WhatsApp, mídia que não pôde ser baixada, instância desconectada. |
| 401 | API key ou token ausente/inválido. |
| 403 | O token JWT não tem acesso à instância informada. |
| 404 | Recurso não encontrado (instância, webhook, produto). |
| 429 | Muitas requisições por IP, ou fila de envio da instância cheia. Aguarde e tente de novo. |
| 504 | O 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ável | Padrão | O que faz |
|---|---|---|
SEND_MIN_INTERVAL_MS | 900 | Intervalo mínimo entre mensagens da mesma instância. |
SEND_JITTER_MS | 600 | Variação aleatória somada ao intervalo. |
SEND_MAX_PER_MINUTE | 40 | Máximo de mensagens por minuto por instância (as excedentes aguardam). |
SEND_QUEUE_LIMIT | 500 | Tamanho máximo da fila. Acima disso, a API responde 429. |
TYPING_SIMULATION | true | Mostra "digitando..." (ou "gravando..." para áudio) antes de enviar. |
THROTTLE_LIMIT | 600 | Requisições HTTP por minuto por IP. |
MEDIA_MAX_MB | 32 | Tamanho máximo de mídia baixada. Imagens de cabeçalho e de produto: até 5 MB. |
BODY_LIMIT | 25mb | Tamanho 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
/api/instanceAPI keyRetorna 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"{
"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
/api/check-numbersAPI keyConfere quais números têm WhatsApp, até 50 por chamada. Ideal para limpar listas antes de uma campanha.
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
numbers | string[] | sim | De 1 a 50 números. |
{ "numbers": ["5511999999999", "5521988887777"] }[
{ "number": "5511999999999", "exists": true, "jid": "5511999999999@s.whatsapp.net" },
{ "number": "5521988887777", "exists": false, "jid": null }
]Enviar texto
/api/send/textAPI key| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
to | string | sim | Destino: número com DDI+DDD (5511999999999), grupo (...@g.us) ou JID. Veja Destinatário. |
text | string | sim | Texto da mensagem (até 65.536 caracteres). Aceita a formatação do WhatsApp: *negrito*, _itálico_, ~riscado~, ```mono``` e quebras de linha \n. |
delay | número | não | Tempo em ms (0 a 25000) exibindo "digitando..." antes do envio. Se omitido, é calculado pelo tamanho do texto. |
{
"to": "5511999999999",
"text": "Olá, *Maria*! 👋\nSeu pedido #1234 foi enviado.",
"delay": 1500
}Enviar imagem
/api/send/imageAPI key| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
to | string | sim | Destino: número com DDI+DDD (5511999999999), grupo (...@g.us) ou JID. Veja Destinatário. |
imageUrl | string | sim | URL pública http(s) da imagem ou data URI base64 (data:image/png;base64,...). |
caption | string | não | Legenda (até 4.096 caracteres). |
delay | número | não | Tempo em ms (0 a 25000) exibindo "digitando..." antes do envio. Se omitido, é calculado pelo tamanho do texto. |
{
"to": "5511999999999",
"imageUrl": "https://suaempresa.com/promo.jpg",
"caption": "Promoção da semana 🔥"
}Enviar vídeo
/api/send/videoAPI key| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
to | string | sim | Destino: número com DDI+DDD (5511999999999), grupo (...@g.us) ou JID. Veja Destinatário. |
videoUrl | string | sim | URL pública ou data URI. Prefira MP4 (H.264). |
caption | string | não | Legenda (até 4.096 caracteres). |
delay | número | não | Tempo em ms (0 a 25000) exibindo "digitando..." antes do envio. Se omitido, é calculado pelo tamanho do texto. |
{ "to": "5511999999999", "videoUrl": "https://suaempresa.com/tutorial.mp4", "caption": "Veja como usar" }Enviar áudio
/api/send/audioAPI key| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
to | string | sim | Destino: número com DDI+DDD (5511999999999), grupo (...@g.us) ou JID. Veja Destinatário. |
audioUrl | string | sim | URL pública ou data URI (MP3, OGG, M4A, WAV…). |
ptt | boolean | não | true (padrão) envia como mensagem de voz (com a onda sonora, como se gravado na hora). false envia como arquivo de áudio. |
delay | número | não | Tempo em ms (0 a 25000) exibindo "digitando..." antes do envio. Se omitido, é calculado pelo tamanho do texto. |
{ "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
/api/send/documentAPI key| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
to | string | sim | Destino: número com DDI+DDD (5511999999999), grupo (...@g.us) ou JID. Veja Destinatário. |
documentUrl | string | sim | URL pública ou data URI (PDF, planilhas, ZIP…). |
filename | string | não | Nome exibido para o arquivo, com extensão (ex: boleto.pdf). Se omitido, usa o nome da URL. |
caption | string | não | Legenda. |
delay | número | não | Tempo em ms (0 a 25000) exibindo "digitando..." antes do envio. Se omitido, é calculado pelo tamanho do texto. |
{
"to": "5511999999999",
"documentUrl": "https://suaempresa.com/boletos/1234.pdf",
"filename": "boleto-outubro.pdf",
"caption": "Segue seu boleto 📄"
}Enviar contato
/api/send/contactAPI key| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
to | string | sim | Destino: número com DDI+DDD (5511999999999), grupo (...@g.us) ou JID. Veja Destinatário. |
contactName | string | sim | Nome do contato. |
contactPhone | string | sim | Telefone com DDI (ex: 5511988887777). |
organization | string | não | Empresa. |
delay | número | não | Tempo em ms (0 a 25000) exibindo "digitando..." antes do envio. Se omitido, é calculado pelo tamanho do texto. |
{ "to": "5511999999999", "contactName": "Suporte Loja", "contactPhone": "5511988887777", "organization": "Minha Loja" }Enviar localização
/api/send/locationAPI key| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
to | string | sim | Destino: número com DDI+DDD (5511999999999), grupo (...@g.us) ou JID. Veja Destinatário. |
latitude | número | sim | Latitude (-90 a 90). |
longitude | número | sim | Longitude (-180 a 180). |
name | string | não | Nome do local. |
address | string | não | Endereço. |
delay | número | não | Tempo em ms (0 a 25000) exibindo "digitando..." antes do envio. Se omitido, é calculado pelo tamanho do texto. |
{
"to": "5511999999999",
"latitude": -23.561684,
"longitude": -46.655981,
"name": "Loja Paulista",
"address": "Av. Paulista, 1000 - São Paulo"
}Enviar enquete
/api/send/pollAPI key| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
to | string | sim | Destino: número com DDI+DDD (5511999999999), grupo (...@g.us) ou JID. Veja Destinatário. |
question | string | sim | Pergunta (até 255 caracteres). |
options | string[] | sim | De 2 a 12 opções (repetidas são removidas). |
selectableCount | número | não | Quantas opções o contato pode marcar. Padrão: 1. |
delay | número | não | Tempo em ms (0 a 25000) exibindo "digitando..." antes do envio. Se omitido, é calculado pelo tamanho do texto. |
{
"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
/api/send/buttonsAPI keyEnvia uma mensagem com botões interativos (formato native flow do WhatsApp). Há quatro tipos de botão, que podem ser combinados na mesma mensagem:
| type | O que faz ao tocar | Campos extras |
|---|---|---|
reply | Envia uma resposta rápida de volta. Você recebe o id do botão no webhook. | id (opcional) |
url | Abre um link. | url (obrigatório, http/https) |
call | Liga para um telefone. | phoneNumber (obrigatório) |
copy | Copia um código (cupom, chave Pix, código de rastreio). | copyCode (obrigatório), id (opcional) |
Campos da mensagem
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
to | string | sim | Destino: número com DDI+DDD (5511999999999), grupo (...@g.us) ou JID. Veja Destinatário. |
text | string | sim | Corpo da mensagem (até 1.024 caracteres). |
title | string | não | Título no cabeçalho (até 60). Ignorado quando há imageUrl. |
footer | string | não | Rodapé em cinza (até 60). |
imageUrl | string | não | Imagem no cabeçalho: URL pública ou data URI, até 5 MB. |
buttons | array | sim | De 1 a 10 botões (campos abaixo). |
delay | número | não | Tempo em ms (0 a 25000) exibindo "digitando..." antes do envio. Se omitido, é calculado pelo tamanho do texto. |
Campos de cada botão
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
type | string | sim | reply, url, call ou copy. |
text | string | sim | Texto exibido no botão (até 25 caracteres). |
id | string | não | Identificador 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. |
url | string | não | Obrigatório no tipo url. Deve começar com http:// ou https://. |
phoneNumber | string | não | Obrigatório no tipo call. Com DDI, 8 a 15 dígitos (ex: +5511999999999). |
copyCode | string | não | Obrigatório no tipo copy. Texto copiado (até 512). |
Exemplo 1: respostas rápidas (menu de atendimento)
{
"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
{
"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
{
"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" }
]
}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:
{
"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 terhttps://.Botão 3: Botão de copiar requer o código: faltoucopyCode.ID de botão duplicado: dois botões com o mesmoid.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
/api/send/catalogAPI keyEnvia 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.
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
to | string | sim | Destino: número com DDI+DDD (5511999999999), grupo (...@g.us) ou JID. Veja Destinatário. |
text | string | não | Texto (padrão: "Confira nosso catálogo 🛍️"). |
title | string | não | Título do cabeçalho (até 60). |
footer | string | não | Rodapé (até 60). |
buttonText | string | não | Texto do botão (padrão: "Ver catálogo", até 25). |
imageUrl | string | não | Imagem do cabeçalho. |
catalogPhone | string | não | Número dono do catálogo, só dígitos. Padrão: o número da própria instância. |
delay | número | não | Tempo em ms (0 a 25000) exibindo "digitando..." antes do envio. Se omitido, é calculado pelo tamanho do texto. |
{
"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
/api/send/productAPI keyEnvia o cartão nativo de um produto (foto, nome, preço e botão "Ver"). A instância precisa ser WhatsApp Business.
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
to | string | sim | Destino: número com DDI+DDD (5511999999999), grupo (...@g.us) ou JID. Veja Destinatário. |
productId | string | sim | ID numérico do produto no catálogo: o número em wa.me/p/ID/telefone (copie o link do produto no app). |
title | string | não | Nome do produto (até 120). |
price | número | não | Preço em reais, ex: 59.9. |
imageUrl | string | não | Foto do produto (até 5 MB). |
currency | string | não | Moeda ISO, padrão BRL. |
description | string | não | Descrição (até 500). |
url | string | não | Link do produto no seu site. |
text | string | não | Texto que acompanha o cartão. |
footer | string | não | Rodapé. |
delay | número | não | Tempo em ms (0 a 25000) exibindo "digitando..." antes do envio. Se omitido, é calculado pelo tamanho do texto. |
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.{
"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
/api/catalogAPI keyParâ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"{
"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
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,429ou falhas de rede geram 3 novas tentativas (após 2 s, 10 s e 30 s). Use oX-Delivery-Ide odata.key.idpara ignorar duplicados. - Respostas
4xxnão são repetidas.
Mensagem recebida (MESSAGES_UPSERT)
{
"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, ounull.messageType: tipo bruto (conversation,extendedTextMessage,imageMessage,audioMessage,interactiveResponseMessage…).key.fromMe:truequando a mensagem foi enviada pelo próprio celular conectado. Ignore essas mensagens no seu bot para não criar loops.- Grupos:
chatJidtermina em@g.usekey.participantindica quem enviou.
Mensagens enviadas pela API não chegam como MESSAGES_UPSERT: elas geram o evento SEND_MESSAGE.
Outros payloads úteis
// 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
$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 + 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
});Lista de eventos
| Evento | Quando dispara |
|---|---|
MESSAGES_UPSERT | Mensagem nova recebida (inclui respostas de botões). |
MESSAGES_UPDATE | Status de mensagens (entregue, lida) e votos de enquete. |
MESSAGES_DELETE | Mensagem apagada. |
MESSAGES_SET | Sincronização de histórico concluída (apenas contagem). |
SEND_MESSAGE | Mensagem enviada pela API, painel, bot ou Chatwoot. |
CONNECTION_UPDATE | Instância conectou, desconectou ou está conectando. |
QRCODE_UPDATED | Novo QR Code gerado. |
LOGOUT_INSTANCE | Sessão encerrada (desconectada pelo celular ou pelo painel). |
REMOVE_INSTANCE | Instância removida. |
CALL | Chamada recebida. |
PRESENCE_UPDATE | Contato digitando, gravando, online. |
CONTACTS_UPSERT / CONTACTS_UPDATE / CONTACTS_SET | Contatos adicionados, alterados ou sincronizados. |
CHATS_UPSERT / CHATS_UPDATE / CHATS_DELETE / CHATS_SET | Conversas criadas, alteradas, apagadas ou sincronizadas. |
GROUPS_UPSERT / GROUP_UPDATE | Grupo criado/entrou, ou dados do grupo alterados. |
GROUP_PARTICIPANTS_UPDATE | Participantes adicionados, removidos, promovidos. |
LABELS_EDIT / LABELS_ASSOCIATION | Etiquetas (WhatsApp Business) editadas ou aplicadas. |
TYPEBOT_START / TYPEBOT_CHANGE_STATUS | Eventos da integração Typebot. |
APPLICATION_STARTUP | API 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).
// 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.
/auth/loginPúblicocurl -X POST https://api.suaempresa.com/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "admin@suaempresa.com", "password": "SUA_SENHA"}'{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": { "id": "...", "email": "admin@suaempresa.com", "name": "admin", "role": "ADMIN" }
}| Rota | Descrição |
|---|---|
GET /auth/me | Dados 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/options | Pú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.
| Rota | Descrição |
|---|---|
GET /instances | Lista as instâncias (com apiKey, status, phone). |
POST /instances | Cria: { "name": "Atendimento" }. A resposta já traz a apiKey. |
GET /instances/:id | Detalhes de uma instância. |
PUT /instances/:id | Renomeia: { "name": "Vendas" }. |
DELETE /instances/:id | Desconecta e remove. |
POST /instances/:id/connect | Inicia a conexão. Retorna { status, qr, qrCode }, onde qrCode é uma imagem base64. |
GET /instances/:id/qr | QR Code atual. status: QR_READY, WAITING, CONNECTED ou DISCONNECTED. |
POST /instances/:id/disconnect | Encerra a sessão (logout do aparelho). |
POST /instances/:id/reconnect | Reconecta (pode gerar novo QR). |
POST /instances/:id/restart | Reinicia a conexão sem deslogar. |
POST /instances/:id/rotate-key | Gera nova API key: { "apiKey": "goapikey_..." }. A antiga deixa de funcionar. |
POST /auth/shared-token | Link de QR para o cliente escanear sem acessar o painel: { "instanceId", "expiresInHours": 24 } (1 a 168 h). Retorna { token, expiresAt }. |
GET /instances/shared/:token | Público. QR Code da instância a partir do token compartilhado. |
Fluxo para conectar um número por código
# 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
/instances/:id/settingsJWT/instances/:id/settingsJWTEnvie apenas os campos que deseja alterar.
| Campo | Descrição |
|---|---|
rejectCalls / rejectCallMessage | Rejeita chamadas e responde com a mensagem informada. |
ignoreGroups | Não processa mensagens de grupos. |
alwaysOnline | Mantém o número como "online". |
readMessages | Marca mensagens recebidas como lidas. |
readStatus | Visualiza os status dos contatos. |
syncFullHistory | Sincroniza 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 / chatwoot | Integrações nativas (mais fácil configurar pelo painel). |
{
"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/groups | Grupos do número: { groups: [{ id, name, participants, creation, owner }] }. O id (...@g.us) é usado no to. |
GET /instances/:id/contacts/groups/:groupId/participants | Participantes: { groupName, groupId, participants: [{ id, phone, admin, name }] }. |
GET /instances/:id/contacts/all?search=maria | Contatos conhecidos (até 5.000), com busca por nome ou número. |
Webhooks globais
| Rota (JWT) | Descrição |
|---|---|
GET /webhooks/events | Lista de eventos disponíveis. |
GET /webhooks | Seus webhooks (o segredo vem mascarado). |
POST /webhooks | { "url", "events": [...], "secret"?, "instanceId"? }. Sem instanceId, recebe de todas as instâncias. |
PUT /webhooks/:id | Altera url, events, active, secret, instanceId (null remove). |
POST /webhooks/:id/test | Envia um evento TEST e informa o HTTP retornado. |
DELETE /webhooks/:id | Remove 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 key | Equivalente JWT |
|---|---|
POST /api/send/text | POST /messages/text |
POST /api/send/image · video · audio · document | POST /messages/image · video · audio · document |
POST /api/send/contact · location · poll | POST /messages/contact · location · poll |
POST /api/send/buttons · catalog · product | POST /messages/buttons · catalog · product |
GET /api/catalog | GET /messages/catalog?instanceId=... |
POST /api/check-numbers | POST /messages/check-numbers |
| - | GET /messages/stats: { today, total } mensagens enviadas. |
{ "instanceId": "4b8f2c1e-6a1d-4f5e-9d3a-2c7b8e9f0a1b", "to": "5511999999999", "text": "Olá!" }Saúde da API
| Rota (pública) | Descrição |
|---|---|
GET /status | Verificação simples: { "status": "ok", "timestamp": "..." }. |
GET /health | Inclui 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
// 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
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
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.