# CastPoint Developer API — initial authenticated release

Base URL: `https://api.castpoint.app`. External access is under `/v4/data`. This document describes the implementation introduced with the admin portal; see deployment evidence in `docs/admin-portal.md` before assuming a specific environment is live.

## Authentication and provisioning

Sign into `https://admin.castpoint.app` with an invited administrator account and complete MFA. Open API clients, supply an integration name, select scopes, quotas and expiration, and create a key. Copy it once to your server-side secret manager. Keys are random256-bit secrets with a public identifier, formatted `cp_<24hex-id>.<43char-secret>`. Only SHA256 hashes are stored. This is a high-entropy API credential, not a password.

Send every request with `X-API-Key`. Never put a key in a query string, frontend JavaScript, mobile binary, screenshot, log or source control. No OAuth exchange is required for an outside API client. Portal Cognito tokens are a different credential and do not serve as outside API keys.

```bash
# CASTPOINT_API_KEY is loaded from your secret manager, not committed here.
curl --fail-with-body \
  -H "X-API-Key: $CASTPOINT_API_KEY" \
  'https://api.castpoint.app/v4/data/v3/radar?source=native&latitude=28.0&longitude=-82.5&station=auto'
```

Rotation is create replacement -> update integration -> verify -> revoke old key. Rotation does not overwrite an old secret. Revocation and expiration are checked per request with strongly consistent reads; there is no positive auth cache. In-flight work authorized just before revocation may finish. Deleting an expired DynamoDB item later is housekeeping, not the expiry enforcement mechanism.

## Scopes and routes

| Scope | Allowed underlying routes (prefix each with `/v4/data`) |
|---|---|
| `forecast:read` | `/v1/forecast` |
| `radar:read` | `/v2/radar`, `/v2/radar/...`, `/v3/radar`, `/v3/radar/...` |
| `activity:read` | `/v1/solunar`, `/v2/activity`, `/v2/activity/month` |
| `stations:read` | `/v1/stations` |

GET only. Routes not listed, including administration, lightning subscriptions, arbitrary hosts and mutable operations, are not available via outside keys. Scopes do not imply every station/geography/provider has coverage. Underlying handlers still validate their own exact paths and query parameters.

### Native radar + Rainbow nowcast

`/v4/data/v3/radar?source=native&latitude=...&longitude=...&station=auto` selects the nearest fresh supported station. Manual `station=KTBW` is allowed only among nearby choices. `/v4/data/v3/radar/stations?latitude=...&longitude=...` lists choices/status. MRMS and legacy Local catalogs use `source=mrms` or `source=local` without coordinates. `source=rainbow` returns Rainbow observations/forecast.

Ready response: `schemaVersion:3`, `snapshot`, `frames`, optional `station`, `bounds`, `nowcastUnavailable`. Each frame has epoch-second `time`, `kind`, `product`, `source`, and `path`; optional `format:'polar'`, `station`, `maxZoom`, model/requestedTime. Follow returned paths rather than inventing timestamps. Catalog JSON frame paths are rewritten into authenticated `/v4/data/...` paths, including recursive nested objects. Native cold data can return HTTP202 with `state:'preparing'` and empty frames; back off about30seconds and retry. No fresh nearby station may report unavailable. Do not treat missing coverage as clear weather.

Native frames are JSON polar samples, NOT PNG tiles: `schemaVersion:1`, `encoding:'uint8-half-dbz-offset66'`, station position, rays/gates, range geometry, azimuth/elevation arrays and base64 values. Decode row-major unsigned bytes:0missing,1reserved, otherwise `(code-66)/2`dBZ. Geometry/rendering details are in `docs/handoff/radar-nowcast.md`. A station's scan time is not necessarily the current time.

Rainbow forecast paths normally return provider-colored PNG. For our custom client renderer, append exact `?color=dbz_u8` to a v2 precip-global tile path. Decode red `(R mod128)-32`dBZ; high bit is snow; alpha0=no coverage. **Raw encoded PNG is not presentation imagery.** Current mobile appearance is produced on-device by `src/rainbowPalette.ts`, with fade40/blur4/blend100/opacity0.85. No server-rendered styled-nowcast endpoint exists in this release. Do not advertise the raw endpoint as our finished styled image service. A proposed style-versioned endpoint is documented separately in the handoff.

Alternate Xweather forecasts can be requested with `forecast=xweather&latitude=...&longitude=...` on v3 Native/MRMS/Local catalogs. Requested10minute times do not establish the provider's native forecast cadence. Keep product/provenance labels when switching between observations and predictions.

### Fishing activity

