Waitlist in Next.js
Prerequisites
Section titled “Prerequisites”- Nordva Launch account with a project and publishable API key (
nv_pub_...) - Next.js 13+ (App Router or Pages Router both work)
Get your publishable key
Section titled “Get your publishable key”In the Nordva Launch dashboard, open API Keys and click New publishable key. Name it “Homepage” or similar. Copy the key — it is shown only once.
Add it to your .env.local:
NEXT_PUBLIC_NORDVA_KEY=nv_pub_live_YOUR_KEYThe component
Section titled “The component”Create components/WaitlistForm.tsx:
'use client';
import { useState, FormEvent } from 'react';
export function WaitlistForm() { const [email, setEmail] = useState(''); const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'duplicate' | 'error'>('idle'); const [position, setPosition] = useState<number | null>(null);
async function handleSubmit(e: FormEvent) { e.preventDefault(); setStatus('loading');
const res = await fetch('https://api.nordva.dev/v1/waitlist/signups', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.NEXT_PUBLIC_NORDVA_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ email }), });
if (!res.ok) { setStatus('error'); return; }
// Signup is idempotent — 201 = new, 200 with data.already_registered = duplicate. const { data } = await res.json(); setPosition(data.position); setStatus(data.already_registered ? 'duplicate' : 'success'); }
if (status === 'success') return ( <p>You're #{position} on the list. We'll be in touch.</p> );
if (status === 'duplicate') return ( <p>You're already on the list (#{position}).</p> );
return ( <form onSubmit={handleSubmit}> <input type="email" value={email} required onChange={e => setEmail(e.target.value)} disabled={status === 'loading'} /> <button type="submit" disabled={status === 'loading'}> {status === 'loading' ? 'Joining...' : 'Join waitlist'} </button> {status === 'error' && <p>Something went wrong — please try again.</p>} </form> );}Use it in your page
Section titled “Use it in your page”import { WaitlistForm } from '@/components/WaitlistForm';
export default function Home() { return ( <main> <h1>Coming soon</h1> <WaitlistForm /> </main> );}Zero-code alternative: script-tag widget
Section titled “Zero-code alternative: script-tag widget”Drop this into any HTML page to get a working form with no React needed:
<script src="https://cdn.nordva.dev/v1/waitlist.js" data-key="nv_pub_live_YOUR_KEY" data-placeholder="Enter your email" data-button="Join waitlist" data-success="You're on the list!" data-theme="auto"></script><nordva-waitlist></nordva-waitlist>Where signups came from
Section titled “Where signups came from”Every signup can carry its first-touch source: the referring site, UTM parameters and the page the form was on. The script-tag widget and the hosted page at launch.nordva.dev/waitlist/<slug> collect it automatically. No cookie or browser storage is used, so it needs no consent banner. The values are read once from document.referrer and the page URL.
From your own component, pass it yourself:
const params = new URLSearchParams(window.location.search);const referrer = document.referrer ? new URL(document.referrer).hostname : undefined;
body: JSON.stringify({ email, source: { referrer: referrer !== window.location.hostname ? referrer : undefined, utm_source: params.get('utm_source') ?? undefined, utm_medium: params.get('utm_medium') ?? undefined, utm_campaign: params.get('utm_campaign') ?? undefined, landing_path: window.location.pathname, },}),| Field | Stored as | Notes |
|---|---|---|
source.referrer | source_referrer | A URL or a hostname. Only the hostname is kept (www. removed), because full referrer URLs can carry tokens and personal data |
source.utm_source, utm_medium, utm_campaign | same names | Max 255 characters each |
source.landing_path | landing_path | Path only. Any query string or fragment is dropped |
All of source is optional, and a value that cannot be parsed is stored as empty instead of rejecting the signup. Only the first signup for an email records a source; a repeat signup never overwrites it. The source shows in the dashboard’s Waitlist table and as the last five columns of the CSV export.
It only covers people who signed up through a Nordva Launch form. It is not page analytics.
If you embed the iframe widget from widget.nordva.dev, the iframe cannot see your page’s URL or referrer. Forward the UTM values on the iframe URL (/waitlist/<slug>?utm_source=...) and they are recorded.
Moving an existing list here
Section titled “Moving an existing list here”If you already have a list in another tool, import it. In the dashboard, open Waitlist and choose Import CSV. The file needs an email column; a signup date column (created_at, joined, date) and a source column are picked up when present, and a plain list with one email per line works too.
Through the API (secret key only, at most 1000 rows per call):
curl -X POST https://api.nordva.dev/v1/waitlist/import \ -H "Authorization: Bearer nv_live_YOUR_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{ "consent_confirmed": true, "rows": [ { "email": "[email protected]", "created_at": "2026-03-01T09:00:00Z" }, { "email": "[email protected]", "source": { "utm_source": "old-tool" } } ] }'consent_confirmed must be true: you are confirming these people agreed to hear from you about this product. Imported rows are stored with consent_source: "imported".
- Nobody is emailed by an import.
- A bad row never fails the file. The response reports
inserted,skipped, andskipped_by_reason(invalid_row,duplicate_in_file,already_on_list,disposable_email,plan_limit_reached), plus the first 100 skipped rows with their index. - Running the same file twice is safe. People already on the list are skipped.
created_atkeeps the original signup order, so positions survive the move.- Imports count toward your plan’s signup limit. What does not fit is reported, not dropped silently.
From an MCP client, ask your assistant to import the list; it uses the import_waitlist tool and will ask you to confirm consent first.
What you should see
Section titled “What you should see”On first submit for an email: 201 Created. The form shows You're #1 on the list.
On a repeat submit of the same email: 200 OK with data.already_registered: true (and the existing position). The form shows the duplicate message — signup is idempotent, so duplicates are never an error.
View your signups
Section titled “View your signups”In the Nordva Launch dashboard → Waitlist, or via the API:
curl https://api.nordva.dev/v1/waitlist/signups \ -H "Authorization: Bearer nv_live_YOUR_SECRET_KEY"Common errors
Section titled “Common errors”| Error | Cause | Fix |
|---|---|---|
401 INVALID_API_KEY | Wrong key or key not found | Re-copy key from dashboard |
403 KEY_INSUFFICIENT_PERMISSIONS | Secret key used in browser code | Use nv_pub_... key, not nv_live_... |
| CORS error in console | Wrong endpoint URL | Verify URL is https://api.nordva.dev/v1/waitlist/signups |
422 WAITLIST_PLAN_LIMIT_REACHED | Plan cap hit (Free: 100, Indie: 2,500, Builder: 25,000) | Upgrade plan |