> ## Documentation Index
> Fetch the complete documentation index at: https://docs.userkit.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Waitlist

> Collect sign-ups before the product opens, and let people in when it does — without a spreadsheet in the middle.

Waitlist mode holds the door: people sign up, nothing mints a session, and you
let them in when you are ready.

The reason it belongs here rather than in a form builder is that the list is
made of **contacts** from its first minute. First-touch attribution, the
anonymous-to-identified merge, the audit trail and the active-contact meter all
already work on contacts — so "convert the waitlist into real accounts" is a
timestamp, not a migration. Nobody ever migrates a spreadsheet into their auth.

<Note>
  It is a per-environment switch, so a launch can be rehearsed in test while live
  still holds the line. It only exists in **hosted** mode: in federated mode your
  own backend decides who exists, and a gate of ours there would be a gate on a
  door we do not own.
</Note>

## Turning it on

In the panel, under **API keys → Identidade dos contatos**, or over the API:

```bash theme={null}
curl -s -X PATCH $API/v1/organization/environments/{id} \
  -H "Authorization: Bearer uk_st_…" \
  -H 'Content-Type: application/json' \
  -d '{ "waitlist_mode": true }'
```

`GET /v1/config/{publishable_key}` starts answering `"waitlist": true`, which is
what lets a sign-up screen change shape without a deploy.

## Collecting

```bash theme={null}
curl -s $API/v1/waitlist \
  -H 'Origin: https://example.com' \
  -H 'Content-Type: application/json' \
  -d '{
    "publishable_key": "uk_pk_live_…",
    "email": "grace@example.com",
    "name": "Grace Hopper",
    "anonymous_id": "anon_2f9c1b",
    "metadata": { "building": "an API for compilers" }
  }'
```

`202`, always — the same posture the rest of the hosted surface holds: this
endpoint never becomes the place somebody learns which addresses are on the
list. A confirmation goes to the inbox, carrying no link and no credential,
because there is nothing yet to click.

Signing up twice keeps the first place in line and the first answers. People
forget they already signed up, and that must not cost them their position.

<Note>
  Pass the `anonymous_id` your page already has (`@userkit/js` does it for you)
  and the entry inherits the visit: the UTM parameters and the referrer that
  brought this person to the landing page are on the contact, and they are still
  there the day they become a paying customer.
</Note>

### With the SDK

`<SignUp />` renders the waitlist form on its own while the mode is on — an
address, no password, no social buttons — because it reads the config rather
than taking it as a prop. **A sign-up form you shipped months before launch
keeps working on launch day.**

```tsx theme={null}
import { SignUp } from "@userkit/react";

// Waitlist today, ordinary sign-up the moment you flip the switch.
<SignUp />
```

Directly, when you have your own form:

```ts theme={null}
await userkit.joinWaitlist({
  email: "grace@example.com",
  metadata: { building: "an API for compilers" },
});
```

### Or through the ordinary sign-up

`POST /v1/contact-auth/signup` joins the line too while the mode is on, and it
answers `{ "waitlist": true }` so a custom form can say so. The password the
person chose **parks on the entry** and attaches when they are let in and their
address is proven — never before. A password on an address nobody confirmed is
not evidence of anything, which is the same reasoning that closes the
pre-created-account takeover.

## The line actually holds

While somebody is waiting, **no route mints a session for them**: not the
password, not a magic link, not an email code, and not "continue with Google".
Every one of them answers `403 waitlisted`.

That is enforced where sessions are minted rather than at each door, for the
same reason two-factor is. A line you can walk around by choosing another
button is a mailing list with extra steps.

<Note>
  Anonymous visitors are never held. The widget mints a visitor session on every
  landing-page load, and a waitlist whose landing page cannot run the widget is a
  waitlist with no attribution — which is most of the reason to keep the list
  here.
</Note>

## Letting people in

```bash theme={null}
# a chosen few
curl -s $API/v1/organization/waitlist/admit?environment=live \
  -H "Authorization: Bearer uk_st_…" \
  -H 'Content-Type: application/json' \
  -d '{ "entry_ids": ["3f9a…"] }'

# or everybody, on the day you open
curl -s $API/v1/organization/waitlist/admit?environment=live \
  -H "Authorization: Bearer uk_st_…" \
  -H 'Content-Type: application/json' \
  -d '{ "all": true }'
```

Admitting is the act that turns an entry into an account somebody can use:

<Steps>
  <Step title="The line stops holding them">
    Every sign-in route works for that person from this moment.
  </Step>

  <Step title="The way in is mailed">
    A single link that proves the address, attaches the password they parked, and
    signs them in. One click from "you are in" to being in — anything longer
    loses the people who waited.
  </Step>
</Steps>

It is idempotent: an entry already admitted is skipped, so a second call mails
nothing.

<Warning>
  **Turning waitlist mode off does not admit anybody.** The two are separate on
  purpose: the people already in line have a password parked on their entry that
  never attached, and flipping the switch alone would leave them discovering that
  at the login screen. Admit everybody first, then turn the mode off — or leave
  it on and admit as you go.
</Warning>

## Reading the queue

```bash theme={null}
curl -s "$API/v1/organization/waitlist?environment=live" \
  -H "Authorization: Bearer uk_st_…"
```

Entries come back in joining order, and that order **is** the position. Reading
takes `customers:read`; admitting takes `customers:write`, because it is what
creates the account.
