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 groupMethodHeader
Public routesNone
Admin routesAuthorization: Bearer <ADMIN_TOKEN>Or x-admin-token header, or egac_admin_token cookie
Cron routesAuthorization: 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:

StatusCodeReason
400INVALID_JSONUnparseable body
422VALIDATION_ERRORMissing/invalid field
429RATE_LIMITEDToo 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:

StatusCodeReason
404INVITE_NOT_FOUNDToken does not exist
409INVITE_USEDInvite already accepted
409DUPLICATE_BOOKINGAlready booked this date
409SLOT_FULLSession at capacity
410INVITE_EXPIREDInvite older than 14 days
422MISSING_TOKENNo token in body
422MISSING_DATENo date in body
422SLOT_INVALIDDate not a valid session day
422NO_AGE_GROUPDOB 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:

ParamValuesDefault
statuspending / processed / academyall
searchname or email substring
pageinteger ≥ 11

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>.

RouteMethodAction
/api/cron/expire-invitesPOSTExpire stale invites (14-day TTL)
/api/cron/retry-invitesPOSTRetry pending invite emails
/api/cron/send-remindersPOSTSend 7-day session reminders
/api/cron/academy-rolloverPOSTClose expired seasons, roll waitlist

All return { "ok": true, "data": { ...counts } } on success.