Come collego WhatsApp a un foglio Google Sheets con WAIA Connect?
Incolli un Apps Script nel tuo foglio, lo pubblichi come app web e registri quell'URL come webhook in Connect. Ogni messaggio che ricevi diventa una riga e, se vuoi, lo script risponde da solo. Qui sotto c'è il codice completo e le tre stranezze di Apps Script che conviene sapere prima.
Prima di iniziare
- Un account WAIA Connect con un numero collegato.
- Una chiave API (
wc_live_…): nel pannello, API → Crea API key. Si vede una sola volta: copiala. - Un account Google e un foglio di calcolo nuovo (o quello che usi già).
1. Incolla lo script nel tuo foglio
- Nel foglio: Estensioni → Apps Script.
- Cancella quello che c'è in
Codice.gse incolla tutto lo script qui sotto. - Salva (l'icona del dischetto).
/**
* 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);
}
Lo script crea da solo due schede: Messages (una riga per messaggio) e Errors (se qualcosa va storto, resta scritto lì).
Se chiedi modifiche a ChatGPT, chiedigli di toccare solo la funzione buildReply: è quella che decide cosa rispondere. Il resto (il token, i duplicati, la protezione dalle formule) serve a proteggerti.
2. Metti i tuoi dati nelle proprietà dello script (non nel codice)
- In Apps Script: Impostazioni progetto (l'ingranaggio) → Proprietà script → Aggiungi.
CONNECT_API_KEY= la tua chiavewc_live_….CONNECT_WEBHOOK_TOKEN= una password lunga inventata da te (30 caratteri o più, lettere e numeri). È quella che dimostra che il messaggio arriva da Connect.AUTO_REPLY_TEXT(facoltativo) = il testo con cui vuoi rispondere a ogni messaggio. Se lo lasci vuoto, lo script salva e basta.
3. Pubblicalo come app web
- Esegui il deployment → Nuovo deployment. Nell'ingranaggio di «Tipo», scegli App web.
- Esegui come: Io. Chi ha accesso: Chiunque. ⚠ «Chiunque abbia un Account Google» NON va bene: Connect non ha un account Google e riceverebbe la pagina di accesso.
- Esegui il deployment, accetta i permessi che chiede Google (è il tuo script) e copia l'URL dell'app web, quello che finisce con
/exec.
4. Registra il webhook in Connect
- Nel pannello: Webhook → Aggiungi endpoint.
- URL: quello di
/execcon il tuo token alla fine:https://script.google.com/macros/s/…/exec?token=IL_TUO_TOKEN(lo stesso valore diCONNECT_WEBHOOK_TOKEN). - Eventi: message.received. Aggiungi message.echo solo se vuoi salvare anche quello che mandi dal telefono (lo script non risponde mai a un eco).
- Premi Prova: deve dire 200, e in Messages compare una riga
webhook.test. Poi mandati un WhatsApp da un altro telefono e guarda la scheda Messages.
Le stranezze di Apps Script (leggile anche se tutto funziona)
- Il 302. Google esegue il tuo
doPoste solo dopo risponde con un reindirizzamento (302) versoscript.googleusercontent.com, dove lascia quello che ha restituito il tuo script. Connect segue quel reindirizzamento con un GET e conta il 200 finale come consegnato. Se provi concurl, usa-Le non mettere-X POST. - Risponde sempre 200, anche se lo script fallisce. Apps Script non permette di scegliere il codice di risposta, quindi un errore dentro
doPostarriva a Connect come «consegnato». Se il bot non risponde, Connect non lo vede: guarda la scheda Errors e, in Apps Script, Esecuzioni (lì compare ogni chiamata con il suo errore). - Non può leggere le intestazioni. L'evento di
doPostnon contiene le intestazioni HTTP, quindi la firmaX-Connect-Signaturenon si può verificare in Apps Script. Per questo il token va nell'URL. Chi vede il tuo pannello Connect vede quell'URL: se trapela, cambia il token da entrambe le parti (proprietà dello script e URL del webhook). - Ogni modifica al codice richiede una versione nuova. Salvare non basta: Esegui il deployment → Gestisci deployment → modifica (matita) → Versione: Nuova versione → Esegui il deployment. Altrimenti l'URL
/execcontinua a eseguire il codice vecchio. Così l'URL non cambia e non devi toccare Connect. - 10 secondi. Connect aspetta 10 s la risposta. Se il tuo script impiega di più (per esempio perché interroga ChatGPT), Connect riprova lo stesso evento: lo script lo riconosce e non lo salva due volte, e l'
Idempotency-Keyfa sì che Connect invii la risposta una sola volta. Google interrompe qualsiasi esecuzione a 6 minuti.
I limiti di Google
- Chiamate ad altri URL (ogni risposta che lo script invia è una): 20.000 al giorno con un account gmail.com, 100.000 con Google Workspace.
- 30 esecuzioni contemporanee per utente. Se scrivono in molti nello stesso momento, quelle in più falliscono e Connect le riprova.
- Il ricordo di «già elaborato» dura 6 ore (il massimo della cache di Apps Script). Un nuovo tentativo più tardivo verrebbe salvato di nuovo, ma la risposta non parte due volte: la ferma l'
Idempotency-Key. - Un foglio non è un database: con decine di migliaia di righe diventa lento. Archivia la scheda Messages ogni tanto.
Se qualcosa non funziona
- Prova dice 200 ma non compare nessuna riga: il token nell'URL non coincide con
CONNECT_WEBHOOK_TOKEN(lo script ignora la richiesta e risponde comunque 200), oppure hai pubblicato senza Nuova versione. - La riga compare ma non risponde: guarda Errors.
Connect API 401= la chiave è sbagliata;CONNECT_API_KEY is not set= manca la proprietà; nessun errore =AUTO_REPLY_TEXTvuoto. - Prova non dà 200: controlla che l'accesso sia Chiunque e che l'URL finisca con
/exec(non/dev). - Vuoi la forma esatta di ogni evento? È in Ricevere il webhook (in inglese).
Provarlo senza WhatsApp (facoltativo, da un terminale)
Questo invia un messaggio inventato direttamente al tuo script, come farebbe Connect. Deve comparire una riga nuova in 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":"ciao"}},"contacts":[{"profile":{"name":"Test"}}]}}' \
'https://script.google.com/macros/s/<…>/exec?token=<CONNECT_WEBHOOK_TOKEN>'
Fonti (consultate il 29/09/2026)
Connect your first number today
Meta-verified technology provider. Coexistence in one click.
Get started