Overview
The Jurisdiction API answers "who governs this property?" Give it any coordinate or land parcel in South Africa and it returns the full chain of administrative authority in one call: the province, the district and local municipality, the ward, the registration division and the Deeds Office of jurisdiction — each with its code, name and vintage.
It sits alongside the Parcel Intelligence API and the Encumbrance API: where those describe the parcel itself and what restricts it, this one answers who rates it, who services it, and where its paperwork belongs.
| Use case | How the API helps |
|---|---|
| Rates & municipal accounts | One call per property returns the municipality that rates and services it — with the metro / local / district distinction made for you. |
| Conveyancing & deeds work | The registration division and the Deeds Office of jurisdiction for any point, so documents are filed at the right registry the first time. |
| Bulk-tagging a customer database | POST /batch takes up to 5,000 coordinates per call and
returns the chain for each, in order — municipality and ward columns
for a whole CRM in a handful of calls. |
| Constituency & ward reporting | The 2016-delimitation ward for any address or asset, plus
/boundaries/{type}/{code}/children to walk a
municipality's wards. |
| Service-area qualification | /boundaries listings, boundary polygons and fuzzy
/search ("Overstrand", "Ward 42 Ekurhuleni") for
building coverage rules. |
Authentication
Every request carries your API key in the X-API-KEY header. Keys
are issued per contract by PlotFinder and carry the jurisdiction
product entitlement; a PlotFinder key for another product answers
403 ProductNotPermitted until the product is added to it.
curl -H "X-API-KEY: YOUR_API_KEY_HERE" \ "https://api.starlightsoftware.co.bw/api/v1/jurisdiction/at?lat=-34.4187&lon=19.2345"
https://api.starlightsoftware.co.bw/api/v1/jurisdiction
Response conventions
GET /atandGET /parcel/{lpi}return one top-leveljurisdictionobject; list endpoints return aboundariesarray. Every response carries a human-readablemessage,as_ofanddataset_version.- Every chain link carries its own
vintage— the boundary set it comes from. See Vintage & accuracy. - A chain slot that does not apply is an explicit
nullwith a note saying why — a metro has no district municipality, and some layers are published for part of the country only. A null is never an error. - Every read endpoint takes
?as_at=YYYY-MM-DDand resolves against the boundaries in force on that date (default: today). - A point outside South Africa's demarcated area answers
404
NoJurisdictionAtPointwith the nearest boundary underdetails.nearest— never a wrong answer. - Every response carries an
X-Request-Idheader; error bodies repeat it asrequestId. Quote it when reporting a problem.
Vintage & accuracy
Administrative boundaries change on a cycle, and different layers are published at different moments. Every chain link names its vintage so there is never a question about which boundary set answered:
| Layers | Vintage | What that means |
|---|---|---|
| Province, district municipality, local municipality | 2018 demarcation | The Municipal Demarcation Board's 2018 boundary set, in force since the 2016 local government elections. |
| Ward | 2016 delimitation | The ward set used at the 2016 local government elections. |
| Registration division, Deeds Office, township | cadastral register | The national cadastral dataset's administrative layers — a continuously maintained register rather than a dated cycle. |
How ?as_at behaves. Boundaries carry the dates
they took and left force. A request without as_at resolves
against today's boundaries. With as_at, only the sets in force
on that date answer — so when a future demarcation is loaded, both eras stay
queryable side by side. A date before the earliest loaded set answers with
an empty chain and a note, not a wrong answer.
Points are resolved against the full-resolution boundary geometry, including within a metre of a shared edge — the same coordinate always returns the same chain.
Municipality categories
South African municipalities come in three constitutional categories:
| Category | Kind | In this API |
|---|---|---|
| A | Metropolitan — one municipality for a whole metro (Johannesburg, Cape Town, eThekwini, …) | Returned as the local_municipality with
"category": "A"; district_municipality is
null with a note, because a metro is both the local and
the district authority. |
| B | Local — a town or rural municipality inside a district | Returned as the local_municipality, with its Category C
district in district_municipality. |
| C | District — the umbrella over several Category B locals | Returned in district_municipality, and listable with
its children. |
Coverage
GET /coverage reports live counts, vintages and the load ledger.
The standing picture:
| Layer | Boundaries | Coverage |
|---|---|---|
| Province | 9 | National |
| District municipality | 52 | National (44 districts + the 8 metros' Category A records) |
| Local municipality | 213 | National (205 locals + 8 metros) |
| Ward | 4,392 | National |
| Registration division | 221 | National |
| Township | 5,796 | National — the suburb-scale layer |
| Deeds Office area | 5 | Partial: published for FS, GP and NC only — a null can simply mean the point lies outside the published areas |
| Suburb | 74 | Partial: EC, FS and NC only |
| Pre-1994 municipal | 452 | Partial: EC, FS, NC and WC — for legacy title research |
Rate limits
Limits are per key, per minute, set by your contract. Responses carry the IETF draft standard headers:
RateLimit-Policy: 60;w=60 RateLimit: limit=60, remaining=57, reset=41
Exceeding the limit answers 429
RateLimitExceeded with a Retry-After header.
POST /batch exists for volume work — one request covers up to
5,000 points.
Worked examples
1 · The chain at a point
A coordinate in Hermanus. One call returns everything from the province down to the township, each link carrying its vintage:
curl -H "X-API-KEY: YOUR_API_KEY_HERE" \ "https://api.starlightsoftware.co.bw/api/v1/jurisdiction/at?lat=-34.4187&lon=19.2345"
{
"jurisdiction": {
"province": {
"code": "WC",
"name": "Western Cape",
"vintage": "2018 demarcation",
"effective_from": "2016-08-03"
},
"district_municipality": {
"code": "DC3",
"name": "Overberg",
"category": "C",
"vintage": "2018 demarcation",
"effective_from": "2016-08-03"
},
"local_municipality": {
"code": "WC032",
"name": "Overstrand",
"category": "B",
"vintage": "2018 demarcation",
"effective_from": "2016-08-03"
},
"ward": {
"code": "10302003",
"name": "WC032_3",
"vintage": "2016 delimitation",
"effective_from": "2016-08-03"
},
"registration_division": {
"code": "C013",
"name": "C013",
"category": "ADMIN",
"vintage": "cadastral register"
},
"deeds_office": null,
"township": {
"code": "C0130013",
"name": "HERMANUS",
"vintage": "cadastral register"
}
},
"notes": [
"Deeds Office areas are published for three provinces only (FS, GP, NC); a null here can mean the point lies outside the published areas."
],
"as_at": "2026-08-20",
"message": "Administrative jurisdiction at this point",
"as_of": "2026-08-20",
"dataset_version": "boundaries-2026-08-20"
}
2 · Where do I file?
A property in central Johannesburg: the deeds registry that holds its records, its registration division, and the metro that rates it — with the Category A note making the null district self-explanatory:
curl -H "X-API-KEY: YOUR_API_KEY_HERE" \ "https://api.starlightsoftware.co.bw/api/v1/jurisdiction/at?lat=-26.2041&lon=28.0473"
{
"jurisdiction": {
"province": {
"code": "GP",
"name": "Gauteng",
"vintage": "2018 demarcation",
"effective_from": "2016-08-03"
},
"district_municipality": null,
"local_municipality": {
"code": "JHB",
"name": "City of Johannesburg",
"category": "A",
"vintage": "2018 demarcation",
"effective_from": "2016-08-03"
},
"ward": {
"code": "79800060",
"name": "JHB_60",
"vintage": "2016 delimitation",
"effective_from": "2016-08-03"
},
"registration_division": {
"code": "IR",
"name": "IR",
"category": "ADMIN",
"vintage": "cadastral register"
},
"deeds_office": {
"code": "RAND TOWNSHIP REGISTER",
"name": "Rand Township Register",
"vintage": "cadastral register"
},
"township": {
"code": "T0IR0310",
"name": "JOHANNESBURG",
"vintage": "cadastral register"
}
},
"notes": [
"This point falls inside a Category A metropolitan municipality, which is both the local and the district authority — no separate district municipality applies."
],
"as_at": "2026-08-20",
"message": "Administrative jurisdiction at this point",
"as_of": "2026-08-20",
"dataset_version": "boundaries-2026-08-20"
}
3 · Bulk-tagging a customer list
The volume product: up to 5,000 coordinates per call, results in input
order, null where a point falls outside South Africa's
demarcated area. Tag a whole customer database with municipality and ward
in a handful of calls:
curl -X POST -H "X-API-KEY: YOUR_API_KEY_HERE" -H "Content-Type: application/json" \
-d '{
"points": [
{"latitude": -34.4187, "longitude": 19.2345},
{"latitude": -26.2041, "longitude": 28.0473},
{"latitude": -29.8285, "longitude": 31.0303},
{"latitude": -35.5, "longitude": 19.0}
]
}' \
"https://api.starlightsoftware.co.bw/api/v1/jurisdiction/batch"
const rows = customers.map((c) => ({ latitude: c.lat, longitude: c.lng }));
const res = await fetch(
"https://api.starlightsoftware.co.bw/api/v1/jurisdiction/batch",
{
method: "POST",
headers: {
"X-API-KEY": process.env.PLOTFINDER_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({ points: rows.slice(0, 5000) }),
},
);
const { results } = await res.json();
customers.forEach((c, i) => {
c.municipality = results[i]?.jurisdiction.local_municipality?.name ?? null;
c.ward = results[i]?.jurisdiction.ward?.code ?? null;
});
{
"results": [
{
"jurisdiction": {
"province": {
"code": "WC",
"name": "Western Cape",
"vintage": "2018 demarcation",
"effective_from": "2016-08-03"
},
"district_municipality": {
"code": "DC3",
"name": "Overberg",
"category": "C",
"vintage": "2018 demarcation",
"effective_from": "2016-08-03"
},
"local_municipality": {
"code": "WC032",
"name": "Overstrand",
"category": "B",
"vintage": "2018 demarcation",
"effective_from": "2016-08-03"
},
"ward": {
"code": "10302003",
"name": "WC032_3",
"vintage": "2016 delimitation",
"effective_from": "2016-08-03"
},
"registration_division": {
"code": "C013",
"name": "C013",
"category": "ADMIN",
"vintage": "cadastral register"
},
"deeds_office": null,
"township": {
"code": "C0130013",
"name": "HERMANUS",
"vintage": "cadastral register"
}
},
"notes": [
"Deeds Office areas are published for three provinces only (FS, GP, NC); a null here can mean the point lies outside the published areas."
]
},
{
"jurisdiction": {
"province": {
"code": "GP",
"name": "Gauteng",
"vintage": "2018 demarcation",
"effective_from": "2016-08-03"
},
"district_municipality": null,
"local_municipality": {
"code": "JHB",
"name": "City of Johannesburg",
"category": "A",
"vintage": "2018 demarcation",
"effective_from": "2016-08-03"
},
"ward": {
"code": "79800103",
"name": "JHB_103",
"vintage": "2016 delimitation",
"effective_from": "2016-08-03"
},
"registration_division": {
"code": "IR",
"name": "IR",
"category": "ADMIN",
"vintage": "cadastral register"
},
"deeds_office": null,
"township": {
"code": "T0IR0591",
"name": "SANDOWN",
"vintage": "cadastral register"
}
},
"notes": [
"This point falls inside a Category A metropolitan municipality, which is both the local and the district authority — no separate district municipality applies.",
"Deeds Office areas are published for three provinces only (FS, GP, NC); a null here can mean the point lies outside the published areas."
]
},
{
"jurisdiction": {
"province": {
"code": "KZ",
"name": "KwaZulu-Natal",
"vintage": "2018 demarcation",
"effective_from": "2016-08-03"
},
"district_municipality": null,
"local_municipality": {
"code": "ETH",
"name": "eThekwini",
"category": "A",
"vintage": "2018 demarcation",
"effective_from": "2016-08-03"
},
"ward": {
"code": "59500027",
"name": "ETH_27",
"vintage": "2016 delimitation",
"effective_from": "2016-08-03"
},
"registration_division": {
"code": "FU",
"name": "FU",
"category": "ADMIN",
"vintage": "cadastral register"
},
"deeds_office": null,
"township": {
"code": "N0FU0085",
"name": "DURBAN",
"vintage": "cadastral register"
}
},
"notes": [
"This point falls inside a Category A metropolitan municipality, which is both the local and the district authority — no separate district municipality applies.",
"Deeds Office areas are published for three provinces only (FS, GP, NC); a null here can mean the point lies outside the published areas."
]
},
null
],
"found": 3,
"not_found": 1,
"as_at": "2026-08-20",
"message": "Resolved 3 of 4 points",
"as_of": "2026-08-20",
"dataset_version": "boundaries-2026-08-20"
}
Endpoint reference
The machine-readable versions are at
/jurisdiction/openapi.json and
/jurisdiction/postman.json.
GET /api/v1/jurisdiction/at
The full chain of administrative authority at a point
Who rates and taxes this property, who services it, which ward covers it, where to file: province, district municipality, local municipality, ward, registration division, Deeds Office of jurisdiction and township, resolved from one coordinate. Each link carries its code, name and vintage.
| Parameter | In | Required | Description |
|---|---|---|---|
lat | query | no | Latitude, -90..90 |
lon | query | no | Longitude, -180..180 |
as_at | query | no | Resolve as at this date (YYYY-MM-DD) |
GET /api/v1/jurisdiction/parcel/{lpi}
The chain for a land parcel, by LPI
The same chain as /at, resolved from the parcel's label point — with an explicit note when the parcel's boundary straddles an administrative boundary, and which boundaries it crosses.
| Parameter | In | Required | Description |
|---|---|---|---|
lpi | path | yes | |
as_at | query | no | Resolve as at this date (YYYY-MM-DD) |
POST /api/v1/jurisdiction/batch
The chain for up to 5,000 coordinates in one call
Bulk-tag a customer database with municipality and ward: POST {"points": [{"latitude": …, "longitude": …}, …]} and results come back in input order — null where a point lies outside South Africa's demarcated area.
Send a JSON body; see the worked examples above for the shape.
GET /api/v1/jurisdiction/boundaries/{boundary_type}
List boundaries of a type
| Parameter | In | Required | Description |
|---|---|---|---|
boundary_type | path | yes | |
province | query | no | Family code, e.g. GP |
municipality | query | no | Parent municipality code, e.g. CPT |
parent | query | no | Parent code at any level |
as_at | query | no | |
limit | query | no | |
cursor | query | no |
GET /api/v1/jurisdiction/boundaries/{boundary_type}/{code}
One boundary, with attributes
| Parameter | In | Required | Description |
|---|---|---|---|
boundary_type | path | yes | |
code | path | yes | |
as_at | query | no |
GET /api/v1/jurisdiction/boundaries/{boundary_type}/{code}/geometry
A boundary's polygon
| Parameter | In | Required | Description |
|---|---|---|---|
boundary_type | path | yes | |
code | path | yes | |
format | query | no | |
simplify | query | no | metres |
as_at | query | no |
GET /api/v1/jurisdiction/boundaries/{boundary_type}/{code}/children
The boundaries one level down
Wards in a municipality, municipalities in a district, districts in a province.
| Parameter | In | Required | Description |
|---|---|---|---|
boundary_type | path | yes | |
code | path | yes | |
as_at | query | no |
GET /api/v1/jurisdiction/search
Fuzzy name search across all boundary types
Trigram-matched: "Overstrand", "Ward 42 Ekurhuleni", "Bloemfontein".
| Parameter | In | Required | Description |
|---|---|---|---|
q | query | yes | |
types | query | no | comma list; default all |
as_at | query | no | |
limit | query | no |
GET /api/v1/jurisdiction/coverage
Layer vintages, counts and known gaps
GET /api/v1/jurisdiction/heatmap
Coverage-map data: administrative boundaries for a viewport
Drives coverage maps, including the PlotFinder admin panel's Jurisdiction layer. Always geometry (the whole set is small): province, district and municipal outlines at any span, wards and townships scale-gated the way every family map layer behaves.
| Parameter | In | Required | Description |
|---|---|---|---|
north | query | no | |
south | query | no | |
east | query | no | |
west | query | no | |
types | query | no | comma list; default the municipal chain |
GET /api/v1/jurisdiction/tiles/{z}/{x}/{y}.mvt
Administrative overlay vector tiles
| Parameter | In | Required | Description |
|---|---|---|---|
z | path | yes | |
x | path | yes | |
y | path | yes | |
layers | query | no |
Error codes
Errors share one flat envelope; details appears when there is something structured to say:
{
"error": "ValidationError",
"message": "Invalid query parameters",
"details": [{ "field": "lat", "message": "lat is required" }],
"requestId": "78be60d3-8000-4601-9fda-7d5e6838388d"
}
| Status | error | Meaning |
|---|---|---|
| 400 | ValidationError | A parameter or body failed validation; see details. |
| 401 | ApiKeyMissing / ApiKeyInvalid / ApiKeySuspended / ApiKeyExpired | The X-API-KEY header is absent, wrong, suspended, or past its expiry. |
| 403 | ProductNotPermitted | The key does not carry the Jurisdiction API product. |
| 403 | CountryNotPermitted | The key is scoped to Botswana only; this product serves South Africa. |
| 404 | NoJurisdictionAtPoint | No boundary contains the point; the nearest boundary is under details.nearest. |
| 404 | BoundaryNotFound | No boundary of that type carries the requested code at the requested date. |
| 404 | ParcelNotFound | No parcel carries the requested LPI. |
| 429 | RateLimitExceeded | Per-minute limit reached; honour Retry-After. |
| 503 | JurisdictionDataUnavailable | The boundary dataset is not installed on this deployment yet. |