← All docs

Newsletter docs

Gather subscribers for a launch, write issues in an HTML editor, and queue each campaign for every active subscriber.

Read the article · Open newsletter tool

Subscribe panel

The Subscribe panel tab has the same snippet as the waitlist, with the button labelled Subscribe.

html<div data-b59-list="<list id>"></div>
<script src="https://b59.link/sdk/b59-lists.js" async></script>

Newsletter editor

Newsletters tab, then New newsletter. Fill in a title (only you see it), the subject line and the HTML body. The preview renders the HTML with scripts switched off. Starter template gives a simple layout. Save keeps the draft.

Scripts are rejected on save. The HTML is limited to 500 KB.

html<h1>We launched</h1>
<p>Thanks for waiting.</p>
<p><a href="https://example.com">Try it now</a></p>
Newsletter editor with HTML and preview
The HTML editor with a live preview.

Send as campaign

Send as campaign… asks for confirmation, then queues one email per active subscriber. Unsubscribed people are skipped. Each email gets a footer with the company line (Biz59 LLC, New York, NY) and a unique unsubscribe link. The footer goes inside </body> when your HTML has one.

Queued emails show as pending in the Campaigns table, then as sent or failed once the email service has handled them.

Newsletters tab with saved issues and the campaigns table
Saved issues and the campaigns table.

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.