Quickstart
SuperWaitlist is API-first. Three steps to your first signup:
Create a waitlist. Define the fields you want to collect (email, name, etc.).
Create an API key. Copy it — you only see it once.
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
| Authorization | Bearer <api-key> |
| Content-Type | application/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 |
|---|---|---|
| text | string ≤ 2000 chars | trimmed string |
| RFC-shaped string | lowercased + trimmed | |
| phone | +, digits, spaces, dashes, parens (6–20 chars) | trimmed string |
| number | number or numeric string | number |
| select | one of the configured options | string |
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 |
|---|---|
| 400 | Body shape is wrong, unknown field, or validation failed |
| 401 | Missing or malformed Authorization header |
| 403 | API key not authorized for this waitlist |
| 404 | Waitlist not found |
| 429 | Rate-limited (60 requests / minute / IP) |