Como conecto o WhatsApp a uma planilha do Google Sheets com o WAIA Connect?
Você cola um Apps Script na sua planilha, publica como aplicativo da web e registra essa URL como webhook no Connect. Cada mensagem que chega vira uma linha e, se quiser, o script responde sozinho. Abaixo está o código completo e as três coisas estranhas do Apps Script que convém saber antes.
Antes de começar
- Uma conta do WAIA Connect com um número conectado.
- Uma chave de API (
wc_live_…): no painel, API → Criar API key. Ela aparece uma única vez: copie. - Uma conta do Google e uma planilha nova (ou a que você já usa).
1. Cole o script na sua planilha
- Na planilha: Extensões → Apps Script.
- Apague o que houver em
Código.gse cole o script abaixo, inteiro. - Salve (o ícone do disquete).
/**
* WAIA Connect → Google Sheets (Apps Script web app)
* Guide: https://waiaconnect.com/guias/google-sheets-webhook-apps-script
*
* What it does, for every event Connect sends to your webhook:
* 1. checks the secret token in the URL (Apps Script cannot read HTTP headers, so the
* X-Connect-Signature header is not available here — see the guide);
* 2. ignores an event it already handled (Connect retries if your script is slow);
* 3. saves it as a row in the "Messages" sheet;
* 4. optionally answers a text message through the Connect API.
*
* Script properties (Project Settings → Script properties):
* CONNECT_API_KEY your wc_live_… key (Connect panel → API keys). Never paste it in the code.
* CONNECT_WEBHOOK_TOKEN a long random secret. The SAME value goes at the end of the
* webhook URL you register in Connect: …/exec?token=<value>
* AUTO_REPLY_TEXT optional. If set, every incoming text message gets this answer.
* Leave it empty to only save messages.
*
* ⚠ After ANY change to this code: Deploy → Manage deployments → edit → Version: "New version".
* Otherwise the /exec URL keeps running the old code.
*/
var CONNECT_API_URL = 'https://api.waiaconnect.com/v1/messages';
var MESSAGES_SHEET = 'Messages';
var ERRORS_SHEET = 'Errors';
var MESSAGE_COLUMNS = ['Received at', 'Event', 'From', 'Name', 'Text', 'Event id', 'Number (connection)'];
var SEEN_SECONDS = 21600; // 6 h: the longest CacheService keeps a value
function doPost(e) {
try {
var props = PropertiesService.getScriptProperties();
// 1. The token. A request without it is not from Connect: answer and do nothing.
var expected = props.getProperty('CONNECT_WEBHOOK_TOKEN');
var given = e && e.parameter ? String(e.parameter.token || '') : '';
if (!expected || !sameText_(given, expected)) {
return answer_('ignored');
}
var evt = JSON.parse(e.postData.contents);
if (!evt || !evt.id || !evt.type) {
return answer_('ignored');
}
// 2. Once per event. Connect retries with the SAME event id if your script took
// longer than 10 seconds; the lock stops two copies running at the same time.
var lock = LockService.getScriptLock();
lock.waitLock(20000);
try {
var cache = CacheService.getScriptCache();
if (cache.get('evt:' + evt.id)) {
return answer_('duplicate');
}
cache.put('evt:' + evt.id, '1', SEEN_SECONDS);
} finally {
lock.releaseLock();
}
// 3. Save it.
var data = evt.data || {};
var msg = data.message || {};
var contact = (data.contacts && data.contacts[0]) || {};
var name = (contact.profile && contact.profile.name) || '';
var text = msg.type === 'text' && msg.text ? String(msg.text.body || '') : '[' + (msg.type || evt.type) + ']';
sheet_(MESSAGES_SHEET, MESSAGE_COLUMNS).appendRow([
new Date(),
evt.type,
cell_(msg.from || ''),
cell_(name),
cell_(text),
evt.id,
(evt.connection && evt.connection.id) || ''
]);
// 4. Answer — only a text a person sent you. Never an echo (message.echo is what YOU
// sent): answering it would make the bot talk to itself.
if (evt.type === 'message.received' && msg.type === 'text' && msg.from) {
var reply = buildReply(text, name);
if (reply) {
sendText_(evt.connection.id, msg.from, reply, evt.id);
}
}
return answer_('ok');
} catch (err) {
logError_(err);
return answer_('error');
}
}
/**
* The bot's answer. This is the ONE function to change (or to ask ChatGPT to change):
* return the text to send, or null to send nothing.
*/
function buildReply(text, name) {
var fixed = PropertiesService.getScriptProperties().getProperty('AUTO_REPLY_TEXT');
return fixed ? fixed : null;
}
function sendText_(connectionId, to, body, eventId) {
var key = PropertiesService.getScriptProperties().getProperty('CONNECT_API_KEY');
if (!key) {
throw new Error('CONNECT_API_KEY is not set in Script properties');
}
var res = UrlFetchApp.fetch(CONNECT_API_URL, {
method: 'post',
contentType: 'application/json',
headers: {
Authorization: 'Bearer ' + key,
// Same event → same key: if this runs twice, Connect sends the answer only once.
'Idempotency-Key': 'sheets-reply-' + eventId
},
payload: JSON.stringify({ connectionId: connectionId, to: to, type: 'text', text: { body: body } }),
muteHttpExceptions: true
});
var code = res.getResponseCode();
if (code !== 200 && code !== 202) {
throw new Error('Connect API ' + code + ': ' + res.getContentText().slice(0, 300));
}
}
// Apps Script always answers 200 (it cannot send another status), so what we return
// here is only for you to read in a test.
function answer_(text) {
return ContentService.createTextOutput(text).setMimeType(ContentService.MimeType.TEXT);
}
function sheet_(name, columns) {
var book = SpreadsheetApp.getActiveSpreadsheet();
var sh = book.getSheetByName(name);
if (!sh) {
sh = book.insertSheet(name);
sh.appendRow(columns);
}
return sh;
}
// A cell that starts with = + - @ would be run as a FORMULA by Sheets. Someone could
// send you "=IMPORTXML(...)" over WhatsApp: the leading apostrophe keeps it as text.
function cell_(value) {
var s = String(value);
return /^[=+\-@]/.test(s) ? "'" + s : s;
}
function sameText_(a, b) {
if (a.length !== b.length) return false;
var diff = 0;
for (var i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
return diff === 0;
}
function logError_(err) {
try {
sheet_(ERRORS_SHEET, ['When', 'Error']).appendRow([new Date(), cell_(String(err && err.stack ? err.stack : err))]);
} catch (ignored) {
// Even the error sheet failed: it still shows in Apps Script → Executions.
}
console.error(err);
}
O script cria sozinho duas abas: Messages (uma linha por mensagem) e Errors (se algo falhar, fica escrito ali).
Se pedir mudanças ao ChatGPT, peça para mexer só na função buildReply: é ela que decide o que responder. O resto (o token, as duplicatas, a proteção de fórmula) está ali para proteger você.
2. Coloque seus dados nas propriedades do script (não no código)
- No Apps Script: Configurações do projeto (a engrenagem) → Propriedades do script → Adicionar.
CONNECT_API_KEY= a sua chavewc_live_….CONNECT_WEBHOOK_TOKEN= uma senha longa inventada por você (30 caracteres ou mais, letras e números). É ela que prova que a mensagem vem do Connect.AUTO_REPLY_TEXT(opcional) = o texto que você quer responder a cada mensagem. Se ficar vazio, o script só salva.
3. Publique como aplicativo da web
- Implantar → Nova implantação. Na engrenagem de «Tipo», escolha App da Web.
- Executar como: Eu. Quem pode acessar: Qualquer pessoa. ⚠ «Qualquer pessoa com uma Conta do Google» NÃO serve: o Connect não tem conta do Google e receberia a tela de login.
- Implantar, aceite as permissões que o Google pede (é o seu próprio script) e copie a URL do app da Web, a que termina em
/exec.
4. Registre o webhook no Connect
- No painel: Webhooks → Adicionar endpoint.
- URL: a do
/execcom o seu token no final:https://script.google.com/macros/s/…/exec?token=SEU_TOKEN(o mesmo valor deCONNECT_WEBHOOK_TOKEN). - Eventos: message.received. Some message.echo só se também quiser salvar o que você manda pelo telefone (o script nunca responde um eco).
- Aperte Testar: tem que dizer 200, e aparece uma linha
webhook.testem Messages. Depois mande um WhatsApp de outro telefone e olhe a aba Messages.
O que o Apps Script tem de estranho (leia mesmo que tudo funcione)
- O 302. O Google executa o seu
doPoste só depois responde com um redirecionamento (302) parascript.googleusercontent.com, onde deixa o que o seu script devolveu. O Connect segue esse redirecionamento com um GET e conta o 200 final como entregue. Se testar comcurl, use-Le não coloque-X POST. - Sempre responde 200, mesmo quando o script falha. O Apps Script não deixa escolher o código de resposta, então um erro dentro do
doPostchega ao Connect como «entregue». Se o bot não responde, o Connect não vai ver: olhe a aba Errors e, no Apps Script, Execuções (ali aparece cada chamada com o seu erro). - Não consegue ler cabeçalhos. O evento do
doPostnão traz os cabeçalhos HTTP, então a assinaturaX-Connect-Signaturenão pode ser verificada no Apps Script. Por isso o token vai na URL. Quem vê o seu painel do Connect vê essa URL: se vazar, troque o token nos dois lados (propriedade do script e URL do webhook). - Cada mudança de código precisa de uma versão nova. Salvar não basta: Implantar → Gerenciar implantações → editar (lápis) → Versão: Nova versão → Implantar. Senão, a URL
/execcontinua rodando o código velho. Assim a URL não muda e você não precisa mexer no Connect. - 10 segundos. O Connect espera 10 s pela resposta. Se o seu script demora mais (por exemplo, porque pergunta ao ChatGPT), o Connect tenta de novo o mesmo evento: o script o reconhece e não salva duas vezes, e o
Idempotency-Keyfaz o Connect mandar a resposta uma vez só. O Google corta qualquer execução aos 6 minutos.
Os limites do Google
- Chamadas a outras URLs (cada resposta que o script manda é uma): 20.000 por dia com uma conta gmail.com, 100.000 com o Google Workspace.
- 30 execuções ao mesmo tempo por usuário. Se muita gente escreve no mesmo instante, as que sobram falham e o Connect tenta de novo.
- A memória de «já processei» dura 6 horas (o máximo do cache do Apps Script). Uma nova tentativa mais tardia seria salva de novo, mas a resposta não sai duas vezes: o
Idempotency-Keysegura. - A planilha não é um banco de dados: com dezenas de milhares de linhas fica lenta. Arquive a aba Messages de vez em quando.
Se algo não funciona
- Testar diz 200 mas nenhuma linha aparece: o token da URL não bate com
CONNECT_WEBHOOK_TOKEN(o script ignora o pedido e mesmo assim responde 200), ou você publicou sem Nova versão. - A linha aparece mas não responde: olhe Errors.
Connect API 401= a chave está errada;CONNECT_API_KEY is not set= falta a propriedade; sem erro =AUTO_REPLY_TEXTvazio. - Testar não dá 200: confira que o acesso seja Qualquer pessoa e que a URL termine em
/exec(não em/dev). - Quer ver a forma exata de cada evento? Está em Receber o webhook (em inglês).
Testar sem WhatsApp (opcional, de um terminal)
Isto manda uma mensagem inventada direto para o seu script, como o Connect faria. Tem que aparecer uma linha nova em Messages.
curl -L -H 'Content-Type: application/json' \
-d '{"id":"evt_prueba_1","type":"message.received","connection":{"id":"conn_…"},"data":{"message":{"from":"5493511234567","type":"text","text":{"body":"ola"}},"contacts":[{"profile":{"name":"Test"}}]}}' \
'https://script.google.com/macros/s/<…>/exec?token=<CONNECT_WEBHOOK_TOKEN>'
Fontes (consultadas em 29/09/2026)
Conecte o seu primeiro número hoje
Provedor de tecnologia verificado pela Meta. Coexistence em um clique.
Começar