Skip to content
Sunset Beach, NC
Get Support

Public JSON API

The iOS app and the web page both read a public, read-only JSON API at https://sunsetbeach.neuralconfig.com/api/…. It covers exactly one beach — Sunset Beach, North Carolina — and needs no key and no account. Every route answers to GET, returns compact JSON with Cache-Control: public, max-age=60, an ETag over the exact bytes and Access-Control-Allow-Origin: *, and honours If-None-Match with a 304. Add ?pretty=1 for indented output. Fetched panels share one envelope, { data, meta }, where meta says where the data came from, when it was fetched, and whether it is a last-known-good copy. Unknown /api/ paths return {"error":"Not found"} with status 404; anything outside /api/ serves the web app.

Routes that fetch an upstream return either a success or an error envelope:

{
"data": { "...": "the parsed payload" },
"meta": {
"source": "NDBC 41024 — Sunset Nearshore (SUN2)",
"url": "https://www.ndbc.noaa.gov/data/realtime2/41024.txt",
"fetchedAt": "2026-08-16T13:05:12.345Z",
"ok": true,
"stale": false
}
}
{
"data": null,
"meta": {
"source": "NDBC 41024 — Sunset Nearshore (SUN2)",
"url": "https://www.ndbc.noaa.gov/data/realtime2/41024.txt",
"ok": false,
"error": "https://www.ndbc.noaa.gov/data/realtime2/41024.txt -> HTTP 503"
}
}
meta fieldMeaning
sourceHuman name of the upstream. For launches it changes to include “(dev mirror — data may lag)” when the Worker fell back to The Space Devs’ mirror.
urlThe upstream URL or page the data came from.
fetchedAtWhen the served copy actually came off the upstream — not when you asked. For a composite panel, the oldest part’s time.
oktrue with data; false with data: null and error.
staletrue when the upstream failed and this is the last-known-good copy from KV. false when live.
staleReasonPresent when stale: the failure that caused the fallback, e.g. "… -> HTTP 429" or "parse failed: …"; for composites, part: reason joined with ; .
errorPresent when ok is false. "unknown error" if nothing more specific was recorded.

Inside /api/conditions every fetched panel is one of these envelopes under its own key; computed panels (astro, almanac, planner, parking, beach, cams, ships, timeline, activities, verdict) have no meta because they have no upstream.

All routes are GET on https://sunsetbeach.neuralconfig.com.

RouteReturnsCaching
/api/conditionsEverything the app shows: site, generatedAt, verdict, activities, astro, surfZone, tides, surge, coastalWaters, tropical, buoyLocal, buoyWaves, buoyNearshore, weather, alerts, marine, launches (with visibleCount, nextVisible), sky, air, events (with public), rivers, marineLife, turtles, almanac, planner, sightings, rightWhales, birds, waterQuality, shellfish, spills, fishing, migration, parking, beach, cams, ships, timeline. Roughly 440 KB compact.Served from the copy the 5-minute cron publishes; rebuilt only if that copy is over 10 minutes old, so the ETag holds still between ticks.
/api/summaryThe small (under 2 KB) cut the widget reads: generatedAt, top-level stale (true if any of buoy, weather, surf zone, tides or sky is last-known-good), verdict (score, headline, best, warnings), alerts (event names), tide (currentHeightFt, trend, next two extremes), surf (rip risk and level, surf height, water temperature), sun (sunrise/sunset with their ratings), next (the best almanac window in the next 12 hours).Computed per request from cached sources.
/api/healthgeneratedAt, elapsedMs, ok, degraded, and sources — the 28 upstream rows. See How fresh the data is.Edge-memoised 60 s.
/api/astroSun and moon for the beach right now: sunrise, sunset, solar noon, civil and nautical dawn/dusk, golden hour, sun elevation and azimuth, moonrise/moonset with azimuth, compass point and whether over the ocean, moon phase and illumination, fullMoonOverOcean. No meta.Per request.
/api/almanacwindows (the “When to go” windows for the next 7 days), next, year (12 month highlights), thisMonth, horizonDays.Per request from cached sources.
/api/plannerThe seven days ahead, one column per local day, with the honesty flags wholeZone and coarse.Per request.
/api/timelineEvents, launches and sky (sunset, moonrise) as one ordered list, each row with its own sourceName / sourceUrl.Per request.
/api/calibrationgeneratedAt, explains, last30, last90, rows (newest first, up to 90 daily roll-ups) — what the app said against what the sky did. Not about the beach; nothing on the phone reads it. Returns { "error": "No KV binding, so there is no record to report." } if the Worker has no KV.Edge-memoised 300 s.

