API Documentation

The HOTA public read-only API — v1

Overview

The Heritage on the Air API gives approved applications programmatic, read-only access to public HOTA data: heritage sites, site codes, activations, live activations, leaderboards, public member profiles and the badge catalogue. All responses are JSON.

Every request must be authenticated with an API key and is subject to per-key rate limits. API keys are issued manually by the HOTA administrator (ZS6SHZ). To request a key, contact us with a short description of your application.

Base URL: https://heritageontheair.co.za/api/v1

Authentication

Send your API key with every request, using either the Authorization bearer header (preferred) or the X-API-Key header:

curl https://heritageontheair.co.za/api/v1/status \
  -H "Authorization: Bearer hota_your_api_key_here"

# or

curl https://heritageontheair.co.za/api/v1/status \
  -H "X-API-Key: hota_your_api_key_here"

Keys look like hota_xxxxxxxx…. Keep them secret — treat a key like a password. A missing, invalid, revoked or expired key returns 401 Unauthorized. Compromised keys can be revoked at any time; contact the administrator for a replacement.

Rate limits

Each key has a per-minute and per-day request limit (defaults: 60/minute and 5,000/day; higher limits are available on request). Every response includes the standard rate-limit headers:

  • X-RateLimit-Limit — your per-minute allowance
  • X-RateLimit-Remaining — requests left in the current window
  • Retry-After — seconds to wait (only when limited)

Exceeding a limit returns 429 Too Many Requests. Cache responses where you can and spread requests out rather than bursting.

Errors

Errors use standard HTTP status codes and a consistent JSON envelope:

{
  "error": {
    "code": "not_found",
    "message": "Resource not found."
  }
}
StatusCodeMeaning
401unauthorizedMissing/invalid/revoked key
404not_foundNo such resource
422validation_failedInvalid query parameters
429rate_limit_exceededToo many requests
500server_errorSomething went wrong our side

Endpoints

GET /status

Health check that also echoes your key's limits. Handy for verifying authentication.

curl https://heritageontheair.co.za/api/v1/status \
  -H "Authorization: Bearer hota_your_api_key_here"

{
  "data": {
    "status": "ok",
    "version": "v1",
    "time": "2026-07-24T10:00:00+00:00",
    "key": { "name": "My App", "rate_limit_per_minute": 60, "rate_limit_per_day": 5000 }
  }
}

GET /sites

Approved heritage sites (paginated).

Query parameters

ParamDescription
typeFilter by site code, e.g. BF (battlefield)
townFilter by town (partial match)
provinceFilter by province (partial match)
searchSearch site name or HOTA number
page, per_pagePagination
curl "https://heritageontheair.co.za/api/v1/sites?type=BF&province=Free%20State" \
  -H "Authorization: Bearer hota_your_api_key_here"

{
  "data": [
    {
      "hota_number": "BF0042",
      "name": "Battle of Blood River",
      "type": { "code": "BF", "description": "Battlefield" },
      "town": "Dundee",
      "province": "KwaZulu-Natal",
      "year": "1838",
      "grid_locator": "KG50",
      "latitude": -28.1076,
      "longitude": 30.5432,
      "url": null,
      "first_activated": "2024-09-24T00:00:00+00:00",
      "last_activated": "2026-06-16T00:00:00+00:00"
    }
  ],
  "links": { "next": "..." },
  "meta": { "current_page": 1, "total": 291 }
}

GET /sites/{hotaNumber}

A single site by its HOTA number, e.g. /sites/BF0042.

GET /activations

Logged activations, newest first (paginated).

Query parameters

ParamDescription
siteFilter by HOTA number, e.g. BF0042
callsignFilter by an activator callsign
from, toDate range (YYYY-MM-DD) on activation date
page, per_pagePagination
curl "https://heritageontheair.co.za/api/v1/activations?site=BF0042" \
  -H "Authorization: Bearer hota_your_api_key_here"

{
  "data": [
    {
      "id": 1234,
      "date": "2026-06-16",
      "start_time": "09:30",
      "first_activation": false,
      "points": 5,
      "activators": ["ZS6SHZ", "ZS6ABC"],
      "site": { "hota_number": "BF0042", "name": "Battle of Blood River", "town": "Dundee", "province": "KwaZulu-Natal" }
    }
  ]
}

GET /activations/live

Today's scheduled activations with status (Upcoming, Starting Soon, Live Now).

GET /leaderboard

Top 10 activators and chasers for the current scoring period and all-time.

curl https://heritageontheair.co.za/api/v1/leaderboard \
  -H "Authorization: Bearer hota_your_api_key_here"

{
  "data": {
    "scoring_period": { "start": "2025-10-29", "end": "2026-10-27" },
    "current_activators": [ { "rank": 1, "callsign": "ZS6SHZ", "first_name": "Shaun", "points": 320 } ],
    "all_time_activators": [ ... ],
    "current_chasers": [ ... ],
    "all_time_chasers": [ ... ]
  }
}

Note: first_name is null for members who keep a private profile; callsigns are always shown.

GET /members

Members with a public profile (paginated). Supports ?search=. No personal contact details are ever exposed.

curl "https://heritageontheair.co.za/api/v1/members?search=ZS6" \
  -H "Authorization: Bearer hota_your_api_key_here"

{ "data": [ { "callsign": "ZS6SHZ", "first_name": "Shaun", "points": 540 } ] }

GET /members/{callsign}

Full public profile with stats and earned badges. Private profiles return 404.

{
  "data": {
    "callsign": "ZS6SHZ",
    "first_name": "Shaun",
    "country": "South Africa",
    "stats": { "total_points": 540, "activation_count": 42, "chase_count": 130, "badge_count": 7 },
    "badges": [ { "id": 1, "name": "First Activation", "category": "milestone", "image_url": "...", "awarded_at": "2024-09-24T00:00:00+00:00" } ]
  }
}

GET /badges

The full badge catalogue.

GET /site-codes

Reference list of site type codes (e.g. BF = Battlefield) with descriptions and score weights.

Versioning

The API is versioned in the URL (/api/v1). Backwards-incompatible changes will be released under a new version; additive changes (new fields, new endpoints) may appear in v1 without notice, so write clients that ignore unknown fields.