Waitlist for a new SaaS

Waitlist for a new SaaS6 min read

How to add a waitlist form to a Webflow site

Point a Webflow Form Block at Ikiform, tag signups by source with a hidden field, and keep Webflow's success state with a short script for every form.

Preet SutharFounder, Ikiform

Keep the Form Block you designed in Webflow and change its action to your Ikiform endpoint with the POST method. Add a hidden ref input and a honeypot in a Code Embed, then paste one site-wide script that posts every waitlist form with fetch, fills ref from the link and shows Webflow's own success block with the person's email.

Webflow is good at the page and fine at the form. Its native form handling stores submissions and emails you, but it caps how many it keeps by site plan and can't tag a signup with the campaign it came from. A custom action fixes both without leaving the Designer.

What's different on Webflow

  • A custom action takes the form away from Webflow. The browser posts straight to Ikiform, so Webflow never sees the signup: it isn't in Webflow's form submissions, Webflow sends no email, and it uses none of your plan's form allowance.
  • Webflow's success and error blocks stop working. They only appear when Webflow handled the post. Without a script, visitors land on Ikiform's thank-you page or your Redirect after submit URL.
  • There's no hidden input element. Hidden fields and the honeypot go in a Code Embed inside the form, which needs a paid site plan, as does custom code.
  • Nothing runs in the Designer. Custom actions and scripts only work on the published site, so test on your webflow.io staging domain.
A Webflow waitlist form before and after a custom actionWebflow handles the formCustom action with the scriptForm BlockSend to:WebflowWebflowstores it,counts towardthe plansuccessblock.w-form-doneForm Blockaction = yourendpoint,POSTscriptfills ref,posts withfetchIkiformstores,emails,webhookssuccessblockshown by thescriptpostsubmitJSON201A Webflow waitlist form before and after a custom actionWebflow handles the formCustom action with the scriptForm BlockSend to: WebflowWebflowstores it, counts toward the plansuccess block.w-form-doneForm Blockaction = your endpoint, POSTscriptfills ref, posts with fetchIkiformstores, emails, webhookssuccess blockshown by the scriptpostsubmitJSON201
  1. By default, the Form Block posts to Webflow, which stores the signup against your plan's allowance and shows the success block.
  2. With a custom action, the same Form Block points at Ikiform.
  3. The script stops the normal post, fills the ref field from the link and posts with fetch.
  4. Ikiform stores the signup, emails you and sends webhooks, then answers 201.
  5. The script hides the form and shows Webflow's success block with the person's email.
The design stays in Webflow. Only where the data goes changes.

Set up the Form Block

  1. Select the form and open Settings. Remove Webflow and Email notifications under Send to, then add Custom action, enter https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx, choose POST and save.
  2. Select the email input and set its Name to email. Webflow fills names from labels, so you'd otherwise get Email or Email 2, which Ikiform treats as a different field.
  3. Delete any reCAPTCHA element. Ikiform doesn't verify it, and with a field definition its g-recaptcha-response value is refused as an unknown field.
  4. Drag a Code Embed inside the form, next to the button, and paste the two inputs below.
  5. Set Redirect after submit in the Ikiform form's settings to a thank-you page you've published in Webflow. That's where people land if the script doesn't run.
Code Embed inside the form
<input type="hidden" name="ref" value="webflow">
<input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden>

Add the field definition on Ikiform's Fields tab with email required and ref as an optional text field with max_length 40. Every other name posted, such as a leftover Name input, then gets a 422, which shows you a mislabelled input on the first test.

The site-wide script

Paste this under Site settings › Custom code › Footer code, so it covers the hero form, the footer form and any page you add later. It only touches forms whose action starts with https://ikiform.com/f/.

