Waitlist guides
03 · Public form

You render the form. We take the signup.

Waitlist is API-only — there is no hosted page and no embed script. Two anonymous endpoints: read the waitlist, post an entry.


1

Read the waitlist (optional)

Fetch copy and branding at render time so marketing can change the headline without a redeploy. Anonymous — no token, no CORS credentials.

GET /v1/waitlists/:workspace_slug/:waitlist_slug (public)
curl 'https://api.waitlist.productcraft.co/v1/waitlists/acme/acme-pro?lang=pt-BR'
200 OK
{
  "slug":                     "acme-pro",
  "workspace_slug":           "acme",
  "canonical_workspace_slug": "acme",
  "display_name":             "Acme Pro early access",
  "description":              "Sign up to be among the first to try Acme Pro.",
  "status":                   "active",
  "custom_fields":            [],
  "confirmation_message":     "You're on the list. We'll be in touch.",
  "recaptcha_site_key":       null,
  "referral_rewards":         { "milestones": [] },
  "variant":                  null,
  "brand": {
    "brand_name":     "Acme",
    "brand_logo_url": "https://cdn.acme.com/logo.png",
    "primary_color":  "#1E40AF"
  }
}

404 means “not found or not active” — a draft, paused or closed waitlist looks identical to a missing one from the outside.

?lang= (BCP-47) beats the Accept-Language header when picking which variant's copy to serve. When a variant is active, variant carries { id, locale, kind } — echo that id back as variant_id on submit so the entry is attributed. See chapter 5.

canonical_workspace_slug is there so a link built against a renamed workspace slug can 301 itself: compare it to the slug in your URL.


2

Submit an entry

POST /v1/waitlists/:workspace_slug/:waitlist_slug/entries (public)
curl https://api.waitlist.productcraft.co/v1/waitlists/acme/acme-pro/entries \
  -H 'content-type: application/json' \
  -d '{
    "email":         "ada@example.com",
    "name":          "Ada Lovelace",
    "interest":      "The realtime collaboration bits",
    "referrer":      "Twitter",
    "referral_code": "C67AB5HS",
    "metadata":      { "company": "Acme Industries", "role": "engineer" }
  }'

email and name are both required. Everything else is optional: interest (free text), referrer (how they heard about you), referral_code (another entry's code — credits that entry), variant_id, recaptcha_token, and metadata — a free-form object for whatever else your form collects. There is no server-side field schema to register first; put your extra inputs in metadata and they come back on the entry.

Response:

201 Created
{
  "id":                 "150489b4-558d-4c2a-93b2-dec1b657558c",
  "email":              "ada@example.com",
  "name":               "Ada Lovelace",
  "position":           147,
  "status":             "approved",
  "referral_code":      "C67AB5HS",
  "created_at":         "2026-07-28T20:09:16.626Z",
  "effective_position": 147
}

referral_code is this signup's code to share. Waitlist doesn't host a page, so build the share link yourself against your own form URL — https://acme.com/join?ref=C67AB5HS — and pass the value straight back as referral_code on the next submission.

status is approved on a waitlist that doesn't require manual review, otherwise pending. position is the immutable join order — it is never renumbered, including when earlier entries are deleted. Render position; effective_position is a forward-compatible mirror of it.


3

Errors to render

  • 400 — validation failed (missing name, malformed email), a disposable-email domain, or a rejected reCAPTCHA token. The body's message is an array of per-field messages you can map onto inputs.
  • 404 — waitlist not found or not active.
  • 409 This email is already on this waitlist — the friendliest handling is “you're already in”, since positions are never disclosed anonymously for an existing email.
  • 422 Waitlist is not accepting entries — the list is paused, closed or full.
  • 429 — per-IP, per-waitlist rate limit. The message carries the retry window in seconds.

4

Leaderboard

Anonymous top-referrer ranking — the “skip the line” mechanic from chapter 5. Same access rule as the public info endpoint (active waitlists only).

GET /v1/waitlists/:workspace_slug/:waitlist_slug/leaderboard (public)
curl 'https://api.waitlist.productcraft.co/v1/waitlists/acme/acme-pro/leaderboard?limit=10'
200 OK
[
  { "position": 1, "referral_count": 42, "masked_email": "a***@example.com" },
  { "position": 2, "referral_count": 31, "masked_email": "b***@acme.org" }
]

A bare array, not an envelope. Emails are masked and only entries with at least one referral appear. limit is 1–50, default 50. Unmasked referrer data is admin-only, on GET .../analytics.