Stop contact form spam

Stop contact form spam6 min read

How to stop Webflow form spam

Protect a Webflow form that posts to a form backend, where Webflow's own spam settings never run, with an off-screen honeypot and a human check.

Preet SutharFounder, Ikiform

Webflow's reCAPTCHA, bot blocking and spam filter only work on submissions Webflow itself receives. If your form uses a custom action to post to a form backend, those settings never see the post, so the protection has to happen at the backend. Add an off-screen honeypot in the Designer, set your domains and rate limit on the backend, and turn on its human check, which works with the normal Webflow post and no code.

What's different on Webflow

Webflow has its own spam tools: reCAPTCHA v2, a bot blocking setting that runs Cloudflare Turnstile, and a spam filter that reads each submission's content. They all live on Webflow's side, so they only act on posts that reach Webflow. The moment you set a form's Send to to a custom action, the browser posts straight to that URL. Webflow never sees the submission, so none of its checks run, however they are set.

Where spam checks run for a Webflow form, with and without a custom actionWebflow handles the formCustom action to IkiformWebflowformWebflowreCAPTCHA,bot blocking,spam filterWebflowsubmissionsWebflowformthe sameDesigner formWebflow'schecksnever see thepostIkiformhoneypot,score, limit,checkreceived orspamWhere spam checks run for a Webflow form, with and without a custom actionWebflow handles the formCustom action to IkiformWebflow formWebflowreCAPTCHA, bot blocking, spamfilterWebflow submissionsWebflow formthe same Designer formWebflow's checksnever see the postIkiformhoneypot, score, limit, checkreceived or spam
  1. When Webflow handles the form, the post goes to Webflow, which runs reCAPTCHA, bot blocking and its spam filter before storing it.
  2. With a custom action, the browser posts the same form straight to Ikiform.
  3. Webflow's checks never see that post, whatever their settings.
  4. Ikiform runs the honeypot, scoring, rate limit and human check, then stores the post as received or spam.
Moving a form to a custom action moves its spam protection too. Set it up on the backend the same day.

Webflow's own settings, after the switch

Webflow settingWith a custom actionWhat to do
reCAPTCHA element and validationNot verified by anyone. Its g-recaptcha-response value arrives as an extra fieldRemove the element. Turn reCAPTCHA validation off only if no form Webflow still handles needs it, because the setting covers every form on the site
Bot blockingDoesn't apply to posts that go elsewhereLeave it for any forms Webflow still handles
Spam filteringDoesn't applyUse the backend's content scoring instead
Form submission limits and notificationsDon't applyThe backend stores and emails instead

The Webflow guide in the docs covers pointing the Form Block at Ikiform. The steps below add the spam protection on top.

Add the honeypot in the Designer

Webflow's quick way to hide an input is Display: none. Some bots skip fields hidden that way, so put the trap in an off-screen wrapper instead. It takes a minute and needs no paid plan:

  • Inside the form, add a Div Block and give it a class such as hp. In the Style panel set Position to Absolute, Left to -10000px, Width and Height to 1px, and Overflow to Hidden.
  • In the Div Block's Custom attributes, add aria-hidden set to true, so screen readers skip everything inside it.
  • Put a Label with the text Leave this empty and an Input inside the Div Block. Set the Input's Name to _gotcha, and add the custom attributes tabindex = -1 and autocomplete = off.
  • Don't tick Required on it. A required field nobody can see blocks every honest submit.

A filled _gotcha adds 5 to Ikiform's spam score, enough to reach the threshold of 5 by itself. You can rename the field per form under Honeypot field if a template you use already has a trap with another name.

Name the fields for scoring

Webflow names each input after its label, so you get Name, Email and Field 2. Rename them in each input's settings to name, email and message. The names matter for spam scoring as well as for your data:

  • A link in a name field, such as name, first_name or subject, adds 3. Spammers often put their URL there, and no real person does.
  • An address on a disposable email domain adds 2, read from any field that holds an email address.
  • Three or more links across the form add 2, and a known spam phrase adds 2.

Turn on the human check

In the Ikiform form's settings, set Spam check to Ikiform. Nothing changes in Webflow. When the form posts the normal way, Ikiform answers with a short Checking you're human page on ikiform.com that runs Cloudflare Turnstile and sends the fields on, then the visitor lands on your Redirect after submit page. No Cloudflare account, keys or custom code.

