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

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

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.

|   | App Store pre-orders or Play pre-registration | Your own waitlist |
| --- | --- | --- |
| Needs a store listing | Yes, approved and ready | No. It works from the first day of the landing page |
| What you get | A count, and a store notification or automatic download at release | Each person's email, platform and answers |
| Can invite testers | No | Yes, by email to TestFlight or a Play closed test |
| Limits | Play pre-registration stays off for new personal accounts until the testing requirement is met | You 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.

Form: App waitlist

- **Email** (`email`, email, required)
- **First name** (`first_name`, text)
- **Which phone do you use?** (`platform`, radio, required)
- **Phone model** (`device`, text)
- **Invite me to test the app before launch** (`beta`, checkbox)

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

```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](https://ikiform.com/guides/waitlist.md#form) and add these fields.

## Route testers to each store

Diagram: How iPhone and Android testers reach each store's beta

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

```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](https://ikiform.com/docs/webhooks.md). 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

| Problem | What to do |
| --- | --- |
| The relay's deliveries fail with `502` and Apple answered `401` | The 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 email | The 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 link | The 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_failed` | The 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 arrived | It 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

### Do I need a waitlist if the App Store has pre-orders?

They do different jobs. Pre-orders and Play pre-registration need an approved store listing and give you a count, not contacts. A waitlist works from day one, gives you each person's email and platform, and lets you invite testers.

### How do I find 12 testers for Google Play's closed testing requirement?

Ask for them on your waitlist. A beta checkbox and a platform question show you who has an Android phone and wants to test. Put their Google accounts on the closed test's email list and send them the opt-in link.

### Can Ikiform add testers to TestFlight automatically?

Not directly. Ikiform sends a signed webhook for each signup, and a short relay you deploy adds the address to a TestFlight group through the App Store Connect API, as shown 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.

### Is Ikiform free?

No, there is no free plan. Testing is free: you can send 50 test submissions a month from localhost before you pay. To go live, Pro starts at $6/mo for 500 submissions, or you can pay once for 10,000 submissions for $49 or 50,000 submissions for $149, with credits that never expire.

## 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.
- [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.
- [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.
- [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.
- [Next.js waitlist](https://ikiform.com/guides/waitlist/nextjs.md): A Next.js App Router waitlist: a server action that forwards signups with a secret key, useActionState, an optimistic thank-you and a per-visitor limit.
- [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.
