Waitlist for a new SaaS

Waitlist for a new SaaS8 min read

How to build a pre-launch email list for a Product Hunt launch

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.

Preet SutharFounder, Ikiform

Start collecting emails on your own page four to six weeks before launch, with a hidden source field so you know which channel each signup came from. Ask people to opt in to a launch-day email, then export the list the week before and split it into two sends: one right after the Product Hunt day begins at 12:01 a.m. Pacific, and one for the Americas in their morning.

A Product Hunt launch has a fixed date and a 24-hour window. The people most likely to try your product and leave a thoughtful comment are the ones who asked to hear from you, so the list matters more than the launch post. This page covers what changes when the list is built for that one day.

What's different about a Product Hunt list

  • The deadline is fixed. A normal waitlist can wait until the product is ready. Here, the list has to be exported, cleaned and loaded into your email tool before a date you picked weeks ago.
  • The day runs on Pacific Time. Launches go live at 12:01 a.m. PT and compete for that day's rank. Your list lives in every time zone, so one send at one hour reaches half of it asleep.
  • The ask is feedback, not votes. Product Hunt's rules forbid asking for upvotes, and doing so can get a launch disqualified. Ask the list to try the product and comment on what they'd change. It's within the rules, and comments read better than a count anyway.
  • Launch day brings a spike. Product Hunt visitors land on your site and sign up within minutes of each other. Form limits that never mattered for weeks can matter for a few hours.
A Product Hunt launch list from the first signup to the day afterBeforeLaunch day, Pacific Time6 to 4weeksoutpagelive, refperchannel1 weekoutexport,dedupe,splitwavesthe daybeforeraise theform-widelimit12:01a.m.launchgoes livewave 1Europe,Africa,Asia,Oceania6 a.m.wave 2:theAmericasnextdaythank-you,comparerefsshare weeklyminutesA Product Hunt launch list from the first signup to the day afterBeforeLaunch day, Pacific Time6 to 4 weeks outpage live, ref per channel1 week outexport, dedupe, split wavesthe day beforeraise the form-wide limit12:01 a.m.launch goes livewave 1Europe, Africa, Asia, Oceania6 a.m.wave 2: the Americasnext daythank-you, compare refsshare weeklyminutes
  1. Four to six weeks out, the page goes live with the form, and each channel you share it in gets its own ref value.
  2. A week before, export the list, remove duplicates and split the people who opted in into two waves by country.
  3. The day before, raise the form's limit for the whole form so launch traffic isn't turned away.
  4. At 12:01 a.m. Pacific the launch goes live.
  5. Minutes later, send wave 1 to everyone outside the Americas, where it's morning or afternoon.
  6. At about 6 a.m. Pacific, send wave 2 to the Americas.
  7. The next day, thank the list and compare which ref values brought the people who signed up and stayed.
Ikiform records the country of each signup, so the split needs no extra question on the form. Unknown countries go in the second wave.

The form

Start from the email-only form in the main guide and add two things: a checkbox that asks for the launch-day email, and a hidden ref filled from the link people arrived on.

index.html
<form id="launch-list" action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
  <label for="email">Email</label>
  <input id="email" name="email" type="email" autocomplete="email" required>
  <label>
    <input name="launch_reminder" type="checkbox" value="true">
    Email me the morning we launch on Product Hunt
  </label>
  <input type="hidden" name="ref" value="direct">
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden>
  <button>Get early access</button>
</form>

<script>
  const params = new URLSearchParams(location.search);
  const ref = params.get("ref") || params.get("utm_source");
  if (ref) {
    document.querySelector('#launch-list [name="ref"]').value = ref.toLowerCase().replace(/[^a-z0-9_-]/g, "").slice(0, 40);
  }
</script>
  • The checkbox starts unticked. A launch-day email is a separate thing from early access, so people opt in to it. Ikiform stores true or false with the signup's timestamp.
  • The script copies ?ref= or ?utm_source= into the hidden input and strips anything that isn't a letter, digit, - or _. Without either, ref stays direct.
  • Product Hunt usually adds ref=producthunt to the links on your launch page, so launch-day signups tag themselves.

Add the field definition on the form's Fields tab. Ikiform then refuses posts that are missing the email, stores the checkbox as a real true or false, and caps ref at 40 characters so nobody can stuff it.

Fields
{
  "version": 1,
  "title": "Launch list",
  "fields": [
    { "id": "email", "type": "email", "label": "Email", "required": true },
    { "id": "launch_reminder", "type": "checkbox", "label": "Email me the morning we launch on Product Hunt" },
    { "id": "ref", "type": "text", "label": "Source", "validation": { "max_length": 40 } }
  ]
}

One ref per channel

Decide the values before you share the link anywhere, and use them every time:

Where you shareLinkStored ref
Your X or Bluesky postsexample.com/?ref=xx
LinkedInexample.com/?ref=linkedinlinkedin
A community post, such as Indie Hackersexample.com/?ref=ihih
Your newsletter or blogexample.com/?utm_source=newsletternewsletter
Your Product Hunt launch pageAdded by Product Huntproducthunt

