Email Waitlist API

Plug-and-play email collection microservice. Multi-tenant, rate-limited, CORS-aware.

Quick Start

1

Create a project (admin only, one-time)

curl -X POST https://emailwaitlist.ayushojha.com/api/v1/projects \
  -H "X-Admin-Key: YOUR_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"My App","slug":"my-app","allowed_origins":["https://myapp.com"]}'
2

Collect emails from your frontend

fetch('https://emailwaitlist.ayushojha.com/api/v1/subscribe', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-Key': 'wl_pub_your_publishable_key'
  },
  body: JSON.stringify({
    email: 'user@example.com',
    metadata: { name: 'Jane', source: 'landing-page' }
  })
})

Authentication

Each project has two keys, plus a server-wide admin key:

HeaderValueUsed for
X-API-KeyPublishable key (wl_pub_...)POST /subscribe only — safe to embed in frontend code
X-API-KeySecret key (wl_sec_...)All project-scoped endpoints — keep server-side only
X-Admin-KeyServer admin keyProject management endpoints

Never ship the secret key to a browser — it can read and export your subscriber list. The publishable key can't.

Endpoints

POST /api/v1/subscribe Collect an email API Key
Request Body
FieldTypeDescription
emailstringrequiredEmail address (max 320 chars, normalized to lowercase)
metadataobjectoptionalArbitrary JSON (max 4KB)
referral_codestringoptionalCaller-provided referral slug for this subscriber
referred_by_codestringoptionalReferral code of the referring subscriber
Example
curl -X POST https://emailwaitlist.ayushojha.com/api/v1/subscribe \
  -H "Content-Type: application/json" \
  -H "X-API-Key: wl_pub_abc123" \
  -d '{"email":"user@example.com","metadata":{"name":"Jane"}}'
Response 201
{
  "message": "Successfully joined the waitlist!",
  "subscriber": {
    "id": "uuid",
    "project_id": "uuid",
    "email": "user@example.com",
    "metadata": {"name": "Jane"},
    "subscribed_at": "2026-03-12T10:00:00Z",
    "position": 41,
    "referral_count": 0
  }
}
Errors
CodeReason
400Invalid email or request body
401Missing or invalid API key
403Origin not in the project's allowed_origins
409Email already subscribed, or referral code taken
429Rate limit exceeded (30 req/min/IP)
GET /api/v1/subscribers List emails (paginated) API Key
Query Parameters
ParamTypeDefaultDescription
limitint50Results per page (max 500)
offsetint0Skip N results
Example
curl https://emailwaitlist.ayushojha.com/api/v1/subscribers?limit=20&offset=0 \
  -H "X-API-Key: wl_sec_abc123"
Response 200
{
  "subscribers": [...],
  "total": 142,
  "limit": 20,
  "offset": 0
}
GET /api/v1/subscribers/export CSV download API Key
Example
curl https://emailwaitlist.ayushojha.com/api/v1/subscribers/export \
  -H "X-API-Key: wl_sec_abc123" \
  -o subscribers.csv
Response

Returns a CSV file with columns: email, metadata, subscribed_at

DELETE /api/v1/subscribers/{email} Unsubscribe API Key
Example
curl -X DELETE https://emailwaitlist.ayushojha.com/api/v1/subscribers/user@example.com \
  -H "X-API-Key: wl_sec_abc123"
Response 200
{"message": "subscriber removed"}
Errors
CodeReason
404Email not found in this project
GET /api/v1/stats Dashboard stats API Key
Example
curl https://emailwaitlist.ayushojha.com/api/v1/stats \
  -H "X-API-Key: wl_sec_abc123"
Response 200
{
  "total": 142,
  "today": 8,
  "this_week": 34,
  "this_month": 89,
  "by_day": [
    {"date": "2026-03-10", "count": 12},
    {"date": "2026-03-11", "count": 14},
    {"date": "2026-03-12", "count": 8}
  ]
}
POST /api/v1/projects Create project Admin
Request Body
FieldTypeDescription
namestringrequiredDisplay name
slugstringrequiredURL-safe identifier (lowercase, hyphens)
allowed_originsstring[]optionalCORS origins. Empty = allow all. Use ["*"] for wildcard.
Example
curl -X POST https://emailwaitlist.ayushojha.com/api/v1/projects \
  -H "Content-Type: application/json" \
  -H "X-Admin-Key: YOUR_ADMIN_KEY" \
  -d '{"name":"My App","slug":"my-app","allowed_origins":["https://myapp.com"]}'
