[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"guide-\u002Fgrowth-hub\u002Fguides\u002Fadd-waitlist-to-nextjs":3},{"_path":4,"title":5,"description":6,"navigation":7,"image":14,"head":17,"body":39},"\u002Fgrowth-hub\u002Fguides\u002Fadd-waitlist-to-nextjs","Add a Waitlist to a Next.js App: 3 Working Ways · Waitlister","Copy-paste guide to adding a waitlist signup to Next.js or React: a no-backend HTML form, a server action with the waitlister SDK, or a REST route handler. All verified code.",{"title":8,"thumbnailSrc":9,"order":10,"description":11,"date":12,"timeToRead":13},"Add a Waitlist to a Next.js App (3 Ways, ~5 Minutes)","https:\u002F\u002Ffirebasestorage.googleapis.com\u002Fv0\u002Fb\u002Fwaitinglist-app-c24fc.appspot.com\u002Fo\u002Fresources%2Fguides%2Fguide_add-waitlist-to-nextjs.webp?alt=media",993,"Three working ways to add a waitlist signup to a Next.js (or any React) app: a plain form action with no backend, a server action with the SDK, or a REST route handler.","05 Jul 2026",6,{"src":9,"width":15,"height":16},400,300,{"meta":18,"og":31,"twitter":35},[19,22,25,28],{"name":20,"content":21},"keywords","nextjs waitlist, next.js waitlist page, add waitlist to website, react waitlist component, waitlist api, waitlist form nextjs, waitlist signup react, waitlister sdk",{"name":23,"content":24},"robots","index, follow",{"name":26,"content":27},"author","Waitlister",{"name":29,"content":30},"copyright","© 2026 Waitlister",{"title":32,"description":33,"type":34,"image":9},"Add a Waitlist to a Next.js App: 3 Working Ways","A no-backend HTML form, a server action with the SDK, or a REST route handler — copy-paste code for each, verified against the live API.","article",{"card":36,"title":37,"description":38,"image":9},"summary_large_image","Add a Waitlist to Next.js in ~5 Minutes","Three verified patterns: plain form action, server action + SDK, or REST route handler.",{"type":40,"children":41,"toc":567},"root",[42,50,58,65,79,122,136,142,147,160,195,200,206,211,222,235,244,255,260,297,303,316,325,355,361,405,411,418,439,445,450,456,484,490,517,523],{"type":43,"tag":44,"props":45,"children":47},"element","h1",{"id":46},"add-a-waitlist-to-a-nextjs-app-3-ways-5-minutes",[48],{"type":49,"value":8},"text",{"type":43,"tag":51,"props":52,"children":53},"p",{},[54],{"type":43,"tag":55,"props":56,"children":57},"img",{"alt":8,"src":9},[],{"type":43,"tag":59,"props":60,"children":62},"h2",{"id":61},"the-short-answer",[63],{"type":49,"value":64},"The short answer",{"type":43,"tag":51,"props":66,"children":67},{},[68,70,77],{"type":49,"value":69},"The fastest way to add a waitlist to a Next.js app is to point a plain HTML form at a hosted waitlist endpoint; no backend code at all. If you want the signup handled server-side (custom UI, position shown in your own page), use a server action or route handler with the ",{"type":43,"tag":71,"props":72,"children":74},"code",{"className":73},[],[75],{"type":49,"value":76},"waitlister",{"type":49,"value":78}," SDK. All three patterns below are copy-paste and were verified against the live API.",{"type":43,"tag":51,"props":80,"children":81},{},[82,88,90,95,97,105,107,113,115,120],{"type":43,"tag":83,"props":84,"children":85},"strong",{},[86],{"type":49,"value":87},"Prerequisite for all three:",{"type":49,"value":89}," a waitlist and its ",{"type":43,"tag":83,"props":91,"children":92},{},[93],{"type":49,"value":94},"waitlist key",{"type":49,"value":96},". Create one free at ",{"type":43,"tag":98,"props":99,"children":103},"a",{"href":100,"rel":101},"https:\u002F\u002Fwaitlister.me",[102],"nofollow",[104],{"type":49,"value":27},{"type":49,"value":106}," (the key is on your waitlist's Overview page — ",{"type":43,"tag":98,"props":108,"children":110},{"href":109},"\u002Fdocs\u002Fquickstart",[111],{"type":49,"value":112},"quickstart here",{"type":49,"value":114},"). Options 2 and 3 also need an ",{"type":43,"tag":83,"props":116,"children":117},{},[118],{"type":49,"value":119},"API key",{"type":49,"value":121}," (waitlist Settings; API access needs the Growth plan). Option 1 works on every plan.",{"type":43,"tag":51,"props":123,"children":124},{},[125,127,134],{"type":49,"value":126},"Building this with an AI coding agent? Hand it ",{"type":43,"tag":98,"props":128,"children":131},{"href":129,"rel":130},"https:\u002F\u002Fwaitlister.me\u002Fskill.md",[102],[132],{"type":49,"value":133},"waitlister.me\u002Fskill.md",{"type":49,"value":135},"; it contains everything on this page in one fetchable file.",{"type":43,"tag":59,"props":137,"children":139},{"id":138},"option-1-plain-form-action-no-backend-any-plan",[140],{"type":49,"value":141},"Option 1 — plain form action, no backend (any plan)",{"type":43,"tag":51,"props":143,"children":144},{},[145],{"type":49,"value":146},"Point a form straight at the waitlist's form-action endpoint. Works in Next.js, plain React, or raw HTML; the subscriber is redirected to a hosted thank-you page showing their queue position and referral link.",{"type":43,"tag":148,"props":149,"children":155},"pre",{"className":150,"code":152,"language":153,"meta":154},[151],"language-tsx","\u002F\u002F app\u002Fpage.tsx — works as-is in a server or client component\nexport default function Home() {\n  return (\n    \u003Cform action=\"https:\u002F\u002Fwaitlister.me\u002Fs\u002FYOUR_WAITLIST_KEY\" method=\"POST\">\n      \u003Cinput type=\"email\" name=\"email\" placeholder=\"you@example.com\" required \u002F>\n      \u003Cbutton type=\"submit\">Join the waitlist\u003C\u002Fbutton>\n    \u003C\u002Fform>\n  )\n}\n","tsx","",[156],{"type":43,"tag":71,"props":157,"children":158},{"__ignoreMap":154},[159],{"type":49,"value":152},{"type":43,"tag":51,"props":161,"children":162},{},[163,165,170,172,178,180,186,187,193],{"type":49,"value":164},"One setup step: add your site's domain to the waitlist's ",{"type":43,"tag":83,"props":166,"children":167},{},[168],{"type":49,"value":169},"whitelisted domains",{"type":49,"value":171}," (waitlist → Settings), or submissions return 403. Optional fields: ",{"type":43,"tag":71,"props":173,"children":175},{"className":174},[],[176],{"type":49,"value":177},"name",{"type":49,"value":179},", ",{"type":43,"tag":71,"props":181,"children":183},{"className":182},[],[184],{"type":49,"value":185},"phone",{"type":49,"value":179},{"type":43,"tag":71,"props":188,"children":190},{"className":189},[],[191],{"type":49,"value":192},"referred_by",{"type":49,"value":194},"; any other input name is stored as metadata.",{"type":43,"tag":51,"props":196,"children":197},{},[198],{"type":49,"value":199},"Use this when you want zero server code and the hosted thank-you page is fine.",{"type":43,"tag":59,"props":201,"children":203},{"id":202},"option-2-server-action-sdk-app-router",[204],{"type":49,"value":205},"Option 2 — server action + SDK (App Router)",{"type":43,"tag":51,"props":207,"children":208},{},[209],{"type":49,"value":210},"Keep users on your page and control the whole experience. Install the SDK:",{"type":43,"tag":148,"props":212,"children":217},{"className":213,"code":215,"language":216,"meta":154},[214],"language-bash","npm install waitlister\n","bash",[218],{"type":43,"tag":71,"props":219,"children":220},{"__ignoreMap":154},[221],{"type":49,"value":215},{"type":43,"tag":51,"props":223,"children":224},{},[225,227,233],{"type":49,"value":226},"Set your keys in ",{"type":43,"tag":71,"props":228,"children":230},{"className":229},[],[231],{"type":49,"value":232},".env.local",{"type":49,"value":234}," (the SDK reads these names automatically):",{"type":43,"tag":148,"props":236,"children":239},{"className":237,"code":238,"language":216,"meta":154},[214],"WAITLISTER_API_KEY=your-api-key\nWAITLISTER_WAITLIST_KEY=your-waitlist-key\n",[240],{"type":43,"tag":71,"props":241,"children":242},{"__ignoreMap":154},[243],{"type":49,"value":238},{"type":43,"tag":148,"props":245,"children":250},{"className":246,"code":248,"language":249,"meta":154},[247],"language-ts","\u002F\u002F app\u002Factions.ts\n'use server'\nimport { Waitlister } from 'waitlister'\n\nconst wl = new Waitlister() \u002F\u002F picks up the env vars\n\nexport async function joinWaitlist(formData: FormData) {\n  const result = await wl.signUp({\n    email: formData.get('email') as string,\n    name: (formData.get('name') as string) || undefined\n  })\n  return {\n    position: result.position,\n    referralCode: result.referral_code\n  }\n}\n","ts",[251],{"type":43,"tag":71,"props":252,"children":253},{"__ignoreMap":154},[254],{"type":49,"value":248},{"type":43,"tag":51,"props":256,"children":257},{},[258],{"type":49,"value":259},"Wire it to a client component and show the position on success. Two behaviors worth knowing: signups are idempotent per email (re-submitting returns the existing position, not an error), and the API key never reaches the browser because the action runs server-side.",{"type":43,"tag":51,"props":261,"children":262},{},[263,265,271,273,279,281,287,289,295],{"type":49,"value":264},"Plan notes: ",{"type":43,"tag":71,"props":266,"children":268},{"className":267},[],[269],{"type":49,"value":270},"wl.signUp",{"type":49,"value":272}," needs API access (Growth plan). On a plan without it, the SDK throws a typed ",{"type":43,"tag":71,"props":274,"children":276},{"className":275},[],[277],{"type":49,"value":278},"PlanError",{"type":49,"value":280}," rather than failing silently. On the free plan you can keep the exact same server-action pattern with ",{"type":43,"tag":71,"props":282,"children":284},{"className":283},[],[285],{"type":49,"value":286},"signUpViaForm",{"type":49,"value":288}," from the same package — no API key, and it returns the hosted thank-you page URL to ",{"type":43,"tag":71,"props":290,"children":292},{"className":291},[],[293],{"type":49,"value":294},"redirect()",{"type":49,"value":296}," to instead of position data.",{"type":43,"tag":59,"props":298,"children":300},{"id":299},"option-3-rest-route-handler-no-sdk-or-pages-router",[301],{"type":49,"value":302},"Option 3 — REST route handler (no SDK, or Pages Router)",{"type":43,"tag":51,"props":304,"children":305},{},[306,308,314],{"type":49,"value":307},"The same thing with a route handler and plain ",{"type":43,"tag":71,"props":309,"children":311},{"className":310},[],[312],{"type":49,"value":313},"fetch",{"type":49,"value":315},", if you'd rather not add a dependency:",{"type":43,"tag":148,"props":317,"children":320},{"className":318,"code":319,"language":249,"meta":154},[247],"\u002F\u002F app\u002Fapi\u002Fwaitlist\u002Froute.ts\nexport async function POST(req: Request) {\n  const { email, name } = await req.json()\n\n  const res = await fetch(\n    `https:\u002F\u002Fwaitlister.me\u002Fapi\u002Fv1\u002Fwaitlist\u002F${process.env.WAITLISTER_WAITLIST_KEY}\u002Fsign-up`,\n    {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application\u002Fjson',\n        'X-Api-Key': process.env.WAITLISTER_API_KEY!\n      },\n      body: JSON.stringify({ email, name })\n    }\n  )\n\n  const data = await res.json()\n  return Response.json(data, { status: res.status })\n}\n",[321],{"type":43,"tag":71,"props":322,"children":323},{"__ignoreMap":154},[324],{"type":49,"value":319},{"type":43,"tag":51,"props":326,"children":327},{},[328,330,336,338,345,347,353],{"type":49,"value":329},"Auth is the ",{"type":43,"tag":71,"props":331,"children":333},{"className":332},[],[334],{"type":49,"value":335},"X-Api-Key",{"type":49,"value":337}," header (not a Bearer token). The full request\u002Fresponse schema, including error codes, is in the machine-readable ",{"type":43,"tag":98,"props":339,"children":342},{"href":340,"rel":341},"https:\u002F\u002Fwaitlister.me\u002Fopenapi.json",[102],[343],{"type":49,"value":344},"OpenAPI spec",{"type":49,"value":346},"; the human version is in the ",{"type":43,"tag":98,"props":348,"children":350},{"href":349},"\u002Fdocs\u002Fapi",[351],{"type":49,"value":352},"API reference",{"type":49,"value":354},".",{"type":43,"tag":59,"props":356,"children":358},{"id":357},"verify-it-works",[359],{"type":49,"value":360},"Verify it works",{"type":43,"tag":362,"props":363,"children":364},"ol",{},[365,371,392],{"type":43,"tag":366,"props":367,"children":368},"li",{},[369],{"type":49,"value":370},"Submit a real email address. Deliverability is validated at signup, so fake test addresses get rejected with a 400.",{"type":43,"tag":366,"props":372,"children":373},{},[374,376,382,384,390],{"type":49,"value":375},"Expect ",{"type":43,"tag":71,"props":377,"children":379},{"className":378},[],[380],{"type":49,"value":381},"success: true",{"type":49,"value":383}," with a ",{"type":43,"tag":71,"props":385,"children":387},{"className":386},[],[388],{"type":49,"value":389},"position",{"type":49,"value":391}," (options 2–3) or a redirect to the thank-you page (option 1).",{"type":43,"tag":366,"props":393,"children":394},{},[395,397,403],{"type":49,"value":396},"Submit the same email again: you should get ",{"type":43,"tag":71,"props":398,"children":400},{"className":399},[],[401],{"type":49,"value":402},"is_new_sign_up: false",{"type":49,"value":404}," with the same position. That confirms the signup persisted.",{"type":43,"tag":59,"props":406,"children":408},{"id":407},"faq",[409],{"type":49,"value":410},"FAQ",{"type":43,"tag":412,"props":413,"children":415},"h3",{"id":414},"can-i-add-a-waitlist-to-nextjs-without-a-backend",[416],{"type":49,"value":417},"Can I add a waitlist to Next.js without a backend?",{"type":43,"tag":51,"props":419,"children":420},{},[421,423,429,431,437],{"type":49,"value":422},"Yes. Point any form's ",{"type":43,"tag":71,"props":424,"children":426},{"className":425},[],[427],{"type":49,"value":428},"action",{"type":49,"value":430}," at ",{"type":43,"tag":71,"props":432,"children":434},{"className":433},[],[435],{"type":49,"value":436},"https:\u002F\u002Fwaitlister.me\u002Fs\u002F{your-waitlist-key}",{"type":49,"value":438}," (option 1). No API key, no server code; it works on static exports and every plan, as long as your domain is whitelisted in the waitlist settings.",{"type":43,"tag":412,"props":440,"children":442},{"id":441},"does-this-work-with-plain-react-or-vite-apps",[443],{"type":49,"value":444},"Does this work with plain React or Vite apps?",{"type":43,"tag":51,"props":446,"children":447},{},[448],{"type":49,"value":449},"Options 1 and 3 do, unchanged: option 1 is plain HTML, and option 3's route handler can live in any backend. Option 2's server action is Next.js App Router-specific.",{"type":43,"tag":412,"props":451,"children":453},{"id":452},"how-do-subscribers-see-their-queue-position",[454],{"type":49,"value":455},"How do subscribers see their queue position?",{"type":43,"tag":51,"props":457,"children":458},{},[459,461,466,468,474,476,482],{"type":49,"value":460},"Option 1 redirects them to a hosted thank-you page with their position and referral link. Options 2–3 return ",{"type":43,"tag":71,"props":462,"children":464},{"className":463},[],[465],{"type":49,"value":389},{"type":49,"value":467}," and ",{"type":43,"tag":71,"props":469,"children":471},{"className":470},[],[472],{"type":49,"value":473},"referral_code",{"type":49,"value":475}," in the response, so you render it however you want — or use the returned ",{"type":43,"tag":71,"props":477,"children":479},{"className":478},[],[480],{"type":49,"value":481},"redirect_url",{"type":49,"value":483}," to send them to the hosted thank-you page.",{"type":43,"tag":412,"props":485,"children":487},{"id":486},"how-do-referrals-work-in-a-custom-integration",[488],{"type":49,"value":489},"How do referrals work in a custom integration?",{"type":43,"tag":51,"props":491,"children":492},{},[493,495,500,502,508,510,516],{"type":49,"value":494},"Every signup gets a ",{"type":43,"tag":71,"props":496,"children":498},{"className":497},[],[499],{"type":49,"value":473},{"type":49,"value":501},". Put it in your share links as ",{"type":43,"tag":71,"props":503,"children":505},{"className":504},[],[506],{"type":49,"value":507},"?ref=\u003Ccode>",{"type":49,"value":509},", then pass incoming codes back as referredBy (SDK), metadata.referred_by (REST), or referred_by\u002Fref (form fields). Points, position changes, and fraud detection are handled for you. Details: ",{"type":43,"tag":98,"props":511,"children":513},{"href":512},"\u002Ffeatures\u002Freferral-program",[514],{"type":49,"value":515},"referral program",{"type":49,"value":354},{"type":43,"tag":59,"props":518,"children":520},{"id":519},"helpful-resources",[521],{"type":49,"value":522},"Helpful resources",{"type":43,"tag":524,"props":525,"children":526},"ul",{},[527,548,557],{"type":43,"tag":366,"props":528,"children":529},{},[530,535,537,541,542],{"type":43,"tag":98,"props":531,"children":532},{"href":109},[533],{"type":49,"value":534},"Quickstart",{"type":49,"value":536}," · ",{"type":43,"tag":98,"props":538,"children":539},{"href":349},[540],{"type":49,"value":352},{"type":49,"value":536},{"type":43,"tag":98,"props":543,"children":545},{"href":544},"\u002Fdocs\u002Fform-action-endpoint",[546],{"type":49,"value":547},"Form action endpoint",{"type":43,"tag":366,"props":549,"children":550},{},[551],{"type":43,"tag":98,"props":552,"children":554},{"href":553},"\u002Fintegrations\u002Freact",[555],{"type":49,"value":556},"React integration",{"type":43,"tag":366,"props":558,"children":559},{},[560],{"type":43,"tag":98,"props":561,"children":564},{"href":562,"rel":563},"https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fwaitlister",[102],[565],{"type":49,"value":566},"waitlister on npm",{"title":154,"searchDepth":568,"depth":568,"links":569},3,[570,572,573,574,575,576,582],{"id":61,"depth":571,"text":64},2,{"id":138,"depth":571,"text":141},{"id":202,"depth":571,"text":205},{"id":299,"depth":571,"text":302},{"id":357,"depth":571,"text":360},{"id":407,"depth":571,"text":410,"children":577},[578,579,580,581],{"id":414,"depth":568,"text":417},{"id":441,"depth":568,"text":444},{"id":452,"depth":568,"text":455},{"id":486,"depth":568,"text":489},{"id":519,"depth":571,"text":522}]