Plug-and-play email collection microservice. Multi-tenant, rate-limited, CORS-aware.
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"]}'
Collect emails from your frontend
fetch('https://emailwaitlist.ayushojha.com/api/v1/subscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'wl_your_project_api_key'
},
body: JSON.stringify({
email: 'user@example.com',
metadata: { name: 'Jane', source: 'landing-page' }
})
})
Two auth mechanisms — use the right header for each endpoint type:
| Header | Value | Used for |
|---|---|---|
X-API-Key | Project API key (wl_...) | All project-scoped endpoints |
X-Admin-Key | Server admin key | Project management endpoints |
| Field | Type | Description | |
|---|---|---|---|
email | string | required | Email address (max 320 chars) |
metadata | object | optional | Arbitrary JSON (max 4KB) |
curl -X POST https://emailwaitlist.ayushojha.com/api/v1/subscribe \
-H "Content-Type: application/json" \
-H "X-API-Key: wl_abc123" \
-d '{"email":"user@example.com","metadata":{"name":"Jane"}}'
{
"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"
}
}
| Code | Reason |
|---|---|
400 | Invalid email or request body |
401 | Missing or invalid API key |
409 | Email already subscribed to this project |
429 | Rate limit exceeded (30 req/min/IP) |
| Param | Type | Default | Description |
|---|---|---|---|
limit | int | 50 | Results per page (max 500) |
offset | int | 0 | Skip N results |
curl https://emailwaitlist.ayushojha.com/api/v1/subscribers?limit=20&offset=0 \
-H "X-API-Key: wl_abc123"
{
"subscribers": [...],
"total": 142,
"limit": 20,
"offset": 0
}
curl https://emailwaitlist.ayushojha.com/api/v1/subscribers/export \
-H "X-API-Key: wl_abc123" \
-o subscribers.csv
Returns a CSV file with columns: email, metadata, subscribed_at
curl -X DELETE https://emailwaitlist.ayushojha.com/api/v1/subscribers/user@example.com \
-H "X-API-Key: wl_abc123"
{"message": "subscriber removed"}
| Code | Reason |
|---|---|
404 | Email not found in this project |
curl https://emailwaitlist.ayushojha.com/api/v1/stats \
-H "X-API-Key: wl_abc123"
{
"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}
]
}
| Field | Type | Description | |
|---|---|---|---|
name | string | required | Display name |
slug | string | required | URL-safe identifier (lowercase, hyphens) |
allowed_origins | string[] | optional | CORS origins. Empty = allow all. Use ["*"] for wildcard. |
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"]}'
{
"message": "Project created. Save your API key — it won't be shown again.",
"project": {
"id": "uuid",
"name": "My App",
"slug": "my-app",
"api_key": "wl_abc123...",
"allowed_origins": ["https://myapp.com"],
"created_at": "2026-03-12T10:00:00Z"
}
}
curl https://emailwaitlist.ayushojha.com/api/v1/projects \
-H "X-Admin-Key: YOUR_ADMIN_KEY"
{"projects": [...]}
{"status": "ok"}
Follow these three steps to add email collection to any website.
Create a project to get an API key. 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 api_key from the response — it starts with wl_ and won't be shown again.
Drop this into any page. Works with React, Vue, Svelte, plain HTML — anything that can call fetch.
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_your_project_api_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.' };
}
}
<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_your_project_api_key'
},
body: JSON.stringify({ email })
});
const data = await res.json();
msg.textContent = res.ok ? data.message : data.error;
});
</script>
Use these endpoints with the same API key to view, export, or remove subscribers.
| Action | Request |
|---|---|
| List subscribers | GET /api/v1/subscribers?limit=50&offset=0 |
| Export CSV | GET /api/v1/subscribers/export |
| Unsubscribe | DELETE /api/v1/subscribers/{email} |
| Dashboard stats | GET /api/v1/stats |
All requests require the X-API-Key header.
| Status | Meaning | What to show the user |
|---|---|---|
201 | Subscribed | Success message |
400 | Bad email | "Please enter a valid email" |
409 | Duplicate | "You're already on the waitlist" |
429 | Rate limited | "Please try again in a minute" |
401 | Bad API key | Check your X-API-Key header |
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.
Each project can define allowed_origins to restrict which domains can call the API from browsers. If no origins are set, all origins are allowed. The API handles OPTIONS preflight requests automatically.