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.
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.
| Item | 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.
Preview
Runs Ikiform's validator in your browser. Nothing is sent.
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.
<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
requiredmakes them choose. inputmode="email"andautocomplete="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
- An iPhone user who ticks the beta box triggers a signed webhook.
- Your relay verifies it and calls the App Store Connect API to add the address to a TestFlight group.
- Apple emails the TestFlight invite once the group has a build available.
- Android testers are collected the same way, then exported as CSV and filtered by platform.
- You paste the Google accounts into the closed test's email list in Play Console.
- You email them the test's opt-in link from your email tool.
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.
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_KEYis the contents of the.p8file Apple gives you, newlines included.ASC_KEY_IDandASC_ISSUER_IDare on the same page.- Apple's tokens may last at most 20 minutes, so the relay signs a fresh one per delivery.
- A
409means 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 answers502, 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.
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
- Under the form's Allowed domains, add your site's domain, plus
localhostwhile you develop. Leave it empty to accept posts from anywhere. - Open the page on a phone. The right platform is pre-selected, and on Android the Google account hint shows.
- Submit as an iPhone tester. The submission shows
platformasiphoneandbetaastrue. - Open the relay's endpoint under Webhooks. The delivery shows
succeeded, and the address is listed in your TestFlight group. - Submit as an Android tester, export the CSV and confirm the row filters out with
platformset toandroid. - 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
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.
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.
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.
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.
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.
Stuck on a step, or want a guide for your situation? Email hello@ikiform.com.