Footer code
<script>
  const params = new URLSearchParams(location.search);
  const ref = (params.get("ref") || params.get("utm_source") || "").toLowerCase().replace(/[^a-z0-9_-]/g, "").slice(0, 40);
  const key = crypto.randomUUID();

  document.querySelectorAll('form[action^="https://ikiform.com/f/"]').forEach((form, index) => {
    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"]');
    const source = form.querySelector('[name="ref"]');
    if (source && ref) source.value = ref;

    form.addEventListener("submit", async (event) => {
      event.preventDefault();
      const label = button.value;
      button.disabled = true;
      button.value = button.dataset.wait || "Joining";
      fail.style.display = "none";
      try {
        const response = await fetch(form.action, {
          method: "POST",
          headers: { Accept: "application/json", "Idempotency-Key": `${key}-${index}` },
          body: new FormData(form),
        });
        const result = await response.json();
        if (result.ok) {
          const email = form.querySelector('[name="email"]').value;
          const line = done.querySelector("div") ?? done;
          line.textContent = `You're on the list. We'll write to ${email} on launch day.`;
          form.style.display = "none";
          done.style.display = "block";
          return;
        }
        const wait = response.headers.get("Retry-After");
        const message = wait ? `Lots of signups right now. Try again in ${wait} seconds.` : result.error.fields?.email?.[0] ?? result.error.message;
        (fail.querySelector("div") ?? fail).textContent = message;
      } catch {
        (fail.querySelector("div") ?? fail).textContent = "Couldn't reach the server. Try again.";
      }
      fail.style.display = "block";
      button.disabled = false;
      button.value = label;
    });
  });
</script>
  • ?ref= or ?utm_source= in the link fills every hidden ref input on the page. Without either, the value set in the embed is kept, so set it per form, such as hero and footer, to see which one converts.
  • The idempotency key is made once per page load and suffixed per form. A double click stores one signup; a second signup on another form on the page still counts.
  • Webflow's success block gets a line with the person's email, which catches typos: people notice gmial.com when it's written back to them.
  • The button shows the Loading text you set in the Designer while the post is in flight.
  • A 429 shows how long to wait, from Ikiform's Retry-After header, instead of a generic failure.
The script posts with fetch, so leave the form's spam check off. With the check on, Ikiform answers 403 challenge_required and the error block shows. If you need the check, skip the script; the plain post runs it with no code.

Allowed domains

Where the form runsAdd to Allowed domains
Stagingyour-site.webflow.io
Your custom domainexample.com, which covers www.example.com
The DesignerNothing. It never posts from there

Never add webflow.io on its own; every Webflow site would match. The Webflow docs guide covers the same settings for a contact form, and the main guide shows how to forward signups to your email tool.

Check it works

  1. Publish to your webflow.io staging domain.
  2. Under the form's Allowed domains, add your site's domain, plus localhost while you develop. Leave it empty to accept posts from anywhere.
  3. Open the page with ?ref=test and sign up. Webflow's success block shows your email, and the submission in Ikiform has ref set to test.
  4. Sign up again from the footer form with no query string. ref reads the value from that form's embed.
  5. Turn off JavaScript and submit. You land on your Webflow thank-you page.
  6. Check the inbox of each notification recipient. Each gets an email with the fields, and Reply goes to the submitter when the form has an email field.
  7. 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.

Gotchas

ProblemWhat to do
404 not_found after submitMethod is GET, so the browser opened the endpoint like a link. Set it to POST and publish again.
422 with Unknown field Email or g-recaptcha-responseAn input's Name doesn't match the definition, or a reCAPTCHA element is left in the form. Rename the input to email or remove the element.
The script does nothingYou're in the Designer, the site has no paid plan, or Action has a space in it. Test on the published site.
403 challenge_requiredThe form's spam check is set to Ikiform and a fetch post had no token. Get one with challenge(formId) from @ikiform/sdk before each submit, or turn the check off.
403 origin_not_allowed, or every post lands in spamThe page's domain isn't in the form's Allowed domains. Add it, plus localhost for local tests. Subdomains of a listed domain are allowed.

Questions

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