DEV Community

Dovetail Scripts
Dovetail Scripts

Posted on Edited on

Collect leads in a Google Sheet with a free Apps Script webhook (no Zapier)

Zapier and Make are fine tools, but paying a monthly fee to move a form submission into a spreadsheet stings when you have ten leads a month. A Google Apps Script web app gives you a free HTTPS endpoint that writes submissions straight into a Google Sheet. Here is the whole thing.

The endpoint

Open a sheet, go to Extensions > Apps Script, and paste:

const SECRET = 'change-me';

function doPost(e) {
  const p = e.parameter;
  if (p.token !== SECRET) {
    return json({ ok: false, error: 'bad token' });
  }
  const email = String(p.email || '').trim();
  if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
    return json({ ok: false, error: 'bad email' });
  }
  SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Leads')
    .appendRow([new Date(), safe(p.name), safe(email), '', '']);
  return json({ ok: true });
}

// Keep Sheets from evaluating user input as a formula.
function safe(v) {
  const s = String(v || '').trim();
  return /^[=+\-@]/.test(s) ? "'" + s : s;
}

function json(obj) {
  return ContentService.createTextOutput(JSON.stringify(obj))
    .setMimeType(ContentService.MimeType.JSON);
}
Enter fullscreen mode Exit fullscreen mode

Then deploy it: Deploy > New deployment > type "Web app" > Execute as "Me" > Who has access "Anyone". Copy the URL ending in /exec.

The form

Any HTML form can post to it:

<form action="YOUR_EXEC_URL" method="POST">
  <input name="name" placeholder="Your name">
  <input name="email" type="email" required placeholder="you@company.com">
  <input type="hidden" name="token" value="change-me">
  <button type="submit">Send</button>
</form>
Enter fullscreen mode Exit fullscreen mode

Each submission lands as a row: timestamp, name, email, plus two empty columns (status, sent_at) that an auto-reply script can use later. Name the tab Leads so the script finds it.

Honest caveats

  • The token in a public page is visible in the source. It stops drive-by bots, not a determined human. For anything sensitive, add real spam protection (a honeypot field helps too).
  • Input that starts with =, +, - or @ becomes a live formula in Sheets unless you escape it; the safe() helper above prefixes it with an apostrophe. And getSheetByName('Leads') pins the target tab, where getActiveSheet() silently follows whatever tab is first. (Both flagged by commenter Hayrullah Kar - thanks.)
  • "Anyone" access means anyone with the URL can post. That is the point of a lead form, but do not reuse the same script for private data.
  • Apps Script caps concurrent executions and daily runtime. For ordinary lead volumes you will never notice; for a launch-day spike, test first.

Where this goes next

Once leads land in the sheet, the auto-reply script from my earlier post answers them within minutes, and the follow-up script bumps the quiet ones after 48 hours.

All three pieces - the webhook, the auto-reply, and the follow-up - are packaged with editable email templates and a 10-minute setup guide in the LeadReply Kit. The kit is mock-tested sample code, so read it before pointing it at real leads.

Top comments (8)

Collapse
 
hayrullahkar profile image
Hayrullah Kar •

I like that the caveats section admits the token in the page source only stops drive-by bots. One gap it misses is formula injection. appendRow treats a string that starts with = as a formula, so a visitor who types =HYPERLINK(...) or =IMPORTXML(...) into the name field gets a live formula in your sheet, and the later auto-reply script may read the computed result instead of what was typed. Prefixing any value that starts with =, +, - or @ with an apostrophe before appending keeps it as plain text. A smaller one: in a web app, getActiveSheet() resolves to the first tab, so if someone reorders the tabs the leads quietly land somewhere else. getSheetByName('Leads') pins the target.

Collapse
 
dovetailscripts profile image
Dovetail Scripts •

Both great catches, thank you. Formula injection is exactly the kind of thing that slips through when you test with polite data - appendRow happily evaluates a leading =, and the apostrophe prefix is the right fix. Same for getSheetByName('Leads') over getActiveSheet(); tab order should never decide where leads land. I'm folding both into the post. Appreciate the close read.

Collapse
 
hayrullahkar profile image
Hayrullah Kar •

Glad both were useful, and "testing with polite data" is a good way to put why this one slips past. Thanks for taking them on so openly, the post was already a clear walkthrough to build from.

Thread Thread
 
dovetailscripts profile image
Dovetail Scripts •

Thanks for reading that closely - catches like yours are what make the post better for the next person who builds from it.

Thread Thread
 
hayrullahkar profile image
Hayrullah Kar •

Thinking of the next person who builds from it is a nice way to see it, and that's exactly who the polite-data catch was for. Happy to have helped a little.

Collapse
 
dovetailscripts profile image
Dovetail Scripts •

Appreciate that, Hayrullah - the post is genuinely better for your catches.

Collapse
 
hayrullahkar profile image
Hayrullah Kar •

Glad the apostrophe trick and getSheetByName were useful. The webhook setup itself was easy to follow, which is why I read the caveats so closely.

Thread Thread
 
dovetailscripts profile image
Dovetail Scripts •

Thanks again, Hayrullah - readers like you are why the caveats were worth writing.

⌂ 换网址