If you added the docs' script to keep Webflow's success and error blocks, it posts with fetch, and a fetch post without a token gets 403 challenge_required. This version handles that: it falls back to a normal post for the human check, and shows the wait time when a visitor hits the rate limit.

Before </body> tag
<script>
  document.querySelectorAll('form[action^="https://ikiform.com/f/"]').forEach((form) => {
    const wrapper = form.closest(".w-form");
    const done = wrapper?.querySelector(".w-form-done");
    const fail = wrapper?.querySelector(".w-form-fail");
    const button = form.querySelector('[type="submit"]');

    function showError(text) {
      const slot = fail?.querySelector("div") ?? fail;
      if (slot) slot.textContent = text;
      if (fail) fail.style.display = "block";
    }

    form.addEventListener("submit", async (event) => {
      event.preventDefault();
      const label = button?.value;
      if (button) {
        button.disabled = true;
        button.value = button.dataset.wait || label;
      }
      try {
        const response = await fetch(form.action, {
          method: "POST",
          headers: { Accept: "application/json" },
          body: new FormData(form),
        });
        const result = await response.json();
        if (result.ok) {
          form.style.display = "none";
          if (fail) fail.style.display = "none";
          if (done) done.style.display = "block";
          return;
        }
        if (result.error.code === "challenge_required") {
          form.submit();
          return;
        }
        showError(
          response.headers.has("Retry-After")
            ? `Too many messages. Try again in ${response.headers.get("Retry-After")} seconds.`
            : result.error.message,
        );
      } catch {
        form.submit();
      } finally {
        if (button) {
          button.disabled = false;
          button.value = label;
        }
      }
    });
  });
</script>
  • form.submit() posts the form without firing the submit event, so the fallback never loops back into the script.
  • Paste it under Page settings > Custom code > Before </body> tag, or in the site's footer code for every form. Custom code needs a paid site plan and only runs on the published site.
  • It only touches forms whose action starts with https://ikiform.com/f/, so forms Webflow still handles are left alone.

Allowed domains and the rate limit

Under the Ikiform form's Allowed domains, add your custom domain and your staging address, such as your-site.webflow.io. Never add webflow.io on its own: every Webflow site would match, including other people's. Posts from anywhere else are stored as spam, or refused if you choose Reject.

For a contact form, set Rate limit to Strict, 2 every 10 minutes per visitor. Nobody honest writes twice in ten minutes. A signup form on a busy launch day suits Relaxed, 20 a minute per visitor better. Every form starts on Standard, 5 a minute.

A post judged as spam gets the same success answer as a real one, so a bot learns nothing. It is stored with the status spam, sends no notification email and no webhook, doesn't count toward your usage, and is deleted after 30 days. Every Monday, a workspace that blocked spam gets one summary email with the total, the busiest forms and the top reasons.

Check it works

  1. Publish to your webflow.io staging domain. Custom actions only post from the published site.
  2. Submit the form normally. With the human check on, you see the check page briefly, then your thank-you page.
  3. Inspect the published form and confirm the honeypot wrapper has aria-hidden and sits off-screen.
  4. Post with the honeypot filled in, _gotcha=bot. It still gets a success answer, but the submission is stored as spam and sends no email or webhook.
  5. Open Submissions in the dashboard. The new submission is there with every field you sent.
  6. Nothing arrived? Switch the dashboard's status filter to Spam. Spam gets the same success answer as a real submission.

Gotchas

  • Spam with Webflow's settings on. If spam keeps arriving after you turned on Webflow's tools, check the form's Send to. A custom action bypasses all of them.
  • reCAPTCHA left in place. With fields defined on the Ikiform form, a leftover g-recaptcha-response fails with 422 validation_failed as an unknown field. Remove the element.
  • Duplicated forms. Copying a form copies the honeypot input but not always its custom attributes. Check tabindex and aria-hidden on each copy.
  • Interactions on the wrapper. A Webflow interaction that animates every Div Block in the form can move the honeypot into view. Exclude its class.

Questions

Stuck on a step, or want a guide for your situation? Email hello@ikiform.com.