API Reference
All routes are served from a single Cloudflare Worker. The router in src/api/router.ts dispatches to the correct handler based on the exact pathname.
Authentication
| Route group | Method | Header |
|---|---|---|
| Public routes | None | — |
| Admin routes | Authorization: Bearer <ADMIN_TOKEN> | Or x-admin-token header, or egac_admin_token cookie |
| Cron routes | Authorization: Bearer <CRON_SECRET> | — |
In development environment only, the token dev is accepted for both admin and cron routes.
Response envelope
All endpoints return JSON with a consistent shape:
// Success
{ "ok": true, "data": { ... } }
// Error
{ "ok": false, "error": "Human-readable message", "code": "MACHINE_READABLE_CODE" }
Public API
POST /api/enquiry
Creates an enquiry, resolves the age group, and dispatches the first email (booking invite or academy waitlist confirmation).
Rate limited: 10 requests/minute per IP.
Request (JSON or application/x-www-form-urlencoded):
{
"enquiry_for": "other",
"enquirer_name": "Jane Smith",
"enquirer_email": "jane@example.com",
"enquirer_phone": "07700 900000",
"athlete_name": "Tom Smith",
"athlete_dob": "2015-04-12",
"source": "website"
}
Response 201:
{ "ok": true, "data": { "message": "Enquiry received" } }
Error codes:
| Status | Code | Reason |
|---|---|---|
400 | INVALID_JSON | Unparseable body |
422 | VALIDATION_ERROR | Missing/invalid field |
429 | RATE_LIMITED | Too many requests from this IP |
POST /api/booking
Creates a taster session booking from an invite token and date.
Request:
{
"token": "abc123def456...",
"date": "2026-10-14",
"age_group_id": "optional-override-id"
}
Response 200:
{
"ok": true,
"data": {
"booking_id": "bkg_...",
"session_date": "2026-10-14",
"session_time": "18:30",
"age_group": "Under 13",
"venue": "East Grinstead Leisure Centre",
"message": "Booking confirmed. A confirmation email is on its way."
}
}
Error codes:
| Status | Code | Reason |
|---|---|---|
404 | INVITE_NOT_FOUND | Token does not exist |
409 | INVITE_USED | Invite already accepted |
409 | DUPLICATE_BOOKING | Already booked this date |
409 | SLOT_FULL | Session at capacity |
410 | INVITE_EXPIRED | Invite older than 14 days |
422 | MISSING_TOKEN | No token in body |
422 | MISSING_DATE | No date in body |
422 | SLOT_INVALID | Date not a valid session day |
422 | NO_AGE_GROUP | DOB doesn’t match any age group |
POST /api/academy/respond
Records a parent’s yes/no response to an Academy invitation.
Request:
{ "token": "xyz789...", "response": "yes" }
Response 200:
{
"ok": true,
"data": {
"message": "Thank you — we've noted your interest...",
"response": "yes"
}
}
Idempotent — calling again after responding returns already_responded: true with the original response.
GET /api/health
Liveness check. Returns 200 with uptime info.
{ "ok": true, "data": { "status": "ok", "timestamp": "2026-10-14T18:30:00.000Z" } }
Admin API
All admin routes require the admin token.
GET /api/admin/enquiries
Paginated list of enquiries.
Query params:
| Param | Values | Default |
|---|---|---|
status | pending / processed / academy | all |
search | name or email substring | — |
page | integer ≥ 1 | 1 |
Response:
{
"ok": true,
"data": {
"enquiries": [...],
"page": 1,
"hasNextPage": true,
"pageSize": 20
}
}
POST /api/admin/enquiries
Manually create an enquiry (admin use — skips routing and email).
{ "name": "Jane Smith", "email": "jane@example.com", "dob": "2015-04-12" }
GET /api/admin/enquiries/:id
Full enquiry detail including invite, bookings, and parsed event log.
POST /api/admin/invites/resend
Resend the booking invite for an enquiry.
{ "enquiry_id": "enq_..." }
GET /api/admin/bookings
List of all bookings with enquiry details. Supports ?date=YYYY-MM-DD and ?status=confirmed filters.
POST /api/admin/bookings/:id/attendance
Record attendance for a booking.
{
"status": "attended",
"note": "Good session",
"send_membership_link": true
}
Valid statuses: attended, no_show, cancelled.
When send_membership_link: true and status: "attended", a membership_otp is created and a membership_invite email is sent.
GET /api/admin/reports
Aggregate stats for the reports dashboard. Returns:
- Enquiry totals (all-time, this month, last month)
- Booking totals (confirmed, attended, no-show, conversion rate)
- Academy waitlist totals
- Upcoming 4 sessions with per-age-group booking counts
- 6-month enquiry trend
- Conversion funnel: enquiries → invites sent → booked → attended
GET /api/admin/memberships
Membership form submissions. Accepts ?format=csv for a CSV export.
GET /api/admin/templates
List all email templates with key, name, and active status.
GET /api/admin/templates/:id
Single template with full HTML, text, and variables list.
PUT /api/admin/templates/:id
Update a template’s subject, HTML, text content, or active flag.
GET /api/admin/templates/:id/preview
Returns rendered HTML using sample variables (for the admin preview pane).
POST /api/admin/templates/:id/test-send
Sends a test email to a specified address using sample variable values.
{ "to": "admin@example.com" }
Cron API
All cron routes require Authorization: Bearer <CRON_SECRET>.
| Route | Method | Action |
|---|---|---|
/api/cron/expire-invites | POST | Expire stale invites (14-day TTL) |
/api/cron/retry-invites | POST | Retry pending invite emails |
/api/cron/send-reminders | POST | Send 7-day session reminders |
/api/cron/academy-rollover | POST | Close expired seasons, roll waitlist |
All return { "ok": true, "data": { ...counts } } on success.