1. Home
  2. Signup protection
  3. Webflow and Framer forms

Signup protection recipe

Filter fake Webflow and Framer form signups

Webflow and Framer can both send each form submission to a webhook URL. Point it at a small Worker that finds the email field, checks it with SimpleVerifier, drops disposable, invalid and misspelled addresses, and forwards everything else, with a check flag, to the destination you already use (Zapier catch hook, ESP, CRM).

Where it runs
Runs after the visitor submits. It keeps fakes out of your list but can't show an error on the hosted form.
Timeout
Nobody is waiting on the response, so the Worker allows the full 30 seconds a slow mail server can take.

You need an API key from the API keys page (Pro, Business or Agency), stored server-side as SIMPLEVERIFIER_KEY. Reviewed 2026-10-31.

Block, warn or allow

  • Block: disposable or temporary inbox (reason_code "disposable" (action "do_not_send"))
  • Block: invalid: bad format, no mail server, mailbox not found (action "do_not_send" (reason_code "syntax", "no_mx", "mailbox_not_found", "domain_not_hosted"))
  • Block: misspelled domain (gmial.com, hotmial.com) (reason_code "typo_domain" (action "do_not_send"))
  • Warn: role inbox (info@, sales@, admin@) (reason_code "role")
  • Warn: catch-all domain (reason_code "catch_all")
  • Warn: looks invented (test@, john.doe@, example.com) (likely_invented true)
  • Allow: valid, or the check could not finish (action "send", "send_with_care", "retry_later", or any non-2xx answer or timeout)

The reasoning for each row is on the signup protection overview.

  1. 1. Write the webhook

    A Cloudflare Worker; any serverless function works the same way. A secret token in the URL keeps strangers from posting to it. It accepts Webflow's form_submission payload (fields under payload.data) and Framer's flat field map.

    src/index.js
    const API_URL = "https://simpleverifier.com/api/v1/verify";
    const WARN_CODES = new Set(["role", "catch_all"]);
    const ALLOW = { decision: "allow", reasonCode: null, message: null };
    
    function blockMessage(reasonCode, reason) {
      if (reasonCode === "disposable") return "Please use a permanent email address, not a temporary inbox.";
      const hint = reason.match(/did you mean ([^?\s]+)\??/i)?.[1];
      if (hint) return `Check the spelling of your email address. Did you mean ${hint}?`;
      return "This email address can't receive mail. Please check it.";
    }
    
    async function checkSignupEmail(email, apiKey, timeoutMs = 8000) {
      if (!apiKey || !email) return ALLOW;
      let result;
      try {
        const res = await fetch(API_URL, {
          method: "POST",
          headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
          body: JSON.stringify({ email: email.trim() }),
          signal: AbortSignal.timeout(timeoutMs),
        });
        if (!res.ok) return ALLOW; // fail open
        result = await res.json();
      } catch {
        return ALLOW; // timeout or network error: fail open
      }
      const reasonCode = result.reason_code ?? null;
      if (result.action === "do_not_send") {
        return { decision: "block", reasonCode, message: blockMessage(reasonCode ?? "", result.reason ?? "") };
      }
      if (result.likely_invented || WARN_CODES.has(reasonCode)) return { decision: "warn", reasonCode, message: null };
      return { decision: "allow", reasonCode, message: null };
    }
    
    function fieldsOf(body) {
      return body?.payload?.data ?? body?.data ?? body ?? {};
    }
    
    function emailIn(fields) {
      const key = Object.keys(fields).find((k) => /e-?mail/i.test(k));
      return key ? String(fields[key]) : "";
    }
    
    export default {
      async fetch(request, env) {
        const url = new URL(request.url);
        if (request.method !== "POST" || url.searchParams.get("token") !== env.FORM_WEBHOOK_TOKEN) {
          return new Response("Not found", { status: 404 });
        }
        const body = await request.json().catch(() => ({}));
        const fields = fieldsOf(body);
        const verdict = await checkSignupEmail(emailIn(fields), env.SIMPLEVERIFIER_KEY, 30000);
    
        if (verdict.decision === "block") {
          console.log("dropped submission", verdict.reasonCode);
          return new Response("ok"); // answer 200 so the form tool doesn't retry
        }
        await fetch(env.FORWARD_TO_URL, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ ...fields, email_check: verdict.decision, email_reason: verdict.reasonCode }),
        });
        return new Response("ok");
      },
    };
  2. 2. Set the secrets and deploy

    Your webhook URL is the Worker URL plus ?token=<FORM_WEBHOOK_TOKEN>.

    npx wrangler secret put SIMPLEVERIFIER_KEY
    npx wrangler secret put FORM_WEBHOOK_TOKEN   # any long random string
    npx wrangler secret put FORWARD_TO_URL       # e.g. your Zapier catch hook or CRM webhook
    npx wrangler deploy
  3. 3. Webflow: add the webhook

    In Site settings > Apps & integrations > Webhooks, add a webhook with the Form submission trigger and paste the URL. Name the form's email field with "email" in it so the Worker finds it.

  4. 4. Framer: send the form to the webhook

    Select the form, set its Send To option to Webhook, and paste the URL. Framer posts the field names and values as JSON.

Common questions

Can I show an error on the Webflow or Framer form?
Not with a webhook: the visitor has already seen the success message. To show an error inline, post the form to your own endpoint with custom code (see the Next.js or Cloudflare Worker recipe).
What if the forward step fails?
The Worker returns 500 when fetch throws, and both tools may retry. Add your own retry or a queue if the destination is often slow.
Which plan includes the API?
Pro ($29.99/month, 10,000 calls a month included), Business and Agency. Each address checked is one call. Starter and Growth have no API access.
How do I try it without blocking anyone?
Set SIGNUP_GUARD_DRY_RUN=1. The code logs what it would block and lets everyone through. Read the logs for a few days, then remove the variable.

Other recipes

Clean your next list before it bounces

100 free email checks when you sign in with Google, no card. Then monthly plans from $9, or one flat price with fair use, never cut off.

100 free checks, no card Cancel anytime Secure payment by Stripe