← All docs

Waitlist docs

Collect sign-ups before launch. The panel tells each visitor their place in the order people joined.

Read the article · Open waitlist tool

Create a waitlist

Sign in, open Waitlist at /wl, name the list and create it. The list page has three tabs: Entries, Waitlist panel and Settings.

Waitlist page with the create form
Create a waitlist from /wl.

Waitlist panel

The Waitlist panel tab has the snippet to copy and a preview. Visitors enter an email address and an optional name. After joining, the panel shows their place: You're #N on the waitlist. Joining again with the same address doesn't add a second entry.

html<div data-b59-list="<list id>"></div>
<script src="https://b59.link/sdk/b59-lists.js" async></script>
Waitlist panel tab with the snippet and preview
The Waitlist panel tab.

Entries and export

The Entries tab lists every entry with email, name, source (embed or manual), status and date. Export CSV downloads the whole list, including unsubscribed entries.

csvemail,name,source,unsubscribed,created_at
ana@example.com,Ana,embed,no,2026-10-04T09:12:00
Entries tab with emails and Export CSV
Entries, with Export CSV at the top.

React component

For a React site, copy this component into your project. It posts to the same endpoint and needs no package.

tsx// B59Signup.tsx: copy into your React project. No packages needed.
import { useState, type FormEvent } from "react";

const LIST_ID = "<list id>";

export default function B59Signup({ buttonLabel = "Join" }: { buttonLabel?: string }) {
  const [email, setEmail] = useState("");
  const [name, setName] = useState("");
  const [website, setWebsite] = useState(""); // honeypot: people never fill it
  const [message, setMessage] = useState("");
  const [status, setStatus] = useState<"idle" | "busy" | "done" | "error">("idle");

  async function submit(e: FormEvent) {
    e.preventDefault();
    setStatus("busy");
    try {
      const res = await fetch(`https://b59.link/api/lists/public/${LIST_ID}/join`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email, name: name || null, website }),
      });
      const body = await res.json();
      if (!res.ok) throw new Error(body.detail ?? "Something went wrong. Try again.");
      setMessage(body.position ? `${body.message} You're #${body.position} on the waitlist.` : body.message);
      setStatus("done");
    } catch (err) {
      setMessage((err as Error).message);
      setStatus("error");
    }
  }

  if (status === "done") return <p>{message}</p>;

  return (
    <form onSubmit={submit} style={{ maxWidth: 420, display: "grid", gap: 8 }}>
      <input type="email" required value={email} onChange={(e) => setEmail(e.target.value)} placeholder="you@example.com" aria-label="Email" />
      <input value={name} onChange={(e) => setName(e.target.value)} placeholder="Name (optional)" aria-label="Name" />
      <input tabIndex={-1} autoComplete="off" aria-hidden="true" value={website} onChange={(e) => setWebsite(e.target.value)} style={{ position: "absolute", left: -9999, opacity: 0 }} />
      <button type="submit" disabled={status === "busy"}>{status === "busy" ? "Sending…" : buttonLabel}</button>
      {status === "error" && <p style={{ color: "#ef4444" }}>{message}</p>}
    </form>
  );
}

// Use it: <B59Signup buttonLabel="Join the waitlist" />

Public join endpoint

The sign-up panel calls this endpoint. You can call it from your own form too. No login is needed.

httpPOST https://b59.link/api/lists/public/<list id>/join
Content-Type: application/json

{"email": "ana@example.com", "name": "Ana"}

→ 201 {"message": "Thanks! You're on the list.", "position": 1}

Limits and privacy

  • Up to 50 lists per account.
  • Adding emails by hand: up to 5,000 addresses at a time. Duplicates and invalid addresses are reported back, not stored.
  • Lists are private to the account that made them. Only that account can see or export the entries.
  • The sign-up form has a hidden field: bots that fill it are dropped without an error.