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_pub_your_publishable_key'
},
body: JSON.stringify({
email: 'user@example.com',
metadata: { name: 'Jane', source: 'landing-page' }
})
})
Each project has two keys, plus a server-wide admin key:
| Header | Value | Used for |
|---|---|---|
X-API-Key | Publishable key (wl_pub_...) | POST /subscribe only — safe to embed in frontend code |
X-API-Key | Secret key (wl_sec_...) | All project-scoped endpoints — keep server-side only |
X-Admin-Key | Server admin key | Project management endpoints |
Never ship the secret key to a browser — it can read and export your subscriber list. The publishable key can't.
| Field | Type | Description | |
|---|---|---|---|
email | string | required | Email address (max 320 chars, normalized to lowercase) |
metadata | object | optional | Arbitrary JSON (max 4KB) |
referral_code | string | optional | Caller-provided referral slug for this subscriber |
referred_by_code | string | optional | Referral code of the referring subscriber |
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"}}'
{
"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
}
}
| Code | Reason |
|---|---|
400 | Invalid email or request body |
401 | Missing or invalid API key |
403 | Origin not in the project's allowed_origins |
409 | Email already subscribed, or referral code taken |
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_sec_abc123"
{
"subscribers": [...],
"total": 142,
"limit": 20,
"offset": 0
}
curl https://emailwaitlist.ayushojha.com/api/v1/subscribers/export \
-H "X-API-Key: wl_sec_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_sec_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_sec_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 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.
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 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.
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_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.' };
}
}
<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>
Use these endpoints with your secret key (wl_sec_...) from a server or CLI — the publishable key is rejected here.
| 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. 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.