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.

ParameterRequiredMeaning
latyesLatitude, decimal degrees.
lonyesLongitude, decimal degrees.
accept-languagenokm (default) or en. Any other value falls back to km.
formatnojson 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.

ParameterRequiredMeaning
qyesThe place name. Maximum 200 characters.
limitnoResults to return, 1 to 20. Defaults to 5. Zero, negative and non-numeric values all mean the default.
accept-languagenokm (default) or en, as above.
formatnojson 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.

StatusCodeWhen
400missing_coordinatelat or lon absent.
400bad_coordinatelat or lon not a number.
400missing_queryq absent or empty.
400query_too_longq over 200 characters.
401missing_keyNo X-Api-Key header.
401invalid_keyKey not recognised.
401revoked_keyKey was revoked.
401disabled_keyKey disabled after twelve months unused.
422outside_coverageOutside Cambodia, or inside the bounding box but not inside any commune — territorial water, or a sliver on the border.
400unsupported_formatA format this service does not return. JSON is the only one; send json, jsonv2, or nothing.
429rate_limitedBurst or sustained rate exceeded. Retry-After is in seconds.
429account_quota_exceededYour account's daily total is spent. Another key will not help — it is the same allowance.
429daily_quota_exceededDaily quota spent. Resets 00:00 Phnom Penh time.
502 / 504upstream_unavailableThe 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.