Loading...
This guide shows how to send TailorTalk webhook data to Google Sheets without any third-party extension.
You will set up:
lead_contact as the primary keySheet1 (or update the script tab name)every_message)Sheet1 (recommended for this script).Extensions -> Apps Script.

function doPost(e) {
const sh = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Sheet1');
const payload = JSON.parse(e.postData.contents || '{}');
// Use fields inside "data" key (fallback to full payload)
const rawData = (payload && typeof payload.data === 'object' && payload.data !== null)
? payload.data
: payload;
// Flatten nested objects: lead_attributes.email_id -> lead_attributes_email_id
const data = flattenObject(rawData);
const keyField = 'lead_contact';
const keyValue = String(data[keyField] || '').trim();
// Existing headers
let headers = sh.getLastColumn() > 0
? sh.getRange(1, 1, 1, sh.getLastColumn()).getValues()[0].filter(Boolean)
: [];
// Ensure standard columns
['updated_at', keyField].forEach((h) => {
if (!headers.includes(h)) headers.push(h);
});
// Add new incoming fields as new columns
Object.keys(data).forEach((k) => {
if (!headers.includes(k)) headers.push(k);
});
// Write headers
sh.getRange(1, 1, 1, headers.length).setValues([headers]);
const colMap = Object.fromEntries(headers.map((h, i) => [h, i]));
const keyCol = colMap[keyField] + 1;
const lastRow = sh.getLastRow();
let targetRow = -1;
if (keyValue && lastRow > 1) {
const existingKeys = sh.getRange(2, keyCol, lastRow - 1, 1).getValues().flat();
const idx = existingKeys.findIndex(v => String(v || '').trim() === keyValue);
if (idx !== -1) targetRow = idx + 2;
}
if (targetRow !== -1) {
// Update existing row
const row = sh.getRange(targetRow, 1, 1, headers.length).getValues()[0];
Object.entries(data).forEach(([k, v]) => {
row[colMap[k]] = v ?? '';
});
row[colMap['updated_at']] = new Date();
sh.getRange(targetRow, 1, 1, headers.length).setValues([row]);
} else {
// Insert new row
const row = headers.map(() => '');
Object.entries(data).forEach(([k, v]) => {
row[colMap[k]] = v ?? '';
});
row[colMap['updated_at']] = new Date();
sh.appendRow(row);
}
return ContentService
.createTextOutput(JSON.stringify({ ok: true }))
.setMimeType(ContentService.MimeType.JSON);
}
function flattenObject(obj, prefix = '', out = {}) {
Object.entries(obj || {}).forEach(([k, v]) => {
const key = prefix ? `${prefix}_${k}` : k;
if (v && typeof v === 'object' && !Array.isArray(v)) {
flattenObject(v, key, out);
} else if (Array.isArray(v)) {
out[key] = v.join(','); // keep arrays sheet-friendly
} else {
out[key] = v;
}
});
return out;
}
Deploy -> New deployment.Web app.Execute as: MeWho has access: AnyoneDeploy./exec.


Use URL format like:
https://script.google.com/macros/s/AKfy.../exec
Avoid domain-scoped /a/macros/... URLs because they often return 401 Unauthorized.
Agent Setup in TailorTalk.Agent Card.Webhook option.every_message.Save.

lead_contact.data are flattened with _ separator.lead_contact update the same row.updated_at is refreshed on each update.401 Unauthorized:
Who has access = Anyone/macros/s/.../exec URL instead of /a/macros/...lead_contact exists in webhook dataSheet1)JSON.stringify(data) append-only version)