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);
}
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>
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; thesafe()helper above prefixes it with an apostrophe. AndgetSheetByName('Leads')pins the target tab, wheregetActiveSheet()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)
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.
appendRowtreats 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.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.
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.
Thanks for reading that closely - catches like yours are what make the post better for the next person who builds from it.
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.
Appreciate that, Hayrullah - the post is genuinely better for your catches.
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.
Thanks again, Hayrullah - readers like you are why the caveats were worth writing.