waitlister
v1.3.0
Published
Official TypeScript SDK for Waitlister — waitlist software with hosted landing pages, referral programs, and email automation. Add signups, manage subscribers, verify webhooks, or point any HTML form at your waitlist.
Maintainers
Readme
waitlister
Official TypeScript/JavaScript SDK for Waitlister — waitlist software for product launches, with hosted landing pages, a referral program, and email automation built in.
This is the only official npm package. waitlister-js, waitlister-sdk, waitlister-node, waitlister-api, and @waitlister/sdk are aliases that install this package.
npm install waitlisterZero dependencies. Node 18+, edge runtimes, and Cloudflare Workers. Full TypeScript types, ESM + CJS.
60-second quickstart
You need: a waitlist (create one free at waitlister.me) and its waitlist key, shown at the top of the waitlist's Overview page.
Option A — no API key (works on every plan, including free)
Point any HTML form at your waitlist:
<form action="https://waitlister.me/s/YOUR_WAITLIST_KEY" method="POST">
<input type="email" name="email" required />
<button type="submit">Join the waitlist</button>
</form>Or from code:
import { formAction, formSnippet, signUpViaForm } from 'waitlister'
formAction('YOUR_WAITLIST_KEY') // → "https://waitlister.me/s/YOUR_WAITLIST_KEY"
formSnippet('YOUR_WAITLIST_KEY') // → ready-to-paste HTML form
// Programmatic signup from your server — no API key:
const result = await signUpViaForm('YOUR_WAITLIST_KEY', {
email: '[email protected]',
origin: 'https://yourapp.com' // must be in your waitlist's whitelisted domains
})
result.redirectUrl // subscriber's hosted thank-you page (position + referral link)
// Need position/referral data in code? Use the API client below instead.The form endpoint only accepts submissions from domains you've whitelisted (waitlist → Settings). Browsers send the domain automatically; from a server, pass it as
origin.
Option B — account API key (create waitlists from code, works on every plan)
Get an account key (wl_acct_…) from your account's Settings → API keys. One key for every waitlist you own — no dashboard step needed to go from zero to collecting signups:
import { Waitlister } from 'waitlister'
const wl = new Waitlister({ accountKey: process.env.WAITLISTER_ACCOUNT_KEY })
// Check the key and what it may do — the cheapest first call:
const me = await wl.me()
me.plan.name // 'Free', 'Growth', …
me.waitlists.can_create // whether the next create() would succeed
// Create a waitlist (within your plan's waitlist cap):
const { waitlist, notes } = await wl.waitlists.create({ name: 'My Product' })
waitlist.form_action_url // point any HTML form here — signups work immediately
// Operate on any waitlist you own:
const scope = wl.waitlist(waitlist.key)
await scope.signUp({ email: '[email protected]' })
await scope.stats() // { subscribers_total, views_total, referral_program_enabled }
// List everything you own:
const { waitlists } = await wl.waitlists.list()Creating waitlists, signups, and stats work on every plan (rate limits are lower on free). Subscriber data — subscribers.list/get/update/delete — requires the Growth plan or higher.
Option C — per-waitlist API key (Growth plan or higher)
Get the API key from one waitlist's Settings page; the client is scoped to that waitlist.
import { Waitlister } from 'waitlister'
const wl = new Waitlister({
apiKey: process.env.WAITLISTER_API_KEY, // or set the env var and omit
waitlistKey: process.env.WAITLISTER_WAITLIST_KEY
})
const result = await wl.signUp({ email: '[email protected]', name: 'Ada' })
result.position // queue position
result.referral_code // give this to the user to share
result.redirect_url // hosted thank-you page with their position + referral linkAPI reference
new Waitlister(options)
| Option | Default | Notes |
|---|---|---|
| apiKey | WAITLISTER_API_KEY env var | Growth plan+. Per-waitlist key. |
| accountKey | WAITLISTER_ACCOUNT_KEY env var | wl_acct_…, account Settings → API keys. Works on every waitlist you own; unlocks waitlists.*. Any plan. |
| waitlistKey | WAITLISTER_WAITLIST_KEY env var | Shown on the waitlist Overview page. Optional with an account key — use waitlist(key) instead. |
| baseUrl | https://waitlister.me/api/v1 | |
| timeoutMs | 30000 | |
| maxRetries | 2 | Automatic on 429/502/503/504 + network errors, with backoff. |
| fetch | globalThis.fetch | Inject for testing. |
wl.me() (account key required)
Read-only. Works on every plan. The cheapest way to tell a dead key from a wrong request, and to check whether creating a waitlist would succeed before you try.
const me = await wl.me()| Field | Notes |
|---|---|
| account.id / account.username | Who the key belongs to. |
| plan.name | Display name, e.g. Free, Growth. |
| plan.api_access | Whether the general API is available. false still allows create/list waitlists, sign-up, stats and landing pages with an account key — it rules out subscriber data. |
| waitlists.count | Waitlists the account owns. |
| waitlists.max | The plan's cap: a number, or the string 'unlimited'. |
| waitlists.can_create | Whether waitlists.create() would be allowed right now. Computed server-side by the same helper create gates on, so the two never disagree. |
| ai_credits.remaining | 0 when the plan has no AI access; null if unresolved. |
| api_key | name, created_at, last_used_at. last_used_at reflects the previous call — usage is recorded without blocking the response. |
An invalid or revoked key throws AuthenticationError here with nothing else in flight, which is the point: a dead key stops looking like a malformed request. Calling me() with a per-waitlist key throws locally without a round trip.
The response never includes your API key, its hash, or your email.
wl.waitlists (account key required)
const { waitlist, notes } = await wl.waitlists.create({
name: 'My Product', // required
slug: 'my-product', // optional — derived from name when omitted
description: 'One-liner',
referralRewards: { enabled: true, signupPoints: 50, referralPoints: 30 },
validateEmails: true,
landingPage: { headline: 'Join the beta', theme: 'dark' } // optional — provisions the
// hosted page as a draft in the same request (see wl.landingPage below)
})
// waitlist.key → use with wl.waitlist(key) and in form_action_url
// waitlist.form_action_url → collect signups immediately (whitelist your domain in settings)
// Names/slugs are unique across all of Waitlister: a taken name or slug is
// rejected with a 409 whose message says what to change (pick another name,
// or pass an explicit unique slug).
// Hitting your plan's waitlist cap throws PlanError with the cap in the message.
const { waitlists, total } = await wl.waitlists.list()
// each: { id, key, name, slug, subscribers, views, created_at }wl.waitlist(key)
Scope the client to any waitlist you own (account key), without constructing a new client:
const scope = wl.waitlist('abc123def456')
await scope.signUp({ email: '[email protected]' })
await scope.stats()
await scope.subscribers.list() // Growth+
await scope.analytics.logView({ visitorId: 'v1' })
await scope.landingPage.get() // any planwl.stats()
Cheap social-proof counters, either key type, every plan. Counters update asynchronously (typically within seconds):
const { subscribers_total, views_total, referral_program_enabled } = await wl.stats()wl.signUp(params)
Add a subscriber. Idempotent per email — an existing email returns is_new_sign_up: false with the current position instead of an error. With double opt-in enabled, new signups return is_pending_confirmation: true until the subscriber confirms by email.
await wl.signUp({
email: '[email protected]', // required
name: 'Ada',
phone: '+15551234567',
referredBy: 'referral-code-of-referrer',
clientIp: '203.0.113.7', // end-user IP when proxying: enables fraud detection + geo
fingerprint: 'fp_abc', // browser fingerprint if you collect one
metadata: { role: 'founder', source_campaign: 'launch-week' }
})wl.subscribers
const page = await wl.subscribers.list({ page: 1, limit: 100, sortBy: 'points', sortDir: 'desc' })
// → { subscribers, total, page, limit, pages }
for await (const sub of wl.subscribers.all()) { /* every subscriber, auto-paginated */ }
const sub = await wl.subscribers.get('[email protected]') // by email or id
await wl.subscribers.update('[email protected]', {
points: 250, // triggers queue position recalculation
metadata: { vip: true } // merged into existing metadata
})
await wl.subscribers.delete('[email protected]') // by email or id
// Deletion cascades (email history removed, counters decremented, positions
// compacted) and completes asynchronously — allow a few seconds before the
// subscriber disappears from list() and stats().wl.analytics.logView(params)
Log a landing-page view for waitlist analytics when you host your own page:
await wl.analytics.logView({ visitorId: 'stable-visitor-id' }) // dedupes per visitorwl.landingPage
The waitlist's hosted landing page — works on every plan. One page per waitlist; pages are drafts until published. Provision it together with the waitlist (waitlists.create({ …, landingPage: { headline: '…' } })) or manage it here:
// Create from structured fields (draft; 409 if a page already exists)
await wl.landingPage.create({
headline: 'Join the beta', // required, 3–120 chars, plain text
description: 'Early access.',
buttonText: 'Get early access',
collectName: true, // add a name input to the form
theme: 'dark',
background: { type: 'color', value: '#0F172A' }, // or a CSS gradient string
seo: { title: 'My Product — join the waitlist', ogTitle: '…' }
})
const page = await wl.landingPage.get()
// page.status ('draft' | 'published' | 'unpublished'), page.page_type
// ('standard' | 'ai'), page.hosted_page_url (null until published), analytics…
await wl.landingPage.update({ headline: 'Join 500+ others' })
// Published pages update live (edge cache is purged). Unknown keys → 400
// listing the allowed keys.
const { hosted_page_url } = await wl.landingPage.publish()
// live at https://waitlister.me/p/{slug} — API publishes skip the social-share
// (OG) image; publishing once from the dashboard generates it
await wl.landingPage.unpublish()
// AI builder — same engine as the dashboard. Costs 1 AI credit per call
// (shared balance; throws with status 402 when out). Synchronous, ~20–50s.
const gen = await wl.landingPage.generate({ prompt: 'dark, bold, dev-tool aesthetic' })
gen.credits_remaining
await wl.landingPage.generate({ prompt: 'make the headline shorter', isEdit: true })AI pages vs standard pages: after generate(), the page's copy lives inside the generated page — update() returns 409 for copy/structure fields (headline, description, buttonText, collectName, collectPhone, background); change those with another generate({ isEdit: true }). theme and seo update fine on both page types. generate() is never auto-retried (a retry could consume another credit).
Webhooks
Waitlister signs deliveries with HMAC-SHA256: X-Webhook-Signature: sha256=<hex>. Verify with the raw request body:
import { constructWebhookEvent } from 'waitlister'
// Next.js route handler:
export async function POST(req: Request) {
const rawBody = await req.text()
const event = await constructWebhookEvent({
payload: rawBody,
signature: req.headers.get('x-webhook-signature'),
secret: process.env.WAITLISTER_WEBHOOK_SECRET!
}) // throws on invalid signature
if (event.event === 'waitlist.signup_created') {
event.data.email // …
}
return Response.json({ received: true })
}Errors
Every non-2xx response throws a typed error with an actionable message:
| Class | When |
|---|---|
| ValidationError | 400 — bad input (invalid email, etc.) |
| AuthenticationError | 401 — missing/invalid API key or waitlist key |
| PlanError | 403 — your plan doesn't include API access (free alternative: formAction) |
| NotFoundError | 404 — waitlist or subscriber not found |
| RateLimitError | 429 after retries — has .retryAfter (seconds) |
| ServerError | 5xx |
| ConnectionError | network failure / timeout |
Recipes
Next.js server action (free plan):
'use server'
import { redirect } from 'next/navigation'
import { signUpViaForm } from 'waitlister'
export async function joinWaitlist(formData: FormData) {
const result = await signUpViaForm(process.env.WAITLISTER_WAITLIST_KEY!, {
email: formData.get('email') as string,
origin: 'https://yourapp.com'
})
redirect(result.redirectUrl) // hosted thank-you page with position + referral link
}Referral loop: pass the new subscriber's referral_code into your share links as ?ref=<code>, and send it back as referredBy on signups that arrive with it — Waitlister scores points, moves positions, and detects fraud automatically.
Links
- Waitlister — create a waitlist free, no credit card
- Documentation · API reference · Pricing
MIT © Waitlister
