[{"data":1,"prerenderedAt":655},["ShallowReactive",2],{"blog-how-to\u002Ftest-waitlist-before-launch":3},{"id":4,"title":5,"body":6,"category":624,"date":625,"dateModified":625,"description":626,"draft":627,"extension":628,"faq":629,"featured":627,"keywords":638,"meta":639,"navigation":640,"ogDescription":641,"ogTitle":642,"path":643,"readTime":644,"schemaType":645,"seo":646,"sitemap":647,"stem":648,"tags":649,"twitterCard":653,"__hash__":654},"blog\u002Fblog\u002Fhow-to\u002Ftest-waitlist-before-launch.md","Test Your Waitlist Before Launch: A Fake Signup Checklist",{"type":7,"value":8,"toc":614},"minimark",[9,22,25,31,36,43,75,81,87,148,167,171,174,184,203,206,214,221,239,243,246,249,257,260,266,284,287,293,296,347,366,370,376,391,394,400,413,428,432,435,457,460,470,479,483,486,489,509,516,522,526,537,590],[10,11,12,13,17,18,21],"p",{},"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 ",[14,15,16],"code",{},"https:\u002F\u002Fexample.com"," on a site that redirected to ",[14,19,20],{},"https:\u002F\u002Fwww.example.com",", so every browser signup got a 403 and the form showed a generic error that nobody screenshotted.",[10,23,24],{},"A silent zero looks exactly like no demand. That is why you put one fake signup through the real page before anyone else does.",[26,27,28],"tldr",{},[10,29,30],{},"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.",[32,33,35],"h2",{"id":34},"why-localhost-passes-and-production-fails","Why Localhost Passes and Production Fails",[10,37,38,39,42],{},"When a browser sends a signup, the API reads the ",[14,40,41],{},"Origin"," header and applies three rules in order:",[44,45,46,57,68],"ol",{},[47,48,49,50,52,53,56],"li",{},"No ",[14,51,41],{}," header at all: allowed. This is what ",[14,54,55],{},"curl"," sends by default.",[47,58,59,60,63,64,67],{},"Hostname is ",[14,61,62],{},"localhost"," or ",[14,65,66],{},"127.0.0.1",": allowed, whatever port.",[47,69,70,71,74],{},"Anything else: compared to the project URL, scheme plus host, character for character. A mismatch returns ",[14,72,73],{},"403 Origin not allowed",".",[10,76,77,78,80],{},"Rule 1 and rule 2 are the trap. A local dev server passes. A raw ",[14,79,55],{}," passes. Only a real browser on the real domain exercises rule 3, which is the only rule your visitors will ever hit.",[10,82,83,84,86],{},"These all fail rule 3 when the saved URL is ",[14,85,16],{},":",[88,89,90,103],"table",{},[91,92,93],"thead",{},[94,95,96,100],"tr",{},[97,98,99],"th",{},"Page is served from",[97,101,102],{},"Result",[104,105,106,116,126,135],"tbody",{},[94,107,108,113],{},[109,110,111],"td",{},[14,112,20],{},[109,114,115],{},"403, different host",[94,117,118,123],{},[109,119,120],{},[14,121,122],{},"http:\u002F\u002Fexample.com",[109,124,125],{},"403, different scheme",[94,127,128,133],{},[109,129,130],{},[14,131,132],{},"https:\u002F\u002Fstaging.example.com",[109,134,115],{},[94,136,137,142],{},[109,138,139],{},[14,140,141],{},"file:\u002F\u002F\u002FUsers\u002Fdan\u002Findex.html",[109,143,144,145],{},"403, browsers send ",[14,146,147],{},"Origin: null",[149,150,151],"warning-box",{},[10,152,153,154,156,157,159,160,162,163,166],{},"A passing ",[14,155,55],{}," test is the most misleading result in this whole post. ",[14,158,55],{}," sends no ",[14,161,41],{}," header, so it skips the check entirely. Add ",[14,164,165],{},"-H \"Origin: https:\u002F\u002Fyourdomain.com\""," or skip curl and use a browser.",[32,168,170],{"id":169},"step-1-confirm-the-script-loaded","Step 1: Confirm the Script Loaded",[10,172,173],{},"Open your live page in a normal window. Open the console and run:",[175,176,182],"pre",{"className":177,"code":179,"language":180,"meta":181},[178],"language-javascript","window.OperatorStack;\n","javascript","",[14,183,179],{"__ignoreMap":181},[10,185,186,187,190,191,194,195,198,199,202],{},"You want an object with ",[14,188,189],{},"joinWaitlist",", ",[14,192,193],{},"getShareLinks",", and ",[14,196,197],{},"isSignedUp"," on it. If you see ",[14,200,201],{},"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.",[10,204,205],{},"Also check the tag itself matches the project you are looking at in the dashboard:",[175,207,212],{"className":208,"code":210,"language":211,"meta":181},[209],"language-html","\u003Cscript src=\"https:\u002F\u002Foperatorstack.dev\u002Fos.js\" data-project=\"pk_abc123\">\u003C\u002Fscript>\n","html",[14,213,210],{"__ignoreMap":181},[10,215,216,217,220],{},"A ",[14,218,219],{},"data-project"," key from a different project means signups succeed and land in the wrong dashboard. That looks identical to a silent zero.",[222,223,225],"step",{"number":224},"1",[10,226,227,231,232,235,236,238],{},[228,229,230],"strong",{},"Check the object, then the key."," ",[14,233,234],{},"window.OperatorStack"," is defined, and ",[14,237,219],{}," matches the key shown on this project's page.",[32,240,242],{"id":241},"step-2-submit-a-real-signup-with-a-test-address","Step 2: Submit a Real Signup With a Test Address",[10,244,245],{},"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.",[10,247,248],{},"Pick an address you control and can recognize later. A plus address works well on Gmail and most Google Workspace domains:",[175,250,255],{"className":251,"code":253,"language":254},[252],"language-text","you+test1@gmail.com\n","text",[14,256,253],{"__ignoreMap":181},[10,258,259],{},"After you submit, run this in the console to confirm the browser remembered the signup:",[175,261,264],{"className":262,"code":263,"language":180,"meta":181},[178],"window.OperatorStack.isSignedUp(); \u002F\u002F true\n",[14,265,263],{"__ignoreMap":181},[10,267,268,271,272,275,276,279,280,283],{},[14,269,270],{},"isSignedUp()"," reads a ",[14,273,274],{},"localStorage"," entry named ",[14,277,278],{},"os_signup_"," plus your project key, written only after the API returned a success. A ",[14,281,282],{},"true"," here means the request completed, not just that the button was clicked.",[10,285,286],{},"If you built your own form, call the SDK and read the real error:",[175,288,291],{"className":289,"code":290,"language":180,"meta":181},[178],"try {\n  const result = await OperatorStack.joinWaitlist({\n    email: \"you+test1@gmail.com\",\n    name: \"Test\",\n  });\n  console.log(result.referral_code, result.referral_link);\n} catch (err) {\n  console.error(err.status, err.message);\n}\n",[14,292,290],{"__ignoreMap":181},[10,294,295],{},"The status tells you which rule broke:",[88,297,298,308],{},[91,299,300],{},[94,301,302,305],{},[97,303,304],{},"Status",[97,306,307],{},"Meaning",[104,309,310,320,328,339],{},[94,311,312,315],{},[109,313,314],{},"404",[109,316,317,319],{},[14,318,219],{}," key does not exist",[94,321,322,325],{},[109,323,324],{},"403",[109,326,327],{},"Origin does not match the saved project URL",[94,329,330,333],{},[109,331,332],{},"422",[109,334,335,336],{},"Email failed validation, usually a stray space or ",[14,337,338],{},"@@",[94,340,341,344],{},[109,342,343],{},"429",[109,345,346],{},"Rate limited, from submitting repeatedly while testing",[222,348,350],{"number":349},"2",[10,351,352,355,356,358,359,361,362,365],{},[228,353,354],{},"Sign up through the form on the live URL."," You get a success state and ",[14,357,270],{}," returns ",[14,360,282],{},". If you built a custom form, log ",[14,363,364],{},"err.status"," so a failure names its cause.",[32,367,369],{"id":368},"step-3-test-the-duplicate-rule-on-purpose","Step 3: Test the Duplicate Rule on Purpose",[10,371,372,373,375],{},"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 ",[14,374,278],{}," entry.",[10,377,378,379,382,383,386,387,390],{},"The API finds the existing row for that email and returns the original ",[14,380,381],{},"referral_code"," and ",[14,384,385],{},"referral_link",". It skips the rest: no new signup row, no ",[14,388,389],{},"waitlist_signup"," event, no notification, no confirmation email.",[10,392,393],{},"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.",[175,395,398],{"className":396,"code":397,"language":180,"meta":181},[178],"const a = await OperatorStack.joinWaitlist({ email: \"you+test1@gmail.com\" });\nconst b = await OperatorStack.joinWaitlist({ email: \"you+test1@gmail.com\" });\nconsole.log(a.referral_code === b.referral_code); \u002F\u002F true\n",[14,399,397],{"__ignoreMap":181},[10,401,402,403,406,407,412],{},"Case is not normalized, so ",[14,404,405],{},"You+Test1@gmail.com"," counts as a different contact. The ",[408,409,411],"a",{"href":410},"\u002Fblog\u002Fhow-to\u002Fclean-contact-list-before-launch","clean-contact-list post"," covers what that does to your real list.",[222,414,416],{"number":415},"3",[10,417,418,421,422,190,425,74],{},[228,419,420],{},"Submit the same email twice."," Both calls return one referral code. For every fresh end-to-end run afterward, bump the plus address: ",[14,423,424],{},"+test2",[14,426,427],{},"+test3",[32,429,431],{"id":430},"step-4-find-the-row-in-the-dashboard","Step 4: Find the Row in the Dashboard",[10,433,434],{},"Open the project dashboard and look in two places:",[436,437,438,444],"ul",{},[47,439,440,443],{},[228,441,442],{},"Waitlist tab:"," your test address with a timestamp and a position number.",[47,445,446,449,450,453,454,74],{},[228,447,448],{},"Contacts:"," the same email with ",[14,451,452],{},"source"," set to ",[14,455,456],{},"waitlist",[10,458,459],{},"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.",[10,461,462,463,465,466,469],{},"Then test the referral path once. Copy the ",[14,464,385],{}," 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 ",[14,467,468],{},"?ref="," parameter is being stripped by a redirect before the script reads it.",[222,471,473],{"number":472},"4",[10,474,475,478],{},[228,476,477],{},"Read the dashboard, then test one referral."," The test row appears in Waitlist and Contacts, and the referred signup credits the first address.",[32,480,482],{"id":481},"plan-for-the-cleanup-you-cannot-do","Plan for the Cleanup You Cannot Do",[10,484,485],{},"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.",[10,487,488],{},"Keep test addresses easy to filter:",[436,490,491,501,506],{},[47,492,493,494,497,498,74],{},"Always use ",[14,495,496],{},"+test"," in the local part, or one domain you own such as ",[14,499,500],{},"qa@yourdomain.com",[47,502,503,504,74],{},"Export the CSV before the launch email and drop every row whose email contains ",[14,505,496],{},[47,507,508],{},"Do not test with a friend's real address. It becomes a real contact you cannot remove.",[10,510,511,512,515],{},"The export has six columns (",[14,513,514],{},"email,name,source,referral_code,referrals_count,created_at","), so the email column is your only reliable filter.",[517,518,519],"tip-box",{},[10,520,521],{},"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.",[32,523,525],{"id":524},"run-this-again-after-every-deploy-that-touches-the-page","Run This Again After Every Deploy That Touches the Page",[10,527,528,529,532,533,536],{},"Checks like this rot. A redesign that swaps the ",[14,530,531],{},"\u003Chead>",", a move from Netlify to Vercel, or a new ",[14,534,535],{},"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.",[538,539,540,547,562,573,584],"faq-section",{},[541,542,544],"faq-item",{"question":543},"How do I test my waitlist before launch?",[10,545,546],{},"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.",[541,548,550],{"question":549},"Why does my waitlist work on localhost but fail in production?",[10,551,552,553,557,558,561],{},"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 ",[408,554,16],{"href":16,"rel":555},[556],"nofollow"," and your page is served from ",[408,559,20],{"href":20,"rel":560},[556],", production signups get a 403 Origin not allowed while localhost keeps working.",[541,563,565],{"question":564},"Can I sign up with the same email twice to test again?",[10,566,567,568,572],{},"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 ",[408,569,571],{"href":570},"mailto:you+test2@gmail.com","you+test2@gmail.com"," for each fresh run.",[541,574,576],{"question":575},"Can I delete my test signups afterward?",[10,577,578,579,583],{},"Not from the dashboard. Contacts are append-only, with no delete route. Use one recognizable address pattern for tests, such as ",[408,580,582],{"href":581},"mailto:test@yourdomain.com","test@yourdomain.com"," or +test plus addresses, and filter those rows out of your export before you send the launch email.",[541,585,587],{"question":586},"How do I know the embed script is actually loading?",[10,588,589],{},"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.",[591,592,593,594],"content-related-articles",{},"\n  ",[595,596,593,600],"contentrelatedcard",{"href":597,"title":598,"description":599},"\u002Fblog\u002Fhow-to\u002Fset-up-waitlist","How to Set Up a SaaS Waitlist in Under 5 Minutes","The setup this checklist verifies: one script tag, one widget div, and the dashboard views to check afterward.",[595,601,593,604],{"href":410,"title":602,"description":603},"Clean Your Email List Before Launch: 6 Checks to Run","Test signups cannot be deleted, so here is how to filter them and the other bad rows out of the export before you send.",[595,605,609],{"href":606,"title":607,"description":608},"\u002Fblog\u002Fbest-practices\u002Fpre-launch-checklist","Pre-Launch Checklist: Validate Your SaaS Idea in 2 Weeks","Where a working-signup test sits in the full two-week plan before you start sending traffic.",[610,611],"cta-box",{"href":612,"label":613},"\u002F","Get Started Free",{"title":181,"searchDepth":615,"depth":615,"links":616},2,[617,618,619,620,621,622,623],{"id":34,"depth":615,"text":35},{"id":169,"depth":615,"text":170},{"id":241,"depth":615,"text":242},{"id":368,"depth":615,"text":369},{"id":430,"depth":615,"text":431},{"id":481,"depth":615,"text":482},{"id":524,"depth":615,"text":525},"how-to","2026-10-30","Put one fake signup through your waitlist before real traffic arrives. Four checks catch a silent zero: origin, the saved record, the duplicate rule, and the dashboard.",false,"md",[630,631,633,635,637],{"question":543,"answer":546},{"question":549,"answer":632},"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:\u002F\u002Fexample.com and your page is served from https:\u002F\u002Fwww.example.com, production signups get a 403 Origin not allowed while localhost keeps working.",{"question":564,"answer":634},"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.",{"question":575,"answer":636},"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.",{"question":586,"answer":589},"test waitlist before launch,test waitlist signup,waitlist not collecting signups,waitlist origin not allowed,verify waitlist works",{},true,"A silent zero is the worst launch bug. Run one fake signup through the real production page and read the four places it should show up.","Test Your Waitlist Before Launch","\u002Fblog\u002Fhow-to\u002Ftest-waitlist-before-launch","7 min","HowTo",{"title":5,"description":626},{"loc":643},"blog\u002Fhow-to\u002Ftest-waitlist-before-launch",[456,650,651,652],"testing","launch","embed","summary_large_image","66M00vYoOKGD1Sistx88xH0j2vlBlu3ebyegb_N3g2A",1793355877724]