Keep bots out of your Google Sheet lead form (honeypot + time trap)
The moment a lead form is public, bots start posting to it. In the webhook post I used a shared token, which stops drive-by scripts but not a bot that reads your page source. Here are two free additions that catch almost
The moment a lead form is public, bots start posting to it. In the webhook post I used a shared token, which stops drive-by scripts but not a bot that reads your page source. Here are two free additions that catch almost everything else: a honeypot field and a time trap. No CAPTCHA, no third-party service.
The form side
Add two things to the form from the webhook post:
<form action="YOUR_EXEC_URL" method="POST">
<input name="name" placeholder="Your name">
<input name="email" type="email" required placeholder="[email protected]">
<!-- Honeypot: invisible to humans, irresistible to bots -->
<input name="company" tabindex="-1" autocomplete="off"
aria-hidden="true" style="position:absolute;left:-9999px">
<input type="hidden" name="token" value="change-me">
<input type="hidden" name="t" id="t">
<button type="submit">Send</button>
</form>
<script>
document.getElementById('t').value = Date.now();
</script>
Two details matter. Hide the honeypot with off-screen CSS, not type="hidden", because many bots skip hidden inputs. And give it a tempting name like company or website, since bots fill every field they can see. The aria-hidden and tabindex="-1" keep screen readers and keyboard users out of it.
The script side
Extend the doPost from the webhook post:
const MIN_SECONDS = 3;
function doPost(e) {
const p = e.parameter;
if (p.token !== SECRET) {
return json({ ok: false, error: 'bad token' });
}
// Honeypot: a human never fills this.
if (String(p.company || '') !== '') {
return json({ ok: true }); // pretend it worked, save nothing
}
// Time trap: real people need a few seconds to type an email.
const t = Number(p.t || 0);
if (!t || (Date.now() - t) < MIN_SECONDS * 1000) {
return json({ ok: true }); // same fake success
}
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 });
}
Notice both bot rejections return ok: true. Telling a bot exactly why it failed is free debugging help, so give it a fake win and move on.
Why this works, and where it stops
- Bots fill every field; humans never see the honeypot. That alone catches most form spam.
- Bots submit instantly; humans take a few seconds. The time trap catches the ones clever enough to skip odd fields.
- A determined human spammer still gets through. At lead-form volumes, that is a reply-to-the-weird-email problem, not a code problem.
If you want one more layer, dedupe in the sheet: before appendRow, scan the email column and skip repeats. Keeps the sheet clean when someone double-submits.
This sits on top of the earlier pieces: the webhook that catches leads, the auto-reply that answers them, and the follow-up for the quiet ones. All of it, plus editable templates and a setup guide, is in the LeadReply Kit. The kit is mock-tested sample code, so read it before pointing it at real leads.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.