npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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.

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 waitlister

Zero 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 link

API 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 plan

wl.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 visitor

wl.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

MIT © Waitlister