Guides / Static Hosting
How to Stop Fake Signups on Your Website Forms
To stop fake signups on a website form, use two layers. First, turn on the honeypot and reCAPTCHA in your Static.app form settings, which stops most bots. Then check each email address with an email verifier such as Unspam's, which catches disposable inboxes, mistyped domains and mailboxes that do not exist. On Static.app the check runs in a small function that receives each entry by webhook, so your API token never appears in the page.
Why fake signups get through
Fake signups come from three places, and each one needs a different fix:
- Bots fill in every form they find, often hundreds of times a day. A honeypot or a CAPTCHA stops them.
- Throwaway addresses come from real people who want a download or a trial without giving their real email. A CAPTCHA does not stop them, because a person is typing.
- Typos and dead mailboxes, such as
[email protected], come from people who meant well. Every email you send to them bounces, and too many bounces hurt your sender reputation.
The form settings deal with the first group. An email verifier deals with the other two.
Step 1: Stop the bots in your form settings
Every website on Static.app has two bot filters for forms built with the static-form attribute. Open your website in the dashboard and go to its form settings.
- Honeypot: in Honeypot Field Name (optional), enter a name such as
_honeyand save. The form script adds a hidden field with that name. People never see it, so anything that fills it in is a bot, and its entry is quietly thrown away. - reCAPTCHA v3: tick Enable reCAPTCHA v3, create a v3 key pair in the Google reCAPTCHA admin console for your domain, and paste the site key and secret key. Visitors see no puzzle. Entries that Google scores as likely bots are refused.
For a simple contact form this is usually enough. Keep going if your form collects signups for a newsletter, a waitlist or a trial, where every bad address costs you.
Step 2: Check each address with Unspam's email verifier
Unspam's email verifier checks an address's syntax and domain, the mail server, and whether the mailbox accepts mail. It also flags disposable, role and catch-all addresses. Each answer carries a status, a reason and a suggested action, so your code does not have to interpret the details.
Why the check cannot run in the page
The Unspam API needs a secret token with every request. Anything in your page's JavaScript can be read by visitors, so a token placed there could be copied and used to spend your credits. The API does not accept requests from a browser anyway. Because Static.app does not run server code, the check runs in a small function on a service such as Cloudflare Workers. Static.app sends each new entry to it by webhook.
What you need
- An Unspam account with API access, which comes with the Custom plan. See Unspam's pricing for the current terms. Create an API token in your Unspam account settings.
- A Cloudflare account. A function this small fits within the free Workers plan for most websites.
- A place for clean signups to go, such as the webhook URL of your mailing list tool, Zapier or Make.
Create the function
In Cloudflare, create a new Worker, replace its code with the code below and deploy it. Then add four variables in the Worker's settings, and mark the first two as secrets:
UNSPAM_TOKEN: your Unspam API tokenWEBHOOK_SECRET: a long random string. You will paste the same string into Static.app.FORWARD_URL: where clean signups should goEMAIL_FIELD: thenameof your form's email input, usuallyemail
const CHECKS = 'https://api.unspam.email/ext/v2/email-verification/instant-checks';
export default {
async fetch(request, env, ctx) {
if (request.method !== 'POST') {
return new Response('Method not allowed', { status: 405 });
}
const body = await request.text();
const signature = request.headers.get('X-Webhook-Signature');
if (!(await validSignature(body, signature, env.WEBHOOK_SECRET))) {
return new Response('Bad signature', { status: 401 });
}
// Answer Static.app at once and verify in the background,
// so a slow check never makes the webhook time out and retry.
ctx.waitUntil(handleEntry(JSON.parse(body), env));
return new Response('ok');
},
};
async function handleEntry(entry, env) {
const email = String(entry.fields?.[env.EMAIL_FIELD || 'email'] || '').trim();
if (!email) return;
const result = await verify(email, env);
if (shouldBlock(result)) return;
await fetch(env.FORWARD_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...entry, verification: result }),
});
}
async function verify(email, env) {
const headers = {
Authorization: `Bearer ${env.UNSPAM_TOKEN}`,
'Content-Type': 'application/json',
};
try {
let res = await fetch(CHECKS, { method: 'POST', headers, body: JSON.stringify({ email }) });
if (!res.ok) return null;
let check = await res.json();
for (let i = 0; i < 10 && check.status === 'pending'; i++) {
await new Promise((resolve) => setTimeout(resolve, 2000));
res = await fetch(`${CHECKS}/${check.id}`, { headers });
if (!res.ok) return null;
check = await res.json();
}
return check.status === 'completed' ? check.result : null;
} catch {
return null;
}
}
function shouldBlock(result) {
if (!result) return false; // no answer: let the signup through
if (result.action === 'remove') return true;
const flags = result.flags || [];
return flags.includes('disposable') || flags.includes('throwaway');
}
async function validSignature(body, header, secret) {
if (!secret || !header || !/^sha256=[0-9a-f]{64}$/i.test(header)) return false;
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
'raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']
);
const bytes = new Uint8Array(header.slice(7).match(/../g).map((h) => parseInt(h, 16)));
return crypto.subtle.verify('HMAC', key, bytes, encoder.encode(body));
}
The function checks the signature first, so only Static.app can send it entries. It answers straight away and runs the check in the background, because Static.app waits only 10 seconds for an answer and resends the entry if it does not get one. Each resend would cost another check. The verifier answers after a few seconds, so the function asks again every 2 seconds, for up to 20 seconds.
Connect the webhook in Static.app
- Open your website in the dashboard, go to its forms apps, click Add App and choose Webhooks.
- Give the webhook a name and paste your Worker's address as the URL.
- Choose the signup form under Form.
- In Secret (optional), paste the same string you used for
WEBHOOK_SECRET. Static.app then signs every request, and the function rejects anything unsigned. - Make sure the webhook is Active and save.
Submit the form once with your own address and once with a throwaway one. Only the first should reach your list. If nothing arrives, open the Worker's logs in Cloudflare.
Decide what counts as fake
The function above uses Unspam's suggested action. Here is what each value means and what the code does with it:
keep: the mailbox exists and accepts mail. The signup goes through.review: the address might work, for example on a catch-all domain that accepts every name. The signup goes through, since many company domains work this way.remove: the address is broken, the domain does not exist or the mailbox was refused. The signup is dropped.retry, or no answer: the mail server did not reply in time. The signup goes through, because losing a real customer costs more than letting one fake address in.
Addresses flagged as disposable or throwaway are dropped whatever their action, since they usually stop working within hours. If you also want to turn away shared inboxes such as info@, add the role flag to shouldBlock. When Unspam spots a likely typo, the result includes did_you_mean with the corrected address, which you can store next to the entry.
What this setup does not change
The webhook runs after Static.app has saved the entry, so fake entries still appear in your website's form entries, and the notification email for them is still sent. The function keeps them out of the tools that come after: your mailing list, your CRM, your welcome emails.
If you only want to hear about real signups, untick Enable email notifications in your form settings and have the tool at FORWARD_URL notify you instead. To tidy the dashboard, select the entries you do not want and use Mark as Spam.
Clean up the signups you already have
You do not need any code to check signups collected before you set this up:
- In your website's forms, open the form's menu and choose Save as CSV.
- Upload the file to Unspam's email list check. It finds the address column on its own, and duplicates and malformed rows are not charged.
- Remove the addresses marked as remove from your mailing list, and mark their entries as spam in Static.app.
Common questions
The answers below cover API keys in the page, form speed, how complete the check is, and what it costs.
Can I call the email verifier from my form's JavaScript?
No. The API token would be visible to every visitor, and the Unspam API does not accept requests from a browser. Bots also send entries straight to the form endpoint without running your JavaScript, so a check in the page would not stop them anyway. A function that receives the webhook is the place where the check cannot be skipped.
Will this slow down my form?
No. Visitors see the usual success message as soon as Static.app saves the entry. The check happens afterwards, in the function.
Does an email verifier catch every fake address?
No. Catch-all domains accept every address, so they cannot be checked mailbox by mailbox. Yahoo and AOL do not confirm whether a mailbox exists either. A person who signs up with a working address they never read will pass too. Together with the honeypot and reCAPTCHA, the check removes most of the junk, and double opt-in in your email tool catches what is left.
Do I need this for a contact form?
Usually not. Contact messages do not go to a mailing list, so a bad address costs you little. The honeypot and reCAPTCHA are enough. The address check pays off on signup, waitlist and download forms.
What does it cost?
Forms, the honeypot, reCAPTCHA and webhooks are part of your Static.app website. Unspam's first checks each day are free, after which each definite answer uses one unit of your plan's quota or credits, and unknown answers are never charged. Its API comes with the Custom plan. Cloudflare's free Workers plan is enough for most signup volumes. To put a form on your website in the first place, see Static Forms.
Ready to try it?
Add a form to your website