Docs

Everything you need to wire SuperWaitlist into your stack.

Quickstart

SuperWaitlist is API-first. Three steps to your first signup:

1

Create a waitlist. Define the fields you want to collect (email, name, etc.).

2

Create an API key. Copy it — you only see it once.

3

Submit a signup. Each waitlist has its own URL — find it on the waitlist's detail page.

curl -X POST https://api.superwaitlist.me/w/<PUBLIC_ID> \
  -H "Authorization: Bearer sw_pub_..." \
  -H "Content-Type: application/json" \
  -d '{"email":"alice@example.com","name":"Alice"}'

Submit a signup

POST /w/:publicId — adds a row to the waitlist. Idempotent on the request level (we don't deduplicate signups — duplicate emails are allowed unless you handle it client-side).

Headers

AuthorizationBearer <api-key>
Content-Typeapplication/json

Body

A flat JSON object. Keys must match the waitlist's configured field names (case-sensitive). Unknown keys return 400.

{
  "email": "alice@example.com",
  "name": "Alice",
  "company": "Acme"
}

Response

{ "ok": true }

We always return 200 even when the honeypot trips — bots see no signal that they've been filtered.

Field types

Each waitlist defines its own set of fields. The backend coerces and validates per type before saving.

Type Accepts Stored as
textstring ≤ 2000 charstrimmed string
emailRFC-shaped stringlowercased + trimmed
phone+, digits, spaces, dashes, parens (6–20 chars)trimmed string
numbernumber or numeric stringnumber
selectone of the configured optionsstring

Required fields throw 400 if missing or empty. Optional fields are omitted from data if not supplied.

Welcome emails

Each waitlist can fire a welcome email on every signup. Configure on the waitlist's Email tab. Two routing modes:

  • Built-in Mail — sends from noreply@superwaitlist.me. Free tier gets 10 sends/mo; Pro gets 1,000; Scale gets 10,000.
  • Your own provider — connect Resend / SendGrid / SMTP under Email Providers. Pro and Scale only.

Template variables

Use {{field_name}} in the subject, from-name, from-email, and body. Variables resolve against the submission data. Missing values render as empty strings (so the email never breaks).

Subject: Welcome, {{name}}!

Body:
Hi {{name}},
Thanks for joining the {{company}} waitlist.

Notifications

Get pinged when signups arrive. Connect channels under Notifications, then route per-waitlist on the waitlist's Notifications tab.

Channels

  • Telegram — bot token + chat_id. Setup via @BotFather and @userinfobot.
  • Discord — channel webhook URL.
  • Slack — workspace webhook URL.
  • Webhook — generic POST to any URL. See payload format below.

Frequency

Per waitlist you set everyNSubmissions. Default 1 (ping on every signup). Set to 10 to get one ping per 10 signups — useful when traffic is heavy. The counter is atomic and resets on fire, so you won't miss or double-fire under concurrent submissions.

Webhook payload

For the Webhook channel type, we POST this JSON to your URL:

{
  "title": "🎉 New signup on \"Founders waitlist\"",
  "fields": [
    { "label": "email", "value": "alice@example.com" },
    { "label": "name",  "value": "Alice" }
  ],
  "footer": "Total signups: 47",
  "waitlist": {
    "name": "Founders waitlist",
    "publicId": "sw_pub_..."
  },
  "submission": {
    "id": "65b...",
    "data": { "email": "alice@example.com", "name": "Alice" },
    "createdAt": "2026-05-09T10:23:00.000Z"
  },
  "totalSignups": 47
}

title, fields, and footer are pre-formatted for chat-style consumers. Use submission.data for the raw structured fields.

HMAC verification

If you set a signing secret when connecting a Webhook channel, every fire includes a header:

X-SuperWaitlist-Signature: sha256=<hex>

The hex is HMAC-SHA256 over the raw request body, keyed with your secret. Compute the same on your side and compare in constant time.

Node example

import crypto from 'crypto'

function verify(rawBody: Buffer, header: string, secret: string) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex')
  return crypto.timingSafeEqual(
    Buffer.from(header),
    Buffer.from(expected),
  )
}

Use the raw bytes of the body, before any JSON parsing. If your framework already parsed it, re-stringify exactly — easiest is to keep the raw buffer.

Errors

Errors come back as JSON with a message field. Status codes follow standard HTTP semantics:

Status Means
400Body shape is wrong, unknown field, or validation failed
401Missing or malformed Authorization header
403API key not authorized for this waitlist
404Waitlist not found
429Rate-limited (60 requests / minute / IP)