¿Cómo vincular el Webhook de un Modal de Sitios con Google Sheets?
Los Modales de Sitios permiten recolectar información de los usuarios a través de distintos formularios (Pop-up de suscripción, Modal embebido, Pop-up de carrito abandonado, etc.) y almacenar esos datos en icomm. Además de guardarlos en icomm, es posible utilizar un Webhook para enviar automáticamente esa información a otro sistema apenas se completa el formulario.
Esta guía explica cómo armar esa conexión de punta a punta cuando el sistema externo elegido es Google Sheets: cómo preparar una planilla para que reciba los datos, y cómo activar el Webhook dentro del Modal para que apunte a ella.
¿En qué Modales puedo usar esta integración?
El Webhook puede activarse en cualquier Modal que cuente con un formulario de captura de datos, por ejemplo:
- Pop-up de suscripción: para enviar los nuevos registros a la planilla apenas se suscriben.
- Modal embebido: para conectar los formularios integrados dentro del sitio.
- Pop-up de carrito abandonado: para enviar los datos de usuarios que dejaron productos en su carrito.
- Cualquier otro Modal con formulario de captura de información.
¿Qué necesitas antes de empezar?
- Una cuenta de Google con acceso a Google Sheets y Google Apps Script.
- El Modal ya creado y configurado en icomm, con el formulario que se quiere conectar.
- Unos minutos: la conexión no requiere programación avanzada, pero sí copiar y pegar un script y seguir el orden de los pasos.
Paso 1: Preparar la planilla de Google Sheets
- Crea una planilla nueva en Google Sheets.
- Anota el nombre exacto de la hoja donde se van a guardar los datos (por defecto
Sheet1uHoja 1, según el idioma de la cuenta). Es sensible a mayúsculas y espacios. - Deja la hoja completamente vacía, sin encabezados. El script los crea automáticamente a partir de los campos del formulario.
- Revisa la configuración regional en Archivo → Configuración → Configuración regional, ya que determina cómo Sheets interpreta las fechas que lleguen como texto.

Paso 2: Crear el receptor del Webhook con Apps Script
Google Sheets no expone un endpoint propio para recibir datos por Webhook. Por eso se usa Apps Script: un pequeño proyecto ligado a la planilla que se publica como aplicación web y expone una URL pública donde icomm va a enviar la información.
2.1. Abrir el editor de Apps Script
Desde la planilla: Extensiones → Apps Script.

