{
  "openapi": "3.0.3",
  "info": {
    "title": "Waitlister API",
    "version": "1.0.0",
    "description": "REST API for [Waitlister](https://waitlister.me) — waitlist software with hosted landing pages, a referral program, and email automation.\n\n## Getting started\n1. Create a waitlist (free) at https://waitlister.me — your **waitlist key** is shown on the waitlist's Overview page.\n2. For the authenticated API, get your **API key** from the waitlist's Settings page. API access requires the **Growth plan or higher**.\n3. No API access? The public **form-action endpoint** (`POST /s/{waitlistKey}`) collects signups on every plan with no API key.\n\n## Official SDK\n`npm install waitlister` — typed client for every endpoint here, plus the free form-action path and webhook signature verification.\n\n## Rate limits\nPer API key, per minute: subscriber endpoints 60 RPM (Growth) / 120 RPM (Business); log-view 200 / 400 RPM. Every response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` (Unix timestamp) headers.\n\n## Webhooks\nWaitlister can POST signup events to your endpoint, signed with HMAC-SHA256 over the raw body in the `X-Webhook-Signature` header (`sha256=<hex>`). See https://waitlister.me/docs/webhooks.",
    "contact": {
      "name": "Waitlister Support",
      "url": "https://waitlister.me/support",
      "email": "support@waitlister.me"
    },
    "license": {
      "name": "Proprietary",
      "url": "https://waitlister.me"
    }
  },
  "externalDocs": {
    "description": "Full documentation",
    "url": "https://waitlister.me/docs/api"
  },
  "servers": [
    {
      "url": "https://waitlister.me",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Subscribers",
      "description": "Add and manage waitlist subscribers (API key required, Growth plan+)."
    },
    {
      "name": "Analytics",
      "description": "Log landing-page views for waitlist analytics (API key required, Growth plan+)."
    },
    {
      "name": "Public forms",
      "description": "No-authentication signup endpoint for HTML forms and no-code tools. Works on every plan. The submitting domain must be whitelisted in the waitlist's settings."
    }
  ],
  "paths": {
    "/api/v1/waitlist/{waitlistKey}/sign-up": {
      "post": {
        "tags": ["Subscribers"],
        "operationId": "signUp",
        "summary": "Add a subscriber",
        "description": "Adds a subscriber to the waitlist. **Idempotent per email**: signing up an existing email returns `is_new_sign_up: false` with the subscriber's current position (HTTP 200, not an error). When the waitlist has double opt-in enabled, new signups are pending (`is_pending_confirmation: true`, no position) until the subscriber confirms by email. Reserved `metadata` keys `referred_by`, `client_ip`, `fingerprint`, and `referring_domain` drive the referral program, fraud detection, and analytics; all other metadata keys are stored as custom fields.",
        "security": [{ "ApiKeyAuth": [] }],
        "parameters": [{ "$ref": "#/components/parameters/WaitlistKey" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/SignUpRequest" },
              "example": {
                "email": "user@example.com",
                "name": "Ada Lovelace",
                "metadata": {
                  "referred_by": "happy-star-1a2b",
                  "client_ip": "203.0.113.7",
                  "role": "founder"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Signup processed (new, already-registered, or pending confirmation).",
            "headers": {
              "X-RateLimit-Limit": { "$ref": "#/components/headers/X-RateLimit-Limit" },
              "X-RateLimit-Remaining": { "$ref": "#/components/headers/X-RateLimit-Remaining" },
              "X-RateLimit-Reset": { "$ref": "#/components/headers/X-RateLimit-Reset" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SignUpResult" },
                "example": {
                  "success": true,
                  "is_new_sign_up": true,
                  "is_pending_confirmation": false,
                  "message": "Successfully signed up",
                  "position": 42,
                  "inflated_position": 142,
                  "points": 50,
                  "referral_code": "happy-star-1a2b",
                  "sign_up_token": "tok_abc123",
                  "redirect_url": "https://waitlister.me/thank-you/{waitlistKey}/tok_abc123"
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/PlanForbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/api/v1/waitlist/{waitlistKey}/subscribers": {
      "get": {
        "tags": ["Subscribers"],
        "operationId": "listSubscribers",
        "summary": "List subscribers",
        "description": "Paginated list of the waitlist's subscribers.",
        "security": [{ "ApiKeyAuth": [] }],
        "parameters": [
          { "$ref": "#/components/parameters/WaitlistKey" },
          {
            "name": "page",
            "in": "query",
            "description": "1-based page number.",
            "schema": { "type": "integer", "minimum": 1, "default": 1 }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Page size (max 100).",
            "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 }
          },
          {
            "name": "sort_by",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": ["position", "points", "date", "referral_count", "email"],
              "default": "date"
            }
          },
          {
            "name": "sort_dir",
            "in": "query",
            "schema": { "type": "string", "enum": ["asc", "desc"], "default": "desc" }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of subscribers.",
            "headers": {
              "X-RateLimit-Limit": { "$ref": "#/components/headers/X-RateLimit-Limit" },
              "X-RateLimit-Remaining": { "$ref": "#/components/headers/X-RateLimit-Remaining" },
              "X-RateLimit-Reset": { "$ref": "#/components/headers/X-RateLimit-Reset" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SubscriberListResponse" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/PlanForbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/api/v1/waitlist/{waitlistKey}/subscribers/{subscriberIdOrEmail}": {
      "get": {
        "tags": ["Subscribers"],
        "operationId": "getSubscriber",
        "summary": "Get a subscriber",
        "description": "Fetch one subscriber by document id or email address.",
        "security": [{ "ApiKeyAuth": [] }],
        "parameters": [
          { "$ref": "#/components/parameters/WaitlistKey" },
          { "$ref": "#/components/parameters/SubscriberIdOrEmail" }
        ],
        "responses": {
          "200": {
            "description": "The subscriber.",
            "headers": {
              "X-RateLimit-Limit": { "$ref": "#/components/headers/X-RateLimit-Limit" },
              "X-RateLimit-Remaining": { "$ref": "#/components/headers/X-RateLimit-Remaining" },
              "X-RateLimit-Reset": { "$ref": "#/components/headers/X-RateLimit-Reset" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SubscriberResponse" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/PlanForbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      },
      "put": {
        "tags": ["Subscribers"],
        "operationId": "updateSubscriber",
        "summary": "Update a subscriber",
        "description": "Update a subscriber's points, name, phone, or metadata. At least one field is required. `metadata` is **merged** into existing metadata, not replaced. Updating `points` triggers a queue-position recalculation for the waitlist.",
        "security": [{ "ApiKeyAuth": [] }],
        "parameters": [
          { "$ref": "#/components/parameters/WaitlistKey" },
          { "$ref": "#/components/parameters/SubscriberIdOrEmail" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/UpdateSubscriberRequest" },
              "example": { "points": 500, "metadata": { "vip": true } }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated subscriber (reduced shape).",
            "headers": {
              "X-RateLimit-Limit": { "$ref": "#/components/headers/X-RateLimit-Limit" },
              "X-RateLimit-Remaining": { "$ref": "#/components/headers/X-RateLimit-Remaining" },
              "X-RateLimit-Reset": { "$ref": "#/components/headers/X-RateLimit-Reset" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/UpdateSubscriberResponse" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/PlanForbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/api/v1/waitlist/{waitlistKey}/log-view": {
      "post": {
        "tags": ["Analytics"],
        "operationId": "logView",
        "summary": "Log a page view",
        "description": "Log a landing-page view for waitlist analytics (use when you host your own page). Pass a stable `visitor_id` to dedupe: a repeat view for the same visitor returns HTTP 200 with `success: false`.",
        "security": [{ "ApiKeyAuth": [] }],
        "parameters": [{ "$ref": "#/components/parameters/WaitlistKey" }],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/LogViewRequest" },
              "example": { "visitor_id": "vis_8f2k1", "metadata": { "page": "/landing-b" } }
            }
          }
        },
        "responses": {
          "200": {
            "description": "View logged (or duplicate detected: `success: false`).",
            "headers": {
              "X-RateLimit-Limit": { "$ref": "#/components/headers/X-RateLimit-Limit" },
              "X-RateLimit-Remaining": { "$ref": "#/components/headers/X-RateLimit-Remaining" },
              "X-RateLimit-Reset": { "$ref": "#/components/headers/X-RateLimit-Reset" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/LogViewResult" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/PlanForbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/s/{waitlistKey}": {
      "post": {
        "tags": ["Public forms"],
        "operationId": "formSignUp",
        "summary": "Form-action signup (no API key, every plan)",
        "description": "Public signup endpoint for HTML forms and no-code tools (Webflow, Framer, Wix…). **No API key** — works on every plan including free. Requirements: the submitting site's domain must be in the waitlist's **whitelisted domains** (checked via the `Referer`/`Origin` header; browsers send it automatically, server-side callers must set it), and per-IP rate limits apply.\n\nAccepts `application/json` or form-encoded bodies. The fields `email`, `name`, `phone` (or `phone_number`), `referred_by` (or `ref`), and `fingerprint` are recognized; any other fields are stored as subscriber metadata.\n\n**Response depends on request Content-Type**: JSON requests get a JSON result; form-encoded requests get a `302` redirect to the subscriber's thank-you page.",
        "security": [],
        "parameters": [{ "$ref": "#/components/parameters/WaitlistKey" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/FormSignUpRequest" },
              "example": { "email": "user@example.com", "name": "Ada", "ref": "happy-star-1a2b" }
            },
            "application/x-www-form-urlencoded": {
              "schema": { "$ref": "#/components/schemas/FormSignUpRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON requests: signup processed.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/FormSignUpResult" },
                "example": {
                  "success": true,
                  "message": "Successfully joined waitlist",
                  "redirectUrl": "https://waitlister.me/thank-you/{waitlistKey}/tok_abc123"
                }
              }
            }
          },
          "302": {
            "description": "Form-encoded requests: redirect to the subscriber's thank-you page (or confirm-pending page when double opt-in is enabled)."
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "403": {
            "description": "The submitting domain is not in the waitlist's whitelisted domains.",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Api-Key",
        "description": "Per-waitlist API key from the waitlist's Settings page. Requires the Growth plan or higher."
      }
    },
    "parameters": {
      "WaitlistKey": {
        "name": "waitlistKey",
        "in": "path",
        "required": true,
        "description": "The waitlist key shown on the waitlist's Overview page.",
        "schema": { "type": "string" }
      },
      "SubscriberIdOrEmail": {
        "name": "subscriberIdOrEmail",
        "in": "path",
        "required": true,
        "description": "Subscriber document id, or their email address (URL-encoded).",
        "schema": { "type": "string" }
      }
    },
    "headers": {
      "X-RateLimit-Limit": {
        "description": "Maximum requests allowed per minute for this endpoint and plan.",
        "schema": { "type": "integer" }
      },
      "X-RateLimit-Remaining": {
        "description": "Requests remaining in the current window.",
        "schema": { "type": "integer" }
      },
      "X-RateLimit-Reset": {
        "description": "Unix timestamp when the current window resets.",
        "schema": { "type": "integer" }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Invalid input (e.g. missing or undeliverable email, invalid field types).",
        "content": {
          "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } }
        }
      },
      "Unauthorized": {
        "description": "Missing or invalid API key / waitlist key.",
        "content": {
          "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } }
        }
      },
      "PlanForbidden": {
        "description": "API access requires the Growth or Business plan. Free alternative: the public form-action endpoint `POST /s/{waitlistKey}`.",
        "content": {
          "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } }
        }
      },
      "NotFound": {
        "description": "Waitlist or subscriber not found.",
        "content": {
          "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } }
        }
      },
      "RateLimited": {
        "description": "Rate limit exceeded — check the X-RateLimit-* headers and retry after the reset. Signup fraud protection may also return 429 with a `data.retry_after` field (seconds).",
        "content": {
          "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } }
        }
      },
      "ServerError": {
        "description": "Internal server error.",
        "content": {
          "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } }
        }
      }
    },
    "schemas": {
      "SignUpRequest": {
        "type": "object",
        "required": ["email"],
        "properties": {
          "email": { "type": "string", "format": "email" },
          "name": { "type": "string", "nullable": true },
          "phone": { "type": "string", "nullable": true },
          "metadata": {
            "type": "object",
            "additionalProperties": true,
            "description": "Reserved keys: `referred_by` (referrer's referral code), `client_ip` (end-user IPv4 when proxying — enables fraud detection + geo enrichment), `fingerprint` (browser fingerprint), `referring_domain` (analytics override). All other keys are stored as custom fields on the subscriber."
          }
        }
      },
      "SignUpResult": {
        "type": "object",
        "required": ["success", "is_new_sign_up", "is_pending_confirmation", "message"],
        "properties": {
          "success": { "type": "boolean" },
          "is_new_sign_up": { "type": "boolean" },
          "is_pending_confirmation": {
            "type": "boolean",
            "description": "True when double opt-in is enabled and the subscriber has not yet confirmed."
          },
          "message": { "type": "string" },
          "position": {
            "type": "integer",
            "nullable": true,
            "description": "Real queue position. Absent while a double opt-in confirmation is pending."
          },
          "inflated_position": {
            "type": "integer",
            "nullable": true,
            "description": "Position plus the waitlist's configured position inflation."
          },
          "points": {
            "type": "integer",
            "nullable": true,
            "description": "Referral points granted at signup (new signups only)."
          },
          "referral_code": { "type": "string", "nullable": true },
          "sign_up_token": { "type": "string", "nullable": true },
          "redirect_url": {
            "type": "string",
            "description": "The subscriber's hosted thank-you page (or confirm-pending page)."
          }
        }
      },
      "Subscriber": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "email": { "type": "string", "nullable": true },
          "deliverability": {
            "type": "string",
            "nullable": true,
            "description": "Email deliverability status, e.g. deliverable | probably_deliverable | unconfirmed."
          },
          "name": { "type": "string", "nullable": true },
          "phone": { "type": "string", "nullable": true },
          "position": { "type": "integer", "nullable": true },
          "inflated_position": { "type": "integer" },
          "points": { "type": "integer" },
          "referral_code": { "type": "string", "nullable": true },
          "referred_by": { "type": "string", "nullable": true },
          "referral_count": { "type": "integer" },
          "sign_up_token": { "type": "string", "nullable": true },
          "thank_you_url": { "type": "string", "nullable": true },
          "metadata": { "type": "object", "additionalProperties": true },
          "referring_domain": { "type": "string", "nullable": true },
          "ip_address": { "type": "string", "nullable": true },
          "country": { "type": "string", "nullable": true },
          "city": { "type": "string", "nullable": true },
          "timezone": { "type": "string", "nullable": true },
          "joined_with": {
            "type": "string",
            "nullable": true,
            "description": "Signup source, e.g. api | landing_page | form."
          },
          "joined_at": {
            "type": "integer",
            "nullable": true,
            "description": "Unix timestamp in milliseconds."
          }
        }
      },
      "SubscriberListResponse": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean" },
          "data": {
            "type": "object",
            "properties": {
              "subscribers": {
                "type": "array",
                "items": { "$ref": "#/components/schemas/Subscriber" }
              },
              "total": { "type": "integer" },
              "page": { "type": "integer" },
              "limit": { "type": "integer" },
              "pages": { "type": "integer" }
            }
          }
        }
      },
      "SubscriberResponse": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean" },
          "data": {
            "type": "object",
            "properties": {
              "subscriber": { "$ref": "#/components/schemas/Subscriber" }
            }
          }
        }
      },
      "UpdateSubscriberRequest": {
        "type": "object",
        "minProperties": 1,
        "properties": {
          "points": {
            "type": "number",
            "description": "Setting points triggers a queue-position recalculation."
          },
          "name": { "type": "string" },
          "phone": { "type": "string" },
          "metadata": {
            "type": "object",
            "additionalProperties": true,
            "description": "Merged into existing metadata (does not replace it)."
          }
        }
      },
      "UpdatedSubscriber": {
        "type": "object",
        "description": "Reduced subscriber shape returned by the update endpoint.",
        "properties": {
          "id": { "type": "string" },
          "email": { "type": "string", "nullable": true },
          "name": { "type": "string", "nullable": true },
          "phone": { "type": "string", "nullable": true },
          "points": { "type": "integer" },
          "sign_up_token": { "type": "string", "nullable": true },
          "thank_you_url": { "type": "string", "nullable": true },
          "updated_at": { "type": "integer", "nullable": true },
          "metadata": { "type": "object", "additionalProperties": true }
        }
      },
      "UpdateSubscriberResponse": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean" },
          "message": { "type": "string" },
          "data": {
            "type": "object",
            "properties": {
              "subscriber": { "$ref": "#/components/schemas/UpdatedSubscriber" }
            }
          }
        }
      },
      "LogViewRequest": {
        "type": "object",
        "properties": {
          "visitor_id": {
            "type": "string",
            "description": "Stable visitor id used to dedupe repeat views."
          },
          "metadata": { "type": "object", "additionalProperties": true }
        }
      },
      "LogViewResult": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "False when the view was already logged for this visitor_id."
          },
          "message": { "type": "string" }
        }
      },
      "FormSignUpRequest": {
        "type": "object",
        "required": ["email"],
        "properties": {
          "email": { "type": "string", "format": "email" },
          "name": { "type": "string" },
          "phone": { "type": "string" },
          "referred_by": {
            "type": "string",
            "description": "Referrer's referral code. `ref` is accepted as an alias."
          },
          "fingerprint": { "type": "string" }
        },
        "additionalProperties": {
          "description": "Any other fields are stored as subscriber metadata."
        }
      },
      "FormSignUpResult": {
        "type": "object",
        "required": ["success", "message", "redirectUrl"],
        "properties": {
          "success": { "type": "boolean" },
          "message": { "type": "string" },
          "redirectUrl": {
            "type": "string",
            "description": "The subscriber's hosted thank-you page (or confirm-pending page when double opt-in is on) — shows their position and referral link."
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "properties": {
          "error": { "type": "boolean" },
          "url": { "type": "string" },
          "statusCode": { "type": "integer" },
          "statusMessage": { "type": "string" },
          "message": { "type": "string" },
          "data": {
            "type": "object",
            "nullable": true,
            "description": "Optional extra detail, e.g. `retry_after` (seconds) on signup rate limits.",
            "additionalProperties": true
          }
        }
      }
    }
  }
}
