The endpoint
POST the field keys from your form as JSON. Values are coerced the way a browser would send them, so numbers can be strings and checkboxes can be "on". Hidden-by-rule fields are ignored. The response is explicit:
- 201 joined with position, or 200 already_joined with the existing position
- 409 waitlist_full and 423 waitlist_closed, both with reopensAt
- 422 validation_failed with a list of fieldKey, code, and message
- 429 rate_limited with Retry-After
Keys, captcha, and CORS
Browser calls send a Cloudflare Turnstile token. Server calls send a Bearer API key and skip the captcha. CORS is open for the two public endpoints and never uses credentials. A honeypot, a timing check, and per-IP limits stay on regardless.
Webhooks you can verify
Every signup POSTs a JSON payload to your URL with an HMAC-SHA256 signature over a timestamp and the raw body. Non-2xx responses retry six times with exponential backoff, and you can redeliver from the dashboard. The delivery log shows status codes and errors.
Correct under concurrency
Positions and capacity are decided under a database row lock per waitlist. The integration test fires 50 concurrent joins at capacity 10 and gets exactly 10 entries numbered 1 to 10. Per-day, per-week, and per-month capacity use the same lock, with a time zone and reset time you choose.
Render it yourself
GET the form definition and theme as JSON and build a native form in React, Swift, or a terminal. Or point a plain HTML form at the endpoint and let it redirect back with the result. The hosted widget stays available for the pages where you do not want to write anything.