2.2. Pegar el código
Borra el contenido de Código.gs (la función myFunction vacía) y pegá el siguiente script completo:
const HOJA = 'Sheet1';
const TOKEN = 'MITOKEN';
const COL_FECHA = 'fecha';
const LOG_BODY = false;
function doPost(e) {
const t0 = Date.now();
const rid = Utilities.getUuid().slice(0, 8); // correlaciona líneas de una misma request
console.log('[%s] === doPost inicio ===', rid);
const lock = LockService.getScriptLock();
if (!lock.tryLock(30000)) {
console.warn('[%s] lock no obtenido tras 30s -> busy', rid);
return json({ok: false, error: 'busy'});
}
console.log('[%s] lock obtenido', rid);
try {
if (e.parameter.token !== TOKEN) {
console.warn('[%s] token invalido (recibido: %s)', rid, e.parameter.token || '(vacio)');
return json({ok: false, error: 'unauthorized'});
}
if (!e.postData || !e.postData.contents) {
console.error('[%s] request sin body', rid);
return json({ok: false, error: 'body vacio'});
}
console.log('[%s] contentType=%s bytes=%s', rid, e.postData.type, e.postData.contents.length);
if (LOG_BODY) console.log('[%s] body: %s', rid, e.postData.contents);
const body = JSON.parse(e.postData.contents);
const rawInputs = body.data?.inputs ?? body.inputs;
console.log('[%s] inputs origen=%s cantidad=%s', rid,
body.data?.inputs ? 'body.data.inputs' : (body.inputs ? 'body.inputs' : 'ninguno'),
Array.isArray(rawInputs) ? rawInputs.length : 'n/a');
const datos = normalizarInputs(rawInputs);
const claves = Object.keys(datos);
console.log('[%s] claves normalizadas: %s', rid, JSON.stringify(claves));
if (!claves.length) {
console.warn('[%s] inputs vacio -> aborta', rid);
return json({ok: false, error: 'inputs vacio'});
}
const fechaStr = body.timestamp != null ? String(body.timestamp) : '';
if (fechaStr === '') {
console.warn('[%s] timestamp ausente o null. Claves del body: %s',
rid, JSON.stringify(Object.keys(body)));
} else {
console.log('[%s] timestamp="%s"', rid, fechaStr);
}
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(HOJA);
if (!sheet) {
const existentes = SpreadsheetApp.getActiveSpreadsheet().getSheets().map(s => s.getName());
console.error('[%s] hoja "%s" no existe. Hojas: %s', rid, HOJA, JSON.stringify(existentes));
return json({ok: false, error: 'hoja no encontrada'});
}
// --- Encabezados actuales ---
const ultCol = sheet.getLastColumn();
let headers = ultCol > 0
? sheet.getRange(1, 1, 1, ultCol).getValues()[0].map(h => String(h).trim())
: [];
while (headers.length && headers[headers.length - 1] === '') headers.pop();
console.log('[%s] headers leidos (lastColumn=%s): %s', rid, ultCol, JSON.stringify(headers));
let cambio = false;
// --- Garantiza 'fecha' como primera columna ---
if (!headers.length) {
headers = [COL_FECHA];
cambio = true;
console.log('[%s] hoja vacia -> crea columna "%s"', rid, COL_FECHA);
} else if (headers[0] === '') {
headers[0] = COL_FECHA;
cambio = true;
console.log('[%s] A1 vacio -> repara con "%s"', rid, COL_FECHA);
} else if (headers.indexOf(COL_FECHA) === -1) {
sheet.insertColumnBefore(1);
headers.unshift(COL_FECHA);
cambio = true;
console.log('[%s] inserta columna "%s" al inicio (corre el resto)', rid, COL_FECHA);
}
// --- Columnas nuevas ---
const nuevas = claves.filter(k => headers.indexOf(k) === -1);
if (nuevas.length) {
headers = headers.concat(nuevas);
cambio = true;
console.log('[%s] columnas nuevas: %s', rid, JSON.stringify(nuevas));
}
if (cambio) {
sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
console.log('[%s] headers reescritos: %s', rid, JSON.stringify(headers));
}
// --- Claves que llegaron pero no se van a escribir (no deberia pasar) ---
const huerfanas = claves.filter(k => headers.indexOf(k) === -1);
if (huerfanas.length) console.error('[%s] claves sin columna: %s', rid, JSON.stringify(huerfanas));
// --- Fila alineada ---
const idxFecha = headers.indexOf(COL_FECHA);
const fila = headers.map((h, i) => i === idxFecha ? fechaStr : aCelda(datos[h]));
sheet.appendRow(fila);
const nroFila = sheet.getLastRow();
console.log('[%s] fila %s escrita: %s', rid, nroFila, JSON.stringify(fila));
console.log('[%s] === OK en %sms ===', rid, Date.now() - t0);
return json({ok: true, fila: nroFila, columnasNuevas: nuevas});
} catch (err) {
console.error('[%s] EXCEPCION: %s\n%s', rid, err, err.stack || '(sin stack)');
return json({ok: false, error: String(err)});
} finally {
lock.releaseLock();
console.log('[%s] lock liberado', rid);
}
}
// Acepta [{name,value}], [{key,value}], [{campo:valor}] o un objeto plano
function normalizarInputs(inputs) {
const out = {};
if (!inputs) return out;
if (!Array.isArray(inputs)) {
if (typeof inputs === 'object') { Object.assign(out, inputs); return out; }
console.warn('normalizarInputs: tipo inesperado "%s"', typeof inputs);
return out;
}
inputs.forEach((item, i) => {
if (!item || typeof item !== 'object') {
console.warn('normalizarInputs: item[%s] no es objeto: %s', i, JSON.stringify(item));
return;
}
const nombre = item.name !== undefined ? item.name
: item.key !== undefined ? item.key
: null;
if (nombre !== null) {
const k = String(nombre).trim();
if (k) out[k] = item.value;
else console.warn('normalizarInputs: item[%s] con nombre vacio', i);
} else {
console.warn('normalizarInputs: item[%s] sin name/key, se usa como objeto plano: %s',
i, JSON.stringify(item));
Object.keys(item).forEach(k => out[String(k).trim()] = item[k]);
}
});
return out;
}
function aCelda(v) {
if (v === undefined || v === null) return '';
if (typeof v === 'object') return JSON.stringify(v);
const s = String(v);
return /^[=+\-@]/.test(s) ? "'" + s : s;
}
function doGet() { return json({ok: true, status: 'alive'}); }
function json(obj) {
return ContentService.createTextOutput(JSON.stringify(obj))
.setMimeType(ContentService.MimeType.JSON);
}
Guarda con Ctrl+S (o Cmd+S).
Ejemplo visual:

