Skip to content
← All docs

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:4000 in development, https://api.leadfella.com in 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 from POST /api/admin/login).

Conventions

Errors

Errors share a consistent JSON shape:

{ "error": "Human-readable message", "code": "machine_code", "details": null }
StatuscodeWhen
400bad_requestMalformed JSON body
401unauthorized / invalid_credentialsMissing/invalid token or bad login
403forbiddenValid token without admin role
404not_foundUnknown route or missing resource
422validation_errorBody failed validation (details has fieldErrors/formErrors)
429rate_limitedToo many requests
500internal_errorUnexpected server error

Rate limiting

  • General traffic: RATE_LIMIT_MAX requests per RATE_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:

FieldTypeRequiredRules
namestringyes2–120 chars
emailstringyesvalid email, ≤200 chars (lowercased)
companystringno≤160 chars
websitestringnovalid URL, ≤300 chars
sourcestringno≤40 chars (e.g. pricing)
hpstringnohoneypot - must be empty
{ "name": "Ada Lovelace", "email": "ada@example.com", "company": "Analytical Engines", "website": "https://example.com", "source": "pricing" }

Responses:

StatusBodyMeaning
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)
422validation_errorInvalid body
429rate_limitedMore than 5/min

GET /api/waitlist (admin)

List signups, newest first.

Query parameters:

ParamDefaultDescription
page1Page number
page_size50Items per page (max 200)
searchMatches email, name, or company (case-insensitive)
sourceExact source filter
fromISO date/datetime lower bound (on created_at)
toISO 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.

StatusBody
200{ "ok": true }
404not_found

Contact

POST /api/contact (public)

Send a contact message.

FieldTypeRequiredRules
namestringyes2–120 chars
emailstringyesvalid email, ≤200 chars
subjectstringno≤160 chars
messagestringyes5–4000 chars
hpstringnohoneypot - must be empty

Responses:

StatusBodyMeaning
201{ "ok": true, "status": "sent" }Message stored
200{ "ok": true, "status": "received" }Honeypot triggered
422validation_errorInvalid body
429rate_limitedMore 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" }
StatusBody
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"