# 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.

Published 2026-10-25 · https://ikiform.com/guides/waitlist/webflow

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.

Diagram: A Webflow waitlist form before and after a custom action

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`:

```html
<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`:

```html
<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 runs | Add to Allowed domains |
| --- | --- |
| Staging | `your-site.webflow.io` |
| Your custom domain | `example.com`, which covers `www.example.com` |
| The Designer | Nothing. It never posts from there |

Never add `webflow.io` on its own; every Webflow site would match. The [Webflow docs guide](https://ikiform.com/docs/guides/webflow.md) covers the same settings for a contact form, and the [main guide](https://ikiform.com/guides/waitlist.md#confirm) 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

| Problem | What to do |
| --- | --- |
| `404 not_found` after submit | **Method** 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-response` | An 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 nothing | You're in the Designer, the site has no paid plan, or **Action** has a space in it. Test on the published site. |
| `403 challenge_required` | The 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 spam | The 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

### Do waitlist signups sent to Ikiform count toward Webflow's form submission limit?

No. With a custom action the browser posts straight to Ikiform, so Webflow never receives the submission. It doesn't appear in Webflow's form submissions and doesn't use your plan's allowance.

### How do I add a hidden field to a Webflow form?

Webflow has no hidden input element. Put a Code Embed inside the form with an input of type hidden. Code Embed needs a paid Site plan or a Core, Growth, Agency or Freelancer Workspace.

### Can I keep Webflow's success message with a custom action?

Yes, with a script. Webflow only shows its success block when it handled the post, so the script posts with fetch and shows the block itself, as in the sample above.

### Can Ikiform email the person who submitted the form?

No. Ikiform emails you and your team, not the submitter, so there is no autoresponder or confirmation email. Send a webhook to your email tool and send the confirmation from there.

## More guides

- [Waitlist for a new SaaS](https://ikiform.com/guides/waitlist.md): Build a pre-launch waitlist on your own landing page: the form, spam protection, duplicates, confirmation emails and a clean export on launch day.
- [Waitlist on Framer](https://ikiform.com/guides/waitlist/framer.md): Two ways to collect waitlist signups on Framer: Framer's form with a webhook to Ikiform, or a code component that keeps per-visitor limits and spam checks.
- [Waitlist on Carrd](https://ikiform.com/guides/waitlist/carrd.md): Collect waitlist signups on a Carrd page: a Send to URL form on Pro Plus with UTM tracking, or an embedded HTML form with a honeypot on Pro Standard.
- [Plain HTML waitlist](https://ikiform.com/guides/waitlist/html.md): A complete no-build waitlist page in plain HTML that works with JavaScript off, shows an inline thank-you when it loads, and falls back when it must.
- [Product Hunt launch list](https://ikiform.com/guides/waitlist/product-hunt.md): Collect a launch list weeks before Product Hunt, tag each signup by channel, and send the launch-day email in two waves timed to the Pacific Time reset.
- [Beta signup with screening](https://ikiform.com/guides/waitlist/beta-signup.md): A beta application form with screening questions the server enforces, and a script that scores applicants and picks each cohort from the list.
