Waitlist for a new SaaS

Waitlist for a new SaaS8 min read

How to build a waitlist page for a mobile app before App Store launch

A pre-launch waitlist for an iOS or Android app that asks which phone people use, sends iPhone testers to TestFlight and fills a Play closed test.

Preet SutharFounder, Ikiform

Put a waitlist form on your app's landing page that asks which phone the person uses and whether they want to test early. Send iPhone testers into a TestFlight group with a webhook and the App Store Connect API, and load Android testers into a Google Play closed test from an export. On launch day, email each group the link to its own store.

Before an app is in a store, there's no install button to send people to. A waitlist on your own landing page keeps their interest until there is, and it does a second job the stores don't: it finds the testers you need before launch.

What's different for a mobile app

  • Two stores, two lists. The launch email links to the App Store or Google Play, and testing invites go through TestFlight or a Play closed test. You need to know each person's platform to send the right one.
  • Testers have to match the store's account. TestFlight emails an invite to any address. A Play closed test only lets in the Google accounts on its list, so an Android tester who signs up with a work address can't install the build.
  • Google Play may require testers before launch. Personal developer accounts created after 13 November 2023 must run a closed test with at least 12 testers who stay opted in for 14 days in a row before they can publish to production. A waitlist is where those 12 come from.
  • The stores have their own pre-launch tools. They're worth using next to a waitlist, not instead of one. See the comparison below.
ItemApp Store pre-orders or Play pre-registrationYour own waitlist
Needs a store listingYes, approved and readyNo. It works from the first day of the landing page
What you getA count, and a store notification or automatic download at releaseEach person's email, platform and answers
Can invite testersNoYes, by email to TestFlight or a Play closed test
LimitsPlay pre-registration stays off for new personal accounts until the testing requirement is metYou send the launch email yourself

Fields for an app waitlist

Ask for the platform first, because it changes what the email field means. The phone model is optional and tells you which devices to test on. The beta box separates people who want to test from people who want the launch email.

Preview

Runs Ikiform's validator in your browser. Nothing is sent.

On Android, use the Google account you sign in to Google Play with.

Which phone do you use?
The platform radio is required, so every signup lands in a list you can act on. Paste the definition on the form's Fields tab.

The form

The form works as plain HTML on any landing page. The script does two small things: it pre-selects the platform from the visitor's browser, and it shows the Google account hint only to Android users.

index.html
<form id="app-waitlist" action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
  <fieldset>
    <legend>Which phone do you use?</legend>
    <label><input type="radio" name="platform" value="iphone" required> iPhone</label>
    <label><input type="radio" name="platform" value="android"> Android</label>
  </fieldset>

  <label for="email">Email</label>
  <input id="email" name="email" type="email" autocomplete="email" inputmode="email" required>
  <p id="email-hint" hidden>Use the Google account you sign in to Google Play with.</p>

  <label for="first_name">First name</label>
  <input id="first_name" name="first_name" autocomplete="given-name" maxlength="60">

  <label for="device">Phone model</label>
  <input id="device" name="device" maxlength="80" placeholder="iPhone 15, Pixel 8">

  <label><input type="checkbox" name="beta" value="true"> Invite me to test the app before launch</label>
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden>
  <button>Join the waitlist</button>
</form>

<script>
  const form = document.getElementById("app-waitlist");
  const agent = navigator.userAgent;
  const guess = /iPhone|iPad/.test(agent) ? "iphone" : /Android/.test(agent) ? "android" : null;
  if (guess) form.querySelector(`[name="platform"][value="${guess}"]`).checked = true;
  const hint = () => {
    document.getElementById("email-hint").hidden = form.platform.value !== "android";
  };
  form.addEventListener("change", hint);
  hint();
</script>
  • The guess only pre-selects a radio. People can change it, and without the script nothing is selected, so required makes them choose.
  • inputmode="email" and autocomplete="email" matter here more than anywhere: most visitors to an app's landing page are on a phone.
  • Set Redirect after submit to a thank-you page that says what happens next for each platform. For a framework version of the form, see the main guide and add these fields.

Route testers to each store

How iPhone and Android testers reach each store's betaiPhoneAndroidsignupplatform =iphone, beta =truewebhookrelayverifies thesignatureApp StoreConnectAPIadds a betatesterTestFlightinviteemailed byApplesignupplatform =android, beta= trueCSVexportfilter byplatformPlayConsoleclosed testemail listopt-inlinkyou send itsignedweeklyHow iPhone and Android testers reach each store's betaiPhoneAndroidsignupplatform = iphone, beta = truewebhook relayverifies the signatureApp Store Connect APIadds a beta testerTestFlight inviteemailed by Applesignupplatform = android, beta = trueCSV exportfilter by platformPlay Consoleclosed test email listopt-in linkyou send itsignedweekly
  1. An iPhone user who ticks the beta box triggers a signed webhook.
  2. Your relay verifies it and calls the App Store Connect API to add the address to a TestFlight group.
  3. Apple emails the TestFlight invite once the group has a build available.
  4. Android testers are collected the same way, then exported as CSV and filtered by platform.
  5. You paste the Google accounts into the closed test's email list in Play Console.
  6. You email them the test's opt-in link from your email tool.
