ClubsOTA REST API v1

Versioned JSON API for third-party logger integrations. Base URL: /api/v1

Authenticate with Authorization: Bearer cota_…. Create tokens at /settings/api (login required).

Authentication

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

Response envelope

{"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.

Spot object fields

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.

Public endpoints

MethodPathDescription
GET/api/v1/healthService health + database check. Returns 503 when degraded.
GET/api/v1/spots?park=&call=&mode=&club_id=&limit=&include_qrt=1Active native activation spots (ClubsOTA + RBN ingest). limit 1–500 (default 200).
GET/api/v1/spots/potaProxied live POTA activator spots from api.pota.app (short file cache; poll, not SSE).
GET/api/v1/spots/boardMerged board: native/RBN + POTA (deduped; native wins). For UI polling.
GET/api/v1/spots/streamSSE of native spots only: 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/definitionsAward tier definitions
GET/api/v1/leaderboards/activatorsTop activators by unique parks
GET/api/v1/leaderboards/huntersTop hunters by unique parks
GET/api/v1/leaderboards/clubsTop clubs by unique parks
GET/api/v1/leaderboards/by-bandPublic QSO counts by band + site YoY
GET/api/v1/leaderboards/by-modePublic 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/leaderboards/recordsPublic PBs: fastest-to-10, biggest activation (successful only)
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.

Authenticated endpoints

MethodPathScopesDescription
GET/api/v1/meprofile:readCurrent user profile
POST/api/v1/spotsspots:writeJSON: activator_callsign, park_reference (e.g. US-0016), frequency (MHz or kHz), mode, optional comments, club_id
POST/api/v1/spots/{id}/qrtspots:writeMark spot QRT (owner/admin). 404 if missing.
POST/api/v1/spots/{id}/respotspots:writeRefresh spot TTL (owner/admin)
POST/api/v1/logslogs:writeImport 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/logslogs:writeList your uploads with lock-window flags (is_mutable, days_remaining). 30-day rolling edit window.
GET/api/v1/logs/{id}logs:writeUpload detail
DELETE/api/v1/logs/{id}logs:writeWithdraw within 30 days (derived QSOs removed; archive kept). Locked → LOG_LOCKED.
POST/api/v1/logs/{id}/replacelogs:writeRe-upload replacing a mutable log; window does not reset.
POST/api/v1/entitiesprofile:read + adminCreate entity overlay (park/summit + geo)
POST/api/v1/entities/{id}/retireprofile:read + adminRetire overlay
GET/api/v1/me/stats/breakdownprofile:readBand/mode + YoY for successful activation QSOs (optional club_id)
GET/api/v1/me/stats/enrichedprofile:readRollups, award progress, YTD highlights, activation streak, goals
GET/api/v1/me/recordsprofile:readPersonal bests (fastest/biggest/diverse/month/streak/run rates)
GET/api/v1/me/parks/milestonesprofile:readYour parks list with Century / 1K progress
GET/api/v1/me/parks/{reference}/milestonesprofile:readPark Century/1K + fastest-to-N for one park
GET/api/v1/me/goalsprofile:readList personal goals + metric catalog
POST/api/v1/me/goalsprofile:readCreate goal JSON: metric, target, optional period (all_time|calendar_year), year, label
PUT/PATCH/api/v1/me/goals/{id}profile:readUpdate goal target / label
DELETE/api/v1/me/goals/{id}profile:readDelete a personal goal
GET/api/v1/admin/healthprofile:read + staff (admin/helpdesk)Feed daemon status, stuck logs, counts
POST/api/v1/admin/logs/{id}/force-unlockprofile:read + staff (admin/helpdesk)Restart 30-day edit window for a locked upload
GET/api/v1/me/awardsprofile:read, awards:readEarned awards
GET/api/v1/me/activationsprofile:readYour activations (limit 1–200)
GET/api/v1/me/hunter-creditsprofile:readYour hunter credits (limit 1–500)
POST/api/v1/scheduleschedule:writeSchedule activation (planned_start_utc, park_reference, …)
GET/api/v1/tokensprofile:readList your API tokens
POST/api/v1/tokensprofile:readCreate token JSON: name, scopes[]
DELETE/api/v1/tokens/{id}profile:readRevoke token

Log lock rules (30-day window)

Example

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":"US-0016","frequency":"14.062","mode":"CW"}' \
  https://clubsota.app/api/v1/spots
curl -s https://clubsota.app/api/v1/entities