channel-surf-tv API

Read-only JSON API served by the channel-surf-tv Cloudflare Worker in front of the D1 database (channel-surf-tv-spots). All endpoints are GET; any other method returns 405 METHOD_NOT_ALLOWED. Unknown /api/* paths return 404 NOT_FOUND. Errors share the envelope {error, code}. Successful responses are cacheable (Cache-Control: public, max-age=300, stale-while-revalidate=600) and CORS-enabled for allow-listed origins.

This spec is the source of truth for the API surface: npm run api:gen converts it to src/worker/openapi.json (served live at /api/openapi.json) and renders docs/api/API.md. npm run health diffs worker routes against these paths and flags drift.

Servers

Endpoints

Method Path Summary
GET /api/spots Paginated, filterable surf spot list
GET /api/spots/map Lightweight spot markers for the Leaflet map
GET /api/spots/meta Facet counts for the spot filter sidebar
GET /api/spots/{slug} Spot detail with images, tags, and linked surfers
GET /api/surfers Paginated, filterable surfer list
GET /api/surfers/meta Facet counts for the surfer filter sidebar
GET /api/surfers/{slug} Surfer detail with images and linked spots
GET /api/quotes Quotes wall collection
GET /api/quotes/random Quote of the Day
GET /api/clips Video clip inventory
GET /api/events Surf event calendar with derived status
GET /api/events/live-media Recent Instagram posts from the live event's organizer
GET /api/instagram/feed Curated Instagram feed
GET /api/instagram/accounts Curated account scores with follower history
GET /api/openapi.json This OpenAPI specification as JSON
GET /api/docs Human-readable API reference

GET /api/spots

Paginated, filterable surf spot list

Full-text search via q uses SQLite FTS5 with bm25 ranking; without q, ordering is tier-weighted (iconic → regional → other) then alphabetical.

Parameter In Type Description
limit query integer, default 20, 1–100
offset query integer, default 0, 0–
q query string Full-text search (FTS5)
country query string ISO 3166-1 alpha-2 country code
region query string
wave_type query string e.g. reef, beach, point
skill_level query string e.g. beginner, intermediate, expert
tag query string Tag slug
tier query string Curation tier, e.g. iconic, regional
Status Description
200 Page of spots plus total match count
400 Invalid query parameter

GET /api/spots/map

Lightweight spot markers for the Leaflet map

Unpaginated; optionally constrained to a bounding box. Same filters as the list endpoint.

Parameter In Type Description
bbox query string Bounding box as min_lat,min_lng,max_lat,max_lng
q query string Full-text search (FTS5)
country query string ISO 3166-1 alpha-2 country code
region query string
wave_type query string
skill_level query string
tag query string
tier query string Curation tier, e.g. iconic, regional
Status Description
200 Marker payload
400 Invalid query parameter

GET /api/spots/meta

Facet counts for the spot filter sidebar

Parameter In Type Description
q query string Full-text search (FTS5)
country query string ISO 3166-1 alpha-2 country code
region query string
wave_type query string
skill_level query string
tag query string
tier query string Curation tier, e.g. iconic, regional
Status Description
200 Filtered total plus facet value/count pairs
400 Invalid query parameter

GET /api/spots/{slug}

Spot detail with images, tags, and linked surfers

Parameter In Type Description
slug path (required) string
Status Description
200 Spot detail payload
404 Resource not found

GET /api/surfers

Paginated, filterable surfer list

Parameter In Type Description
limit query integer, default 20, 1–100
offset query integer, default 0, 0–
q query string Full-text search (FTS5)
country query string ISO 3166-1 alpha-2 country code
discipline query string
era query string
tier query string Curation tier, e.g. iconic, regional
Status Description
200 Page of surfers plus total match count
400 Invalid query parameter

GET /api/surfers/meta

Facet counts for the surfer filter sidebar

Parameter In Type Description
q query string Full-text search (FTS5)
country query string ISO 3166-1 alpha-2 country code
discipline query string
era query string
tier query string Curation tier, e.g. iconic, regional
Status Description
200 Filtered total plus facet value/count pairs
400 Invalid query parameter

GET /api/surfers/{slug}

Surfer detail with images and linked spots

Parameter In Type Description
slug path (required) string
Status Description
200 Surfer detail payload
404 Resource not found

GET /api/quotes

Quotes wall collection

Full collection, optionally filtered; ordered by id.

Parameter In Type Description
category query string
surfer query string Surfer slug
Status Description
200 Quotes plus count

GET /api/quotes/random

Quote of the Day

Deterministic day-indexed pick from the featured pool (same quote for everyone on a given UTC day). 404 if the featured pool is empty.

Status Description
200 The day's quote
404 Resource not found

GET /api/clips

Video clip inventory

Clips ordered featured-first; src is a public R2 URL.

Status Description
200 Clip list

GET /api/events

Surf event calendar with derived status

Season calendar of surf competitions (WSL CT, ISA, big-wave windows). Status (upcoming/live/window-open/completed) is derived from each event's date window at request time, never stored.

Parameter In Type Description
org query string Filter by organization slug (e.g. wsl, isa).
tour query string Filter by tour/series (e.g. ct, isa-wsg, big-wave).
year query string Filter by start-date year (e.g. 2026).
status query string Filter by derived status.
Status Description
200 Events ordered by start date

GET /api/events/live-media

Recent Instagram posts from the live event's organizer

While an event is live, recent media from the organizing account via Business Discovery (1h D1 cache). Empty payload when no event is live, the organization has no Instagram handle, or Instagram is unconfigured.

Status Description
200 Live event media rail payload

GET /api/instagram/feed

Curated Instagram feed

Served from the D1 cache (refreshed by a 6-hourly cron); lazily refreshed on cache miss. Returns an empty post list when Instagram credentials are not configured; 503 when a live refresh fails with no cache to fall back on.

Status Description
200 Feed payload
503 Upstream dependency unavailable

GET /api/instagram/accounts

Curated account scores with follower history

Latest score snapshot per curated account, ranked by score then followers, each with a recent follower-count history series. Empty list when Instagram credentials are not configured.

Status Description
200 Scored accounts

GET /api/openapi.json

This OpenAPI specification as JSON

The spec bundled into the deployed worker at build time, so it always matches the running version.

Status Description
200 OpenAPI 3.1 document

GET /api/docs

Human-readable API reference

Dependency-free HTML rendered from the bundled spec at request time.

Status Description
200 HTML documentation page