2.3. Configurar las constantes
En el bloque superior del script, ajustá:
Constante | Qué es | Ejemplo |
|---|---|---|
| Nombre exacto de la pestaña destino |
|
| Secreto que valida el request |
|
| Nombre de la columna de fecha/hora |
|
| Si registra el body completo en los logs |
|
El |
2.4. Probar antes de desplegar
Agregá esta función al final del script para validar la lógica sin necesidad de publicar nada todavía:
function testDoPost() {
const fake = {
parameter: { token: TOKEN },
postData: {
type: 'application/json',
contents: JSON.stringify({
timestamp: '2026-09-01 13:20:00',
data: { inputs: [
{name: 'email', value: 'test@gmail.com'},
{name: 'empresa', value: 'icomm'}
]}
})
}
};
const res = doPost(fake);
console.log('respuesta: %s', res.getContent());
}
Seleccioná esta función en el desplegable de la parte superior y presioná Ejecutar. Los console.log aparecen al instante en el panel inferior.

Verificá en la planilla que se haya creado la fila de encabezados (fecha, email, empresa) junto con la fila de datos.
2.5. Desplegar como aplicación web
Implementar → Nueva implementación.
- Hacé clic en el engranaje junto a Seleccionar tipo y elegí Aplicación web.
- Completá:
Campo | Valor | Por qué |
|---|---|---|
Descripción | Libre (ej: | Solo referencia |
Ejecutar como | Yo (tu cuenta) | Corre con tus permisos sobre la planilla |
Quién tiene acceso | Cualquier persona | icomm no inicia sesión con tu cuenta de Google. |
- Hacé clic en Implementar y copiá la URL que termina en
/exec.


2.6. Autorizar el script
La primera vez va a aparecer "Google no verificó esta aplicación". Es esperable, porque el script es propio:
- Hacé clic en Configuración avanzada.
- Elegí Ir a
<nombre del proyecto>(no seguro). - Revisá los permisos solicitados y hacé clic en Permitir.
2.7. Probar el endpoint
Antes de conectarlo con icomm, conviene simular un envío real:
curl -L -X POST "https://script.google.com/macros/s/XXXX/exec?token=TU_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"timestamp": "2026-09-01 13:20:00",
"data": {
"inputs": [
{"name": "email", "value": "test@gmail.com"},
{"name": "empresa", "value": "icomm"}
]
}
}'
El flag -L es imprescindible: sin él, curl se detiene en la redirección y parece que falló aunque la fila se haya escrito correctamente. Si preferís Postman, activá Automatically follow redirects en la configuración de la request.

