API
Every endpoint is generated from the API code. Every example below runs as it is: set your key once, then copy any command. Get a free key with curl -X POST https://geo.dock.mn/v1/signup (see Quickstart).
export DOCK_KEY=dk_your_key_hereThe examples use the spec's sample values. Each endpoint the map page uses (geocode, reverse, nearby, door, door/at, route, confirm, traffic) also shows one real request and answer below, with the codes 7QSVX, BS5SS and ZR2Q8. The answers were saved from geo.dock.mn on 2026-10-20, or, where marked, from the API code on a test database. A sample your org does not hold answers 404. Routes your key has no scope for answer 403 insufficient_scope.
OpenAPI 0.118.0. openapi.json
GET /v1/versions
API versions and deprecated routes with their sunset days. No key needed, not counted.
curl 'https://geo.dock.mn/v1/versions'POST /v1/oauth/token
A short-lived token for an app, from your key. OAuth2 client credentials.
Call it from your server, never from the app: the key stays on the server and the app gets a token that works for an hour, with the scopes you ask for. Send grant_type=client_credentials and the key as the HTTP Basic password (any user name, or the key prefix) or as client_secret, form encoded or JSON. Use the token like a key: ?access_token= or Authorization: Bearer. Limits, quota and usage are the key's. Revoking the key ends its tokens within a minute. A token cannot buy another token.
curl -X POST 'https://geo.dock.mn/v1/oauth/token' \
-u ":$DOCK_KEY" \
-H 'Content-Type: application/json' \
-d '{"grant_type":"client_credentials"}'DELETE /v1/key
End the key that makes this call.
Only the calling key ends, never another key and never the org. The row is kept with revoked_at set. Calls with the key may still answer for up to 60 seconds (the key cache). Not counted, like /usage.
curl -X DELETE 'https://geo.dock.mn/v1/key' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/usage
Your org's calls, tiles and door misses per day and per route, with its caps.
Any key works, whatever its scopes, and sees only its own org. This call is free: it is not counted and does not use the daily quota, so it answers after the quota is reached. Days are Ulaanbaatar days.
curl 'https://geo.dock.mn/v1/usage' \
-H "Authorization: Bearer $DOCK_KEY"GET /health
Liveness. The process is up.
curl 'https://geo.dock.mn/health'GET /health/ready
Readiness. Database and PostGIS reachable.
curl 'https://geo.dock.mn/health/ready'POST /v1/door/report
Report a wrong, missing, duplicate or unmapped door.
The report is queued for review; door data does not change until a person reviews it (PLAN.md 19). The same report from the same organization within 24 h returns the first one (already_queued). A code that finds no door is 404 and counts against the daily door miss cap, like GET /v1/door/{code}. Notes: runs of 6 or more digits and e-mail addresses are removed. Never put a phone number or a name in a report.
curl -X POST 'https://geo.dock.mn/v1/door/report' \
-H "Authorization: Bearer $DOCK_KEY" \
-H 'Content-Type: application/json' \
-d '{"kind":"wrong_location","code":"7QSVX","point":[106.9303,47.9202]}'GET /v1/door/at
The address unit at a point: its code and official address.
For a tap on the map. The хашаа containing the point answers first (in the ger areas the хашаа is the unit; a building inside a fence is its child), else the smallest current building containing it. code is null while the unit has no code yet. Nothing inside a restricted site. Not a code lookup: no miss cap, no resolution log.
curl 'https://geo.dock.mn/v1/door/at?lat=47.9384&lon=106.9011' \
-H "Authorization: Bearer $DOCK_KEY"curl 'https://geo.dock.mn/v1/door/at?lon=106.9011&lat=47.9384' \
-H "Authorization: Bearer $DOCK_KEY"Answer 200, 2026-10-20, recorded from geo.dock.mn.
{
"official_address": {
"country": "MN",
"city_or_aimag": "Улаанбаатар",
"khoroo_or_bag": "10-р хороо",
"district_or_soum": "Чингэлтэй",
"building_or_khashaa": "196",
"street_or_microdistrict": "Чингэлтэй гудамж-7"
},
"official_status": "matched",
"unofficial": false,
"official_source": "egazar.gov.mn cadastre parcels (geoware:ca_parcel_tbl)",
"official_date": "2026-10-19",
"source_date": "2026-10-19",
"code": "ZR2Q8",
"kind": "khashaa",
"official_line": "Чингэлтэй дүүрэг, 10-р хороо, Чингэлтэй гудамж-7, 196-р хашаа",
"name_mn": null,
"name_en": null,
"point": [
106.9011742,
47.9384609
],
"id": "fe7d74db-accd-48aa-b3ef-fc203c12e848"
}GET /v1/door/{code}/preview
A code link's title and description, for a link preview.
No key. For the web server that fills <title>, og:title and og:description of a shared code link (map.dock.mn/7QSVX) in chat apps that run no scripts. Only an address unit (a building or a khashaa) with a current code answers; it gives its Mongolian title ("Сансар 8 · 7QSVX"), its official line and its kind, and nothing else: no point, no person, no door. A building code, a retired code, a code inside a restricted site and an unknown code are all 404. Cached 5 minutes.
curl 'https://geo.dock.mn/v1/door/7QSVX/preview' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/door/{code}
Resolve a 5-character building code to its door.
Accepts lowercase and Cyrillic lookalike keys (В, К, М, Н, Р, С, Т, Х are read as the Latin letters they look like), and a flat number after a space ("7QSVX 17"), and the spoken form ("7QSVX, 12 давхар, 1204 тоот"). The official address comes first, the code after it (PLAN.md 4d). Anything not official_status matched is unofficial and must be shown as such. Codes are never reused: a retired code is 410. point is given to about 0.1 m: its 7th decimal is a mark for your organization (PLAN.md 19). Lookups that find no door (404) count against a daily cap per organization (default 200); at the cap this route answers 429 miss_cap until 00:00 Ulaanbaatar time.
curl 'https://geo.dock.mn/v1/door/7QSVX' \
-H "Authorization: Bearer $DOCK_KEY"curl 'https://geo.dock.mn/v1/door/7QSVX' \
-H "Authorization: Bearer $DOCK_KEY"Answer 200, 2026-10-20, recorded from geo.dock.mn.
{
"official_address": {
"country": "MN",
"postal_code": "13381-0034",
"city_or_aimag": "Улаанбаатар",
"khoroo_or_bag": "3-р хороо",
"district_or_soum": "Баянзүрх",
"building_or_khashaa": "8",
"street_or_microdistrict": "Сансар"
},
"official_status": "matched",
"unofficial": false,
"official_source": "zipcode:4364",
"official_date": "2026-10-18",
"source_date": "2026-10-19",
"code": "7QSVX",
"kind": "building",
"official_line": "Баянзүрх дүүрэг, 3-р хороо, Сансар, 8-р байр · 13381-0034",
"name_mn": "Сансар А 8",
"name_en": "Sansar A 8",
"point": [
106.9376278,
47.9226338
]
}curl 'https://geo.dock.mn/v1/door/BS5SS' \
-H "Authorization: Bearer $DOCK_KEY"Answer 200, 2026-10-20, recorded from geo.dock.mn.
{
"official_address": {
"country": "MN",
"postal_code": "14201-0047",
"city_or_aimag": "Улаанбаатар",
"khoroo_or_bag": "6-р хороо",
"district_or_soum": "Сүхбаатар",
"building_or_khashaa": "1",
"street_or_microdistrict": "Сүхбаатарын талбай"
},
"official_status": "matched",
"unofficial": false,
"official_source": "form, zipcode:18070",
"official_date": "2026-10-19",
"source_date": "2026-10-19",
"code": "BS5SS",
"kind": "building",
"official_line": "Сүхбаатар дүүрэг, 6-р хороо, Сүхбаатарын талбай, 1-р байр · 14201-0047",
"name_mn": "Засгийн газрын ордон",
"name_en": "Government Palace",
"point": [
106.9166354,
47.920506
]
}curl 'https://geo.dock.mn/v1/door/ZR2Q8' \
-H "Authorization: Bearer $DOCK_KEY"Answer 200, 2026-10-20, recorded from geo.dock.mn. ZR2Q8 is a khashaa: a unit code, not a door code.
{
"official_address": {
"country": "MN",
"city_or_aimag": "Улаанбаатар",
"khoroo_or_bag": "10-р хороо",
"district_or_soum": "Чингэлтэй",
"building_or_khashaa": "196",
"street_or_microdistrict": "Чингэлтэй гудамж-7"
},
"official_status": "matched",
"unofficial": false,
"official_source": "egazar.gov.mn cadastre parcels (geoware:ca_parcel_tbl)",
"official_date": "2026-10-19",
"source_date": "2026-10-19",
"code": "ZR2Q8",
"kind": "khashaa",
"official_line": "Чингэлтэй дүүрэг, 10-р хороо, Чингэлтэй гудамж-7, 196-р хашаа",
"name_mn": null,
"name_en": null,
"point": [
106.9011742,
47.9384609
]
}GET /v1/geocode
Find doors and places by name, in Cyrillic or Latin.
Cyrillic and Latin, formal and informal spellings match ("Баянзүрх", "Bayanzurkh", "bayanzurh"). The state's street codes answer as kind street, first, with the state as source ("СБД - 3 - 64", "SBD-3-64", and the code a street was renamed from, "Н-1-0012"); no dock code. Ordinals fold ("3-р хороо", "3r khoroo"). Mongolian case endings fold ("Дуурийн театр", "Гандангийн"). One or two typos are tolerated. When names match fewer places than the limit, places whose description matches the meaning fill the rest ("сургуулийн хажууд цэнхэр дээвэртэй барилга"); those have layer 2 and come after every name match. A query that names a landmark and a side or "near" ("Хүннү моллын ард", "east of Max Mall", "Max Mallの近く") first lists the places within 150 m on that side, each with `near`, the landmark; when the query also describes the place, a reranking model orders the best of them on that description (layer 3). With proximity=lon,lat (where the person is, or the map centre), a name the query starts lists the nearer places of the same match first: whole name, then name start, then a later word, each nearer first. The point is not logged. A registered company (kind business, from a building directory) answers with its building's code, the building's official address, and the floor and flat the state register gives (null when it says none); the same place is never listed twice under one code and name. GET /v1/address/{id}/directory lists a building's companies by id or by code. Doors come with their official address first and the code after it (PLAN.md 4d); anything not matched is unofficial. Nothing inside a restricted site is returned. The query text is not logged. A point is given to about 0.1 m: its 7th decimal is a mark for your organization (PLAN.md 19). When the door_names feature is open to your org and the whole query is a live door name in any spelling that folds to it ("сансар-бэйкери"), the named door comes first, layer 1, with the best layer 1 score or 1.6, the score of a whole name, whichever is higher; it is not repeated below. The holder org is not shown. Serving it writes a resolution log row, as GET /v1/door-names/{name} does; no other result does. With types (door, business, building, district, khoroo, area, street, landmark, comma separated), only those kinds, up to limit of them, in every layer; the landmark a "near" query names is still found among every kind. A named door comes only when types has door. A query that is only a kind of place ("банк", "эмийн сан", "шатахуун", "хоол", "pharmacy") with proximity and no types lists the nearest registered places of that category, as GET /v1/nearby does, within 3 km, else 10 km, each with its category; none there, the name search answers. The bare word "хашаа" (or "хашаанууд") with proximity and no types lists the nearest minted хашаа units instead, within 3 km, else 10 km, nearest first: kind khashaa, the code, the official address and its line in long_mn, the name null; none there, the name search answers.
curl 'https://geo.dock.mn/v1/geocode?q=%D0%A1%D0%B0%D0%BD%D1%81%D0%B0%D1%80%20%D0%90%208' \
-H "Authorization: Bearer $DOCK_KEY"curl 'https://geo.dock.mn/v1/geocode?q=%D0%A1%D0%B0%D0%BD%D1%81%D0%B0%D1%80%20%D0%90%208&limit=2&proximity=106.9177,47.9186' \
-H "Authorization: Bearer $DOCK_KEY"Answer 200, 2026-10-20, recorded from geo.dock.mn.
{
"results": [
{
"official_address": {
"country": "MN",
"postal_code": "13381-0034",
"city_or_aimag": "Улаанбаатар",
"khoroo_or_bag": "3-р хороо",
"district_or_soum": "Баянзүрх",
"building_or_khashaa": "8",
"street_or_microdistrict": "Сансар"
},
"official_status": "matched",
"unofficial": false,
"official_source": "zipcode:4364",
"official_date": "2026-10-18",
"code": "7QSVX",
"kind": "building",
"id": "01a0f58f-f737-77a4-a893-8b507b30eb88",
"name_mn": "Сансар А 8",
"name_en": "Sansar A 8",
"long_mn": "Баянзүрх дүүрэг, 8, Сансар А 8",
"long_en": "Bayanzurkh district, Sansar A 8",
"point": [
106.9376278,
47.9226338
],
"confidence": null,
"source": "osm",
"source_date": "2026-10-01",
"score": 1.692,
"layer": 1,
"near": null,
"lines": null,
"category": null,
"floor": null,
"flat": null
},
{
"official_address": null,
"official_status": "none",
"unofficial": true,
"official_source": null,
"official_date": null,
"code": null,
"kind": "area",
"id": "01a0f796-1efd-7184-9f0f-07c49e1a382f",
"name_mn": "Сансар А",
"name_en": "Sansar A",
"long_mn": null,
"long_en": null,
"point": [
106.936852,
47.922686
],
"confidence": 0,
"source": "osm",
"source_date": "2026-10-01",
"score": 1.025,
"layer": 1,
"near": null,
"lines": null,
"category": null,
"floor": null,
"flat": null
}
]
}GET /v1/reverse
Doors, businesses, buildings and landmarks near a point, nearest first.
District and khoroo come from polygons covering the point; a missing polygon is null, never a guess. Distance 0 means the point is inside the building. Nothing inside a restricted site is returned. With types (door, business, building, landmark, comma separated), only those kinds, up to limit of them. Without types, a point inside a minted хашаа (a ger plot) also gets that хашаа as a row of kind khashaa at distance 0: its code, its official address and its line in long_mn, the name null; a building that holds the point comes before it. A building inside a хашаа with no code of its own (one fence, one unit) carries the хашаа code in code, so the code to use is on its row too. A building row has no footprint_check; read it with GET /v1/address/{id} or the building tilequery. The top-level point is yours, as sent. Result points carry a mark for your organization in the 7th decimal (PLAN.md 19).
curl 'https://geo.dock.mn/v1/reverse?lon=106.9376&lat=47.9226' \
-H "Authorization: Bearer $DOCK_KEY"curl 'https://geo.dock.mn/v1/reverse?lon=106.9376&lat=47.9226&radius=50&limit=3' \
-H "Authorization: Bearer $DOCK_KEY"Answer 200, 2026-10-20, recorded from geo.dock.mn.
{
"point": [
106.9376,
47.9226
],
"district_mn": "Баянзүрх",
"district_en": "Bayanzurkh",
"khoroo": null,
"results": [
{
"official_address": {
"country": "MN",
"postal_code": "13381-0034",
"city_or_aimag": "Улаанбаатар",
"khoroo_or_bag": "3-р хороо",
"district_or_soum": "Баянзүрх",
"building_or_khashaa": "8",
"street_or_microdistrict": "Сансар"
},
"official_status": "matched",
"unofficial": false,
"official_source": "zipcode:4364",
"official_date": "2026-10-18",
"code": "7QSVX",
"kind": "building",
"id": "01a0f58f-f737-77a4-a893-8b507b30eb88",
"name_mn": "Сансар А 8",
"name_en": "Sansar A 8",
"long_mn": "Баянзүрх дүүрэг, 8, Сансар А 8",
"long_en": "Bayanzurkh district, Sansar A 8",
"point": [
106.9376278,
47.9226338
],
"confidence": null,
"source": "osm",
"source_date": "2026-10-01",
"distance_m": 0,
"lines": null,
"category": null
},
{
"official_address": {
"country": "MN",
"postal_code": "13381-0111",
"building_name": "СЭЗДС-ийн ажилчдын орон сууц",
"city_or_aimag": "Улаанбаатар",
"khoroo_or_bag": "3-р хороо",
"district_or_soum": "Баянзүрх",
"building_or_khashaa": "7",
"street_or_microdistrict": "Сансар"
},
"official_status": "matched",
"unofficial": false,
"official_source": "zipcode:4375",
"official_date": "2026-10-18",
"code": "DXK9D",
"kind": "building",
"id": "01a0f58f-f4ee-7bba-bd41-c68f787ff9ce",
"name_mn": "Сансар А 7",
"name_en": "Sansar A 7",
"long_mn": "Баянзүрх дүүрэг, 7, Сансар А 7",
"long_en": "Bayanzurkh district, Sansar A 7",
"point": [
106.9375539,
47.9221414
],
"confidence": null,
"source": "osm",
"source_date": "2026-10-01",
"distance_m": 23.7,
"lines": null,
"category": null
},
{
"official_address": {
"country": "MN",
"postal_code": "13381-0030",
"city_or_aimag": "Улаанбаатар",
"khoroo_or_bag": "3-р хороо",
"district_or_soum": "Баянзүрх",
"building_or_khashaa": "6",
"street_or_microdistrict": "Сансар"
},
"official_status": "matched",
"unofficial": false,
"official_source": "zipcode:4334",
"official_date": "2026-10-18",
"code": "77M0P",
"kind": "building",
"id": "01a0f58f-fc6f-7056-84e8-b1e1ea8997da",
"name_mn": "Сансар А 6",
"name_en": "Sansar A 6",
"long_mn": "Баянзүрх дүүрэг, 6, Сансар А 6",
"long_en": "Bayanzurkh district, Sansar A 6",
"point": [
106.93685,
47.9225876
],
"confidence": null,
"source": "osm",
"source_date": "2026-10-01",
"distance_m": 49.4,
"lines": null,
"category": null
}
]
}GET /v1/nearby
Places of one or more categories near a point, nearest first.
The categories are the map's place icons: a chip such as "Эмийн сан" is category=pharmacy, "Хоол" is category=restaurant,cafe. Banks, fuel, pharmacies, restaurants, cafes and shops are only businesses matched to an official registration; until one is, those categories answer an empty list, never a guess. A store of a registered chain ("GS25 Нийслэл") is kind business and carries the code of the building or хашаа it stands in (within 15 m of its pin); code is null, shown as "код алга", when there is none. Its official fields are that unit's, or none. A registered company of a nearby building (kind business, from the building directory) carries its building's code and the floor and flat the register gives; floor and flat are null on every other row. Each result has the fields of a reverse result plus its category. more counts the matches within radius past the limit, so a list of 20 can say there are 34 more. Nothing inside a restricted site is returned. The point is not logged. The top-level point is yours, as sent. Result points carry a mark for your organization in the 7th decimal (PLAN.md 19). Like reverse, it leaves footprint_check out; GET /v1/address/{id} has it.
curl 'https://geo.dock.mn/v1/nearby?category=pharmacy&lon=106.9177&lat=47.9186' \
-H "Authorization: Bearer $DOCK_KEY"curl 'https://geo.dock.mn/v1/nearby?lon=106.9177&lat=47.9186&category=pharmacy&limit=3' \
-H "Authorization: Bearer $DOCK_KEY"Answer 200, 2026-10-20, recorded from geo.dock.mn.
{
"point": [
106.9177,
47.9186
],
"results": [
{
"official_address": {
"country": "MN",
"postal_code": "14210",
"city_or_aimag": "Улаанбаатар",
"khoroo_or_bag": "1-р хороо",
"district_or_soum": "Сүхбаатар",
"building_or_khashaa": "13/3",
"street_or_microdistrict": "Энх тайваны өргөн чөлөө"
},
"official_status": "matched",
"unofficial": false,
"official_source": "form, zipcode:18099",
"official_date": "2026-10-18",
"code": "W942Q",
"kind": "business",
"id": "01a1501a-d48b-75a5-96f8-e610712fb82c",
"name_mn": "Азифарм Авзага",
"name_en": "Asiapharm Avzaga",
"long_mn": "Сүхбаатар дүүрэг, 1-р хороо, Энх тайваны өргөн чөлөө, 13/3 · 14210",
"long_en": null,
"point": [
106.9232691,
47.9172969
],
"confidence": null,
"source": "osm",
"source_date": null,
"category": "pharmacy",
"distance_m": 440.8,
"lines": null,
"floor": null,
"flat": null
},
{
"official_address": {
"country": "MN",
"postal_code": "14210",
"building_name": "МК төв",
"city_or_aimag": "Улаанбаатар",
"khoroo_or_bag": "1-р хороо",
"district_or_soum": "Сүхбаатар",
"building_or_khashaa": "25",
"street_or_microdistrict": "Энх тайвны өргөн чөлөө"
},
"official_status": "matched",
"unofficial": false,
"official_source": "form, zipcode:20203",
"official_date": "2026-10-18",
"code": "R7XFF",
"kind": "business",
"id": "01a14f99-84f9-79bb-8db5-853fb16ae9a7",
"name_mn": "Билгүүн тун фарм",
"name_en": "Bilguun tun pharm",
"long_mn": "Сүхбаатар дүүрэг, 1-р хороо, Энх тайвны өргөн чөлөө, 25-р байр · 14210",
"long_en": null,
"point": [
106.9248259,
47.9175959
],
"confidence": null,
"source": "osm",
"source_date": null,
"category": "pharmacy",
"distance_m": 544.2,
"lines": null,
"floor": null,
"flat": null
},
{
"official_address": {
"country": "MN",
"postal_code": "14210",
"building_name": "Монголын улаан загалмайн нийгэмлэг",
"city_or_aimag": "Улаанбаатар",
"khoroo_or_bag": "1-р хороо",
"district_or_soum": "Сүхбаатар",
"building_or_khashaa": "18/1",
"street_or_microdistrict": "Г.Чагдаржавын гудамж"
},
"official_status": "matched",
"unofficial": false,
"official_source": "form, zipcode:20196",
"official_date": "2026-10-18",
"code": "91JV9",
"kind": "business",
"id": "01a1505a-81d0-771c-9bba-ac051fb06ad0",
"name_mn": "Монос",
"name_en": "Monos",
"long_mn": "Сүхбаатар дүүрэг, 1-р хороо, Г.Чагдаржавын гудамж, 18/1 · 14210",
"long_en": null,
"point": [
106.9243715,
47.9155879
],
"confidence": null,
"source": "osm",
"source_date": null,
"category": "pharmacy",
"distance_m": 600.7,
"lines": null,
"floor": null,
"flat": null
}
],
"more": 10
}GET /v1/address/{id}
One door or place by the id a geocode or reverse result gave.
Same fields as a search result, plus `parents`: the ids of the building or khashaa, area, street, khoroo and district a door stands in (a business through its door); a khoroo carries its district only. Official address first, code after it (PLAN.md 4d); anything not matched is unofficial. Ids are never reused: a retired door, a demolished building or any retired place is 410. Something inside a restricted site is 404, retired or not. A place added since the last index refresh is 404 until the refresh. A door or business served writes a resolution log row (door, organization, time). point carries a mark for your organization in the 7th decimal (PLAN.md 19).
curl 'https://geo.dock.mn/v1/address/0192f0a4-7c1e-7a3b-9d2e-5b6c7d8e9f01' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/address/{id}/entrances
A building's entrances, each with its building code and official level, in one call.
For the building card: the id is a building's, from geocode, reverse or the building tile. Each entrance has its level (matched бүртгэлтэй, partial хэсэгчилсэн, none албан бус) and, once a door is minted for it, its code. An entrance with no door has the building's official form with its number, and no code. Generated entrances (confidence 0, source generated) are listed and say so: show them as таамаг. Nothing inside a restricted site. Writes no resolution log row: a building's list is no lookup of one door. Points carry a mark for your organization in the 7th decimal (PLAN.md 19).
curl 'https://geo.dock.mn/v1/address/0192f0a4-7c1e-7a3b-9d2e-5b6c7d8e9f01/entrances' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/address/{id}/directory
The organizations registered in a building, by floor.
For "Энд байгаа байгууллагууд" on an office building or mall card: the id is a building's UUID or its 5-character unit code (a card that holds only the code can ask with it; a хашаа code answers an empty list). Each entry has name, floor, unit, category. Only organizations matched to an official registration are listed; a floor or unit the register does not give is null. Nothing inside a restricted site. Writes no resolution log row.
curl 'https://geo.dock.mn/v1/address/0192f0a4-7c1e-7a3b-9d2e-5b6c7d8e9f01/directory' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/entrances/near
The building entrances nearest a point, each with its building code and official level.
For a long-press on the map or "Энд байгаа хаалга" from the user's position: nearest first, within radius, up to limit (default 1). Each entrance as GET /v1/address/{id}/entrances lists it, plus its building and distance_m: its level (matched бүртгэлтэй, partial хэсэгчилсэн, none албан бус), its code once a door is minted, else the building's official form with its number and code null. Generated entrances (confidence 0, source generated) count and say so: show them as таамаг. Nothing inside a restricted site, nothing of a demolished building. Only buildings whose entrance method is approved appear; elsewhere the list is empty. Writes no resolution log row. Points carry a mark for your organization in the 7th decimal (PLAN.md 19); the top-level point is yours, as sent.
curl 'https://geo.dock.mn/v1/entrances/near?lon=106.9301&lat=47.9201' \
-H "Authorization: Bearer $DOCK_KEY"POST /v1/door-names
Register a spoken name for a door, or move your name to another door.
The name is your org's for 365 days from registration; no fee is charged yet. Names are unique by their folded form: case, spaces, punctuation, and Latin letters typed for Cyrillic ones ("Cансар" with a Latin C) do not make another name. A name another org holds is 409 until it expires; then anyone may take it. Sending your own name with another code moves it (moved); the same code answers it (unchanged). Expiry does not change on either. At most 50 unexpired names per org. A code that finds no door is 404 and counts against the daily door miss cap, like GET /v1/door/{code}. Open to an org only when the door_names feature is (GET /v1/usage features); else 403 feature_off.
curl -X POST 'https://geo.dock.mn/v1/door-names' \
-H "Authorization: Bearer $DOCK_KEY" \
-H 'Content-Type: application/json' \
-d '{"name":"Сансар бэйкери","code":"7QSVX"}'GET /v1/door-names
The names your org holds, A to Z.
Expired names stay listed, expired true, until another org takes them. transfer_until shows a transfer code you made that still works. Open to an org only when the door_names feature is (GET /v1/usage features); else 403 feature_off.
curl 'https://geo.dock.mn/v1/door-names' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/door-names/{name}
Resolve a door name to its door.
Any spelling of the name that folds to the same key finds it: "САНСАР БЭЙКЕРИ", "сансар-бэйкери". The door is as GET /v1/door/{code} answers it, official address first; the holder org is not shown. An unknown or expired name, or one whose door was retired, is 404 and counts against the daily door miss cap. Open to an org only when the door_names feature is (GET /v1/usage features); else 403 feature_off.
curl 'https://geo.dock.mn/v1/door-names/%D0%A1%D0%B0%D0%BD%D1%81%D0%B0%D1%80%20%D0%B1%D1%8D%D0%B9%D0%BA%D0%B5%D1%80%D0%B8' \
-H "Authorization: Bearer $DOCK_KEY"POST /v1/door-names/{name}/transfer-code
Make a transfer code for one of your names, to give it to another org.
Names are transferable, like a domain. The code is shown once; give it to the other org outside the API, and it claims the name with POST /v1/door-names/{name}/claim. It works for 7 days, or until the name expires if sooner, and only once. A new code replaces the old one. Until it is claimed the name stays yours: you may still move it or clear the code. Open to an org only when the door_names feature is (GET /v1/usage features); else 403 feature_off.
curl -X POST 'https://geo.dock.mn/v1/door-names/%D0%A1%D0%B0%D0%BD%D1%81%D0%B0%D1%80%20%D0%B1%D1%8D%D0%B9%D0%BA%D0%B5%D1%80%D0%B8/transfer-code' \
-H "Authorization: Bearer $DOCK_KEY"DELETE /v1/door-names/{name}/transfer-code
Clear the transfer code of one of your names.
After this the code is refused. Kept in history. Open to an org only when the door_names feature is (GET /v1/usage features); else 403 feature_off.
curl -X DELETE 'https://geo.dock.mn/v1/door-names/%D0%A1%D0%B0%D0%BD%D1%81%D0%B0%D1%80%20%D0%B1%D1%8D%D0%B9%D0%BA%D0%B5%D1%80%D0%B8/transfer-code' \
-H "Authorization: Bearer $DOCK_KEY"POST /v1/door-names/{name}/claim
Take a name another org transfers to you, with the transfer code it gave you.
The name becomes your org's. Its door and expires_at stay as they were; move it with POST /v1/door-names. The code is used up. A wrong, cleared or expired code is 409 transfer_refused and counts against the daily door miss cap, as an unknown name does. At most 50 unexpired names per org. Open to an org only when the door_names feature is (GET /v1/usage features); else 403 feature_off.
curl -X POST 'https://geo.dock.mn/v1/door-names/%D0%A1%D0%B0%D0%BD%D1%81%D0%B0%D1%80%20%D0%B1%D1%8D%D0%B9%D0%BA%D0%B5%D1%80%D0%B8/claim' \
-H "Authorization: Bearer $DOCK_KEY" \
-H 'Content-Type: application/json' \
-d '{"transfer_code":"dnt_7k2m9q4x8r3t6v1w5y0z2b4c"}'GET /v1/changes
Which doors and places changed since a time, to keep copies in sync without a full pull.
Partner keys (changes:read). Ids, kinds and states only; fetch each current one with GET /v1/address/{id} (geocode:read). The window ends one second before the last index refresh (`until`), so every current id listed can be fetched; a newer change shows after the next refresh. One row per place, its last change, oldest first. A place is listed when its own record changes, or the record that places or names it: entrance, building, khashaa, area or street for a door; the door for a business; the area for a building. Nothing inside a restricted site is listed. First sync: since=1970-01-01T00:00:00Z lists every id served today.
curl 'https://geo.dock.mn/v1/changes?since=2026-10-01T00%3A00%3A00Z' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/confirm/own
Your own driver confirmations and door reports per month: your dashboard (PLAN.md 18). No other organization is read.
Counts by result for the month and for each day, the number of different drivers (by your driver ids, never the ids), and the failure points: up to 100 doors and entrances your drivers said are wrong, missing or a duplicate. A row counts on the Ulaanbaatar day the driver was on site (observed_at, else when received). The month you are in is answered too; closed is true from 8 days after the month ends, when no queued confirmation can join it. Needs confirm:read, a read-only scope for a dashboard key; confirm:write does not read it.
curl 'https://geo.dock.mn/v1/confirm/own?month=2026-10' \
-H "Authorization: Bearer $DOCK_KEY"POST /v1/confirm
A driver on site confirms a door or entrance, or says it is elsewhere or missing.
Partner keys (confirm:write); the free read key cannot write. The code is a building code or a unit code (a building or khashaa): an arrival at a unit code, result confirmed, is stored as one row on the unit (target unit, entrance null, next arrival) and raises no entrance. One row per confirmation; no point moves. A confirmed tap within 5 m raises the entrance to level 2 at once, and the hourly mint gives it a code within the hour; level 3 is three such stops on different days, or one and the customer's tap (PLAN.md 5). wrong_location and missing are reviewed by a person first. The tap is the placed point; device GPS must be within 40 m of it. The same driver saying the same thing within 24 h counts once (already_recorded). A code or entrance that finds nothing is 404 and counts against the daily door miss cap.
curl -X POST 'https://geo.dock.mn/v1/confirm' \
-H "Authorization: Bearer $DOCK_KEY" \
-H 'Content-Type: application/json' \
-d '{"result":"confirmed","code":"7QSVX","tap":[106.9376,47.9226],"device":[106.93765,47.92265],"driver":"drv-7f3a"}'curl -X POST 'https://geo.dock.mn/v1/confirm' \
-H "Authorization: Bearer $DOCK_KEY" \
-H 'Content-Type: application/json' \
-d '{"result":"confirmed","code":"7QSVX","tap":[106.9376,47.9226],"device":[106.93765,47.92265],"driver":"drv-7f3a"}'Answer 200, 2026-10-20, recorded from the API code on a test database. With a key that has confirm:write. This answer was recorded from the API 0.96.1 code on a local test database (not from the live system) holding one building with code 7QSVX. The free read key answers 403 insufficient_scope (last example).
{
"id": "01a15886-d130-75dd-8bed-39cc9f1f493d",
"status": "recorded",
"result": "confirmed",
"target": "unit",
"code": "7QSVX",
"entrance": null,
"moved_m": 0,
"gps_m": 6.7,
"observed_at": null,
"received_at": "2026-10-20T08:56:32.813Z",
"next": "arrival"
}curl -X POST 'https://geo.dock.mn/v1/confirm' \
-H "Authorization: Bearer $DOCK_KEY" \
-H 'Content-Type: application/json' \
-d '{"result":"confirmed","code":"ZR2Q8","tap":[106.9011,47.9384],"device":[106.90115,47.93845],"driver":"drv-7f3a"}'Answer 200, 2026-10-20, recorded from the API code on a test database. A khashaa code (ZR2Q8). Recorded from the API 0.96.1 code on a local test database (not from the live system) holding one khashaa with code ZR2Q8. An arrival at a unit code that is no door code is stored with target "unit" and entrance null.
{
"id": "01a15886-d145-74b6-9ef8-e3278f16b90d",
"status": "recorded",
"result": "confirmed",
"target": "unit",
"code": "ZR2Q8",
"entrance": null,
"moved_m": 0,
"gps_m": 6.7,
"observed_at": null,
"received_at": "2026-10-20T08:56:32.835Z",
"next": "arrival"
}curl -X POST 'https://geo.dock.mn/v1/confirm' \
-H "Authorization: Bearer $DOCK_KEY" \
-H 'Content-Type: application/json' \
-d '{"result":"confirmed","code":"7QSVX","tap":[106.9376,47.9226],"device":[106.93765,47.92265],"driver":"drv-7f3a"}'Answer 403, 2026-10-20, recorded from geo.dock.mn.
{
"error": "insufficient_scope",
"message": "key lacks scope confirm:write"
}POST /v1/confirm/{id}/photo
The photo of the door a driver took with one of your confirmations, for review.
The body is one JPEG as the camera saved it (Content-Type image/jpeg, up to 2 MB, 320 to 8000 px a side). Its metadata (EXIF with GPS, the phone, the time) is removed before it is stored; the orientation stays. Send it after POST /v1/confirm answered, with that id. One photo per confirmation: the same photo again is unchanged, another one is 409. Only your org's own confirmations; one in a restricted site is 404. A person reviews it once the entrance has a door; it is never shown on a public page.
curl -X POST 'https://geo.dock.mn/v1/confirm/0199a1c2-7d3e-7f00-8a1b-2c3d4e5f6a7b/photo' \
-H "Authorization: Bearer $DOCK_KEY" \
-H 'Content-Type: image/jpeg' \
--data-binary @gate.jpgPOST /v1/business/claim
A business says it is here: five fields, no account.
The claim waits as pending with its source and day. It is matched to the state legal-entity register by the register number; a business is published on its unit, with its code, only when it matches. Until then nothing is shown. The same register number and name from the same key within 24 h returns the first claim (already_queued). A key takes at most 20 claims a Ulaanbaatar day and one address at most 5, so one address cannot use up a shared key. The place is a unit code or a point, both optional.
curl -X POST 'https://geo.dock.mn/v1/business/claim' \
-H "Authorization: Bearer $DOCK_KEY" \
-H 'Content-Type: application/json' \
-d '{"reg_number":"5012345","name":"Сансар бэйкери ХХК"}'GET /v1/business/claim/{id}
Where a claim stands: pending, verified or refused, with the reason.
Answers only the key that sent the claim; any other id is 404. pending keeps the reason lane A wrote (not_in_register, name_differs) until the register or the claim changes. Name, phone and place are not returned.
curl 'https://geo.dock.mn/v1/business/claim/0199a1c2-7d3e-7f00-8a1b-2c3d4e5f6a7b' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/business/photos/{id}
A photo listed on the card of a place whose claim the register confirmed.
Any other id, a photo the owner took off the card, or a card with fewer than 3 photos is 404.
curl 'https://geo.dock.mn/v1/business/photos/0190a3c2-7b1e-7c3a-9d4e-5f6a7b8c9d0e' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/business/{id}/card
What the owner put on the card of a claimed place: photos, buttons, menu and services, posts.
Only for a business id (core.business or directory entry) whose registration number has a verified claim and a card; the same rule as claimed:true on /v1/search and /v1/nearby. Anything else is 404. Photos come only from 3 up, cover first, and are read from /v1/business/photos/{id}. buttons keep the owner order: the first is the main button, the second is the one the owner chose.
curl 'https://geo.dock.mn/v1/business/0199a1c2-7d3e-7f00-8a1b-2c3d4e5f6a7b/card' \
-H "Authorization: Bearer $DOCK_KEY"POST /v1/merchant/login
Mails a sign-in link to this address. Always 202, whether the address is known or not.
curl -X POST 'https://geo.dock.mn/v1/merchant/login'POST /v1/merchant/session
Spends a sign-in link (its token) for a session token, valid 30 days.
curl -X POST 'https://geo.dock.mn/v1/merchant/session'DELETE /v1/merchant/session
curl -X DELETE 'https://geo.dock.mn/v1/merchant/session'GET /v1/merchant/cards
The account and its cards: each with its claim state and the newest edit.
curl 'https://geo.dock.mn/v1/merchant/cards'POST /v1/merchant/cards
Claims a business at one place and opens its card. One account, many cards.
The claim form's fields (POST /v1/business/claim) plus floor, stall and categories. At most 50 a day per account.
curl -X POST 'https://geo.dock.mn/v1/merchant/cards'PATCH /v1/merchant/cards/{id}
Saves the card: any of hours, holidays, description, categories, phone, buttons, floor, stall, photos. Each save is kept with its day.
curl -X PATCH 'https://geo.dock.mn/v1/merchant/cards/0190a3c2-7b1e-7c3a-9d4e-5f6a7b8c9d0e'POST /v1/merchant/cards/{id}/photos
Adds one JPEG to the card's photos, cleaned of EXIF; list it in photos (up to 10) to show it.
curl -X POST 'https://geo.dock.mn/v1/merchant/cards/0190a3c2-7b1e-7c3a-9d4e-5f6a7b8c9d0e/photos'GET /v1/merchant/register/{reg}
The state register's record for a registration number, to fill the claim form: name, legal form, legal address, categories, a building code to suggest.
Suggestions only; the owner accepts or changes each. phones_on_record is how many phones the register holds, never the numbers. 404 when the register does not hold the number.
curl 'https://geo.dock.mn/v1/merchant/register/5071234'POST /v1/merchant/cards/{id}/verify/phone
Sends a 6-digit code to the phones the state register holds for the company. The answer masks them.
curl -X POST 'https://geo.dock.mn/v1/merchant/cards/0190a3c2-7b1e-7c3a-9d4e-5f6a7b8c9d0e/verify/phone'POST /v1/merchant/cards/{id}/verify/phone/check
Checks the code. 5 tries per code; a right code verifies the card.
curl -X POST 'https://geo.dock.mn/v1/merchant/cards/0190a3c2-7b1e-7c3a-9d4e-5f6a7b8c9d0e/verify/phone/check'POST /v1/merchant/cards/{id}/verify/plate
Activates the plate at the door with its key (XXXX-XXXX): the card is verified at that code. A plate activates once.
curl -X POST 'https://geo.dock.mn/v1/merchant/cards/0190a3c2-7b1e-7c3a-9d4e-5f6a7b8c9d0e/verify/plate'POST /v1/merchant/cards/{id}/translate
Machine-translates the description into English, Korean, Japanese and Chinese, marked as machine translation.
Only a language with no translation, or a machine one made from an older description, is redone; the owner's own stays. 503 translate_off when no model is set.
curl -X POST 'https://geo.dock.mn/v1/merchant/cards/0190a3c2-7b1e-7c3a-9d4e-5f6a7b8c9d0e/translate'GET /v1/merchant/photos/{id}
curl 'https://geo.dock.mn/v1/merchant/photos/0190a3c2-7b1e-7c3a-9d4e-5f6a7b8c9d0e'GET /v1/tiles/{source}
TileJSON for a tile source (building, entrance).
Tile URLs point back at this API. A key passed as ?access_token= is carried into them.
curl 'https://geo.dock.mn/v1/tiles/building' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/tiles/{source}/{z}/{x}/{y}
One Mapbox Vector Tile. 204 when the tile is empty.
Tiles have no per-minute limit (since 0.78.0); they count against tile_daily_quota per organization per day (default 100,000). Other routes count against rate_limit and daily_quota.
curl 'https://geo.dock.mn/v1/tiles/building/15/26115/11401' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/tiles/{sources}/tilequery/{point}
Features of one or more tile sources at or near a point, nearest first, as GeoJSON.
sources is building or entrance or place, or both joined by a comma, as Mapbox's tileset list. point is lon,lat; a trailing .json is taken, as in Mapbox's path. radius 0, the default, finds what the point is inside; an entrance needs a radius. Each feature is a Point, the part of it nearest the point sent, with its tile fields and tilequery (distance, geometry, layer), as Mapbox's. The same rows as the tiles: current, not demolished, nothing in a restricted site, no building codes. Counts against rate_limit and daily_quota, not the tile budgets. Coverage: Ulaanbaatar.
curl 'https://geo.dock.mn/v1/tiles/building%2Centrance/tilequery/106.9177%2C47.9186' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/route
Route between two points or building codes, through via ends in order. A code routes to the street side of its building or the gate of its yard, not the middle.
from and to are each a building code ("7QSVX") or lon,lat ("106.9301,47.9201"). A code is resolved to the street side of its building or the gate of its yard (PLAN.md 9 Phase 2) and needs door:read as well; it counts against the daily door miss cap like GET /v1/door/{code}. An end may also be an entrance id from GET /v1/address/{id}/entrances or GET /v1/entrances/near (needs geocode:read): the route ends at that entrance, never the building's middle, and to.entrance carries its code (null with no door yet), level and confidence (0 is generated, таамаг). The line follows the road network and ends where the network is nearest the entrance; to.gap_m is the straight walk left from there, which is never drawn as road. A code of a building or a yard ends at the gate when the map has one, else at the street side: the point of the building or yard outline nearest the street the line ends on, so the walk is the few metres from the street to the fence or wall, not to the middle. to.point is that end; to.unit.street_side is true when it is a gate, or within about 25 m of the street point. When the walk is over 15 m the last step says to leave the car ("Машинаа тавиад хаалга хүртэл 17 м алхана уу.", "Leave the car and walk 17 m to the gate."); the arrival says "Хаалга зүүн талд байна", the gate and its side. An end given as a point, not a code, has no gate: its arrival says "Очих газар зүүн талд байна" (the destination and its side). No route starts or ends inside a restricted site (PLAN.md 20.2). Every road closure active today (GET /v1/closures) is avoided in every mode, and closures.avoided names them; over the router's limits for one request the route is refused, never a closure dropped. An end inside an active closure is refused with its id: the router has no road there. duration_s is free-flow. Driving routes also carry measured: the time with each road that has a published moving speed (GET /v1/traces/roads, the latest published month, 3+ partners) at that speed: all hours, or with depart_at the speeds of its hour band (GET /v1/traces/roads?band=), by its Ulaanbaatar weekday and hour. via passes up to 10 more ends on the way, in the order sent, as Mapbox's waypoints between the first and the last: each a code or lon,lat as from and to, and each refused as they are, named by its place (via[1]). The line may turn back at a via end. legs has one hop per pair, via the ends in order with where the line passes them. Coverage: Ulaanbaatar.
curl 'https://geo.dock.mn/v1/route?to=7QSVX&from=106.9177%2C47.9186' \
-H "Authorization: Bearer $DOCK_KEY"curl 'https://geo.dock.mn/v1/route?from=106.9177,47.9186&to=7QSVX' \
-H "Authorization: Bearer $DOCK_KEY"Answer 200, 2026-10-20, recorded from geo.dock.mn. Long lists keep their first 3 items and end with "... N more" to fit the page.
{
"mode": "driving",
"distance_m": 2314,
"duration_s": 356,
"measured": {
"month": "2026-09",
"band": null,
"roads_m": 0,
"duration_s": 356
},
"geometry": {
"type": "LineString",
"coordinates": [
[
106.917828,
47.917943
],
[
106.917813,
47.917942
],
[
106.916771,
47.91785
],
"... 61 more"
]
},
"legs": [
{
"distance_m": 2314,
"duration_s": 356,
"steps": [
{
"kind": "depart",
"location": [
106.917828,
47.917943
],
"distance_m": 109,
"duration_s": 40,
"street": null,
"voice": [
"depart"
],
"signal": false,
"mn": "Замд гарлаа.",
"en": "Start."
},
{
"kind": "left",
"location": [
106.916362,
47.917816
],
"distance_m": 126,
"duration_s": 21,
"street": "Д.Сүхбаатарын гудамж",
"voice": [
"left"
],
"signal": false,
"mn": "Зүүн тийш эргэнэ үү: Д.Сүхбаатарын гудамж.",
"en": "Turn left onto Д.Сүхбаатарын гудамж."
},
{
"kind": "left",
"location": [
106.916229,
47.916689
],
"distance_m": 1450,
"duration_s": 157,
"street": "A0501",
"voice": [
"left"
],
"signal": false,
"mn": "Зүүн тийш эргэнэ үү.",
"en": "Turn left onto A0501."
},
"... 4 more"
]
}
],
"from": {
"point": [
106.9177,
47.9186
],
"snapped": [
106.917828,
47.917943
],
"gap_m": 73.7,
"door": null,
"entrance": null,
"source_date": null
},
"via": [],
"to": {
"point": [
106.93754531853587,
47.92264397546967
],
"snapped": [
106.937405,
47.922622
],
"gap_m": 10.7,
"door": null,
"entrance": null,
"source_date": "2026-10-19",
"unit": {
"code": "7QSVX",
"kind": "building",
"official_address": {
"country": "MN",
"postal_code": "13381-0034",
"city_or_aimag": "Улаанбаатар",
"khoroo_or_bag": "3-р хороо",
"district_or_soum": "Баянзүрх",
"building_or_khashaa": "8",
"street_or_microdistrict": "Сансар"
},
"official_status": "matched",
"unofficial": false,
"street_side": true
}
},
"closures": {
"as_of": "2026-10-20",
"avoided": []
},
"source": "valhalla; © OpenStreetMap contributors"
}GET /v1/matrix
Travel time and distance from every from to every to: up to 10 by 10 routes in one call.
from and to are each 1 to 10 ends joined by ";", each end as in GET /v1/route: a building code ("7QSVX", to its street side or gate; needs door:read as well) or lon,lat. durations_s[i][j] and distances_m[i][j] go from from[i] to to[j]; null where no path joins them. Every code is looked up and every one that is no door counts against the daily door miss cap, then the first is refused. Restricted sites and today's closures as GET /v1/route: an end in either is refused, every closure is avoided. Free-flow times only: no measured speeds, no depart_at, no line. One call, whatever the number of cells. Coverage: Ulaanbaatar.
curl 'https://geo.dock.mn/v1/matrix?to=7QSVX%3B106.9301%2C47.9201&from=106.9177%2C47.9186%3B106.9050%2C47.9210' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/isochrone
The area reached from one end within each of up to 4 times, as GeoJSON polygons.
from is one end as in GET /v1/route: a building code ("7QSVX", from its entrance; needs door:read as well) or lon,lat. contours_minutes is 1 to 4 whole minutes up to 60, increasing, as Mapbox's. The answer is a GeoJSON FeatureCollection, one polygon per contour, largest first; add it to a map as a source. A code that is no door counts against the daily door miss cap. Restricted sites and today's closures as GET /v1/route: a from in either is refused, every closure is avoided. Free-flow times only: no measured speeds, no depart_at. Coverage: Ulaanbaatar.
curl 'https://geo.dock.mn/v1/isochrone?contours_minutes=5%2C10%2C15&from=106.9177%2C47.9186' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/matching
A GPS trace matched to the road network: the roads it went along, and where each point lies on them.
coordinates is 2 to 100 points as lon,lat joined by ";", in the order recorded, as Mapbox's. timestamps, optional, is one Unix time in seconds per point; with them two far points seconds apart are a break, not a fast road. The answer has one matching per unbroken piece and one tracepoint per point, null where no road was found for it. A point inside a restricted site (PLAN.md 20.2) is left out before matching and is null too, as POST /v1/traces leaves it out; fewer than 2 points left refuses the trace. Today's closures are not avoided: a trace is where someone went. The trace is neither stored nor logged; to share a trip with the map, send it to POST /v1/traces. Coverage: Ulaanbaatar.
curl 'https://geo.dock.mn/v1/matching?coordinates=106.9177%2C47.9186%3B106.9200%2C47.9188%3B106.9230%2C47.9190' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/optimized-trips
The order to visit stops in with the least time on the road, and the trip in that order.
coordinates is 2 to 12 ends joined by ";", each end as in GET /v1/route: a building code ("7QSVX", to its street side or gate; needs door:read as well) or lon,lat. The trip starts at the first, as Mapbox's source=first. roundtrip true (the default) goes back to it; false ends at the last sent, as destination=last. The stops between are put in the order with the least time on the road. waypoints are the stops in the order sent, each with waypoint_index, its place in the trip. Every code is looked up and every one that is no door counts against the daily door miss cap, then the first is refused. Restricted sites and today's closures as GET /v1/route: a stop in either is refused, every closure is avoided. Free-flow times only, no time at the stops. Coverage: Ulaanbaatar.
curl 'https://geo.dock.mn/v1/optimized-trips?coordinates=106.9177%2C47.9186%3B7QSVX%3B106.9050%2C47.9210%3B106.9250%2C47.9150' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/closures
Road closures current today, with the ring Valhalla avoids. No partner name, no note.
Every closure not over today: active now or starting later, so a phone that stays offline for a week still knows announced works. Send as exclude_polygons the rings of the items active on your route day (from <= route day, and route day <= to when to is set). A point closes a square of 30 m, a line a 15 m band: Valhalla drops every edge that crosses one, the whole edge between two junctions. Over 10000 m of ring or 100 vertices in one request Valhalla refuses: refuse the route, never drop a closure. ids_sha256 changes when a closure is added or lifted; it is the closures_sha256 of an offline pack index. One request counts as one call.
curl 'https://geo.dock.mn/v1/closures' \
-H "Authorization: Bearer $DOCK_KEY"POST /v1/closures
Report a stretch or a crossing that cannot be driven: flooded, washed out, a bridge down, snow, mud, works.
One report per call, kept forever: never edited or deleted, lift it when the road opens. The sender is your key's organization. Routes on dock avoid it from its from day; GET /v1/closures serves it to every partner without your name or note. Confidence 0.5: one partner's word. No reports inside a restricted site. Send an Idempotency-Key so a retry after a timeout cannot store the report twice: the same key with the same body answers the first report, stores nothing and sets Idempotent-Replayed: true; the same key with another body is 422. Keys are per organization and never expire.
curl -X POST 'https://geo.dock.mn/v1/closures' \
-H "Authorization: Bearer $DOCK_KEY" \
-H 'Content-Type: application/json' \
-d '{"geometry":{"type":"Point","coordinates":[102.10018,46.79994]},"kind":"flood"}'GET /v1/closures/{id}
One of your reports, as stored, note included: your export (PLAN.md 20.13).
Other organizations' ids are 404.
curl 'https://geo.dock.mn/v1/closures/cl-000001' \
-H "Authorization: Bearer $DOCK_KEY"POST /v1/closures/{id}/lift
The road is open again: lift one of your reports from a day, today by default.
The report stays, with lifted set. Lifting twice keeps the first day and answers it. Other organizations' ids are 404.
curl -X POST 'https://geo.dock.mn/v1/closures/cl-000001/lift' \
-H "Authorization: Bearer $DOCK_KEY"POST /v1/clean
Clean up to 100 raw addresses: district, khoroo, building, entrance, floor, unit, matched building.
Spans come back as typed ("12 давхар") and, under numbers, as values (floor 12). The building is a footprint id and point, with the method that found it and that method's share right on test rows. No state address register is matched yet (PLAN.md 4d), so every row the engine answers is unofficial. An ambiguous address gives up to 3 choices, each with a hint (PLAN.md 4b). A building in a restricted site is withheld, in choices too (PLAN.md 20.2). Coverage: Ulaanbaatar. When the door_names feature is open to your org and a whole row is a live door name, in any spelling that folds to it ("сансар-бэйкери"), the row answers that door: method door_name, the door's official address and status, its entrance point (marked for your org, as GET /v1/door/{code} marks it), the building the entrance is on (null for a gate on a plot), no choices, match_confidence null. The parsed parts stay as read. The holder org is not shown. Each such row writes one resolution log row; no other row does.
curl -X POST 'https://geo.dock.mn/v1/clean' \
-H "Authorization: Bearer $DOCK_KEY" \
-H 'Content-Type: application/json' \
-d '{"addresses":["ХУД, 11-р хороо, Их Монгол Улсын гудамж, Их монгол плаза, 1-р давхар"]}'POST /v1/signup
A free company and its first key, at once. A name and an e-mail if you like.
Makes a free organization with one key: door:read, geocode:read, route:read, tiles:read; 60 requests and 600 tiles a minute; 1,000 requests, 25,000 tiles and 50 door misses a day. The key is in this answer only. A few keys per address per day, and a cap on free keys per day for everyone; both reset at 00:00 Ulaanbaatar time. For more, ask dock for a partner key. The body may carry {"email": ...}, the account's contact (dock.mn/account): kept with the organization, never logged, never in an answer or an export; that address is the company's owner (GET /v1/org). {"name": ...} names the company (2 to 80 characters). The key also has org:admin: it makes the company's other keys (POST /v1/keys) and invites its people (POST /v1/org/invites). No body is fine.
curl -X POST 'https://geo.dock.mn/v1/signup'GET /v1/org
Your company: name, plan, limits for all its keys together, its people (org:admin keys), its live keys.
curl 'https://geo.dock.mn/v1/org' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/keys
Your company's keys, by prefix; never a key itself, never another company's.
curl 'https://geo.dock.mn/v1/keys' \
-H "Authorization: Bearer $DOCK_KEY"POST /v1/keys
A new key for your company, with some of your key's scopes.
The key is in this answer only. Its calls count in the company's daily limit with all its other keys. Up to 50 live keys.
curl -X POST 'https://geo.dock.mn/v1/keys' \
-H "Authorization: Bearer $DOCK_KEY" \
-H 'Content-Type: application/json' \
-d '{"name":"checkout page"}'DELETE /v1/keys/{prefix}
Revoke one of your company's keys. It stops within a minute.
curl -X DELETE 'https://geo.dock.mn/v1/keys/dk_0123abcd' \
-H "Authorization: Bearer $DOCK_KEY"POST /v1/org/invites
Invite a person to your company: admin, developer or driver.
Answers a one-use link for 7 days; send it yourself (dock sends no e-mail yet). Opening it gives the person a key of their own: an admin your scopes, a developer your scopes but org:admin, a driver what the driver app needs. Inviting the same address again replaces the open link.
curl -X POST 'https://geo.dock.mn/v1/org/invites' \
-H "Authorization: Bearer $DOCK_KEY" \
-H 'Content-Type: application/json' \
-d '{"email":"[email protected]","role":"admin"}'POST /v1/org/invites/accept
Accept an invite: the token from its link gives you your own key in that company.
curl -X POST 'https://geo.dock.mn/v1/org/invites/accept' \
-H "Authorization: Bearer $DOCK_KEY" \
-H 'Content-Type: application/json' \
-d '{"token":"iv_..."}'POST /v1/orders/import
Deliveries from a spreadsheet: paste it (tabs) or send CSV; each address found by dock's search.
Columns by their header (ref, address, note, in Mongolian or English), else ref, address, note. Other columns, such as the customer's name or phone, are not kept. An address is a building code, lon,lat, or words. One dock does not find as a door, building, business or landmark stays unlocated: listed, never planned, never a guessed point. Up to 500 rows.
curl -X POST 'https://geo.dock.mn/v1/orders/import' \
-H "Authorization: Bearer $DOCK_KEY" \
-H 'Content-Type: application/json' \
-d '{"sheet":"ref\taddress\tnote\nA-1\tСансар 24, 12 давхар, 1204 тоот\t"}'GET /v1/orders
Your company's open deliveries and those done in the last 24 hours, by driver and stop.
curl 'https://geo.dock.mn/v1/orders' \
-H "Authorization: Bearer $DOCK_KEY"POST /v1/dispatch/plan
Plan the open deliveries over your drivers: who takes which, in what order, and when each arrives.
Stops are split by bearing from the depot, the same number each, then each driver's order is the shortest round trip by road, closures avoided. Arrival times add 3 minutes a stop. Unlocated deliveries are left out.
curl -X POST 'https://geo.dock.mn/v1/dispatch/plan' \
-H "Authorization: Bearer $DOCK_KEY" \
-H 'Content-Type: application/json' \
-d '{"depot":[106.9177,47.9186]}'PATCH /v1/orders/{id}
Give one delivery to another driver (driver), or put it where it is (point, from a search pick).
driver: last on their list, its arrival time cleared until the next plan. point: an unlocated or misplaced delivery placed by the dispatcher, match point, confidence 1, source dispatcher; planned on the next plan.
curl -X PATCH 'https://geo.dock.mn/v1/orders/0192f0a4-7c1e-7a3b-9d2e-5b6c7d8e9f01' \
-H "Authorization: Bearer $DOCK_KEY" \
-H 'Content-Type: application/json' \
-d '{"driver":"[email protected]","point":[106.9177,47.9186],"code":"7QSVX"}'POST /v1/orders/{id}/track
A link for the customer: status, arrival time, and the driver on a map while theirs is the next stop. A new link replaces the old.
curl -X POST 'https://geo.dock.mn/v1/orders/0192f0a4-7c1e-7a3b-9d2e-5b6c7d8e9f01/track' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/track/{token}
What a tracking link shows: no key, no name, no phone; the driver only while this is their next stop.
curl 'https://geo.dock.mn/v1/track/tr_0123456789abcdefghijklmn' \
-H "Authorization: Bearer $DOCK_KEY"POST /v1/track/{token}/door
The customer taps "энэ миний хаалга" on the tracking link: their word that the delivery's door is theirs. With one delivered stop within 8 m it makes the door trusted (level 3). No photo, no name, once per delivery.
curl -X POST 'https://geo.dock.mn/v1/track/tr_0123456789abcdefghijklmn/door' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/fleet/positions
Your drivers' latest fixes of the last 10 minutes, as the driver app sends them.
curl 'https://geo.dock.mn/v1/fleet/positions' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/driver/jobs
The driver app: this driver's stops today, in order. A driver's own key (their invite) only.
curl 'https://geo.dock.mn/v1/driver/jobs' \
-H "Authorization: Bearer $DOCK_KEY"POST /v1/driver/position
The driver app's latest fix; only the latest is kept.
curl -X POST 'https://geo.dock.mn/v1/driver/position' \
-H "Authorization: Bearer $DOCK_KEY" \
-H 'Content-Type: application/json' \
-d '{"lon":106.9177,"lat":47.9186}'POST /v1/driver/jobs/{id}/status
The driver marks one of their stops: arrived, delivered or failed. Delivered with the driver's fix of the last 2 minutes within 8 m of the door's entrance is a stop for that door (3 on three days, or 1 and the customer's word, make it trusted).
curl -X POST 'https://geo.dock.mn/v1/driver/jobs/0192f0a4-7c1e-7a3b-9d2e-5b6c7d8e9f01/status' \
-H "Authorization: Bearer $DOCK_KEY" \
-H 'Content-Type: application/json' \
-d '{"status":"arrived"}'POST /v1/driver/jobs/{id}/later
The driver puts one of their open stops last in their list. Stops that are done stay where they are.
curl -X POST 'https://geo.dock.mn/v1/driver/jobs/0192f0a4-7c1e-7a3b-9d2e-5b6c7d8e9f01/later' \
-H "Authorization: Bearer $DOCK_KEY"POST /v1/styles
Save a map style: dock's colors and extrusion, or a whole MapLibre style. Returns its id.
Every field is optional; a missing one takes the dock default. Entrance colors show confidence and are the same on every map. Or {"name", "style"}: a whole MapLibre style (version 8, as dock.mn/studio saves it), up to 512 KB, valid by the style spec, with access_token taken out of its URLs; a key left in it is refused. A style never changes: to change it, save a new one. Up to 100 per org.
curl -X POST 'https://geo.dock.mn/v1/styles' \
-H "Authorization: Bearer $DOCK_KEY" \
-H 'Content-Type: application/json' \
-d '{"name":"Sansar Delivery","background":"#f4f1ea","building":"#d9d3c7","building_outline":"#a39b8b","extrude":false}'GET /v1/styles
Your org's styles, newest first.
curl 'https://geo.dock.mn/v1/styles' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/styles/{id}
A saved style as a MapLibre style (version 8), sources on this API.
Pass the id to createMap as styleId, or this URL to maplibre-gl as style. A key passed as ?access_token= is carried into the source URLs. Only keys of the org that saved the style can read it.
curl 'https://geo.dock.mn/v1/styles/st_7m4xq2b9kd' \
-H "Authorization: Bearer $DOCK_KEY"POST /v1/alerts
Add a usage alert: tell me when today's calls, tiles or door misses pass a share of the daily cap.
Fires at most once per Ulaanbaatar day, within about a minute. With webhook_url, each firing is one POST, signed in Dock-Signature with the webhook_secret returned here once. An alert never changes. Up to 20 per org. Not counted.
curl -X POST 'https://geo.dock.mn/v1/alerts' \
-H "Authorization: Bearer $DOCK_KEY" \
-H 'Content-Type: application/json' \
-d '{"metric":"calls"}'GET /v1/alerts
Your org's alerts, and their firings of the last 30 days. Not counted.
curl 'https://geo.dock.mn/v1/alerts' \
-H "Authorization: Bearer $DOCK_KEY"POST /v1/telemetry/search
Send up to 50 place searches your users made, and what they picked. Opt-in only.
Send only for users who opted in, with consent: "opt_in". Leave it off by default. dock keeps the text, the result count and the pick, with your organization and the Ulaanbaatar day; no key, IP, device, session or time of day. Runs of 6 or more digits and e-mail addresses are removed before storage. Searches teach area names and aliases (PLAN.md 18); anything shown outside dock is grouped, at least 5 searches a group. Send only what was typed into a place search, never a person's name. One request counts as one call.
curl -X POST 'https://geo.dock.mn/v1/telemetry/search' \
-H "Authorization: Bearer $DOCK_KEY" \
-H 'Content-Type: application/json' \
-d '{"consent":"opt_in","events":[{"q":"Сансар 24","results":5}]}'POST /v1/traces
Send one trip a user recorded, dock.trace.v1, up to 20000 points. Opt-in only.
Send only after the user said yes, with consent: "opt_in". Three keys and nothing else: any other key (device id, user, phone, speed, accuracy) refuses the whole upload. Points are [lon, lat, t], whole seconds UTC. dock keeps the points with your organization and the Ulaanbaatar day; no key, IP or device. Points inside a restricted site are not stored. The same trip sent twice is stored once. Raw points are erased after 90 days (PLAN.md 20.13); DELETE erases them now. One request counts as one call.
curl -X POST 'https://geo.dock.mn/v1/traces' \
-H "Authorization: Bearer $DOCK_KEY" \
-H 'Content-Type: application/json' \
-d '{"format":"dock.trace.v1","consent":"opt_in","points":[[102.71541,47.10125,1790812800],[102.71563,47.10141,1790812805]]}'GET /v1/traces/cells
Trips per 500 m cell and month, all partners together. The only way trip data leaves dock (PLAN.md 19).
A cell is shown only when at least 3 organizations sent trips through it that month and the trips left after taking out the biggest organization's are 5 or more. Every other cell is left out: no cell means too few, not zero. Test and internal organizations do not count. One fixed grid and whole months only, published 14 days after the month ends; trips received later are not counted, so a number changes only when a partner deletes a trip. Counts survive the 90-day erase of the raw points. No ids, times, orgs or lines.
curl 'https://geo.dock.mn/v1/traces/cells?bbox=106.7%2C47.8%2C107.2%2C48.0&month=2026-08' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/traces/speeds
Moving speed per 500 m cell and month, all partners together: where traffic is slow (PLAN.md 18, 19).
Each trip is cut into segments between its stored points. A segment counts when its points are 1 to 60 seconds apart, it moves at 3 to 130 km/h, and its midpoint is more than 200 m from the trip's first and last point: parking and the walk to the door are not road speed. It counts in the cell of GET /v1/traces/cells holding its midpoint. km_h is the counted meters over the counted seconds of all trips in the cell, to 0.1. A cell is shown only when at least 3 organizations had counted trips in it that month and the trips left after taking out the biggest organization's are 5 or more. Every other cell is left out: no cell means too few trips, not a slow or empty road. No trip count: GET /v1/traces/cells has one. Per cell: GET /v1/traces/roads has speed per road. Test and internal organizations do not count. Whole months only, published 14 days after the month ends; trips received later are not counted. Speeds survive the 90-day erase of the raw points. No ids, times, orgs or lines.
curl 'https://geo.dock.mn/v1/traces/speeds?bbox=106.7%2C47.8%2C107.2%2C48.0&month=2026-08' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/traces/roads
Moving speed per road and month, all partners together: speed profiles per road (PLAN.md 18, 19).
Each trip is matched to OSM ways with Valhalla map matching (Meili) when it is stored. A segment between two stored points counts on a way when both points matched that way, they are 1 to 60 seconds apart, it moves at 3 to 130 km/h, and its midpoint is more than 200 m from the trip's first and last point, the rule of GET /v1/traces/speeds. km_h is the counted meters over the counted seconds of all trips on the way, to 0.1. A road is shown only when at least 3 organizations had counted trips on it that month and the trips left after taking out the biggest organization's are 5 or more. Every other road is left out: no road means too few trips, not a slow or empty road. The line is dock's current road with that OSM id: a way dock has no road line for, or one crossing a restricted site, is left out. No trip count. Test and internal organizations do not count. Whole months only, published 14 days after the month ends; a trip is matched only before then. Speeds survive the 90-day erase of the raw points. No trip ids, times, orgs or trip lines. With band, only the trips that started in that hour band count, and each road passes the group rule on those trips alone: a road can be shown for all hours and left out of a band. rule.hours says which hours an answer covers.
curl 'https://geo.dock.mn/v1/traces/roads?bbox=106.7%2C47.8%2C107.2%2C48.0&month=2026-08' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/traces/cells/own
Your own trips per 500 m cell and month: your dashboard (PLAN.md 18, 20.13). No other organization is read.
The same grid and months as GET /v1/traces/cells, your organization's trips only, every cell with one of them: no group rule, the trips are yours. The month you are in is answered too and grows as trips arrive; closed is true from 14 days after the month ends, when the counts are final. Trips received later are stored, not counted. Your DELETE /v1/traces/{id} takes a trip out; the 90-day erase of the raw points keeps it. Needs trace:read, a read-only scope for a dashboard key; trace:write does not read it.
curl 'https://geo.dock.mn/v1/traces/cells/own?bbox=106.7%2C47.8%2C107.2%2C48.0&month=2026-10' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/traces/arrivals/own
Your own arrival minutes per month: how long your trips spend in the last 200 m (PLAN.md 18). No other organization is read.
For each of your trips: whole minutes from the first point after it was last more than 200 m from its end, to its end, 60 meaning 60 or more. Circling inside 200 m counts; a trip that never left 200 m of its end is not counted. The minutes include parking and the walk to the door: compare months and places with them. Month totals with median, p75 and p90 (nearest rank), trips per minute, and up to 100 cells where trips ended, by trips of 5 minutes or more. The same grid and months as GET /v1/traces/cells/own; closed is true from 14 days after the month ends. Your DELETE /v1/traces/{id} takes a trip out; the 90-day erase of the raw points keeps it. Needs trace:read.
curl 'https://geo.dock.mn/v1/traces/arrivals/own?month=2026-10' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/traces/hours/own
Your own trips per weekday and hour, per month: when your trips run (PLAN.md 18). No other organization is read.
Each of your trips counts once, in the Ulaanbaatar weekday (1 Monday to 7 Sunday) and hour (0 to 23) of its first stored point. Month total, 7 weekday counts, 24 hour counts, and every weekday and hour with trips. The same months as GET /v1/traces/cells/own; closed is true from 14 days after the month ends. Your DELETE /v1/traces/{id} takes a trip out; the 90-day erase of the raw points keeps it. Needs trace:read.
curl 'https://geo.dock.mn/v1/traces/hours/own?month=2026-10' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/traces/hours
Trips per weekday and hour and month, all partners together: road load by hour (PLAN.md 18, 19).
Each trip counts once, in the Ulaanbaatar weekday (1 Monday to 7 Sunday) and hour (0 to 23) of its first stored point. A weekday and hour is shown only when at least 3 organizations sent trips that started in it that month and the trips left after taking out the biggest organization's are 5 or more, the rule of GET /v1/traces/cells. Every other slot is left out: no slot means too few, not zero. No total, so a left-out slot cannot be worked out. Test and internal organizations do not count. Whole months only, published 14 days after the month ends; trips received later are not counted. Counts survive the 90-day erase of the raw points. No ids, places, times or orgs.
curl 'https://geo.dock.mn/v1/traces/hours?month=2026-08' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/traces/khoroos
Trips per khoroo and hour of arrival and month, all partners together: where and when trips end (PLAN.md 18, 19).
Each trip counts once, in the Ulaanbaatar hour (0 to 23) of its last stored point and the khoroo holding the centre of the 500 m cell of GET /v1/traces/cells that point is in. A cell on a khoroo boundary counts whole in one khoroo. Current khoroo polygons only: a khoroo with no polygon is not shown. A khoroo and hour is shown only when at least 3 organizations had trips ending in it that month and the trips left after taking out the biggest organization's are 5 or more, the rule of GET /v1/traces/cells. Every other hour is left out: no hour means too few, not zero. No khoroo total. Test and internal organizations do not count. Whole months only, published 14 days after the month ends; trips received later are not counted. Counts survive the 90-day erase of the raw points. No trip ids, points, times or orgs.
curl 'https://geo.dock.mn/v1/traces/khoroos?month=2026-08' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/traces/khoroos/own
Your own trips per khoroo and hour of arrival, per month: where and when your trips end (PLAN.md 18). No other organization is read.
Each of your trips counts once, in the Ulaanbaatar hour (0 to 23) of its last stored point and the khoroo holding the centre of the 500 m cell of GET /v1/traces/cells that point is in, as GET /v1/traces/khoroos. A cell counts whole in one khoroo; where current polygons overlap, the first by district code and number. No group rule: the trips are yours. Month total, trips ending in no current khoroo polygon (no_khoroo), and one feature per khoroo with its total and every hour with trips. Khoroo polygons are missing today: until they are loaded every trip is in no_khoroo. The same months as GET /v1/traces/cells/own; closed is true from 14 days after the month ends. Your DELETE /v1/traces/{id} takes a trip out; the 90-day erase of the raw points keeps it. Needs trace:read.
curl 'https://geo.dock.mn/v1/traces/khoroos/own?month=2026-10' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/traces/kinds/own
Your own trips per kind of building at the trip end, per month: what kind of deliveries you make (PLAN.md 18). No other organization is read.
Each of your trips counts once, by the building at its last stored point: the one whose footprint holds the point, else the nearest within 30 m. The kind is the building type when the trip was stored (apartment, ger_house, commercial, public, industrial, garage, other); unknown is a building with no type yet, none is no building within 30 m. A later change to the building does not move a trip already counted. A building inside a restricted site is not read. No group rule: the trips are yours. Month total and every kind with trips, most first. The same months as GET /v1/traces/cells/own; closed is true from 14 days after the month ends. Your DELETE /v1/traces/{id} takes a trip out; the 90-day erase of the raw points keeps it. Needs trace:read.
curl 'https://geo.dock.mn/v1/traces/kinds/own?month=2026-10' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/traces/{id}
One of your traces as GeoJSON, as stored: your export (PLAN.md 20.13).
geometry is null once the trace was deleted or passed purge_after. Other organizations' ids are 404.
curl 'https://geo.dock.mn/v1/traces/0199a1c2-7d3e-7f00-8a1b-2c3d4e5f6a7b' \
-H "Authorization: Bearer $DOCK_KEY"DELETE /v1/traces/{id}
Delete one of your traces now. The points and times are erased; the id is kept and never reused.
Deleting twice is fine: the second call also answers deleted: true. Other organizations' ids are 404.
curl -X DELETE 'https://geo.dock.mn/v1/traces/0199a1c2-7d3e-7f00-8a1b-2c3d4e5f6a7b' \
-H "Authorization: Bearer $DOCK_KEY"POST /v1/traffic/probes
Send one vehicle's last positions, dock.probe.v1, up to 900 points. Fleets under contract only.
Open to an organization once its fleet contract with dock is signed (feature traffic.probes); that contract is the consent, so no driver is asked and none is named. Send every 30 seconds or so while the vehicle moves. Three keys and nothing else: any other key (driver, phone, plate, device, accuracy) refuses the batch. vehicle is a random token the app makes anew every 15 minutes. dock matches the points to roads at once and keeps only each road's moving meters and seconds per 15-minute window, with a tag that tells vehicles apart in that window; the points, the token and the tags are not kept, and the window's tags are gone when it closes, 5 minutes after it ends. Points inside a restricted site are not matched. One request counts as one call.
curl -X POST 'https://geo.dock.mn/v1/traffic/probes' \
-H "Authorization: Bearer $DOCK_KEY" \
-H 'Content-Type: application/json' \
-d '{"format":"dock.probe.v1","vehicle":"q3v9x0c2m8k4t7wz","points":[[106.9177,47.9186,1791090000],[106.9183,47.9186,1791090005]]}'GET /v1/traffic/typical
The usual speed per road for this hour band, for the map layer. Never live, never one trip.
Open to the scope the public map key has (tiles:read) and counted like a tile request: it has no per-minute limit and counts against tile_daily_quota. Cached 5 minutes. The band is the Ulaanbaatar clock of this moment: am_peak (Monday to Friday 07:00 to 09:59), pm_peak (Monday to Friday 17:00 to 19:59) or off_peak. The speeds are the newest closed month that has any (GET /v1/traces/roads with that band), all partners together, each road passing the group rule (3 or more organizations, no single organization over a share). No trip, vehicle, probe, time or organization is in the answer. A road without enough traces is left out: no road means no speed, not an empty road. When no month has any road in the box, features is empty and typical.month is null. Show it as "ердийн" (typical.label), never as live. Draw at zoom 12 and up.
curl 'https://geo.dock.mn/v1/traffic/typical?bbox=106.7%2C47.8%2C107.2%2C48.0' \
-H "Authorization: Bearer $DOCK_KEY"curl 'https://geo.dock.mn/v1/traffic/typical?bbox=106.91,47.912,106.925,47.924' \
-H "Authorization: Bearer $DOCK_KEY"Answer 200, 2026-10-20, recorded from geo.dock.mn.
{
"type": "FeatureCollection",
"kind": "typical",
"typical": {
"label": "ердийн",
"band": "pm_peak",
"hours": "Monday to Friday, 17:00 to 19:59 Ulaanbaatar time",
"month": null
},
"rule": {
"min_orgs": 3,
"min_trips_without_largest_org": 5,
"orgs": "all partners together; test and internal organizations left out; no organization is named",
"road": "an OSM way, from Valhalla map matching (Meili) of the stored points; the line is dock's current road with that OSM id",
"segment": "two stored points of a trip 1 to 60 seconds apart, at 3 to 130 km/h, its midpoint more than 200 m from the first and last point of the trip, both points matched to the same way",
"km_h": "the counted segments' total meters over their total seconds on the way, to 0.1",
"period": "Ulaanbaatar calendar month, published 14 days after it ends"
},
"source": "trace",
"source_date": null,
"features": []
}GET /v1/traffic
Live speed per road: the last 15 minutes that have ended, roads that 5 or more vehicles passed.
Windows are 15 minutes on the quarter hour; the answer is the last one that has ended, so it is 0 to 15 minutes old. A road is shown only when at least 5 vehicles of partner fleets had counted segments on it in the window. A segment counts when two probes of one vehicle are 1 to 60 seconds apart, move at 3 to 130 km/h, and both matched the same way: a vehicle standing still is not counted, so a jam reads as its crawling speed. km_h is the counted meters over the counted seconds on the way, to 0.1. Every other road is left out: no road means too few vehicles, not an empty road. The line is dock's current road with that OSM id; a road crossing a restricted site is left out. Test and internal organizations do not count. No organizations, vehicles or points. When no road passes the rule (no fleet probes now), the answer is typical: kind is "typical", the roads are the speeds of the hour band of this moment (GET /v1/traces/roads band) in the newest closed month that has any, window is null, vehicles is null, and typical names the band and month. Show it as "ердийн", never as live.
curl 'https://geo.dock.mn/v1/traffic?bbox=106.7%2C47.8%2C107.2%2C48.0' \
-H "Authorization: Bearer $DOCK_KEY"curl 'https://geo.dock.mn/v1/traffic?bbox=106.85,47.88,106.95,47.95' \
-H "Authorization: Bearer $DOCK_KEY"Answer 403, 2026-10-20, recorded from geo.dock.mn. Road speeds need a key with insights:read; the free key answers 403. With no live probes the answer is kind "typical", label "ердийн".
{
"error": "insufficient_scope",
"message": "key lacks scope insights:read"
}POST /v1/events
Open an event: a name, an area and a time. Organizers under contract only.
Open to an organization once its contract with dock is signed (feature events.live). The area is a box; participants' points outside it or outside the time are not stored. 15 minutes after ends_at every point is erased and only the heat cells stay. The event is never deleted.
curl -X POST 'https://geo.dock.mn/v1/events' \
-H "Authorization: Bearer $DOCK_KEY" \
-H 'Content-Type: application/json' \
-d '{"name":"Улаанбаатар марафон 2027","bbox":[106.85,47.88,106.98,47.93],"starts_at":"2027-06-12T08:00:00+08:00","ends_at":"2027-06-12T14:00:00+08:00"}'GET /v1/events/{id}
One of your events.
Other organizations' ids are 404.
curl 'https://geo.dock.mn/v1/events/0199a1c2-7d3e-7f00-8a1b-2c3d4e5f6a7b' \
-H "Authorization: Bearer $DOCK_KEY"POST /v1/events/{id}/positions
Send one participant's last positions, dock.position.v1, up to 900 points.
Send every 10 seconds or so, only after the participant said yes in your app (consent opt_in). Four keys and nothing else: any other key (name, bib, phone, device, accuracy) refuses the batch. participant is a random token your app makes when it starts sending; dock keeps only a tag made from it with the event's own salt, and the salt is deleted when the event closes. Points outside the event's area or time, or in a restricted site, are not stored. The rest are erased 15 minutes after the event ends. One request counts as one call.
curl -X POST 'https://geo.dock.mn/v1/events/0199a1c2-7d3e-7f00-8a1b-2c3d4e5f6a7b/positions' \
-H "Authorization: Bearer $DOCK_KEY" \
-H 'Content-Type: application/json' \
-d '{"format":"dock.position.v1","consent":"opt_in","participant":"h4w2k9m0c7x3v8q1z5r6","points":[[106.9177,47.9186,1812420000],[106.9181,47.9187,1812420010]]}'GET /v1/events/{id}/live
Live dots for your event's page: each participant's last point of the last 2 minutes.
Nothing when fewer than 5 participants are live. Points are rounded to 4 decimals (about 10 m) and sorted by place; no participant id, name or tag, so two answers cannot be joined by id. Poll every 10 seconds or so. Empty once the event has closed.
curl 'https://geo.dock.mn/v1/events/0199a1c2-7d3e-7f00-8a1b-2c3d4e5f6a7b/live' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/events/{id}/heat
Heat per area for your event: participants per 100 m cell, cells with 5 or more.
The whole event, or one 15-minute window with ?window=. Cells are EPSG:3857 squares of 150 units, about 100 m at Ulaanbaatar. A cell with fewer than 5 distinct participants is left out, and so is one touching a restricted site. While the event is open the cells grow; 15 minutes after its end they are final and stay after every point is erased.
curl 'https://geo.dock.mn/v1/events/0199a1c2-7d3e-7f00-8a1b-2c3d4e5f6a7b/heat' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/corridors
Steppe corridors, one compare row each: braided share, envelope, years, confidence.
Every corridor the steppe dashboard renders, ranked by the share of cross-sections with 3 or more ruts in its last clear year. A rank is a lead to check, not a finding: last years and confidence differ by corridor. Numbers come from Sentinel-2 at 10 m and are not field checked; null means unknown. Show the attribution lines with the numbers. One request counts as one call.
curl 'https://geo.dock.mn/v1/corridors' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/corridors/{name}
One steppe corridor with every fact line: text, value, unit, confidence, source, caveat.
The numbers the dashboard and its PDF show a ministry: clear years, stretch, rut km per year, braided share, envelope, pasture, hotspots, the recommended track, verified track km. No line is surer than confidence_cap. Business places are counted, and named only when matched to an official registration. One request counts as one call.
curl 'https://geo.dock.mn/v1/corridors/bayanzag' \
-H "Authorization: Bearer $DOCK_KEY"POST /v1/door-names/{name}/notes
Write access notes for the door your name points at, for review.
Only the org that holds an unexpired name for the door may write. Mongolian, English or both, up to 500 characters each: which entrance, which sign, what to say at the gate. The note waits for a person to review it; GET /v1/door/{code} keeps serving the notes the map holds until then, and GET /v1/door-names/{name}/notes shows whether it was accepted. Writing again replaces your waiting text, kept in history; other orgs' notes for the same door are their own. A note may hold no phone number and no e-mail. A name your org does not hold, or whose door was retired, is 404. Open to an org only when the hotel.notes feature is (GET /v1/usage features); else 403 feature_off.
curl -X POST 'https://geo.dock.mn/v1/door-names/%D0%A1%D0%B0%D0%BD%D1%81%D0%B0%D1%80%20%D0%B1%D1%8D%D0%B9%D0%BA%D0%B5%D1%80%D0%B8/notes' \
-H "Authorization: Bearer $DOCK_KEY" \
-H 'Content-Type: application/json' \
-d '{"instructions_mn":"Зүүн талын хаалга дээр «Сансар» гэсэн самбар.","instructions_en":"Second entrance on the east side, sign «Сансар» above the door."}'GET /v1/door-names/{name}/notes
Your notes for the door your name points at, and whether each was accepted.
Only the org that holds an unexpired name for the door may read, and it reads only the notes it wrote. Each note is waiting, accepted or refused, with the time a person reviewed it. A name your org does not hold, or whose door was retired, is 404, as when writing. Open to an org only when the hotel.notes feature is; else 403 feature_off.
curl 'https://geo.dock.mn/v1/door-names/%D0%A1%D0%B0%D0%BD%D1%81%D0%B0%D1%80%20%D0%B1%D1%8D%D0%B9%D0%BA%D0%B5%D1%80%D0%B8/notes' \
-H "Authorization: Bearer $DOCK_KEY"POST /v1/door-names/{name}/photo
Send a gate photo for the door your name points at, for review.
Only the org that holds an unexpired name for the door may send. The body is one JPEG, Content-Type image/jpeg, up to 2 MB, 320 to 8000 px on a side: the entrance or gate as a guest sees it on arrival. Its metadata is removed before it is stored: GPS, the phone, the time, XMP, comments, thumbnails. The orientation stays. The picture waits for a person to review it, who refuses one that shows a face, a car plate or a phone number; once accepted, GET /v1/door-names/{name}/photo serves it. Sending again replaces your waiting photo, kept in history. A name your org does not hold, or whose door was retired, is 404. Open to an org only when the hotel.notes feature is (GET /v1/usage features); else 403 feature_off.
curl -X POST 'https://geo.dock.mn/v1/door-names/%D0%A1%D0%B0%D0%BD%D1%81%D0%B0%D1%80%20%D0%B1%D1%8D%D0%B9%D0%BA%D0%B5%D1%80%D0%B8/photo' \
-H "Authorization: Bearer $DOCK_KEY" \
-H 'Content-Type: image/jpeg' \
--data-binary @gate.jpgGET /v1/door-names/{name}/photo
The accepted gate photo for the door your name points at, for your app to show your guests.
Only the org that holds an unexpired name for the door may read, and it reads only its own photo: the newest one a person accepted, under any of its names for the door. A waiting or refused photo is never served, nor another org's. The body is the JPEG as stored, metadata removed, orientation kept; the ETag is its sha256, as the send answered it, and If-None-Match with it is 304. Show it inside your app only, never on a public page of the place. A name your org does not hold, whose door was retired or lies in a restricted site, or with no accepted photo, is 404. Open to an org only when the hotel.notes feature is; else 403 feature_off.
curl 'https://geo.dock.mn/v1/door-names/%D0%A1%D0%B0%D0%BD%D1%81%D0%B0%D1%80%20%D0%B1%D1%8D%D0%B9%D0%BA%D0%B5%D1%80%D0%B8/photo' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/audit
Audit samples, newest first. Staff keys only.
curl 'https://geo.dock.mn/v1/audit' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/audit/{tag}
One sample's checklist in walk order, with every answer so far. Staff keys only.
Items now inside a restricted site are left out and counted in hidden (PLAN.md 20.2).
curl 'https://geo.dock.mn/v1/audit/2026-10' \
-H "Authorization: Bearer $DOCK_KEY"PATCH /v1/audit/{tag}/{seq}
What the surveyor found at one item. Staff keys only.
Sets the item's answer; a second call corrects it, result null clears it. Every call is also kept, with the key, in audit.check. The device position is a check only: within 40 m of the checked point (no checked point: of the drawn point; no_access: 150 m). A checked point over 500 m from the drawn point is too_far.
curl -X PATCH 'https://geo.dock.mn/v1/audit/2026-10/1' \
-H "Authorization: Bearer $DOCK_KEY" \
-H 'Content-Type: application/json' \
-d '{"result":"found","checked":[106.930113,47.920104],"method":"rtk","device":[106.93013,47.92012]}'GET /v1/venue
Venues at a point: the indoor maps whose outline holds it or lies within 200 m.
Nearest first, at most 10. Use it to open an indoor map from a map position. One request counts as one call.
curl 'https://geo.dock.mn/v1/venue?near=106.7632%2C47.8431' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/venue/{id}
A venue: its outline and its floors.
Floors are current ones, lowest first. One request counts as one call.
curl 'https://geo.dock.mn/v1/venue/0198a2b4-1c3d-7e5f-8a9b-0c1d2e3f4a5b' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/venue/{id}/level/{n}
One floor of a venue as GeoJSON: units, openings and amenities.
n is the floor number as people say it (0 ground, -1 basement). Each feature has feature_type, category, level, source and confidence.
curl 'https://geo.dock.mn/v1/venue/0198a2b4-1c3d-7e5f-8a9b-0c1d2e3f4a5b/level/1' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/venue/{id}/level/{n}/walk
The walk lines of one floor, for a phone that snaps its own steps to the corridors.
Corridor lines of floor n as GeoJSON LineStrings, plus the connectors that start or end on this floor (stairs, escalator, lift) with the floor they reach. The phone snaps each move to the nearest line within 25 m and offers a floor change at a connector.
curl 'https://geo.dock.mn/v1/venue/0198a2b4-1c3d-7e5f-8a9b-0c1d2e3f4a5b/level/1/walk' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/venue/{id}/imdf
A venue as an IMDF archive (zip).
manifest.json plus venue, building, level, unit, opening, amenity, anchor and occupant as GeoJSON FeatureCollections, current rows only. Every feature carries source and confidence in its properties; confidence null is unknown. Test fixtures are not served.
curl 'https://geo.dock.mn/v1/venue/0198a2b4-1c3d-7e5f-8a9b-0c1d2e3f4a5b/imdf' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/venue/{id}/spot
What a printed plate for one unit says: venue, floor, name and the link its QR holds.
The link opens the indoor view on the unit. Nothing is invented: a unit without a name gets its number ("305 тоот"), else "Сонгосон газар".
curl 'https://geo.dock.mn/v1/venue/0198a2b4-1c3d-7e5f-8a9b-0c1d2e3f4a5b/spot?unit=0198a2b4-1c3d-7e5f-8a9b-0c1d2e3f4a5c' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/venue/{id}/route
The walk inside a venue to one unit, floor by floor, with steps in Mongolian.
from is where the walk starts, lon,lat, on floor from_level (default 0: the entrance the road route used, or a tapped spot). to is a unit id from GET /v1/venue/{id}/level/{n}. The walk follows the corridors drawn for the venue and takes stairs, escalators and lifts between floors (an escalator marked one-way is not taken the wrong way). A corridor with a drawn door that is closed now (its opening_hours) is not walked; when that leaves no way the answer is 422 no_open_route. Nothing is invented: where no corridor reaches the start or the unit, the answer is 422 no_route.
curl 'https://geo.dock.mn/v1/venue/0198a2b4-1c3d-7e5f-8a9b-0c1d2e3f4a5b/route?from=106.7632%2C47.8431' \
-H "Authorization: Bearer $DOCK_KEY"GET /v1/venue/{id}/directions
The whole way to a unit: the road to the right entrance, then the walk inside, one answer.
from is lon,lat. The road goes by the routing engine to the venue's street entrances (an opening with an entrance id or the category "entrance"); the nearest 3 by straight line are tried and the quickest by road is taken. From that entrance the walk is the one GET /v1/venue/{id}/route gives, with steps in Mongolian and the floor on each. Nothing is invented: no drawn entrance is 422 no_entrance, no corridor from the entrance to the unit is 422 no_route.
curl 'https://geo.dock.mn/v1/venue/0198a2b4-1c3d-7e5f-8a9b-0c1d2e3f4a5b/directions?to=0198a2b4-1c3d-7e5f-8a9b-0c1d2e3f4a5c&from=106.9177%2C47.9186' \
-H "Authorization: Bearer $DOCK_KEY"POST /v1/push/devices
Save this phone for push: its token and the building codes it follows.
Idempotent. The list of codes replaces the earlier one. Not counted.
curl -X POST 'https://geo.dock.mn/v1/push/devices' \
-H "Authorization: Bearer $DOCK_KEY" \
-H 'Content-Type: application/json' \
-d '{"token":"fcm-or-apns-token-0123456789abcdef","platform":"ios"}'DELETE /v1/push/devices
Forget this phone: no more push to this token. The row is ended, not deleted.
curl -X DELETE 'https://geo.dock.mn/v1/push/devices' \
-H "Authorization: Bearer $DOCK_KEY" \
-H 'Content-Type: application/json' \
-d '{"token":"fcm-or-apns-token-0123456789abcdef"}'