Get the form ready for launch traffic

The default limits suit a quiet pre-launch page. Launch day isn't quiet. Each visitor can post 5 a minute, which is plenty, but the whole form is capped at 300 a minute. A front-page launch can send more signups than that in a busy minute.

  • The day before, open the form's settings and choose a Custom rate limit with a higher limit for the form. Put it back after the launch week.
  • If you're demoing at a meetup or a co-working space, everyone shares one IP. Choose Relaxed for that day.
  • A refused post gets 429 rate_limited and a Retry-After header. The plain HTML form shows Ikiform's error page with a link back.
  • Turn on the human check under Spam check if launch day brings bots. The plain form needs no change for it; see spam protection.

Split the launch email into two waves

A week before launch, read the list through the REST API and split it. This script keeps the newest signup per email, drops anyone who didn't tick the box, and writes two CSV files by the country Ikiform recorded. It needs a secret key with submissions:read.

waves.mjs
import { writeFileSync } from "node:fs";
import { createClient } from "@ikiform/sdk";

const AMERICAS = new Set(["US", "CA", "MX", "BR", "AR", "CO", "CL", "PE", "EC", "UY", "PY", "BO", "VE", "GT", "CR", "PA", "DO", "PR"]);
const ikiform = createClient({ apiKey: process.env.IKIFORM_SECRET_KEY });
const seen = new Set();
const waves = { midnight: [], morning: [] };

for await (const submission of ikiform.submissions.iterate({ formId: "frm_xxxxxxxxxxxxxxxx", status: "received", limit: 100 })) {
  const email = String(submission.fields.email ?? "").trim().toLowerCase();
  if (!email || seen.has(email)) continue;
  seen.add(email);
  if (submission.fields.launch_reminder !== true) continue;
  const country = submission.meta.country ?? "";
  const wave = !country || AMERICAS.has(country) ? waves.morning : waves.midnight;
  wave.push([email, String(submission.fields.ref ?? "").replaceAll(",", ""), country].join(","));
}

for (const [name, rows] of Object.entries(waves)) {
  writeFileSync(`wave-${name}.csv`, ["email,ref,country", ...rows].join("\n"));
  console.log(`${name}: ${rows.length} people`);
}
  • Signups arrive newest first, so a person who signed up twice is judged by their latest answer. If they unticked the box the second time, they're left out.
  • Import both files into your email tool as separate segments and schedule them: wave 1 for a few minutes after 12:01 a.m. Pacific, wave 2 for around 6 a.m. Pacific.
  • Run it once more on launch eve to pick up the last week of signups.

The launch-day email

Send it from your email tool, so unsubscribes and bounces are handled. Lead with the product, link the launch page second, and ask for a comment, not a vote:

Launch-day email
Subject: We're on Product Hunt today

Hi,

You joined the Acme list a few weeks ago. Today we launched on Product Hunt,
and the product is open to everyone on this list first.

Try it: https://acme.example/start
Our launch page: https://www.producthunt.com/products/acme

If you have a minute, tell us on the launch page what you'd change.
Honest comments help more than anything else today.

Ada, founder of Acme
Ikiform doesn't email the person who fills in the form. Notification emails go to you and your team only, so there is no autoresponder, confirmation email or double opt-in. Send those from your email tool, triggered by a webhook. If you want a welcome email the moment someone joins, forward signups by webhook to your email tool; the main guide shows the relay.

The day after

Export the list again with Export CSV, filtered to the last 7 days, and count signups by ref. The channels that brought people who later signed in are where your next launch post goes. Send the people who joined on launch day a short thank-you from your email tool; they found you through Product Hunt and haven't heard from you yet.

Check it works

Do this a week before launch, not on the night.

  1. Under the form's Allowed domains, add your site's domain, plus localhost while you develop. Leave it empty to accept posts from anywhere.
  2. Open the page with ?ref=x and submit. The submission shows ref as x and launch_reminder as false.
  3. Submit again with the box ticked and no query string. ref reads direct and launch_reminder reads true.
  4. Open Submissions in the dashboard. The new submission is there with every field you sent.
  5. Run the wave script and check each test address is in the file you expect for its country.
  6. Nothing arrived? Switch the dashboard's status filter to Spam. Spam gets the same success answer as a real submission.

Gotchas

ProblemWhat to do
429 rate_limitedOne visitor sent more than the form's rate limit allows. Standard allows 5 a minute per visitor. Wait for Retry-After, or choose Relaxed or Custom for forms that post from one shared IP.
422 with Unknown field for utm_source or another parameterOnly ref is defined. The script reads utm_source into ref instead of sending it, so check you didn't add another hidden input.
Every signup has ref set to directThe script runs before the form exists, or the form's id differs. Place the script after the form and keep id="launch-list".
A country is empty in the exportIkiform couldn't tell where the visitor was. The script puts empty countries in the morning wave, which reaches the Americas awake and everyone else later in their day.
The submission is in the dashboard but no email or webhook arrivedIt was stored as spam. Spam sends nothing. Open it under the Spam filter to see the reasons, and mark it as not spam to send its email and webhooks.

Questions

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