API Reference
The marketing website API (Express) powers the early-access waitlist, the
contact form, and the admin panel. It is separate from the product app's /api.
- Base URL:
http://localhost:4000in development,https://api.leadfella.comin production. - Prefix: every route below is mounted under
/api. - Content type: JSON request and response bodies (
application/json), except the CSV export. - Auth: admin endpoints require an
Authorization: Bearer <token>header (obtained fromPOST /api/admin/login).
Conventions
Errors
Errors share a consistent JSON shape:
{ "error": "Human-readable message", "code": "machine_code", "details": null }
| Status | code | When |
|---|---|---|
| 400 | bad_request | Malformed JSON body |
| 401 | unauthorized / invalid_credentials | Missing/invalid token or bad login |
| 403 | forbidden | Valid token without admin role |
| 404 | not_found | Unknown route or missing resource |
| 422 | validation_error | Body failed validation (details has fieldErrors/formErrors) |
| 429 | rate_limited | Too many requests |
| 500 | internal_error | Unexpected server error |
Rate limiting
- General traffic:
RATE_LIMIT_MAXrequests perRATE_LIMIT_WINDOW_MS(default 60/min). - Submit endpoints (
POST /api/waitlist,POST /api/contact,POST /api/admin/login): 5 requests/minute per IP.
Spam protection (honeypot)
Public submit endpoints accept an optional hidden hp field. If it is non-empty,
the request is treated as a bot: the API returns 200 { "status": "received" }
and stores nothing.
Meta
GET /api
Service banner.
{ "name": "LeadFella website API", "version": "1.0.0" }
GET /api/health
Liveness.
{ "status": "ok", "env": "production", "uptime": 1234 }
GET /api/health/db
Readiness, including a PostgreSQL ping. Returns 200 when healthy, 503 when
not.
{ "status": "ok", "db": true }
Waitlist
POST /api/waitlist (public)
Join the early-access waitlist.
Request body:
| Field | Type | Required | Rules |
|---|---|---|---|
name | string | yes | 2–120 chars |
email | string | yes | valid email, ≤200 chars (lowercased) |
company | string | no | ≤160 chars |
website | string | no | valid URL, ≤300 chars |
source | string | no | ≤40 chars (e.g. pricing) |
hp | string | no | honeypot - must be empty |
{ "name": "Ada Lovelace", "email": "ada@example.com", "company": "Analytical Engines", "website": "https://example.com", "source": "pricing" }
Responses:
| Status | Body | Meaning |
|---|---|---|
| 201 | { "ok": true, "status": "subscribed" } | New signup stored |
| 200 | { "ok": true, "status": "already_subscribed" } | Email already on the list (idempotent) |
| 200 | { "ok": true, "status": "received" } | Honeypot triggered (nothing stored) |
| 422 | validation_error | Invalid body |
| 429 | rate_limited | More than 5/min |
GET /api/waitlist (admin)
List signups, newest first.
Query parameters:
| Param | Default | Description |
|---|---|---|
page | 1 | Page number |
page_size | 50 | Items per page (max 200) |
search | – | Matches email, name, or company (case-insensitive) |
source | – | Exact source filter |
from | – | ISO date/datetime lower bound (on created_at) |
to | – | ISO date/datetime upper bound |
{
"items": [
{ "id": "12", "name": "Ada Lovelace", "email": "ada@example.com", "company": "Analytical Engines", "website": "https://example.com", "source": "pricing", "createdAt": "2026-06-22T03:48:03.001Z" }
],
"total": 1,
"page": 1,
"page_size": 50
}
GET /api/waitlist/export (admin)
Export the (filtered) signups as CSV. Accepts the same search/source/from/
to query params as the list endpoint. Returns text/csv; charset=utf-8 with a
UTF-8 BOM and a Content-Disposition: attachment header. Columns:
id, name, email, company, website, source, created_at.
DELETE /api/waitlist/:id (admin)
Delete a signup by numeric id.
| Status | Body |
|---|---|
| 200 | { "ok": true } |
| 404 | not_found |
Contact
POST /api/contact (public)
Send a contact message.
| Field | Type | Required | Rules |
|---|---|---|---|
name | string | yes | 2–120 chars |
email | string | yes | valid email, ≤200 chars |
subject | string | no | ≤160 chars |
message | string | yes | 5–4000 chars |
hp | string | no | honeypot - must be empty |
Responses:
| Status | Body | Meaning |
|---|---|---|
| 201 | { "ok": true, "status": "sent" } | Message stored |
| 200 | { "ok": true, "status": "received" } | Honeypot triggered |
| 422 | validation_error | Invalid body |
| 429 | rate_limited | More than 5/min |
GET /api/contact (admin)
List messages, newest first. Query: page, page_size (max 200).
{
"items": [
{ "id": "5", "name": "Grace Lee", "email": "grace@example.com", "subject": "Partnership", "message": "Hi there…", "createdAt": "2026-06-22T03:48:03.000Z" }
],
"total": 1,
"page": 1,
"page_size": 50
}
DELETE /api/contact/:id (admin)
Delete a message by numeric id. Returns { "ok": true } or 404 not_found.
Admin
POST /api/admin/login (public)
Exchange admin credentials for a Bearer token.
{ "email": "admin@leadfella.com", "password": "your-password" }
| Status | Body |
|---|---|
| 200 | { "token": "<jwt>", "user": { "email": "admin@leadfella.com" } } |
| 401 | { "error": "Invalid email or password", "code": "invalid_credentials" } |
The token is a JWT valid for JWT_TTL_HOURS (default 12h). Send it as
Authorization: Bearer <token> on admin endpoints.
GET /api/admin/stats (admin)
Dashboard counters and the distinct lead sources (used by the admin source filter).
{
"waitlist": { "total": 128, "this_week": 12, "this_month": 47 },
"contact": { "total": 9 },
"sources": ["features", "homepage", "pricing"]
}
Example: authenticated request
# 1) Log in
TOKEN=$(curl -s -X POST https://api.leadfella.com/api/admin/login \
-H "Content-Type: application/json" \
-d '{"email":"admin@leadfella.com","password":"your-password"}' | jq -r .token)
# 2) Call an admin endpoint
curl -s https://api.leadfella.com/api/admin/stats \
-H "Authorization: Bearer $TOKEN"