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.
https://channel-surf-tv.jgpowers52.workers.dev — Production workerhttp://localhost:8787 — Local wrangler dev| 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/spotsPaginated, 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/mapLightweight 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/metaFacet 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/surfersPaginated, 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/metaFacet 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/quotesQuotes 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/randomQuote 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/clipsVideo clip inventory
Clips ordered featured-first; src is a public R2 URL.
| Status | Description |
|---|---|
| 200 | Clip list |
GET /api/eventsSurf 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-mediaRecent 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/feedCurated 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/accountsCurated 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.jsonThis 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/docsHuman-readable API reference
Dependency-free HTML rendered from the bundled spec at request time.
| Status | Description |
|---|---|
| 200 | HTML documentation page |