```text
/v4/data/v2/activity?latitude=28.0&longitude=-82.5&date=2026-09-20&timezone=America/New_York
/v4/data/v2/activity/month?latitude=28.0&longitude=-82.5&month=2026-09&timezone=America/New_York
```

Use a current supported date, not this fixed example indefinitely. Optional `station=NOAA_ID` must be one of the nearby reference stations returned by the daily route. Without station the API returns solunar-only results and candidates; unlike the mobile UI it does not automatically choose one for you. Score is a planning heuristic, not catch probability. Use top-level score and mode; nested day.score is an older astronomical formula. Dates: local today minus1month through plus12months. Heights are meters above MLLW, times Unix seconds, phase degrees, illumination0..1. Detailed formulas, failure behavior and caches: `docs/handoff/solunar-grading.md`.

### Weather and stations

Forecast requires latitude/longitude; `weather` and `marine` are optional selectors. Weather: best_match, ecmwf_ifs025, gfs_global, icon_global. Marine: best_match, ecmwf_wam, meteofrance_wave. Returns schemaVersion1 with forecast and cache state. Internal forecast temperatures are Fahrenheit, wind knots, precipitation inches; marine wave heights are converted to feet by server/provider.ts. Clients convert to selected units. Inspect src/weather.ts for field names and nullability. `/v1/stations` accepts NO query parameters and returns schemaVersion1, fetchedAt (milliseconds) and the NDBC station collection; callers filter geographic area themselves. Station observations follow the app's field/unit conventions. This wrapper preserves the underlying contract rather than imposing a new normalized schema.

## Limits and errors

Each key has1..600requests/minute,1..100,000/day and1..365day expiration. Defaults in portal:60/minute,1000/day,30days. Usage increments atomically for an authorized request before underlying processing; unsuccessful provider responses still consume quota. Each tile/scan request counts separately, so radar playback consumes many requests. Daily windows UTC calendar dates; minute windows UTC epoch minutes. In addition,600requests/minute/IP/process pre-auth guard and existing provider/process budgets apply. Registry counters shared across API tasks through DynamoDB; old provider limits remain process-local.

| Status | Meaning / action |
|---|---|
|200|Success; inspect product and coverage |
|202|Native scan preparing; retry with backoff |
|400|Invalid query/point/date/station; fix request |
|401|Missing/malformed/expired/revoked key or missing scope; same generic error avoids credential enumeration |
|404|Unsupported route |
|410|Requested radar frame expired; refresh catalog |
|429|Quota or service capacity; Retry-After may be60seconds; a daily exhausted quota needs nextUTCday or administrator adjustment |
|502/503|Provider unavailable, service not configured or temporary failure; exponential backoff |

External responses are `Cache-Control: private, no-store` so a shared CDN cannot bypass authorization. The service retains internal provider caches. CDN authorization/cached finished tiles are future work. CORS is permitted for our admin origin; the external API is initially server-to-server. No arbitrary browser-origin CORS registration, webhook delivery or SLA is implemented. Public legacy mobile routes remain for installed clients; adding v4 does not yet protect all v1-v3 routes from anonymous use.

## Administration API (not for external keys)

`Authorization: Bearer <Cognito access token>` issued by the dedicated pool/app client. Signature/issuer/exp/iat/sub, token_use=access, client_id and group are verified; AdminGetUser checks enabled status. `admin` can mutate, `viewer` can read. Group changes may require token renewal; user disable is checked every request.

| Method / route | Operation |
|---|---|
|GET `/admin/v1/status`|Role, configured providers, explicitly unimplemented customer/mobile-auth capabilities |
|GET `/admin/v1/keys?cursor=...`|50key records/page, no hashes/secrets |
|POST `/admin/v1/keys`|JSON name, scopes[], minuteLimit, dailyLimit, expiresInDays; returns201secret once |
|POST `/admin/v1/keys/{id}/revoke`|Revoke with atomic audit |
|GET `/admin/v1/keys/{id}/usage`|Latest up to31UTC daily counters |
|GET `/admin/v1/users?cursor=...`|Portal Cognito users only |
|POST `/admin/v1/users/status`|JSON username, enabled; self-disable/change rejected |
|GET `/admin/v1/audit?cursor=...`|Newest50events, opaque cursor |

There is no API to read provider secrets or grant admin roles. Administrator invitations/group assignment are operator-controlled AWS Cognito tasks. User state change spans Cognito and DynamoDB, so audit writes intent then completion; absence of completion requires investigation. Key create/revoke and their audit records are atomic DynamoDB transactions. No audit purge UI.
