Overview
The PlotFinder API answers two questions about land in Botswana and South Africa: what plots are near this point? and where is this plot number? It is served from our own database of registered parcels, so responses are fast and do not depend on a third party being online at request time. See Field differences by country for how the two are told apart in responses.
| Use case | How the API helps |
|---|---|
| Logistics & delivery | Turn a customer's plot number into coordinates a driver can navigate to, or confirm which plot a driver is currently parked on. |
| Real estate | Place listings accurately on a map and find comparable plots within a radius. |
| Field services & utilities | Identify the plot at a technician's current GPS position. |
| Insurance & finance | Verify that a stated plot number resolves to a real, located property. |
Base URL
All endpoints are versioned under /api/v1.
https://api.starlightsoftware.co.bw/api/v1
Responses are always JSON. A successful lookup
that simply finds nothing is still
200 with an empty
plots array — finding no plots is a valid answer, not
an error.
Getting Access
API access is issued under a commercial agreement. There is no public signup — every key is created by hand once an agreement is in place, so that usage limits and terms match what was actually agreed.
To request access, email info@starlightsoftware.co.bw with your business name and intended use case. Telling us your expected request volume up front lets us set an appropriate rate limit from day one.
Your key is shown once. When we issue it, the key is displayed a single time and is not recoverable afterwards — we store only a cryptographic hash of it. Save it straight into your secret manager. If it is ever lost or exposed, contact us and we will revoke it and issue a replacement.
Authentication
Every request to /api/v1/plots/* must carry your key in
an X-API-KEY header. There is no OAuth flow and no
token exchange step — the key alone authenticates the request.
curl -H "X-API-KEY: YOUR_API_KEY_HERE" \ "https://api.starlightsoftware.co.bw/api/v1/plots/reverse?latitude=-24.66252002&longitude=25.90249236"
const res = await fetch(
"https://api.starlightsoftware.co.bw/api/v1/plots/reverse?latitude=-24.66252002&longitude=25.90249236",
{ headers: { "X-API-KEY": process.env.PLOTFINDER_API_KEY } }
);
const data = await res.json();
Keep your key server-side. Never ship it in a mobile app, a browser bundle, or a public repository — anything shipped to a user's device can be extracted from it. Call the PlotFinder API from your own backend and let your app talk to that.
Countries
The same endpoints serve every country we cover. They share a response shape; almost nothing else about them is the same. Read whichever one you are buying — and if you are buying more than one, read each, because code that assumes one will misread another without erroring.
Botswana — plots
26 towns and cities, every plot carrying its
district. The identifier is a
plot number.
Plot numbers are not unique. The same number
recurs across towns, and sometimes twice within one town at
locations kilometres apart. Every lookup therefore returns an
array, and what you can rely on is
plot_number plus city plus the
coordinates — never the number alone.
Botswana records carry a district and no
province. This is the dataset the coverage list at
/api/v1/cities returns by default.
South Africa — erven
Erven by township, across all nine provinces. The identifier is
an erf number, and it travels in the same
plot_number field —
parcel_identifier_type tells you which you are
holding.
An erf number on its own identifies nothing.
Erf numbers restart at 1 in every township, so erf 1 exists
thousands of times over. Always qualify a South African lookup
with the township (city) and check the
province on the way back — township names
themselves repeat across provinces.
South African records carry a province and no
district. Erf portions are addressed as
erf/portion: erf 123 portion 4 is
123/4.
Coverage is thousands of townships rather than 26 towns, which
is why /api/v1/cities does not include them unless
you ask: use ?country=SOUTH_AFRICA, or
?country=ALL for everything your key covers.
What your key covers
Keys are issued per country, and optionally per town. Omitting
country searches everything your key covers —
so a Botswana key that omits it gets Botswana, not both. Asking for
a country or a city outside your key returns
403 rather than an empty result, so a missing
entitlement never looks like missing data. A key may also carry a
term, after which it answers 401 ApiKeyExpired naming
the date.
The field-by-field comparison is under Field differences by country.
Endpoints
Three read-only endpoints. All return the same response envelope: a
plots array and a message.
Returns registered plots within a radius of a coordinate,
nearest first. Each result includes
distance_meters from the point you supplied.
Parameters
| Name | Type | Description | |
|---|---|---|---|
latitude |
float | Required | Between −90 and 90. |
longitude |
float | Required | Between −180 and 180. |
radius |
integer | Optional |
Search radius in whole metres, 1–50000. Defaults to
500.
|
limit |
integer | Optional |
Maximum plots to return, 1–500. Defaults to
100.
|
country |
string | Optional |
BOTSWANA or SOUTH_AFRICA,
case-insensitive. Omitted, the search covers both.
|
Botswana
curl -H "X-API-KEY: YOUR_API_KEY_HERE" \ "https://api.starlightsoftware.co.bw/api/v1/plots/search-given-coordinates?latitude=-24.66252002&longitude=25.90249236&radius=200&country=BOTSWANA"
{
"plots": [
{
"id": 1,
"plot_number": "2",
"latitude": -24.66252,
"longitude": 25.9024924,
"city": "GABORONE",
"country": "BOTSWANA",
"parcel_identifier_type": "PLOT_NUMBER",
"district": "SOUTH-EAST DISTRICT",
"distance_meters": 0
},
{
"id": 12346,
"plot_number": "13962",
"latitude": -24.6617906,
"longitude": 25.9015311,
"city": "GABORONE",
"country": "BOTSWANA",
"parcel_identifier_type": "PLOT_NUMBER",
"district": "SOUTH-EAST DISTRICT",
"distance_meters": 126.5
}
],
"message": "Plots fetched successfully"
}
South Africa
curl -H "X-API-KEY: YOUR_API_KEY_HERE" \ "https://api.starlightsoftware.co.bw/api/v1/plots/search-given-coordinates?latitude=-28.732384&longitude=24.7621112&radius=200&country=SOUTH_AFRICA&limit=3"
{
"plots": [
{
"id": 595404,
"plot_number": "11862",
"latitude": -28.732384,
"longitude": 24.7621112,
"city": "KIMBERLEY",
"country": "SOUTH_AFRICA",
"parcel_identifier_type": "ERF_NUMBER",
"province": "NORTHERN CAPE",
"distance_meters": 0
},
{
"id": 630355,
"plot_number": "7361",
"latitude": -28.7320475,
"longitude": 24.7613427,
"city": "KIMBERLEY",
"country": "SOUTH_AFRICA",
"parcel_identifier_type": "ERF_NUMBER",
"province": "NORTHERN CAPE",
"distance_meters": 83.8
},
{
"id": 630076,
"plot_number": "6937",
"latitude": -28.7321936,
"longitude": 24.7629819,
"city": "KIMBERLEY",
"country": "SOUTH_AFRICA",
"parcel_identifier_type": "ERF_NUMBER",
"province": "NORTHERN CAPE",
"distance_meters": 87.6
}
],
"message": "Plots fetched successfully"
}
Townships are dense: a 200 m radius in a South African
suburb can cover dozens of erven, so
limit matters more here than it does in
Botswana.
Returns the coordinates of a plot by its number. No
distance_meters is included, because no reference
point was supplied.
This always returns an array. A plot number
is not unique in Botswana: the same number occurs in several
towns, and can even occur more than once within one town at
different locations. South African erf numbers repeat far more
— they restart at 1 in every township. Pass
city and/or country to narrow the
result, and design your integration to handle more than one
match.
Parameters
| Name | Type | Description | |
|---|---|---|---|
plot_number |
string | Required | Path segment. The plot number to look up. |
city |
string | Optional |
Query parameter. Narrows results to one city.
Case-insensitive —
kasane and KASANE both work.
|
limit |
integer | Optional |
Maximum plots to return, 1–500. Defaults to
100.
|
country |
string | Optional |
BOTSWANA or SOUTH_AFRICA,
case-insensitive. Omitted, both are searched.
|
Botswana
curl -H "X-API-KEY: YOUR_API_KEY_HERE" \ "https://api.starlightsoftware.co.bw/api/v1/plots/73?city=kasane&country=BOTSWANA"
{
"plots": [
{
"id": 160572,
"plot_number": "73",
"latitude": -17.7997276,
"longitude": 25.1532716,
"city": "KASANE",
"country": "BOTSWANA",
"parcel_identifier_type": "PLOT_NUMBER",
"district": "CHOBE DISTRICT"
}
],
"message": "Plots fetched successfully"
}
South Africa
curl -H "X-API-KEY: YOUR_API_KEY_HERE" \ "https://api.starlightsoftware.co.bw/api/v1/plots/11862?city=kimberley&country=SOUTH_AFRICA"
{
"plots": [
{
"id": 595404,
"plot_number": "11862",
"latitude": -28.732384,
"longitude": 24.7621112,
"city": "KIMBERLEY",
"country": "SOUTH_AFRICA",
"parcel_identifier_type": "ERF_NUMBER",
"province": "NORTHERN CAPE"
}
],
"message": "Plots fetched successfully"
}
Dropping city here would return every erf 11862
in the country. The township is not optional in practice,
even though the parameter is.
{
"plots": [],
"message": "No plots found"
}
Returns the single nearest plot to a coordinate — the "what plot am I standing on?" lookup. Useful for drivers confirming a delivery address and for field staff logging a visit.
Parameters
| Name | Type | Description | |
|---|---|---|---|
latitude |
float | Required | Between −90 and 90. |
longitude |
float | Required | Between −180 and 180. |
country |
string | Optional |
BOTSWANA or SOUTH_AFRICA,
case-insensitive. Omitted, both are searched.
|
The search widens progressively until it finds something, so a point right on a plot is answered immediately. If nothing is found within roughly 100 km, an empty array is returned.
Botswana
curl -H "X-API-KEY: YOUR_API_KEY_HERE" \ "https://api.starlightsoftware.co.bw/api/v1/plots/reverse?latitude=-24.66252002&longitude=25.90249236&country=BOTSWANA"
{
"plots": [
{
"id": 1,
"plot_number": "2",
"latitude": -24.66252,
"longitude": 25.9024924,
"city": "GABORONE",
"country": "BOTSWANA",
"parcel_identifier_type": "PLOT_NUMBER",
"district": "SOUTH-EAST DISTRICT",
"distance_meters": 0
}
],
"message": "Plots fetched successfully"
}
South Africa
curl -H "X-API-KEY: YOUR_API_KEY_HERE" \ "https://api.starlightsoftware.co.bw/api/v1/plots/reverse?latitude=-28.732384&longitude=24.7621112&country=SOUTH_AFRICA"
{
"plots": [
{
"id": 595404,
"plot_number": "11862",
"latitude": -28.732384,
"longitude": 24.7621112,
"city": "KIMBERLEY",
"country": "SOUTH_AFRICA",
"parcel_identifier_type": "ERF_NUMBER",
"province": "NORTHERN CAPE",
"distance_meters": 0
}
],
"message": "Plots fetched successfully"
}
Without country this lookup searches both, and
near the border the nearest parcel may be in the other one —
which is correct, and rarely what an application wants.
Every plot object names the country it belongs to, so an
integration written for one keeps working when another is added.
South African records travel with the
erf number in the same
plot_number field, and
parcel_identifier_type says what the value is.
| Field | Botswana | South Africa |
|---|---|---|
country |
BOTSWANA |
SOUTH_AFRICA |
parcel_identifier_type |
PLOT_NUMBER |
ERF_NUMBER |
province |
Not returned | Province name, e.g. NORTHERN CAPE |
city |
Town | Township / allotment area |
district |
District | Not returned |
Rate Limits
Each key has its own per-minute allowance, agreed with you when the key is issued. Every response carries standard rate-limit headers, so you can pace your client rather than waiting to be rejected.
RateLimit-Policy: 60;w=60 RateLimit: limit=60, remaining=59, reset=60
remaining is the number of requests left in the current
window; reset is the seconds until it refills.
Exceeding the limit returns 429. If you
consistently need more, contact us — the limit is a contract
setting, not a technical ceiling.
Errors
Errors return a consistent JSON body:
{
"error": "ValidationError",
"message": "Invalid query parameters",
"details": [{ "field": "latitude", "message": "latitude is required" }],
"requestId": "78be60d3-8000-4601-9fda-7d5e6838388d"
}
Every response includes a requestId, also returned as
the X-Request-Id header.
Quote it when contacting support — it matches our
server logs and turns a vague report into a two-minute diagnosis.
Status codes
| Status | Meaning | What to do |
|---|---|---|
| 200 |
Success. Note that an empty plots array is
still a 200.
|
Check whether plots is empty before reading
[0].
|
| 400 |
Invalid parameters — missing, non-numeric, or out of range.
Every invalid field is listed in details, not
just the first.
|
Fix the parameters listed in details; do not
retry unchanged.
|
| 401 | Key missing, malformed, unrecognised, or suspended. See the codes below. | Check the header is being sent. If suspended, contact us. |
| 404 | No such route. This means the URL is wrong — a plot that does not exist returns 200 with an empty array, not a 404. | Check the path and API version. |
| 429 | Rate limit exceeded for your key. |
Back off until the reset window elapses, then
retry.
|
| 500 | Something failed on our side. |
Retry with backoff. If it persists, send us the
requestId.
|
Authentication error codes
error |
Status | Meaning |
|---|---|---|
ApiKeyMissing |
401 | No X-API-KEY header was sent. |
ApiKeyInvalid |
401 | The key is malformed or not recognised. |
ApiKeySuspended |
401 | The key was revoked. Contact us to reinstate it. |
ApiKeyExpired |
401 | The key reached the end of its term — a trial, or a period that has run out. The message names the date. Contact us to extend it; the same key can be re-dated, so nothing in your integration has to change. |
CountryNotPermitted |
403 | The key asked for a country it does not cover — see Botswana & South Africa. |
CityNotPermitted |
403 |
The key named a city outside the towns it
covers. Keys sold specific towns are refused others rather
than answered with an empty list.
|
ValidationError |
400 | One or more parameters were invalid. |
RateLimitExceeded |
429 | Per-minute allowance exhausted. |
Questions?
Email
info@starlightsoftware.co.bw. If you are reporting a problem with a specific request, include
the requestId from the response.