Each returns the { data, meta } envelope for that source. Details of each upstream are on Where every number comes from.

RouteSource
/api/tidesCO-OPS 8659897 predictions: extremes, series, currentHeightFt, trend, nextExtreme
/api/buoysNot an envelope itself but three: { local, waves, nearshore }, one per NDBC buoy
/api/surfzoneNWS Surf Zone Forecast NCZ110, parsed by day
/api/surfzone/rawThe NWS product as plain text (Content-Type: text/plain), or the body unavailable. The one route that is not JSON and carries no ETag.
/api/marineOpen-Meteo Marine wave forecast
/api/launchesLaunch Library 2 launches with the visibility model applied
/api/weatherNWS gridpoint ILM 70,57
/api/alertsNWS active alerts for the beach
/api/skyOpen-Meteo radiation, UV, layered cloud, sunset/sunrise quality
/api/airOpen-Meteo Air Quality (US AQI)
/api/eventsTown calendar, Sound Waves series and pier concerts merged
/api/riversUSGS gauges
/api/marinelifeOBIS records for six species
/api/turtlesTurtle Watch season totals plus the computed season model
/api/sightingsiNaturalist community observations
/api/rightwhalesNOAA NEFSC slow zones and critical habitat
/api/birdseBird recent and notable (or configured: false without a key)
/api/birdcastBirdCast nightly migration for Brunswick County
/api/waterqualityNC DEQ swim advisories and enterococcus results
/api/shellfishNCDEQ closures and classifications
/api/spillsNCDEQ sanitary sewer overflows near the beach
/api/tropicalNHC Atlantic Tropical Weather Outlook
/api/surgeSpringmaid Pier observed minus predicted
/api/coastalwatersNWS Coastal Waters Forecast AMZ252
/api/fishingCatch report, seasonality almanac and limits together
/api/pierreportThe pier’s daily report on its own
/api/proclamationsN.C. DMF finfish proclamations in effect
RouteReturns
/api/parkingThe paid-parking status computed from the town ordinance for right now. No meta.
/api/camsThe four beach-camera links (name, operator, URL, note).
/api/shipsThe three kinds of traffic and three tracker links.
  • Every JSON response, including errors, carries ETag, Cache-Control: public, max-age=60 and Access-Control-Allow-Origin: *.
  • The ETag is a SHA-256 of the served bytes, truncated to 128 bits and quoted. ?pretty=1 and compact output therefore have different tags.
  • A request whose If-None-Match matches (weak comparison; * matches anything) gets 304 Not Modified with no body — but only for a 200. A 404 or 500 is never revalidated.
  • /api/conditions is the payload where this matters: because the cron publishes one copy that every request serves verbatim, consecutive requests return the same tag and a conditional request costs a zero-byte body instead of ~440 KB.
  • Any Cloudflare-visible client works; the Worker sends its own descriptive User-Agent to upstreams, and nothing about your request is forwarded to them.
StatusBodyWhen
404{"error":"Not found"}Any /api/ path not listed above.
500{"error":"<message>"}An unexpected exception while building a payload. Upstream failures do not produce this — they are reported inside the envelope as ok: false or stale: true with a 200.
200 with data: nullThe envelope’s error formThe upstream failed and no usable last-known-good copy exists. Check meta.error.
200 with stale: trueThe envelope’s success formThe upstream failed; you are reading the last good copy. Check meta.fetchedAt.

Failures that are not error statuses, in other words, are the normal case for this API: always read meta.ok and meta.stale, not just the HTTP status.

  • No authentication, no API key, no rate limit is enforced by the Worker itself. It is a small public API for one beach; the app fetches /api/conditions on launch and on pull-to-refresh, revalidating with the ETag it holds, and the widget reads /api/summary on the system’s schedule. Please stay in that neighbourhood.
  • Everything is already cached and warmed on the Worker; you never trigger an upstream fetch of your own except on a cold edge, so hammering a route buys no fresher data than the cadences on How fresh the data is.
  • Timestamps are ISO-8601 UTC. The beach observes US Eastern time (America/New_York); localise on your side.
  • Payloads deliberately carry caveat, note and promises strings. They are part of the data — the app shows them behind its ⓘ buttons — and a client that drops them is making claims the source doesn’t.