A founder posts the launch thread on a Tuesday morning and refreshes the dashboard for two hours. Zero signups. The page had 600 visitors from the post. The cause was a project URL saved as https://example.com on a site that redirected to https://www.example.com, so every browser signup got a 403 and the form showed a generic error that nobody screenshotted.

A silent zero looks exactly like no demand. That is why you put one fake signup through the real page before anyone else does.

Test on the live production URL, not localhost, because the signup endpoint always allows localhost and checks everything else against your saved project URL. Run four checks: the SDK loads, a signup returns a referral code, a repeat email is treated as a duplicate, and the row shows up in the dashboard. Use a recognizable test address, since contacts cannot be deleted.

Why Localhost Passes and Production Fails

When a browser sends a signup, the API reads the Origin header and applies three rules in order:

  1. No Origin header at all: allowed. This is what curl sends by default.
  2. Hostname is localhost or 127.0.0.1: allowed, whatever port.
  3. Anything else: compared to the project URL, scheme plus host, character for character. A mismatch returns 403 Origin not allowed.

Rule 1 and rule 2 are the trap. A local dev server passes. A raw curl passes. Only a real browser on the real domain exercises rule 3, which is the only rule your visitors will ever hit.

These all fail rule 3 when the saved URL is https://example.com:

Page is served fromResult
https://www.example.com403, different host
http://example.com403, different scheme
https://staging.example.com403, different host
file:///Users/dan/index.html403, browsers send Origin: null

A passing curl test is the most misleading result in this whole post. curl sends no Origin header, so it skips the check entirely. Add -H "Origin: https://yourdomain.com" or skip curl and use a browser.

Step 1: Confirm the Script Loaded

Open your live page in a normal window. Open the console and run:

window.OperatorStack;

You want an object with joinWaitlist, getShareLinks, and isSignedUp on it. If you see undefined, nothing else in this checklist matters yet. The tag is missing from the deployed HTML, a cache is serving an old build, or an ad blocker or consent tool is stopping it.

Also check the tag itself matches the project you are looking at in the dashboard:

<script src="https://operatorstack.dev/os.js" data-project="pk_abc123"></script>

A data-project key from a different project means signups succeed and land in the wrong dashboard. That looks identical to a silent zero.

1

Check the object, then the key. window.OperatorStack is defined, and data-project matches the key shown on this project's page.

Step 2: Submit a Real Signup With a Test Address

Use the form, not the console. You are testing the whole path your visitor takes, including the widget, the CORS preflight, and the success state.

Pick an address you control and can recognize later. A plus address works well on Gmail and most Google Workspace domains:

you+test1@gmail.com

After you submit, run this in the console to confirm the browser remembered the signup:

window.OperatorStack.isSignedUp(); // true

isSignedUp() reads a localStorage entry named os_signup_ plus your project key, written only after the API returned a success. A true here means the request completed, not just that the button was clicked.

If you built your own form, call the SDK and read the real error:

try {
  const result = await OperatorStack.joinWaitlist({
    email: "you+test1@gmail.com",
    name: "Test",
  });
  console.log(result.referral_code, result.referral_link);
} catch (err) {
  console.error(err.status, err.message);
}

The status tells you which rule broke:

StatusMeaning
404data-project key does not exist
403Origin does not match the saved project URL
422Email failed validation, usually a stray space or @@
429Rate limited, from submitting repeatedly while testing
2

Sign up through the form on the live URL. You get a success state and isSignedUp() returns true. If you built a custom form, log err.status so a failure names its cause.

Step 3: Test the Duplicate Rule on Purpose

Clear the stored signup, or open a private window, and submit the same address again. In a private window the widget form is back, because there is no os_signup_ entry.

The API finds the existing row for that email and returns the original referral_code and referral_link. It skips the rest: no new signup row, no waitlist_signup event, no notification, no confirmation email.

That is the right behavior, and it is also why repeat testing with one address misleads you. Your second run looks like it worked while nothing was written.

const a = await OperatorStack.joinWaitlist({ email: "you+test1@gmail.com" });
const b = await OperatorStack.joinWaitlist({ email: "you+test1@gmail.com" });
console.log(a.referral_code === b.referral_code); // true

Case is not normalized, so You+Test1@gmail.com counts as a different contact. The clean-contact-list post covers what that does to your real list.

3

Submit the same email twice. Both calls return one referral code. For every fresh end-to-end run afterward, bump the plus address: +test2, +test3.

Step 4: Find the Row in the Dashboard

Open the project dashboard and look in two places:

  • Waitlist tab: your test address with a timestamp and a position number.
  • Contacts: the same email with source set to waitlist.

If the Waitlist tab shows the row, the whole chain worked, from script to API to database. If you have confirmation emails turned on in the Waitlist settings, also check the inbox of your test address, and look in spam before you decide that the email did not send.

Then test the referral path once. Copy the referral_link from step 2, open it in a private window, and sign up with a second plus address. The Waitlist tab should show a referral count of 1 for the first address. If the visitor sees the form but the count stays at 0, the ?ref= parameter is being stripped by a redirect before the script reads it.

4

Read the dashboard, then test one referral. The test row appears in Waitlist and Contacts, and the referred signup credits the first address.

Plan for the Cleanup You Cannot Do

Contacts are append-only. There is no delete route, so every test address stays on your list. That matters on launch day, when you email everyone.

Keep test addresses easy to filter:

  • Always use +test in the local part, or one domain you own such as qa@yourdomain.com.
  • Export the CSV before the launch email and drop every row whose email contains +test.
  • Do not test with a friend's real address. It becomes a real contact you cannot remove.

The export has six columns (email,name,source,referral_code,referrals_count,created_at), so the email column is your only reliable filter.

Write the test address and its timestamp in your launch notes. When the dashboard says 3 signups an hour before you announce, you will know whether those 3 are you.

Run This Again After Every Deploy That Touches the Page

Checks like this rot. A redesign that swaps the <head>, a move from Netlify to Vercel, or a new www redirect can break the script or the origin rule while the page still looks perfect. Re-run steps 1 and 2 after each change. It takes about two minutes, and a silent zero costs a launch.

Frequently Asked Questions

How do I test my waitlist before launch?

Open the live production URL in a normal browser window, sign up with an address you control, and then check the Waitlist tab in the dashboard. Testing from localhost proves nothing about production, because the signup endpoint always allows localhost origins and checks every other origin against your project URL.

Why does my waitlist work on localhost but fail in production?

The API allows any localhost or 127.0.0.1 origin, and compares every other origin against the project URL you saved, scheme and host included. If that URL says https://example.com and your page is served from https://www.example.com, production signups get a 403 Origin not allowed while localhost keeps working.

Can I sign up with the same email twice to test again?

You can, but the second call tests nothing new. A repeat email returns the original referral code and writes no new signup, no new event, and no confirmation email. Use a plus address such as you+test2@gmail.com for each fresh run.

Can I delete my test signups afterward?

Not from the dashboard. Contacts are append-only, with no delete route. Use one recognizable address pattern for tests, such as test@yourdomain.com or +test plus addresses, and filter those rows out of your export before you send the launch email.

How do I know the embed script is actually loading?

Open the browser console on the live page and run window.OperatorStack. If it prints an object with a joinWaitlist function, the script loaded and initialized. If it prints undefined, the script tag is missing, blocked, or placed behind something that never ran.