Versioned JSON API for third-party logger integrations. Base URL: /api/v1
Authenticate with Authorization: Bearer cota_…. Create tokens at /settings/api (login required).
Scopes (CSV on token): spots:read, spots:write, logs:write, parks:read, awards:read, profile:read, clubs:read, schedule:write
Authorization: Bearer cota_your_token_here
{"ok": true, "data": { ... }, "meta": { ... }}
{"ok": false, "error": "message", "code": "ERROR_CODE", "details": { ... }}
Stable error codes include UNAUTHORIZED, FORBIDDEN, VALIDATION_ERROR, NOT_FOUND, TOKEN_EXPIRED, SERVICE_UNAVAILABLE, INTERNAL_ERROR, IMPORT_FAILED.
Active spots include: id, activator_callsign, park_reference, park_name, frequency, band, mode, comments, spotter_callsign, is_qrt, spot_count, age_seconds, age_label, ttl_seconds, expires_at, club_id, club_name, club_callsign, coordinates when known.
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/health | Service health + database check. Returns 503 when degraded. |
| GET | /api/v1/spots?park=&call=&mode=&club_id=&limit=&include_qrt=1 | Active activation spots. limit 1–500 (default 200). |
| GET | /api/v1/spots/stream | SSE: snapshot on connect, spots on change, : ping heartbeats, done after ~5 minutes. Headers include X-Accel-Buffering: no. |
| GET | /api/v1/parks?q=&location=&limit=&offset= | Search parks catalog. Offset capped. |
| GET | /api/v1/parks/{reference} | Park detail (sanitized fields) |
| GET | /api/v1/parks/nearby?lat=&lng=&radius_km=&limit= | Parks near coordinates (numeric lat/lng required) |
| GET | /api/v1/awards/definitions | Award tier definitions |
| GET | /api/v1/leaderboards/activators | Top activators by unique parks |
| GET | /api/v1/leaderboards/hunters | Top hunters by unique parks |
| GET | /api/v1/leaderboards/clubs | Top clubs by unique parks |
| GET | /api/v1/leaderboards/by-band | Public QSO counts by band + site YoY |
| GET | /api/v1/leaderboards/by-mode | Public QSO counts by mode + site YoY |
| GET | /api/v1/leaderboards/activators/filtered?band=&mode= | Top activators filtered by band and/or mode |
| GET | /api/v1/schedule?limit= | Upcoming scheduled activations |
| GET | /api/v1/entities?kind=&status=&limit=&offset= | ClubsOTA entity overlays (additive; not the POTA parks catalog). status=all for retired too. |
| Method | Path | Scopes | Description |
|---|---|---|---|
| GET | /api/v1/me | profile:read | Current user profile |
| POST | /api/v1/spots | spots:write | JSON: activator_callsign, park_reference (e.g. K-1234), frequency (MHz or kHz), mode, optional comments, club_id |
| POST | /api/v1/spots/{id}/qrt | spots:write | Mark spot QRT (owner/admin). 404 if missing. |
| POST | /api/v1/spots/{id}/respot | spots:write | Refresh spot TTL (owner/admin) |
| POST | /api/v1/logs | logs:write | Import ADIF (multipart adif or JSON adif; optional park_reference, club_id). Strict validation: Call, Date, Time, Band, Mode. Returns parse_errors, mutable_until. |
| GET | /api/v1/logs | logs:write | List your uploads with lock-window flags (is_mutable, days_remaining). 30-day rolling edit window. |
| GET | /api/v1/logs/{id} | logs:write | Upload detail |
| DELETE | /api/v1/logs/{id} | logs:write | Withdraw within 30 days (derived QSOs removed; archive kept). Locked → LOG_LOCKED. |
| POST | /api/v1/logs/{id}/replace | logs:write | Re-upload replacing a mutable log; window does not reset. |
| POST | /api/v1/entities | profile:read + admin | Create entity overlay (park/summit + geo) |
| POST | /api/v1/entities/{id}/retire | profile:read + admin | Retire overlay |
| GET | /api/v1/me/stats/breakdown | profile:read | Band/mode QSO breakdown + YoY for current user (optional club_id) |
| GET | /api/v1/admin/health | profile:read + admin | Feed daemon status, stuck logs, counts |
| POST | /api/v1/admin/logs/{id}/force-unlock | profile:read + admin | Restart 30-day edit window for a locked upload |
| GET | /api/v1/me/awards | profile:read, awards:read | Earned awards |
| GET | /api/v1/me/activations | profile:read | Your activations (limit 1–200) |
| GET | /api/v1/me/hunter-credits | profile:read | Your hunter credits (limit 1–500) |
| POST | /api/v1/schedule | schedule:write | Schedule activation (planned_start_utc, park_reference, …) |
| GET | /api/v1/tokens | profile:read | List your API tokens |
| POST | /api/v1/tokens | profile:read | Create token JSON: name, scopes[] |
| DELETE | /api/v1/tokens/{id} | profile:read | Revoke token |
mutable_until = created_at + 30 days (UTC). Within the window: withdraw or replace.mutable_until (clock does not reset).status=withdrawn.curl -s https://clubsota.app/api/v1/health
curl -s 'https://clubsota.app/api/v1/spots?limit=50&mode=CW'
curl -N https://clubsota.app/api/v1/spots/stream
curl -H "Authorization: Bearer cota_…" https://clubsota.app/api/v1/me
curl -H "Authorization: Bearer cota_…" https://clubsota.app/api/v1/logs
curl -X POST -H "Authorization: Bearer cota_…" -H "Content-Type: application/json" \
-d '{"activator_callsign":"K1ABC","park_reference":"K-1234","frequency":"14.062","mode":"CW"}' \
https://clubsota.app/api/v1/spots
curl -s https://clubsota.app/api/v1/entities