Collect sign-ups before launch. The panel tells each visitor their place in the order people joined.
Read the article · Open waitlist tool
Sign in, open Waitlist at /wl, name the list and create it. The list page has three tabs: Entries, Waitlist panel and Settings.

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>
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
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" />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}