Docs
Three endpoints, all GET. The shape follows Nominatim closely enough that most OpenStreetMap clients work with a base URL change, with one addition: every result carries the official Cambodian administrative codes.
Base URL and authentication
Send your key in an X-Api-Key header on every request. Keys are issued from the dashboard and shown once.
https://geo.checkinme.app/reverse?lat=11.5691&lon=104.9212 -H "X-Api-Key: geo_..."
Reverse /reverse
A coordinate to a structured address.
| Parameter | Required | Meaning |
|---|---|---|
| lat | yes | Latitude, decimal degrees. |
| lon | yes | Longitude, decimal degrees. |
| accept-language | no | km (default) or en. Any other value falls back to km. |
| format | no | json or jsonv2, both of which return the same JSON. Anything else — xml, geojson, or a value we do not know — is refused with a 400 rather than quietly answered in JSON. |
{
"lat": 11.5691,
"lon": 104.9212,
"display_name": "Rolling Tobacco Shop, Samdech Sangkhak Neayok Souk
(Street 136), Sangkat Phsar Thmey Ti Pir,
Khan Daun Penh, Phnom Penh, 120202, Cambodia",
"address": {
"house_number": null,
"road": "Samdech Sangkhak Neayok Souk (Street 136)",
"village": "Sangkat Phsar Thmey Ti Pir",
"commune": "Phsar Thmei Ti Pir",
"district": "Khan Daun Penh",
"province": "Phnom Penh",
"postcode": "120202",
"country_code": "kh"
},
"official": {
"commune_code": "120202",
"commune_en": "Phsar Thmei Ti Pir",
"commune_kh": "ផ្សារថ្មីទី ២",
"district_code": "1202",
"province_code": "12",
"confirmed": true,
"nearest_m": null
},
"licence": "Data © OpenStreetMap contributors, ODbL 1.0"
}The address object
Comes from OpenStreetMap. Any field can be null — road and house number frequently are, and that is coverage, not an error. The names follow the requested script.
- house_number
- Street number, where OSM has one.
- road
- Street name.
- village
- Village or urban quarter, as OSM tags it.
- commune
- Commune name, from the official layer, not from OSM.
- district
- District or khan.
- province
- Province or the capital.
- postcode
- Postcode, where OSM has one.
- country_code
- Always kh — coordinates outside Cambodia are refused.
The official object
The part you cannot get from a general geocoder. Codes are resolved from the coordinate itself, by finding the commune boundary it falls inside — not by matching the name OpenStreetMap returned. That is why they are present even where the street is not.
- commune_code
- Six digits. The first four are the district, the first two the province.
- commune_en
- Latin name from the official 2025 list.
- commune_kh
- Khmer name from the official list. Null where the list has none.
- district_code
- Four digits.
- province_code
- Two digits.
- confirmed
- True when the code still appears in the 2025 official list. The boundaries are the 2018 edition, so about 5% of them carry a code the current list no longer has. Those come back confirmed: false — the geometry is still good, but do not treat the code as authoritative.
- nearest_m
- Null when the coordinate is inside the commune, which is the normal case. A number means it fell that many metres outside every commune and was snapped to the nearest one within 1.5 km — near-shore points mostly, where the 2018 coastline is coarser than the places people actually stand. The commune is right; the containment is approximate.
The whole object is null only if a coordinate somehow resolves outside every commune, which is refused before it gets this far.
Search /search
A place name to coordinates, in Khmer or Latin script.
| Parameter | Required | Meaning |
|---|---|---|
| q | yes | The place name. Maximum 200 characters. |
| limit | no | Results to return, 1 to 20. Defaults to 5. Zero, negative and non-numeric values all mean the default. |
| accept-language | no | km (default) or en, as above. |
| format | no | json or jsonv2, as above. Other values are refused. |
{
"query": "សៀមរាប",
"queried_as": ["សៀមរាប", "រាជធានីសៀមរាប", "ខេត្តសៀមរាប", "ក្រុងសៀមរាប"],
"results": [
{
"name": "ក្រុងសៀមរាប",
"display_name": "ក្រុងសៀមរាប, សៀមរាប, ខេត្តសៀមរាប, 123456, ព្រះរាជាណាចក្រកម្ពុជា",
"lat": 13.3617562,
"lon": 103.8590321,
"category": "place",
"type": "city",
"importance": 0.5037836933076608,
"osm_type": "node",
"osm_id": 257838557,
"official": {
"commune_code": "171002",
"district_code": "1710",
"province_code": "17",
"confirmed": true
}
}
],
"licence": "Data © OpenStreetMap contributors, ODbL 1.0"
}- name
- The object's own name.
- display_name
- Full comma-separated address, as OSM composes it.
- lat, lon
- Numbers, not strings.
- category, type
- OSM classification, e.g. place/city, highway/primary.
- importance
- Nominatim's ranking score. Results are sorted by it, highest first.
- osm_type, osm_id
- The underlying object. Two queries returning the same pair are the same place.
- official
- As above, but without the names — resolved from each result's coordinate.
queried_as
Present on Khmer queries only. Khmer administrative names are written with the type word attached and unspaced — ភ្នំពេញ is stored as រាជធានីភ្នំពេញ — and Khmer has no spaces for a tokeniser to split on, so the bare name a person types matches nothing. We ask for every spelling at once and merge the answers. This field lists the spellings tried, so a surprising result is diagnosable rather than mysterious.
Numbered roads
A query naming a numbered road — “National Road 4”, “Street 240” — only returns roads carrying that number. Nominatim reads the trailing number as a house number, so without this every national road matches equally and you get an arbitrary one back. If nothing carries the number, the result list is empty and a note field says so. An empty list here means we could not find that road, not that it does not exist.
Status /status
Service health and the age of the map data. No key required.
{"status": "ok", "data_updated": "2026-09-01T18:10:55+00:00"}data_updated is when the OpenStreetMap extract behind the index was last refreshed — the age of the data, not of the service. Returns 503 with status of degraded or unavailable when the geocoder is not answering.
Rate limits
Every successful response carries your remaining daily allowance, so you can see a limit approaching rather than discover it.
X-RateLimit-Limit: 25000 X-RateLimit-Remaining: 24447 X-RateLimit-Reset: 1788454800 (unix seconds, next 00:00 Phnom Penh time)
There are four limits: a daily quota per key, the same daily figure across your whole account, a per-second burst, and a sustained rate averaged over ten seconds. Going too fast is a different answer from being out of requests for the day.
Each key counts separately so a staging key cannot spend your production key’s allowance — but five keys are not five allowances. The account ceiling is the same number as the per-key one, so the figure on the pricing page is what you get however many keys you spread it over.
HTTP/1.1 429 Too Many Requests
Retry-After: 1
{"error":"rate_limited",
"message":"Burst limit is 10 requests in a second.",
"upgrade":"https://geo.checkinme.app/plans"}HTTP/1.1 429 Too Many Requests
Retry-After: 37362
X-RateLimit-Limit: 25000
X-RateLimit-Remaining: 0
{"error":"daily_quota_exceeded",
"message":"Free tier allows 25000 requests per day. Resets 00:00 Phnom Penh time.",
"upgrade":"https://geo.checkinme.app/plans"}A refusal is always loud and always says when to come back — never a truncated result, never an empty address that looks like an answer.
Errors
Every error is JSON with an error code and a human message. Match on the code; the message is for people and may change.
| Status | Code | When |
|---|---|---|
| 400 | missing_coordinate | lat or lon absent. |
| 400 | bad_coordinate | lat or lon not a number. |
| 400 | missing_query | q absent or empty. |
| 400 | query_too_long | q over 200 characters. |
| 401 | missing_key | No X-Api-Key header. |
| 401 | invalid_key | Key not recognised. |
| 401 | revoked_key | Key was revoked. |
| 401 | disabled_key | Key disabled after twelve months unused. |
| 422 | outside_coverage | Outside Cambodia, or inside the bounding box but not inside any commune — territorial water, or a sliver on the border. |
| 400 | unsupported_format | A format this service does not return. JSON is the only one; send json, jsonv2, or nothing. |
| 429 | rate_limited | Burst or sustained rate exceeded. Retry-After is in seconds. |
| 429 | account_quota_exceeded | Your account's daily total is spent. Another key will not help — it is the same allowance. |
| 429 | daily_quota_exceeded | Daily quota spent. Resets 00:00 Phnom Penh time. |
| 502 / 504 | upstream_unavailable | The geocoder did not answer. Ours, not yours — retry. |
What we store
We store what you searched for. We never store where anyone was. Those are different kinds of data and we treat them differently.
- Searches
- Kept in full: the text, how many results came back, your key, the time. It is text somebody typed — it says what they were looking for, not where they are. Searches that find nothing are the ones we most want, because they show where Cambodia's address data is missing.
- Reverse geocoding
- The coordinate is rounded to about a square kilometre and folded into a counter before anything is written, and the exact point is discarded. We keep how many lookups a cell received and how many found a street. That is the coverage map; it is not a trail.
- Never
- Anything identifying the person who asked. No device id, no user id, no address of theirs. A coordinate is somebody standing somewhere, and at volume it identifies them without needing a name attached.
The rounding happens before the write rather than after, which makes it a control rather than a promise. There is no window in which the exact coordinate exists in our database and no retention policy standing between it and someone who would like it.
Conventions
- Methods
- GET only. Anything else is a 405.
- CORS
- Enabled for all origins, so you can call this from a browser page. Restrict the key to your website first — a key in page source is visible to anyone who looks, and an unrestricted one can be lifted and spent.
- format
- json and jsonv2 are accepted on every endpoint and return the same JSON. There is no XML, and asking for it is refused with a 400 rather than answered in JSON — a client that needs XML should find out from the status code, not by parsing the body and failing.
- Nulls
- A field that is absent from the data is null, never an empty string and never invented.
- Versioning
- No version in the path yet. Fields will be added; existing ones will not change meaning without notice to every key holder by email.
Attribution
Display “© OpenStreetMap contributors” wherever results appear. It is the licence, not our preference. Every response carries the licence string, so you can render it straight from the payload.
Administrative codes come from the Cambodia Geographical List 2025 (Ministry of Land Management, Urban Planning and Construction) and commune boundaries from OCHA Field Information Services, CC BY-IGO. Both are joined at query time and kept separate from the OpenStreetMap data, so no licence is imposed on another.