Apple has an API for testers; Google Play's closed tests are managed in Play Console or through Google Groups, so the Android side is a weekly export.

iPhone: add testers to TestFlight

In App Store Connect, create an external testing group and an API key under Users and Access › Integrations, with the App Manager or Admin role. Then add a relay that listens for Ikiform's submission.created webhook. This one is a Next.js route handler and uses jose to sign Apple's token; it runs on any server that has fetch.

app/api/testflight/route.ts
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";
import { importPKCS8, SignJWT } from "jose";

async function appStoreToken() {
  const key = await importPKCS8(process.env.ASC_PRIVATE_KEY!, "ES256");
  return new SignJWT({})
    .setProtectedHeader({ alg: "ES256", kid: process.env.ASC_KEY_ID!, typ: "JWT" })
    .setIssuer(process.env.ASC_ISSUER_ID!)
    .setAudience("appstoreconnect-v1")
    .setIssuedAt()
    .setExpirationTime("15m")
    .sign(key);
}

export async function POST(request: Request) {
  const payload = await request.text();
  try {
    const event = await verifyWebhook({ payload, headers: request.headers, secret: process.env.IKIFORM_WEBHOOK_SECRET! });
    if (event.type !== "submission.created") return new Response(null, { status: 204 });
    const { email, first_name, platform, beta } = event.data.fields;
    if (platform !== "iphone" || beta !== true) return new Response(null, { status: 204 });

    const response = await fetch("https://api.appstoreconnect.apple.com/v1/betaTesters", {
      method: "POST",
      headers: { authorization: `Bearer ${await appStoreToken()}`, "content-type": "application/json" },
      body: JSON.stringify({
        data: {
          type: "betaTesters",
          attributes: { email: String(email), firstName: first_name ? String(first_name) : undefined },
          relationships: { betaGroups: { data: [{ type: "betaGroups", id: process.env.ASC_BETA_GROUP_ID! }] } },
        },
      }),
    });
    if (response.ok || response.status === 409) return new Response(null, { status: 204 });
    return new Response(`App Store Connect answered ${response.status}`, { status: 502 });
  } catch (error) {
    if (error instanceof WebhookVerificationError) return new Response(error.reason, { status: 400 });
    throw error;
  }
}
  • ASC_PRIVATE_KEY is the contents of the .p8 file Apple gives you, newlines included. ASC_KEY_ID and ASC_ISSUER_ID are on the same page.
  • Apple's tokens may last at most 20 minutes, so the relay signs a fresh one per delivery.
  • A 409 means Apple refused the change as a conflict, often because the address is already a tester. The relay treats it as done, so if a tester never gets an invite, check the group in App Store Connect. Any other failure answers 502, so Ikiform retries the delivery.
  • TestFlight allows up to 10,000 external testers per app. The first build you give an external group goes through Beta App Review, and each build expires after 90 days.

Add the relay's URL under Webhooks in Ikiform and copy the whsec_ secret into IKIFORM_WEBHOOK_SECRET. A workspace can have 3 endpoints, so this can run next to a relay that feeds your email tool.

Android: fill a closed test

Google Play's publishing API manages closed-test testers through Google Groups, not single addresses, so this side is a weekly export. Export the list with Export CSV, filter it in a spreadsheet to platform = android and beta = true, and add the emails to the closed test's email list under Test and release › Testing › Closed testing › Manage track › Testers in Play Console. Then send those people the test's opt-in link from your email tool.

If you're meeting the 12-tester requirement, invite more than 12. People who opt out or never opt in don't count, and the 14 days need 12 testers opted in the whole time.

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. TestFlight sends its own invite, so iPhone testers hear from Apple. Android testers only hear from you.

Launch day

Export the whole list and split it by platform. Send each half one email with one store link. People who picked iPhone don't need the Play link, and a single email with two badges loses taps on a small screen. If you collected device, check your crash reports against the models people listed before you send.

Check it works

  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 on a phone. The right platform is pre-selected, and on Android the Google account hint shows.
  3. Submit as an iPhone tester. The submission shows platform as iphone and beta as true.
  4. Open the relay's endpoint under Webhooks. The delivery shows succeeded, and the address is listed in your TestFlight group.
  5. Submit as an Android tester, export the CSV and confirm the row filters out with platform set to android.
  6. 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
The relay's deliveries fail with 502 and Apple answered 401The token is wrong: check the key id, the issuer id and that the private key kept its line breaks in the environment variable.
A tester was added but got no TestFlight emailThe group has no build yet, or the first build is still in Beta App Review. Invites go out once a build is available to the group.
An Android tester can't open the opt-in linkThe address on the list isn't the Google account signed in on the phone. Ask them to sign up again with that account.
422 validation_failedThe post doesn't match the form's field definition. error.fields names each field and the reason. Match the input name to the field id.
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.