Zum Hauptinhalt springen

Waitlist Public API

The Waitlist Public API provides two unauthenticated endpoints consumed by the Selgeo marketing landing page. These endpoints run in a dedicated service (apps/public-api, port 4002) and are separate from the main Selgeo API (apps/api, port 4000).

hinweis

These endpoints do not require an API key, bearer token, or any form of authentication. They are rate-limited per IP address and protected by a self-hosted honeypot + time-to-submit anti-spam check (no third-party service).

Base URL

Production: https://app.selgeo.com

Staging: https://staging.selgeo.com

Routes are served under the /api/public/ path prefix.

Endpoints

POST /api/public/waitlist

Add an email address to the pre-launch waitlist.

The submitted email is normalised (lowercased and trimmed) before persistence. The response body is intentionally empty and byte-identical for both a new signup and a duplicate submission — callers cannot distinguish between the two (privacy by design, per FR-009).

The request must include two anti-spam fields alongside the email: a hidden website honeypot field (the landing page renders it hidden so humans never fill it; naive bots do) and an elapsedMs counter measuring milliseconds between form mount and submit. The server rejects requests where the honeypot is non-empty or elapsedMs is outside [1500, 1_800_000].

Request body:

{
"email": "alice@example.com",
"website": "",
"elapsedMs": 2000
}
FieldTypeRequiredDescription
emailstringYesEmail address. Normalised to lowercase. Max 320 characters.
websitestringNo (default "")Honeypot. MUST be empty. Any non-empty value is rejected.
elapsedMsintegerYesMilliseconds between form mount and submit. Must be in [1500, 1_800_000].

Responses:

StatusDescription
201 CreatedEmpty body. Returned for both new signups and duplicates.
400 Bad RequestValidation failure (INVALID_EMAIL) or anti-spam rejection (ANTI_SPAM_FAILED).
429 Too Many RequestsRate limit exceeded. Includes Retry-After header (seconds).

Error codes:

CodeDescription
INVALID_EMAILEmail is missing or not a valid email format.
ANTI_SPAM_FAILEDHoneypot (website) was non-empty, or elapsedMs was missing / out of range.
RATE_LIMITEDIP address exceeded the per-window rate limit.

Example (curl):

curl -sS -D- -X POST https://app.selgeo.com/api/public/waitlist \
-H 'Content-Type: application/json' \
-d '{"email": "alice@example.com", "website": "", "elapsedMs": 2000}'

GET /api/public/waitlist/waitlist-count

Get the displayed waitlist counter for the landing page.

Returns the number of signups taken and the configured total cap. The counter is cached in Redis (default 45-second TTL) so this endpoint is safe to call on every page load. The Cache-Control: public, max-age=30 response header allows CDN and browser caching.

The taken value includes an optional WAITLIST_BASE offset configured by the marketing team — it does not reflect the exact number of database rows.

Response body:

{
"taken": 27,
"total": 50
}
FieldTypeDescription
takenintegerDisplayed number of spots taken (DB count + WAITLIST_BASE, clamped to total).
totalintegerConfigured total cap (WAITLIST_TOTAL).

Example (curl):

curl -sS https://app.selgeo.com/api/public/waitlist/waitlist-count

Privacy

  • Raw email addresses are never logged. Structured log events carry an email_hash (HMAC-SHA256 with IP_HASH_PEPPER) instead. Client IPs are similarly logged as ip_hash.
  • Deduplication uses a unique constraint on the normalised email column — the plaintext email is stored in the database (not the hash) so the operator CLI can service GDPR Art. 15/17 requests by email.
  • Email addresses are retained for 12 months from signup date (FR-024). Rows older than 12 months are purged by a daily cleanup job.
  • GDPR Art. 15 (access) and Art. 17 (erasure) requests are handled via the operator CLI (pnpm --filter @affiliate/public-api ops:access / ops:erasure).

OpenAPI Specification

The full OpenAPI 3.1 specification for this service is available at /public-api-openapi.yaml. You can import this file into Postman, Insomnia, or any OpenAPI-compatible client.