Una respuesta como esta confirma que el endpoint funciona:
{"ok": true, "fila": 2, "columnasNuevas": ["email", "empresa"]}
Paso 3: Activar el Webhook en el Modal de icomm
Con el endpoint de Google ya probado, el último paso es indicarle al Modal que envíe los datos del formulario a esa URL.
- Ingresá a Personalización → Sitios → Modales.
- Seleccioná el Sitio donde se encuentra el Modal que querés configurar.
- Ingresá a la configuración del Modal.
- Dirigite a la sección Webhooks.
- Activá el interruptor de Webhooks.
- Completá los campos:
- Nombre: algo que te permita identificarlo, por ejemplo
Google Sheets - Suscriptores. - URL: pegá la URL
/execque copiaste en el paso 2.5, agregando el token como query string, por ejemplo: https://script.google.com/macros/s/XXXX/exec?token=TU_TOKEN. Debe comenzar con https://.
- Nombre: algo que te permita identificarlo, por ejemplo
- Guardá la configuración del Modal.
A partir de ese momento, cada vez que un usuario complete el formulario, icomm va a enviar automáticamente los datos a la planilla, sin necesidad de exportaciones ni cargas manuales.
Formato de los datos que se reciben
icomm envía los datos del formulario con esta estructura:
{
"event": "form_submit",
"timestamp": "2026-09-01 13:20:00",
"data": {
"inputs": [
{"name": "email", "value": "test@gmail.com"},
{"name": "evento", "value": "open"}
],
"modalId": "...",
"siteName": "...",
"target": "..."
}
}
timestampva a la primera columna (fecha) tal cual llega, sin reformatear.data.inputsconvierte cada{name, value}en una columna y su valor.event,modalId,siteNameytargetno se registran por defecto. Si se necesitan en la planilla, hay que sumarlos al objeto de datos dentro del script.- Si la hoja está vacía, el script crea
fechay las columnas de los campos del formulario. Si llega un campo nuevo, agrega la columna al final. El orden de las columnas queda fijado por el primer envío.
Seguridad y buenas prácticas
- El endpoint de Google es público: cualquiera que tenga la URL y el token puede escribir filas. Usá un token largo y aleatorio, no una palabra simple.
- Las solicitudes que envía icomm incluyen los headers
Webhook-Id,Webhook-TimestampyWebhook-Signature(firma HMAC-SHA256) para que el sistema receptor valide el origen del envío. Apps Script no puede leer headers HTTP entrantes, por lo que este script no valida esa firma: su única protección es elTOKENen la URL. Si se necesita validar la firma real de icomm, hace falta un intermediario (por ejemplo, un API Gateway con una función Lambda) delante de Apps Script. - Como buena práctica adicional, evitá activar
LOG_BODYen producción: si se loguea el body completo, los datos personales de los usuarios quedan guardados en los registros de ejecución de Apps Script. - Este esquema es apto para un volumen de decenas o cientos de envíos diarios (Apps Script tiene una cuota de aproximadamente 20.000 ejecuciones por día en cuentas Workspace). Para volúmenes más altos, conviene una integración directa con un sistema propio.
Problemas frecuentes
Síntoma | Causa probable | Solución |
|---|---|---|
Los cambios en el script no tienen efecto | No se creó una nueva versión de la implementación | Desde Implementar → Administrar implementaciones, editar la implementación activa y elegir Nueva versión |
| El token no coincide o falta en la URL | Revisar el query string de la URL pegada en el Modal |
La fecha no se guarda | El campo | Revisar el payload de origen |
Columnas desalineadas | Encabezados con formato pero sin título | Limpiar el formato de la fila 1 |
El Modal marca error pero la fila se escribe igual | El sistema no sigue la redirección 302 que devuelve Apps Script | Confirmar que el emisor siga redirects ( |
Devuelve una pantalla de login en vez de guardar el dato | El acceso de la implementación quedó en "Cualquier persona con cuenta de Google", o se usó la URL | Volver a implementar con acceso Cualquier persona y usar la URL |
| El script se creó como proyecto independiente, no ligado a la planilla | Recrearlo desde Extensiones → Apps Script dentro de la planilla |
Artículos relacionados
Actualizado el: 07/09/2026
¡Gracias!
