Skip to content

Waitlist in Next.js

  • Nordva Launch account with a project and publishable API key (nv_pub_...)
  • Next.js 13+ (App Router or Pages Router both work)

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_KEY

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&apos;re #{position} on the list. We&apos;ll be in touch.</p>
);
if (status === 'duplicate') return (
<p>You&apos;re already on the list (#{position}).</p>
);
return (
<form onSubmit={handleSubmit}>
<input
type="email" value={email} required
onChange={e => setEmail(e.target.value)}
placeholder="your@email.com"
disabled={status === 'loading'}
/>
<button type="submit" disabled={status === 'loading'}>
{status === 'loading' ? 'Joining...' : 'Join waitlist'}
</button>
{status === 'error' && <p>Something went wrongplease try again.</p>}
</form>
);
}
app/page.tsx
import { WaitlistForm } from '@/components/WaitlistForm';
export default function Home() {
return (
<main>
<h1>Coming soon</h1>
<WaitlistForm />
</main>
);
}

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>

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,
},
}),
FieldStored asNotes
source.referrersource_referrerA 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_campaignsame namesMax 255 characters each
source.landing_pathlanding_pathPath 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.

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):

Terminal window
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": "ada@example.com", "created_at": "2026-03-01T09:00:00Z" },
{ "email": "lin@example.com", "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, and skipped_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_at keeps 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.

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.

In the Nordva Launch dashboardWaitlist, or via the API:

Terminal window
curl https://api.nordva.dev/v1/waitlist/signups \
-H "Authorization: Bearer nv_live_YOUR_SECRET_KEY"
ErrorCauseFix
401 INVALID_API_KEYWrong key or key not foundRe-copy key from dashboard
403 KEY_INSUFFICIENT_PERMISSIONSSecret key used in browser codeUse nv_pub_... key, not nv_live_...
CORS error in consoleWrong endpoint URLVerify URL is https://api.nordva.dev/v1/waitlist/signups
422 WAITLIST_PLAN_LIMIT_REACHEDPlan cap hit (Free: 100, Indie: 2,500, Builder: 25,000)Upgrade plan