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 activation spots. limit 1–500 (default 200).
GET/api/v1/spots/streamSSE: 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/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. K-1234), 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 QSO breakdown + YoY for current user (optional club_id)
GET/api/v1/admin/healthprofile:read + adminFeed daemon status, stuck logs, counts
POST/api/v1/admin/logs/{id}/force-unlockprofile:read + adminRestart 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":"K-1234","frequency":"14.062","mode":"CW"}' \
  https://clubsota.app/api/v1/spots
curl -s https://clubsota.app/api/v1/entities