waiaconnect

Guias

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.

MedidoEm 29/09/2026, nos webhooks dos nossos clientes: 2 de 31 são um Apps Script. As 497 entregas guardadas desses dois responderam 200 depois do 302 do Google, e 28 (5,6 %) demoraram mais de 10 segundos na primeira vez e entraram pela nova tentativa. Por isso o script ignora um evento que já processou.

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

  1. Na planilha: Extensões → Apps Script.
  2. Apague o que houver em Código.gs e cole o script abaixo, inteiro.
  3. 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);
}

Baixar o script (.gs)

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)

  1. No Apps Script: Configurações do projeto (a engrenagem) → Propriedades do script → Adicionar.
  2. CONNECT_API_KEY = a sua chave wc_live_….
  3. 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.
  4. 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

  1. Implantar → Nova implantação. Na engrenagem de «Tipo», escolha App da Web.
  2. 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.
  3. 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

  1. No painel: Webhooks → Adicionar endpoint.
  2. URL: a do /exec com o seu token no final: https://script.google.com/macros/s/…/exec?token=SEU_TOKEN (o mesmo valor de CONNECT_WEBHOOK_TOKEN).
  3. Eventos: message.received. Some message.echo só se também quiser salvar o que você manda pelo telefone (o script nunca responde um eco).
  4. Aperte Testar: tem que dizer 200, e aparece uma linha webhook.test em 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 doPost e só depois responde com um redirecionamento (302) para script.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 com curl, use -L e 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 doPost chega 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 doPost não traz os cabeçalhos HTTP, então a assinatura X-Connect-Signature nã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 /exec continua 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-Key faz 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-Key segura.
  • 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_TEXT vazio.
  • 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