Response 201
{
  "message": "Project created. Save the secret api_key — it won't be shown again. The public_key is safe to embed in your frontend.",
  "project": {
    "id": "uuid",
    "name": "My App",
    "slug": "my-app",
    "api_key": "wl_sec_abc123...",
    "public_key": "wl_pub_def456...",
    "allowed_origins": ["https://myapp.com"],
    "created_at": "2026-03-12T10:00:00Z"
  }
}

The secret api_key is stored hashed and only returned here. The public_key can be retrieved again via GET /api/v1/projects.

GET /api/v1/projects List projects Admin
Example
curl https://emailwaitlist.ayushojha.com/api/v1/projects \
  -H "X-Admin-Key: YOUR_ADMIN_KEY"
Response 200
{"projects": [...]}
GET /health Health check None
Response 200
{"status": "ok"}

Integration Guide

Follow these three steps to add email collection to any website.

Step 1 — Register your site

Create a project to get your keys. Run this once per website (requires the admin key).

curl -X POST https://emailwaitlist.ayushojha.com/api/v1/projects \
  -H "X-Admin-Key: YOUR_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "My Website",
    "slug": "my-website",
    "allowed_origins": ["https://mywebsite.com"]
  }'

Save the secret api_key (wl_sec_...) somewhere safe — it won't be shown again. Use the public_key (wl_pub_...) in your frontend.

Step 2 — Add the form to your frontend

Drop this into any page. Works with React, Vue, Svelte, plain HTML — anything that can call fetch.

React / Next.js
async function subscribe(email, name) {
  const res = await fetch('https://emailwaitlist.ayushojha.com/api/v1/subscribe', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-API-Key': 'wl_pub_your_publishable_key'
    },
    body: JSON.stringify({
      email,
      metadata: { name, source: 'landing-page' }
    })
  });

  const data = await res.json();

  if (res.ok) {
    // Success — show confirmation
    return { success: true, message: data.message };
  } else if (res.status === 409) {
    // Already subscribed
    return { success: false, message: 'You're already on the list!' };
  } else if (res.status === 429) {
    // Rate limited
    return { success: false, message: 'Too many requests. Try again shortly.' };
  } else {
    return { success: false, message: data.error || 'Something went wrong.' };
  }
}
Plain HTML + Vanilla JS
<form id="waitlist-form">
  <input type="email" id="wl-email" placeholder="you@example.com" required />
  <button type="submit">Join Waitlist</button>
  <p id="wl-msg"></p>
</form>

<script>
document.getElementById('waitlist-form').addEventListener('submit', async (e) => {
  e.preventDefault();
  const email = document.getElementById('wl-email').value;
  const msg = document.getElementById('wl-msg');

  const res = await fetch('https://emailwaitlist.ayushojha.com/api/v1/subscribe', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-API-Key': 'wl_pub_your_publishable_key'
    },
    body: JSON.stringify({ email })
  });

  const data = await res.json();
  msg.textContent = res.ok ? data.message : data.error;
});
</script>

Step 3 — Manage your subscribers

Use these endpoints with your secret key (wl_sec_...) from a server or CLI — the publishable key is rejected here.

ActionRequest
List subscribersGET /api/v1/subscribers?limit=50&offset=0
Export CSVGET /api/v1/subscribers/export
UnsubscribeDELETE /api/v1/subscribers/{email}
Dashboard statsGET /api/v1/stats

All requests require the X-API-Key header.

Response Handling Cheatsheet

StatusMeaningWhat to show the user
201SubscribedSuccess message
400Bad email"Please enter a valid email"
409Duplicate"You're already on the waitlist"
429Rate limited"Please try again in a minute"
401Bad API keyCheck your X-API-Key header

Rate Limiting

The POST /api/v1/subscribe endpoint is rate-limited to 30 requests per minute per IP. When exceeded, the API returns 429 Too Many Requests. Other endpoints are not rate-limited.

CORS

Each project can define allowed_origins to restrict which domains can call the API from browsers. Requests whose Origin header isn't in the list are rejected with 403. If no origins are set, all origins are allowed. The API handles OPTIONS preflight requests automatically.