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
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
List endpoints are paginated. Use ?page=N to page and ?per_page=N
to size (default 25, max 100). Responses carry data, links and
meta blocks:
{
"data": [ ... ],
"links": { "first": "...", "last": "...", "prev": null, "next": "..." },
"meta": { "current_page": 1, "last_page": 12, "per_page": 25, "total": 291 }
}
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
| 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=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
| Param | Description |
|---|---|
site | Filter by HOTA number, e.g. 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=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.