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. The request and response values below are illustrative.

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 /api/v1/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 /api/v1/site-codes

All site type codes, ordered by code, with English and Afrikaans descriptions and score weights. Use code as the type filter on /api/v1/sites. This endpoint has no query parameters or pagination.

curl https://heritageontheair.co.za/api/v1/site-codes \
  -H "Accept: application/json" \
  -H "Authorization: Bearer hota_your_api_key_here"

{
  "data": [
    {
      "code": "BF",
      "description": "Battlefield",
      "description_afr": "Slagveld",
      "score_weight": 5
    }
  ]
}

The values above are illustrative. score_weight is an integer or null when unset.

GET /api/v1/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=KwaZulu-Natal" \
  -H "Authorization: Bearer hota_your_api_key_here"

{
  "data": [
    {
      "hota_number": "ZABF0042",
      "name": "Battle of Blood River",
      "info": "Example heritage site description.",
      "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 /api/v1/sites/{hotaNumber}

A single approved site by its HOTA number, e.g. /api/v1/sites/ZABF0042. Both ZABF0042 and BF0042 are accepted; responses include the country prefix.

GET /api/v1/activations

Logged activations, newest first (paginated).

Query parameters

ParamDescription
siteFilter by HOTA number, e.g. ZABF0042 or 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=ZABF0042" \
  -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"],
      "operator_callsign": "ZS6SHZ",
      "contacts": [
        { "id": 42, "callsign": "ZS6XYZ", "points": 2.65 },
        { "id": 43, "callsign": "ZS2NEW", "points": 2.65 }
      ],
      "site": { "hota_number": "ZABF0042", "name": "Battle of Blood River", "town": "Dundee", "province": "KwaZulu-Natal" }
    }
  ]
}

operator_callsign identifies the account that submitted the activation log. contacts includes each qualifying hunter contact with its calculated hunter points. Each contact’s id is its stable activation-line ID, including before that callsign registers. Hunter standings and member totals come from these activator-uploaded contacts; hunters do not upload separate logs.

GET /api/v1/activations/live

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

GET /api/v1/leaderboard

Top 10 activators and hunters 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_hunters": [ ... ],
    "all_time_hunters": [ ... ]
  }
}

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

GET /api/v1/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 /api/v1/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, "hunting_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 /api/v1/badges

The full badge catalogue.

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.