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
Contents
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 allowanceX-RateLimit-Remaining— requests left in the current windowRetry-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."
}
}
| Status | Code | Meaning |
|---|---|---|
| 401 | unauthorized | Missing/invalid/revoked key |
| 404 | not_found | No such resource |
| 422 | validation_failed | Invalid query parameters |
| 429 | rate_limit_exceeded | Too many requests |
| 500 | server_error | Something went wrong our side |
Pagination
/api/v1/sites, /api/v1/activations and /api/v1/members are paginated. Use ?page=N to page and ?per_page=N
to size (default 25, max 100). Responses carry data, links and
meta blocks. /api/v1/site-codes returns the complete list in data without pagination:
{
"data": [ ... ],
"links": { "first": "...", "last": "...", "prev": null, "next": "..." },
"meta": { "current_page": 1, "last_page": 12, "per_page": 25, "total": 291 }
}
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
| Param | Description |
|---|---|
type | Filter by site code, e.g. BF (battlefield) |
town | Filter by town (partial match) |
province | Filter by province (partial match) |
search | Search site name or HOTA number |
page, per_page | Pagination |
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
| Param | Description |
|---|---|
site | Filter by HOTA number, e.g. ZABF0042 or BF0042 |
callsign | Filter by an activator callsign |
from, to | Date range (YYYY-MM-DD) on activation date |
page, per_page | Pagination |
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.