Skip to content

Release notes (written by hand until 2026-10-12) ​

0.85.0 (2026-10-14) ​

  • New: GET /v1/entrances/near?lat=&lon= gives the building entrances nearest a point, for a long-press on the map or "Энд байгаа entrance (internal)" from the user's position: nearest first, each as GET /v1/address/{id}/entrances lists it, plus building and distance_m. Generated entrances count and say so (source generated, confidence 0). radius 50 m by default, up to 1000; limit 1 by default, up to 20. JS SDK 0.80.0: client.entrancesNear([lon, lat]). Nothing removed.

0.84.0 (2026-10-14) ​

  • New: GET /v1/address/{id}/entrances gives a building's entrances in one call, for its card: numbered ones first, each with its level (matched бүртгэлтэй, partial хэсэгчилсэн, none албан бус) and its code once a door is minted. An entrance without a door carries the building's official form from public sources, with its number. Generated entrances have source generated and confidence 0 (таамаг). Demolished 410, restricted 404. JS SDK 0.79.0: client.entrances(id). Nothing removed.

0.83.0 (2026-10-13) ​

  • New: dock Events. POST /v1/events opens an event (name, area, start and end) under the organizer's contract (feature events.live). POST /v1/events/{id}/positions takes a participant's positions (dock.position.v1: format, consent: 'opt_in', a random participant token, [lon, lat, t]); no name, bib, phone or device is sent or stored. Points outside the area or time, or in a restricted site, are not stored. GET /v1/events/{id}/live gives the last point of each participant of the last 2 minutes, to 4 decimals, only with 5 or more live; GET /v1/events/{id}/heat gives 100 m cells with 5 or more participants (trace:read). 15 minutes after the end every point is erased and the cells stay. JS SDK 0.78.0: client.createEvent(), client.event(), client.startEventPositions(), client.stopEventPositions(), client.eventLive(), client.eventHeat(). Nothing removed.

0.82.0 (2026-10-13) ​

  • New: POST /v1/traffic/probes takes a fleet vehicle's last positions (dock.probe.v1: format, a random vehicle token made anew every 15 minutes, [lon, lat, t] points). Open to an org once its fleet contract is signed (feature traffic.probes); no driver, phone, plate or device is sent or stored. GET /v1/traffic?bbox= gives the live speed per road for the last 15 minutes that ended, only roads that 5 or more vehicles passed (insights:read). JS SDK 0.77.0: client.startProbes(), client.stopProbes(), client.traffic(bbox). Nothing removed.

0.81.0 (2026-10-13) ​

  • New: POST /v1/track/{token}/door: the customer taps "энэ миний орох хаалга" on the tracking link; their word that the delivery's door is theirs, once per delivery, no photo, no name. GET /v1/track/{token} gives code (the door's code when the delivery is a door) and door_said. A delivery marked 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.
  • Changed: a door becomes trusted (level 3) by 3 stops within 8 m on 3 different days, or by 1 stop and the customer's word. Three taps on one day no longer make 3. A photo is never needed. Nothing removed.

0.80.0 (2026-10-13) ​

  • New: GET /v1/nearby?lat=&lon=&category= gives the places of one or more categories near a point, nearest first (category=pharmacy, category=restaurant,cafe; radius 1000 m, up to 10000; limit 10, up to 20). The categories are the map's place icons: hospital, school, kindergarten, stop, mall, market and more. Banks, fuel, pharmacies, restaurants, cafes and shops are businesses matched to the state registry only; until one matches, an empty list. JS SDK 0.76.0: client.nearby(point, categories). Nothing removed.

0.79.0 (2026-10-13) ​

  • New: the building tile and GET /v1/tilequery give each building height_source (where its height came from: a tag, levels, a type default, or estimated from footprint shape and size) and height_confidence (3 tag, 2 levels, 1 default, 0 estimated). 0 and 1 are estimates, never measurements. Nothing removed.

0.78.0 (2026-10-13) ​

  • Changed: tiles have no per-minute limit; they count against the organization's daily tile quota only. A map asks for many tiles at once, and a 429 left holes in it. tile_rate_limit stays in responses and is not enforced.
  • New: the entrance tile and GET /v1/tilequery give each entrance its building_id. A new place tile (from z11): districts, areas, public places, registered businesses, building and entrance numbers, each with a category and a rank. Nothing removed.

0.77.0 (2026-10-13) ​

  • Changed: POST /v1/orders/import reads addresses the way people type them. District letters and Latin names ("БГД", "хан уул", "HUD"), a phone number in the cell, entrance, floor and flat, and the words round a landmark ("behind the school", "next to") are not searched. With a district named, the first of the best five places in it is taken. A row found more than 30 km from the sheet's other addresses is searched again near them. Nothing removed.

JS SDK 0.75.0 (2026-10-12) ​

  • New: SearchBox, the search control on the map (map.addControl), with the picked place's card and mark (SEARCH_SOURCE). Nothing removed.

0.76.0 (2026-10-12) ​

  • New: dispatch. POST /v1/orders/import takes deliveries pasted from a spreadsheet (tabs) or CSV; each address is found by dock's search, a district named in it read as where to look, and the place found checked to be in that district. One not found is kept as not found, never guessed. The customer's name and phone columns are not kept. GET /v1/orders, POST /v1/dispatch/plan (the same number of stops a driver, the shortest order by road, arrival times), PATCH /v1/orders/{id} (to another driver), GET /v1/fleet/positions, POST /v1/orders/{id}/track and, with no key, GET /v1/track/{token} (dock.mn/t/...). The driver app: GET /v1/driver/jobs, POST /v1/driver/position, POST /v1/driver/jobs/{id}/status. Each company's apart (row-level security). Nothing removed.

0.75.0 (2026-10-12) ​

  • New: many companies on one platform. POST /v1/signup takes the company's name; with an e-mail, that person is the company's owner. The first key has org:admin too. GET /v1/org: the company's name, one daily limit for all its keys, its people (for org:admin keys), its live keys. GET /v1/keys, POST /v1/keys (scopes picked from your own), DELETE /v1/keys/{prefix}. POST /v1/org/invites (admin, developer, driver) answers a one-use link; POST /v1/org/invites/accept gives the invited person a key of their own. The database itself keeps each company's rows apart (row-level security). Nothing removed.

0.74.0 (2026-10-12) ​

  • New: legs[].steps on GET /v1/route: turn-by-turn in Mongolian (mn) and English (en), with what to do (kind), where (location), metres and seconds to the next step, the street name and the Mongolian voice phrase ids (voice). The last step is at the road's end; when the entrance is over 15 m from the road it says to leave the car and walk. Nothing removed.

JS SDK 0.74.0 (2026-10-12) ​

  • New: Navigator follows a route fix by fix: says each turn ahead and at it, asks a new route after 3 fixes over 40 m off the line, and arrives within 20 m of the road's end. VoicePlayer plays the Mongolian phrases one after another, on by default. RouteStep, RouteLeg. Nothing removed.

0.73.0 (2026-10-12) ​

  • New: POST /v1/styles also takes {"name", "style"}, a whole MapLibre style (version 8), as dock 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 (400 invalid_style). GET /v1/styles/{id} serves it as saved, with your ?access_token= added to source URLs on this API. In a list it shows as {"kind": "maplibre"}. Nothing removed.

0.72.0 (2026-10-12) ​

  • New: POST /v1/signup takes an optional JSON body {"email": ...}, the account's contact (dock.mn/account). Kept with the organization, never logged, never in an answer or an export; not verified yet. A bad address is 400 invalid_email. No body still works. Nothing removed.

JS SDK 0.73.0 (2026-10-12) ​

  • New: the SDK as one script tag, https://dock.mn/sdk/v1/dock.js and dock.css, with maplibre-gl inside, on window.dock; dock.createMap(client, options). No npm step.
  • Changed: the default baseUrl is https://geo.dock.mn (was api.dock.mn, which is not dock's map API). Swift and Android defaults changed the same way. Nothing removed.

JS SDK 0.72.0 (2026-10-10) ​

  • New: sendConfirmPhoto(id, jpeg) (API 0.71.0), the photo of the orts sent after confirm() answered, with its id. Uint8Array, ArrayBuffer or Blob, sent as it is. ConfirmPhotoReceipt. Swift and Android: not yet. Nothing removed.

0.71.0 (2026-10-10) ​

  • New: POST /v1/confirm/{id}/photo, the photo of the orts a driver took with one of your confirmations (the walk, MVP.md). The body is one JPEG as the camera saved it, image/jpeg, up to 2 MB, 320 to 8000 px a side. Its metadata (GPS, the phone, the time) is removed before it is stored. One photo per confirmation: the same photo again is unchanged, another one is 409 photo_exists. Only your 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. Needs confirm:write. Nothing removed.

Playground matching (2026-10-06) ​

  • Playground: a Matching tab (matching): trace (2 to 100 lon,lat, separated by ;), times (optional, one Unix second per point), mode. The roads it went along are drawn on the map with the SDK's matchingSource and matchingLayers; each piece listed in minutes and km, each point with the piece it landed on and how many metres off. A click on the map adds a point. The JS SDK and curl snippets show the request sent. No SDK or API change.

Playground trips (2026-10-06) ​

  • Playground: a Trips tab (optimized-trips): stops (2 to 12 codes or lon,lat, separated by ;), round trip, mode. The trip is drawn on the map with the SDK's optimizedTripSource and optimizedTripLayers, and the stops listed in the order to drive, each leg in minutes and km. A click on the map adds a stop. The JS SDK and curl snippets show the request sent. No SDK or API change.

Playground matrix (2026-10-06) ​

  • Playground: a Matrix tab: from and to (1 to 10 codes or lon,lat a side, separated by ;), mode. The answer is shown as a table, a row per start and a column per end, each cell minutes and km. The JS SDK and curl snippets show the request sent. No SDK or API change.

Playground isochrone (2026-10-06) ​

  • Playground: an Isochrone tab: from (code or lon,lat), minutes (contours_minutes), mode. The answer is drawn on the map with the SDK's isochroneSource and isochroneLayers. The JS SDK and curl snippets show the request sent. No SDK or API change.

Playground tilequery (2026-10-06) ​

  • Playground: a Tilequery tab: point, radius, limit, layers (sources), geometry. The Tiles map shows on it, and a click asks about that point. The JS SDK and curl snippets show the request sent. No SDK or API change.

Playground near (2026-10-06) ​

  • Playground: Search has a Near field (proximity, lon,lat), and a Map centre button puts the Tiles map's centre in it. The JS SDK and curl snippets show the point sent, 7 decimals. Empty: not sent. No SDK or API change.

Playground kinds (2026-10-06) ​

  • Playground: Search and Reverse have a box per kind (types). The ticked kinds go to geocode or reverse, and the JS SDK and curl snippets show the same list. None ticked: every kind, and types is not sent. Search has the 8 geocode kinds, Reverse the 4 it can keep. No SDK or API change.

JS SDK 0.71.0, Swift SDK 0.64.0, Android SDK 0.65.0 (2026-10-06) ​

  • New: types on Suggester in all three SDKs, and on attachSuggest in JS. Suggests only the kinds asked, as types on geocode: a street picker field. A typed door code or a door link is looked up as a door only when types has door. Empty or absent: every kind, as before. In JS another kind is refused with 400 invalid_types before any request. Nothing removed.

JS SDK 0.70.0, Swift SDK 0.63.0, Android SDK 0.64.0 (2026-10-06) ​

  • New: types on geocode (API 0.70.0), in all three SDKs. Keeps only the kinds asked of door, business, building, district, khoroo, area, street and landmark, chosen before the limit: asking for streets gives the best streets. With no network the picked places answer with only those kinds too. Sent comma separated, a repeat once, in the order given. Empty or absent: every kind, as before. In JS another kind is refused before any request with 400 invalid_types, as the API would; GEOCODE_TYPES. Swift and Kotlin take a GeocodeType enum. Nothing removed.

0.70.0 (2026-10-06) ​

  • New: types on GET /v1/geocode, like the types filter of Mapbox's forward geocoding and types on GET /v1/reverse. One or more of door, business, building, district, khoroo, area, street, landmark, comma separated; a repeat is taken once. Only those kinds come back, up to limit of them, in every layer: asking for streets gives the best streets, not the streets among the best places. In a "near" query the landmark is still found among every kind. A named door comes only when types has door. Absent or empty: every kind, as before. Another kind or an empty item is 400 invalid_types. Not in the SDKs yet. Nothing removed.

JS SDK 0.69.0, Swift SDK 0.62.0, Android SDK 0.63.0 (2026-10-06) ​

  • New: types on reverse (API 0.69.0), in all three SDKs. Keeps only the kinds asked of door, business, building and landmark, chosen before the limit: asking for doors gives the nearest doors. Sent comma separated, a repeat once, in the order given. Empty or absent: every kind, as before. In JS another kind is refused before any request with 400 invalid_types, as the API would; REVERSE_TYPES, type ReverseType. Swift and Kotlin take a ReverseType enum. Nothing removed.

0.69.0 (2026-10-06) ​

  • New: types on GET /v1/reverse, like the types filter of Mapbox's reverse geocoding. One or more of door, business, building, landmark, comma separated; a repeat is taken once. Only those kinds come back, up to limit of them: asking for doors gives the nearest doors, not the doors among the nearest places. District and khoroo are the same. Absent or empty: every kind, as before. Another kind or an empty item is 400 invalid_types. Not in the SDKs yet. Nothing removed.

JS SDK 0.68.0, Swift SDK 0.61.0, Android SDK 0.62.0 (2026-10-06) ​

  • New: tilequery (API 0.68.0), the same name in all three SDKs. The buildings and entrances at or near a point, nearest first, as the GeoJSON FeatureCollection the API sends: what a tap on the map is on. sources is building, entrance or both (the default); radius 0 to 1000 metres, sent to the centimetre (TILEQUERY_RADIUS_MAX_M; Swift tilequeryRadiusMaxM); limit 1 to 50 (TILEQUERY_LIMIT_MAX; Swift tilequeryLimitMax); geometry keeps one type. An empty or repeated source is refused before any request with 404 not_found, a point that is not a number with 400 invalid_point, others with 400 invalid_radius or invalid_limit, as the API would. Types Tilequery, TilequeryFeature, TilequeryProperties, TilequeryInfo (Swift and Kotlin also TilequeryPoint, TilequerySource, TilequeryGeometry). Scope tiles:read. Nothing removed.
  • The API reference now types the tilequery fields name_mn, name_en, letter (string), height_m (number), levels and confidence (integer); they read as objects before. The answer itself is unchanged.

0.68.0 (2026-10-06) ​

  • New: GET /v1/tiles/{sources}/tilequery/{lon},{lat}, the tile features at or near a point, nearest first, like Mapbox's Tilequery API. sources is building, entrance or both joined by a comma. radius is 0 to 1000 metres, default 0: the building the point is in; an entrance needs a radius. limit 1 to 50, default 5. geometry is one of polygon, linestring, point. A trailing .json is taken. The answer is a GeoJSON FeatureCollection: each feature is the part of it nearest the point (a Point), its tile fields and tilequery (distance in metres, geometry, layer). The same rows as the tiles: current, not demolished, nothing in a restricted site, no door codes. The 7th decimal of a point is a mark for your organization. Scope tiles:read; counts against rate_limit and daily_quota, not the tile budgets. Not in the SDKs yet. Nothing removed.

JS SDK 0.67.0 (2026-10-06) ​

  • New: routeSource(trip) and routeLayers({ source?, color?, width?, font? }), a route answer with its via ends on a MapLibre map in two lines. The source holds the line (mode, distance_m, duration_s), then one point per end where the line reaches it, in the order passed (number from 1: from, each via, to; role; via, its place in the list sent, null for from and to; gap_m), credited to the answer's source. An answer from an API before 0.67.0 has no via: from and to only. The layers are the line and a ring per end, ids <source>-line and <source>-end; with font, a font stack on the style's glyphs, also <source>-number, each end's number. The dock style has no glyphs: leave font out on it. ROUTE_SOURCE is 'dock-route'. JS only; no API change. Nothing removed.

JS SDK 0.66.0, Swift SDK 0.60.0, Android SDK 0.61.0 (2026-10-06) ​

  • New: via on route (API 0.67.0) in all three SDKs: up to 10 ends passed on the way, in order (ROUTE_VIA_MAX; Swift routeViaMax), each a door code or a point. JS { via }, Swift via: after to:, Android via last, so calls without it read as before. An empty list sends nothing. A long list, or a code holding ;, is refused before any request with the API's 400 invalid_via. Route gains legs (TripLeg) and via (RouteEnd), both optional: absent from an older API. Nothing removed.

0.67.0 (2026-10-06) ​

  • New: via on GET /v1/route, like the waypoints between the first and last coordinates in Mapbox's Directions API. 1 to 10 ends joined by ;, each a door code (to its entrance; needs door:read as well) or lon,lat, passed in the order sent. The line may turn back at a via end. Each is refused as from and to are, named by its place: via[1]: no door has the code ..., via[0] is inside closure .... Sent but empty, or more than 10: 400 invalid_via. The answer gains legs, one per hop (distance_m, duration_s; one without via), and via, the via ends in the order sent as route ends, snapped where the line passes them ([] without). measured covers the whole line. Nothing removed; a route without via is the same as before, plus those two fields.

JS SDK 0.65.0 (2026-10-06) ​

  • New: optimizedTripSource(round) and optimizedTripLayers({ source?, color?, width?, font? }), an optimizedTrips answer on a MapLibre map in two lines. The source holds the trip line (trips_index, distance_m, duration_s), then one point per stop where the trip reaches it, in the order to drive (number from 1, stop, its place in the list sent, gap_m), credited to the answer's source. The layers are the line and a ring per stop, ids <source>-line and <source>-stop; with font, a font stack on the style's glyphs, also <source>-number, each stop's number. The dock style has no glyphs: leave font out on it. An answer whose waypoint_index is not 0 to n - 1 once each is refused with RangeError. OPTIMIZED_TRIP_SOURCE is 'dock-optimized-trip'. JS only; no API change. Nothing removed.

JS SDK 0.64.0, Swift SDK 0.59.0, Android SDK 0.60.0 (2026-10-06) ​

  • New: optimizedTrips (API 0.66.0), the same name in all three SDKs. 2 to 12 stops (OPTIMIZED_TRIPS_MAX; Swift optimizedTripsMax), each a door code or a point, put in the order with the least time on the road, with the trip in that order. roundtrip is optional and sent only when given. The answer is OptimizedTrip: trips (Trip, TripLeg) and waypoints (TripWaypoint, with waypoint_index). A short or long list, or a code holding ;, is refused before any request with 400 invalid_coordinates. Scope route:read, and door:read for a code. Nothing removed.

0.66.0 (2026-10-06) ​

  • New: GET /v1/optimized-trips, the order to visit stops in with the least time on the road, like Mapbox's Optimization API (v1). coordinates is 2 to 12 ends joined by ;, each a door code (to its entrance; needs door:read as well) or lon,lat. The trip starts at the first, as Mapbox's source=first. roundtrip, true by default, goes back to it; false ends at the last sent, as destination=last. The answer has trips, one trip (distance_m, duration_s at the router's free-flow speeds, a GeoJSON geometry, and legs, one per hop), and waypoints, the stops in the order sent, each a route end with waypoint_index, its place in the trip. Codes, restricted sites, today's closures and door misses as GET /v1/matrix. A stop no road reaches is 422 no_route. Scope route:read. Not in the SDKs yet. Nothing removed.

JS SDK 0.63.0 (2026-10-05) ​

  • New: matchingSource(trip) and matchingLayers({ source?, color?, width? }), a matching answer on a MapLibre map in two lines. The source holds one line per matched piece (matchings_index, distance_m, duration_s), then one point per tracepoint on a road (point, its place in the trace sent, matchings_index, gap_m), credited to the answer's source; null tracepoints are left out. The layers are a line and a dot per point, ids <source>-line and <source>-point. MATCHING_SOURCE is 'dock-matching'. JS only; no API change. Nothing removed.

JS SDK 0.62.0, Swift SDK 0.58.0, Android SDK 0.59.0 (2026-10-05) ​

  • New: matching (API 0.65.0), the same name in all three SDKs. A GPS trace of 2 to 100 points (MATCHING_MAX; Swift matchingMax), with optional whole Unix seconds per point, matched to the roads: matchings, one line per unbroken piece, and tracepoints, one per point, null where no road was found. Fewer or more points, and timestamps not one per point, not whole seconds or going back, are refused before any request with 400 invalid_coordinates or invalid_timestamps. In Swift and Kotlin a tracepoint is a MatchTracepoint, apart from the trip recorder's TracePoint. Scope route:read. Nothing removed.
  • Fixed in openapi.json: MatchDto.tracepoints is a list that is never null, of items that may be null. It said the list itself could be null. The answer did not change.

0.65.0 (2026-10-05) ​

  • New: GET /v1/matching, a GPS trace matched to the road network, like Mapbox's Map Matching API. coordinates is 2 to 100 points as lon,lat joined by ;, in the order recorded. timestamps, optional, is one Unix time in whole seconds per point, never going back; with them two far points seconds apart are a break, not a fast road. The answer has matchings, one per unbroken piece (distance_m, duration_s at the router's free-flow speeds, a GeoJSON geometry), and tracepoints, one per point (matchings_index, location, gap_m), null where no road was found. A point inside a restricted site is left out and null; fewer than 2 left is 422 not_accepted. No road near the trace is 422 no_match. Closures are not avoided: a trace is where someone went. The trace is neither stored nor logged. Scope route:read. Not in the SDKs yet. Nothing removed.

JS SDK 0.61.0 (2026-10-05) ​

  • New: isochroneSource(zone) and isochroneLayers({ source?, color?, opacity? }), an isochrone answer on a MapLibre map in two lines. The source holds the zones largest first, credited to the answer's source; the layers are a fill and an outline, ids <source>-fill and <source>-line, so each nearer zone lies darker on top. ISOCHRONE_SOURCE is 'dock-isochrone'. JS only; no API change. Nothing removed.

JS SDK 0.60.0, Swift SDK 0.57.0, Android SDK 0.58.0 (2026-10-05) ​

  • New: isochrone (API 0.64.0), the same name in all three SDKs. The area reached from one end (a door code or a point, as in route) within each of 1 to 4 times, as a GeoJSON FeatureCollection, the largest first. Times are whole minutes from 1 to 60, increasing (ISOCHRONE_CONTOURS_MAX, ISOCHRONE_MINUTES_MAX; Swift isochroneContoursMax, isochroneMinutesMax); others are refused before any request with 400 invalid_contours. In Swift and Kotlin IsochroneGeometry is a Polygon or a MultiPolygon, and polygons gives every piece. Scope route:read, and door:read for a code. Nothing removed.

0.64.0 (2026-10-05) ​

  • New: GET /v1/isochrone, the area reached from one end within some minutes, like Mapbox's Isochrone API. from is one end as in GET /v1/route: a door code (from its entrance; needs door:read too) or lon,lat. contours_minutes is 1 to 4 whole minutes from 1 to 60, increasing, joined by commas (5,10,15). The answer is a GeoJSON FeatureCollection: one polygon per contour, the largest first, properties.contour in minutes. Add it to a map as a source as it is. from comes back as a route end (point, snapped, gap_m, door). Restricted sites and today's closures as for a route. Free-flow speeds. Scope route:read. Not in the SDKs yet. Nothing removed.

JS SDK 0.59.0, Swift SDK 0.56.0, Android SDK 0.57.0 (2026-10-05) ​

  • New: matrix (API 0.63.0), the same name in all three SDKs. Free-flow time and distance from every end in from to every end in to in one call, each end a door code or a point as in route; null (Swift nil) where no path joins them. Up to MATRIX_MAX (Swift matrixMax), 10, ends a side. An empty or longer list, or a code holding ;, is refused before any request with 400 invalid_from or invalid_to. Scope route:read, and door:read for a code. Nothing removed.

0.63.0 (2026-10-05) ​

  • New: GET /v1/matrix, travel time and distance from every from to every to in one call, like Mapbox's Matrix API. from and to are each 1 to 10 ends joined by ;, each as in GET /v1/route: a door code (to its entrance; needs door:read too) or lon,lat. durations_s[i][j] and distances_m[i][j] go from from[i] to to[j], at free-flow speeds; null where no path joins them. from and to list each end as a route end (point, snapped, gap_m, door). Restricted sites and today's closures as for a route. Every code that is no door counts as a door miss, then the first is refused by its place (to[1]: no door has the code ...). One call for the whole matrix. Scope route:read. Not in the SDKs yet. Nothing removed.

JS SDK 0.58.0, Swift SDK 0.55.0, Android SDK 0.56.0 (2026-10-05) ​

  • New: usage gives endpoints (API 0.62.0), calls and door misses per route. Optional: undefined in JS, nil in Swift, null in Android from an API before 0.62.0. Nothing removed.

0.62.0 (2026-10-05) ​

  • GET /v1/usage adds endpoints: every route your org's keys called, over the same days as days. Each row has endpoint (method and route pattern, like GET /v1/door/:code; never a code, a name or a search text), calls and misses (lookups that found no door). Most calls first, ties A to Z. The tiles route counts tiles; the calls of the other routes add up to the calls of the days. Routes with no calls are left out. The usage page shows them in a table. Nothing removed.

JS SDK 0.57.0, Swift SDK 0.54.0, Android SDK 0.55.0 (2026-10-05) ​

  • New: door photo (API 0.60.0, 0.61.0), the same names in all three SDKs. sendDoorPhoto sends one JPEG of the gate of the door your name points at, for review; doorPhoto reads back the photo a person accepted, as bytes and its sha256. Pass the sha256 you hold and an unchanged photo comes back with no bytes (304). Scope names:write, feature hotel.notes, else 403 feature_off. The name is refused before any request as for door names. Nothing removed.

0.61.0 (2026-10-05) ​

  • New: GET /v1/door-names/{name}/photo. The org that holds an unexpired door name reads back the gate photo a person accepted, for its app to show its own guests. The newest accepted photo is served; a waiting or refused one, or another org's, never is. The body is the JPEG as stored, Content-Type: image/jpeg, the ETag is the sha256 the send answered; If-None-Match with it is 304. Cache-Control: private, no-cache. Do not put it on a public page. A name your org does not hold, whose door was retired or lies in a restricted site, or with no accepted photo, is 404. Scope names:write, feature hotel.notes. No SDK change. Nothing removed.

0.60.0 (2026-10-05) ​

  • New: POST /v1/door-names/{name}/photo. The org that holds an unexpired door name sends one photo of its entrance or gate: the body is a JPEG file, Content-Type: image/jpeg, up to 2 MB, 320 to 8000 px on a side. Its metadata is removed before it is stored: GPS, phone model, time, XMP, comments, thumbnails, bytes after the image. The orientation and the colour profile stay. The answer has sha256, bytes, width, height and status (queued, updated, unchanged). A person reviews the photo; no call serves photos yet. Sending again replaces the waiting photo, the old one kept in history. Scope names:write, feature hotel.notes. Not a JPEG is 400 not_jpeg, broken is invalid_photo, too small photo_too_small, too big photo_too_big, over 2 MB 413 too_large. A name your org does not hold, or whose door was retired, is 404. No SDK change. Nothing removed.

JS SDK 0.56.0, Swift SDK 0.53.0, Android SDK 0.54.0 (2026-10-05) ​

  • New: door notes (API 0.58.0, 0.59.0), the same names in all three SDKs. writeDoorNotes writes how guests find the entrance of the door your name points at, in mn, en or both, for review; doorNotes reads your own notes for that door, each waiting, accepted or refused. Scope names:write, feature hotel.notes, else 403 feature_off. The name is refused before any request as for door names: 400 invalid_name or personal_data. A phone number or an e-mail in a note comes back as the API's 400 personal_data. Nothing removed.

0.59.0 (2026-10-05) ​

  • New: GET /v1/door-names/{name}/notes. The org that holds a door name reads the notes it wrote for that door: state is waiting, accepted or refused, with received_at and reviewed_at, the time a person reviewed it. The waiting note first, then the newest review, at most 20. Another org's notes are never listed. Scope names:write, feature hotel.notes. As when writing: a name your org does not hold, or whose door was retired, is 404. No SDK change. Nothing removed.

0.58.0 (2026-10-05) ​

  • New: POST /v1/door-names/{name}/notes. The org that holds an unexpired door name writes how guests find its entrance: instructions_mn, instructions_en or both, up to 500 characters each. The note waits for a person to review it; GET /v1/door/{code} keeps serving the notes the map holds until then. Writing again replaces your waiting text (updated, unchanged); history keeps the old one. Scope names:write, feature hotel.notes, else 403 feature_off. A phone number or an e-mail is 400 personal_data. A name your org does not hold, or whose door was retired, is 404. No gate photo yet. No SDK change. Nothing removed.

0.57.0 (2026-10-05) ​

  • New: GET /v1/corridors/{name} gives label_mn and confidence_note_mn, and each fact line text_mn, the same line in Mongolian with the same numbers. Null when there is none: unknown, not a translation. Lane D writes text_mn only when its numbers equal the English line; the other lines are null until lane D has their words. Compare rows in GET /v1/corridors already carry label_mn. A Mongolian field that is not text or null makes the file not whole: 503 unavailable. No SDK change. Nothing removed.

Change feed clock (2026-10-05) ​

  • GET /v1/changes: until is now one second before the last index refresh, not the refresh itself. A write made just after a refresh could carry a time up to a fraction of a second before it, when the server clock stepped back, and a partner polling from until never saw it. Now it shows in the next window. Every id listed can still be fetched. No API change, still 0.56.0.

JS SDK 0.55.0, Swift SDK 0.52.0, Android SDK 0.53.0 (2026-10-04) ​

  • New: door names (API 0.52.0, 0.53.0), the same names in all three SDKs: registerDoorName, doorNames, doorByName, doorNameTransferCode, cancelDoorNameTransfer, claimDoorName. 403 feature_off until the door_names feature is open. A name with no letter, a phone number or an e-mail is refused with 400 invalid_name or personal_data before any request: the name travels in the URL. In Swift and Android a claim answers a DoorNameReceipt with status transferred. Nothing removed.

JS SDK 0.54.0, Swift SDK 0.51.0, Android SDK 0.52.0 (2026-10-04) ​

  • New: usage gives features (API 0.51.0), the features open to your org, A to Z. Optional: undefined in JS, nil in Swift, null in Android from an API before 0.51.0. Nothing removed.

Swift SDK 0.50.0, Android SDK 0.51.0 (2026-10-04) ​

  • New: door QRs, offline, as in JS SDK 0.53.0. doorQr(_:unit:) (Kotlin doorQr(text, unit)) gives the modules of the doorLink link; doorQrSvg the SVG text. Swift doorQrImage(_:unit:px:margin:) draws a CGImage; Kotlin doorQrPixels(text, unit, px, margin) gives ARGB pixels for a Bitmap. ISO/IEC 18004, error correction level M, the same modules in all three SDKs (sdk/testdata/door-qr.json). nil or null when the text cannot be a code. No library needed. No API or JS change. Nothing removed.

JS SDK 0.53.0 (2026-10-04) ​

  • New: door QRs, offline. doorQrSvg(text, unit, { px, margin }) draws the door's QR as an SVG, the doorLink link inside; doorQr(text, unit) gives its modules (modules[y][x], size, version). ISO/IEC 18004, error correction level M. null when the text cannot be a code. No library needed. No API, Swift or Android change. Nothing removed.

JS SDK 0.52.0, Swift SDK 0.49.0, Android SDK 0.50.0 (2026-10-04) ​

  • New: door links, offline. doorLink(text, unit) (Swift doorLink(_:unit:)) makes the link to share a door, https://map.dock.mn/d/7QSVX?u=17, the unit percent-encoded; null (Swift nil) when the text cannot be a code. parseDoorLink(text) (Swift parseDoorLink(_:)) reads the first door link in a text, a pasted message or the URL a link opened the app with, as parseDoorCode reads a code and a unit; null when there is none. New constant DOOR_LINK_BASE (Swift doorLinkBase). The address field (attachSuggest, Suggester) looks a pasted door link up as its door alone, with no geocode. No API change. map.dock.mn does not serve the door page yet. Nothing removed.

JS SDK 0.51.0, Swift SDK 0.48.0, Android SDK 0.49.0 (2026-10-04) ​

  • New: route takes a departure time (API 0.50.0): JS route(from, to, { departAt }) (a Date or an ISO 8601 string with an offset), Swift route(from:to:mode:departAt:), Android route(from, to, mode, departAt) (a Date). A Date is sent in UTC. measured.band is the hour band read, null (Swift nil) without departAt and from an API before 0.50.0. In Swift and Android band is a String, a RoadBand value. Nothing removed; calls without departAt are unchanged.

JS SDK 0.50.0, Swift SDK 0.47.0, Android SDK 0.48.0 (2026-10-04) ​

  • New: traceRoads takes an hour band (API 0.49.0): JS traceRoads(month, bbox, { band }), Swift traceRoads(month:bbox:band:), Android traceRoads(month, bbox, band). am_peak Monday to Friday 07:00 to 09:59, pm_peak Monday to Friday 17:00 to 19:59, off_peak every other hour, Saturday and Sunday whole, by the Ulaanbaatar time of a trip's first stored point. Each band passes the trip cells rule on its own trips. Left out: all hours, as before. rule.hours says which. New type RoadBand (Swift .amPeak, .pmPeak, .offPeak; Android AM_PEAK, PM_PEAK, OFF_PEAK). Nothing removed; calls without a band are unchanged.

JS SDK 0.49.0, Swift SDK 0.46.0, Android SDK 0.47.0 (2026-10-04) ​

  • New: Route.measured (API 0.48.0), on a driving route: month, the latest month of published road speeds; roads_m (Swift and Android roadsM), metres of the line on roads with a published moving speed; duration_s (Swift and Android durationS), the route time with those roads at that speed. null (Swift nil) when walking or cycling, when the speeds could not be read, or from an older API. duration_s stays free-flow. New type RouteMeasured. Nothing removed.

JS SDK 0.48.0, Swift SDK 0.45.0, Android SDK 0.46.0 (2026-10-04) ​

  • New: traceRoads(month, bbox) (Swift traceRoads(month:bbox:)) reads GET /v1/traces/roads, the moving speed of all partners' trips per road in a month (needs insights:read, API 0.47.0): GeoJSON, one feature per OSM way, id the way id as text, the line dock's current road with that id (LineString, or MultiLineString in pieces), km_h (Swift and Android kmH), counted meters over counted seconds on the way, to 0.1, and name_mn, name_en, highway (Swift and Android nameMn, nameEn), null when unknown. Only roads under the trip cells rule; no road means too few trips, not a slow road. No trip count. A month is published 14 days after it ends (400 month_open before). Empty until dock's road lines are loaded. New types TraceRoads, TraceRoad, TraceRoadGeometry (Swift and Android also TraceRoadProperties; geometry.lines lists every piece). Nothing removed.

JS SDK 0.47.0, Swift SDK 0.44.0, Android SDK 0.45.0 (2026-10-04) ​

  • New: ownKinds(month) (Swift ownKinds(month:)) reads GET /v1/traces/kinds/own, what kind of building your own trips end at in a month, for your dashboard (needs trace:read, API 0.46.0): the month total trips and kinds, each kind with trips and its trips, most first. The kind is the building type when the trip was stored; unknown is a building with no type, none no building within 30 m. No group rule: the trips are yours. The month you are in is answered too; closed once final. New types OwnKinds, OwnKind; in JS kind is BuildingKind, the API's nine values, and a String in Swift and Android. Nothing removed.

JS SDK 0.46.0, Swift SDK 0.43.0, Android SDK 0.44.0 (2026-10-04) ​

  • New: ownKhoroos(month) (Swift ownKhoroos(month:)) reads GET /v1/traces/khoroos/own, where and when your own trips end per khoroo and hour of arrival in a month, for your dashboard (needs trace:read, API 0.45.0): the month total trips, the trips in no current khoroo polygon no_khoroo (Swift and Android noKhoroo), and one feature per khoroo with its polygon, district, number, its trips and every hour with trips. No group rule: the trips are yours. The month you are in is answered too; closed once final. Until khoroo polygons are loaded every trip is in no_khoroo. New types OwnKhoroos, OwnKhoroo (Swift and Android also OwnKhorooProperties); the hours are KhorooHour, and in Swift and Android the geometry is KhorooPolygon. Nothing removed.

JS SDK 0.45.0, Swift SDK 0.42.0, Android SDK 0.43.0 (2026-10-04) ​

  • New: traceSpeeds(month, bbox) (Swift traceSpeeds(month:bbox:)) reads GET /v1/traces/speeds, the moving speed of all partners' trips per 500 m cell in a month (needs insights:read, API 0.44.0): GeoJSON, one feature per cell on the trip cells grid with km_h (Swift and Android kmH), counted meters over counted seconds, to 0.1. Only cells under the trip cells rule; no cell means too few trips, not a slow road. No trip count. Per cell, not per road. A month is published 14 days after it ends (400 month_open before). New types TraceSpeeds, TraceSpeed (Swift and Android also TraceSpeedProperties); the geometry is the trip cells polygon. Nothing removed.

JS SDK 0.44.0, Swift SDK 0.41.0, Android SDK 0.42.0 (2026-10-04) ​

  • New: traceKhoroos(month) (Swift traceKhoroos(month:)) reads GET /v1/traces/khoroos, where and when trips end in a month, all partners together (needs insights:read, API 0.43.0): GeoJSON, one feature per khoroo with its polygon, district, number and each hour of arrival that passed the trip cells rule. No khoroo total. A month is published 14 days after it ends (400 month_open before). The answer is empty until khoroo polygons are loaded. New types TraceKhoroos, TraceKhoroo, KhorooHour (Swift and Android also TraceKhorooProperties, KhorooPolygon). Nothing removed.

JS SDK 0.43.0, Swift SDK 0.40.0, Android SDK 0.41.0 (2026-10-04) ​

  • New: allHours(month) (Swift allHours(month:)) reads GET /v1/traces/hours, when trips start in a month, all partners together (needs insights:read, API 0.41.0): each weekday and hour that passed the trip cells rule, weekday then hour; no total. A month is published 14 days after it ends (400 month_open before). New type AllHours; its slots are HourSlot, as in OwnHours. Nothing removed.

JS SDK 0.42.0, Swift SDK 0.39.0, Android SDK 0.40.0 (2026-10-04) ​

  • New: ownHours(month) (Swift ownHours(month:)) reads GET /v1/traces/hours/own, when your trips start in a month (needs trace:read, API 0.40.0): each trip once, at the Ulaanbaatar weekday and hour of its first point; month total, 7 weekdays, 24 hours, every weekday and hour with trips, closed from 14 days after the month ends. New types OwnHours, HourSlot. Swift and Android name the arrays byWeekday, byHour. Nothing removed.

JS SDK 0.41.0, Swift SDK 0.38.0, Android SDK 0.39.0 (2026-10-04) ​

  • New: ownArrivals(month) (Swift ownArrivals(month:)) reads GET /v1/traces/arrivals/own, how long your trips spend in the last 200 m in a month (needs trace:read, API 0.39.0): trips, slow (5 minutes or more), median, p75 and p90, trips per minute, up to 100 end cells by slow trips, closed from 14 days after the month ends. New types OwnArrivals, ArrivalMinute, ArrivalCell (Swift and Android also ArrivalCellProperties). Nothing removed.

JS SDK 0.40.0, Swift SDK 0.37.0, Android SDK 0.38.0 (2026-10-04) ​

  • New: ownConfirmations(month) (Swift ownConfirmations(month:)) reads GET /v1/confirm/own, your drivers' confirmations and door reports in a month (needs confirm:read, API 0.38.0): counts by result for the month and each day on site, different drivers (never the ids), up to 100 failure points, closed from 8 days after the month ends. New types OwnConfirmations, ConfirmCounts, ConfirmDay, ConfirmFlagged. Nothing removed.

Swift SDK 0.36.0, Android SDK 0.37.0 (2026-10-04) ​

  • New, as JS SDK 0.38.0: ownTraceCells reads your organization's own trips per 500 m cell and month (GET /v1/traces/cells/own, trace:read), closed once the counts are final. traceCells reads all partners' trips per cell for a published month, only cells from 3 or more organizations (GET /v1/traces/cells, insights:read). Both take month YYYY-MM and a BBox, sent at 7 decimals. New types BBox, TraceCells, OwnTraceCells, TraceCell, TraceCellPolygon, TraceCellProperties. Nothing removed.

Swift SDK 0.35.0, Android SDK 0.36.0 (2026-10-04) ​

  • New, as JS SDK 0.39.0: reportClosure sends POST /v1/closures, a stretch or a crossing that cannot be driven; the idempotencyKey goes in the Idempotency-Key header, so a retry after a timeout stores nothing twice. A key the API would refuse throws DockError bad_idempotency_key before any request. closure reads one of your reports back, note included (GET /v1/closures/{id}). liftClosure opens the road again (POST /v1/closures/{id}/lift). All three need closures:write. New types ClosureReport, ClosureGeometry, ClosureKind, Closure, ClosureProperties; Swift isValidIdempotencyKey(_:) and idempotencyKeyPattern, Android IDEMPOTENCY_KEY_RE. Nothing removed.

JS SDK 0.39.0 (2026-10-04) ​

  • New: reportClosure(report, { idempotencyKey }) sends POST /v1/closures, a stretch or a crossing that cannot be driven; the key goes in the Idempotency-Key header, so a retry after a timeout stores nothing twice. A key the API would refuse rejects with DockError bad_idempotency_key before any request. closure(id) reads one of your reports back, note included (GET /v1/closures/{id}). liftClosure(id, { on }) opens the road again (POST /v1/closures/{id}/lift). All three need closures:write. New types ClosureReport, ClosureGeometry, Closure; new constant IDEMPOTENCY_KEY_RE. Nothing removed.

JS SDK 0.38.0 (2026-10-04) ​

  • New: ownTraceCells(month, bbox) reads GET /v1/traces/cells/own, your own trips per 500 m cell and month, with closed (needs trace:read, API 0.37.0). traceCells(month, bbox) reads GET /v1/traces/cells, all partners together, only cells that meet the group rule (needs insights:read). New types BBox, TraceCell, TraceCells, OwnTraceCells. Nothing removed.

JS SDK 0.37.0, Swift SDK 0.34.0, Android SDK 0.35.0 (2026-10-04) ​

  • New: a crossingsOnRoute warning's delta carries lake_name_osm (lakeNameOsm): the OSM text when the pack folded the lake's name to one script, else null. Show lake. Packs before national crossings v11 read as before, with it null. Nothing removed.

JS SDK 0.36.0, Swift SDK 0.33.0, Android SDK 0.34.0 (2026-10-04) ​

  • New: crossingsOnRoute carries name_osm (nameOsm) on each warning: the OSM text when the pack folded a name that mixed Latin and Cyrillic in a word to one script, else null. Show name. The answer adds name_rule and name_rule_mn (nameRule, nameRuleMn), the pack's rule, null when the pack has none. The pack type reads names_folded, name_rule, name_rule_mn. Packs before offline v13 read as before, with these null. Nothing removed.

JS SDK 0.35.0, Swift SDK 0.32.0, Android SDK 0.33.0 (2026-10-03) ​

  • New: sandOnRoute, wetlandsOnRoute, passesOnRoute and waterOnRoute carry name_osm (nameOsm): the OSM text when the pack folded a name that mixed Latin and Cyrillic in a word to one script, else null. Show name. Each answer adds name_rule and name_rule_mn (nameRule, nameRuleMn), the pack's rule, null when the pack has none. The pack types read names_folded, name_rule, name_rule_mn. Packs before offline v11 (sand) and v12 read as before, with these null. Nothing removed.

JS SDK 0.34.0, Swift SDK 0.31.0, Android SDK 0.32.0 (2026-10-03) ​

  • New: packRoute reads the manifest's confidence_note and confidence_note_mn (packs from offline v10) and carries them as confidence_note and confidence_note_mn (confidenceNote, confidenceNoteMn): what confidence means, for the driver. trace_note (traceNote) is now the pack's sentence when the manifest has both, else the SDK's own as before. A pack with one language only gets the SDK's pair, never one from each. On a scored pack with no note, confidence_note is null. Older packs read as before. Nothing removed.

JS SDK 0.33.0, Swift SDK 0.30.0, Android SDK 0.31.0 (2026-10-03) ​

  • New: passesOnRoute reads a pass's heights_differ (heightsDiffer) from the pack: true when the OSM and DEM heights are both there and more than 50 m apart, false within, null when either is missing. differ counts the passes on the route with it true, null when the pack has no flag. PassDem reads differ_m, differ_rule, differ_rule_mn and differ. Packs with passes v2 read as before, with these null. Nothing removed.

JS SDK 0.32.0, Swift SDK 0.29.0, Android SDK 0.30.0 (2026-10-03) ​

  • New: packRoute builds the Valhalla request for a route on an offline steppe pack, through 2 or more stops: the manifest's request (costing and costing_options), the route day's closure rings as exclude_polygons (the pack's closures block, or a newer GET /v1/closures answer), units km. It names the closures avoided and carries attribution, scored and, when the pack has no trace scores, trace_note and trace_note_mn (traceNote, traceNoteMn) to say to the driver. A stop outside the pack's bbox throws; a stop inside an avoided ring throws EndInClosureError (PackRouteError.endInClosure, EndInClosureException). PackRouteManifest reads the manifest in Swift and Kotlin. Nothing removed.

JS SDK 0.31.0, Swift SDK 0.28.0, Android SDK 0.29.0 (2026-10-03) ​

  • New: passesOnRoute reads a pass's DEM height beside the OSM one: dem_m (demM), the whole-metre height of the Copernicus GLO-30 cell the node falls in, null when the pack has none there, never mixed into ele_m. top_dem_m (topDemM) is the highest on the route; dem is the pack's DEM block, with the attribution to show beside a DEM height. PackPasses reads with_dem and dem. Packs with passes v1 read as before, with these null. top_m stays OSM only. Nothing removed.

JS SDK 0.30.0, Swift SDK 0.27.0, Android SDK 0.28.0 (2026-10-03) ​

  • New: checkPack checks a downloaded steppe pack against its manifest.json: each file in the files rows, by size, then sha256, in manifest order. Each file is ok, missing, bytes (another size, not hashed) or sha256 (same size, other bytes); ok only when all are, bad names the rest. A file name that is not a plain file name throws before any file is read. JS reads through your function (Web Crypto); Swift and Kotlin read the pack folder 1 MB at a time. PackManifestFiles reads the manifest in Swift and Kotlin. Nothing removed.

JS SDK 0.29.0, Swift SDK 0.26.0, Android SDK 0.27.0 (2026-10-03) ​

  • New: waterOnRoute gives the OSM wells, springs, taps and drinking water within 2000 m (the pack's near_m, nearM) of a route line, from a steppe pack's water.geojson, in route order: metres from the start and off the line (along_m, off_m; alongM, offM). drinkable is only what OSM says, null (nil) when it says nothing, never guessed. PackWater reads the file. From OSM tags, not field checked. Packs built before 2026-10-03 have no such file. Nothing removed.

JS SDK 0.28.0, Swift SDK 0.25.0, Android SDK 0.26.0 (2026-10-03) ​

  • New: sandOnRoute gives the stretches through OSM sand (natural=sand) a route runs on, from a steppe pack's sand.geojson, in route order, by the rule of wetlandsOnRoute: metres from the start (along_m, alongM) and metres on the stretch (on_m, onM); under 20 m (SAND_MIN_ON_M, sandMinOnM) the route only crosses it, unless it runs on all of it. PackSand reads the file. From OSM tags, not field checked. Packs built before 2026-10-03 have no such file. Nothing removed.

JS SDK 0.27.0, Swift SDK 0.24.0, Android SDK 0.25.0 (2026-10-03) ​

  • New: passesOnRoute gives the OSM mountain passes (mountain_pass, saddle) a route goes over, from a steppe pack's passes.geojson, in route order: passes whose node lies within 5 m (match_m, matchM) of the line, with metres from the start (along_m, alongM), metres off the line (off_m, offM), the OSM height (ele_m, eleM) and the highest height on the route (top_m, topM, null when none has one). PackPasses reads the file. From OSM tags, not field checked. Packs built before 2026-10-03 have no such file. Nothing removed.

JS SDK 0.26.0, Swift SDK 0.23.0, Android SDK 0.24.0 (2026-10-03) ​

  • New: wetlandsOnRoute gives the stretches through OSM wetland (marsh, bog, wet_meadow, fen, unknown) a route runs on, from a steppe pack's wetlands.geojson, in route order: metres from the start (along_m, alongM) and metres on it (on_m, onM). A route on less than 20 m of a stretch (WETLAND_MIN_ON_M, wetlandMinOnM) only crosses it, unless it runs on all of it. PackWetlands reads the file. From OSM tags, not field checked. Packs built before 2026-10-03 have no such file. Nothing removed.

JS SDK 0.25.0, Swift SDK 0.22.0, Android SDK 0.23.0 (2026-10-03) ​

  • New: a steppe pack's crossings.geojson ford ford_node_id (fordNodeId in Swift and Kotlin), on each warning too. A ford from an OSM ford node alone, where OSM draws no water line: waterway is unknown, no name, no flow; say "OSM ford, water not drawn". The pack's ford_nodes, ford_node_rule and ford_node_rule_mn (fordNodes, fordNodeRule, fordNodeRuleMn) give the count and the rule. null (nil) in packs built before that rule. Other warnings unchanged. Nothing removed.

JS SDK 0.24.0, Swift SDK 0.21.0, Android SDK 0.22.0 (2026-10-03) ​

  • New: a steppe pack's crossings.geojson from_seasonal, intermittent_rule and intermittent_rule_mn (fromSeasonal, intermittentRule, intermittentRuleMn in Swift and Kotlin): the count of crossings marked intermittent from the OSM seasonal tag alone, with no intermittent tag, and the rule. null (nil) in packs built before that rule. The crossing intermittent field is unchanged, warnings unchanged. Nothing removed.

JS SDK 0.23.0, Swift SDK 0.20.0, Android SDK 0.21.0 (2026-10-03) ​

  • New: a steppe pack's crossings.geojson one_point, split_rule and split_rule_mn (onePoint, splitRule, splitRuleMn in Swift and Kotlin): the count of crossings joined into the one beside them because their water way is split into two ways at the road, and the rule. null (nil) in packs built before that rule. Warnings unchanged. Nothing removed.

JS SDK 0.22.0, Swift SDK 0.19.0, Android SDK 0.20.0 (2026-10-03) ​

  • New: a steppe pack's crossings.geojson near_bridge, bridge_rule and bridge_rule_mn (nearBridge, bridgeRule, bridgeRuleMn in Swift and Kotlin): the count of none crossings left out of the pack because a bridge is drawn just off the water, and the rule. null (nil) in packs built before that rule. Warnings unchanged. Nothing removed.

JS SDK 0.21.0, Swift SDK 0.18.0, Android SDK 0.19.0 (2026-10-03) ​

  • New: every crossing in a steppe pack carries road: the OSM highway and surface tags of the route ways and paved (true, false, or null with no surface tag). On each warning of crossingsOnRoute. A none crossing on a paved road may be a bridge or culvert OSM does not tag. The pack's on_paved, road_rule and road_rule_mn (onPaved, roadRule, roadRuleMn in Swift and Kotlin). New type CrossingRoad. null (nil) on a new track alone and in packs built before the rule. Nothing removed.

Swift SDK 0.17.0, Android SDK 0.18.0 (2026-10-03) ​

  • New: Suggester, the address field for iOS and Android, the same rules as Suggester and attachSuggest in the JS SDK. Asks from the third character, 200 ms after the last key; a newer key cancels the request in flight and a late answer is dropped; a typed door code is looked up as a door first. show gets the list, choose(item) passes a chosen geocode result to pick. New types Suggestion (Swift .door, .result; Kotlin Suggestion.Door, Suggestion.Result), label for it, constants suggestMinChars and suggestDelayMs (Kotlin SUGGEST_MIN_CHARS, SUGGEST_DELAY_MS). No view ships: the app draws the list. Nothing removed.

JS SDK 0.20.0, Swift SDK 0.16.0, Android SDK 0.17.0 (2026-10-03) ​

  • New: a river crossing on an unnamed river or stream whose water reaches one named river carries flows_into (flowsInto in Swift and Kotlin): name, name_en, river_way_id, ways. Say "unnamed, flows into Орхон гол". Read from the steppe pack's crossings, on each warning of crossingsOnRoute. From the OSM drawing, not checked on the ground. channel_of wins when both are set. null (nil) elsewhere and in packs built before the rule. New type CrossingFlowsInto. Nothing removed.

JS SDK 0.19.0, Swift SDK 0.15.0, Android SDK 0.16.0 (2026-10-03) ​

  • New: a river crossing on an unnamed channel of a named river carries channel_of (channelOf in Swift and Kotlin): name, name_en, river_way_id. Say "unnamed channel of Улаан гол". Read from the steppe pack's crossings, on each warning of crossingsOnRoute. From the OSM drawing, not checked on the ground. null (nil) elsewhere and in packs built before the rule. New type CrossingChannelOf. Nothing removed.

Swift SDK 0.14.0 (2026-10-03) ​

  • New: dockStyle(_:options:), dockSources(_:), dockLayers(_:), client.styleJSON(_:), client.styleURL(_:): the dock style, the same JSON as the JS and Android SDKs. basemapStyle(_:url:options:), PackBasemap.fromManifest(_:): the style for a steppe pack's basemap.pmtiles, the same JSON and the same refusals (StyleError) as the JS and Android SDKs. A style is a JSONValue; jsonString() is the text to write to a file for MapLibre Native iOS. Also StyleOptions, BasemapStyleOptions, BasemapLang, basemapExcluded, ubCenter, isValidStyleId(_:). Nothing removed.

0.56.0 (2026-10-05) ​

  • New: GET /v1/corridors and GET /v1/corridors/{name}, route:read (PLAN 15 product 2, PLAN 17 product 6). The steppe corridors, one compare row each: share of cross-sections with 3 or more ruts, envelope area, years, confidence; with totals and attribution. One corridor: every fact line with text, value, unit, confidence, source and caveat, none surer than confidence_cap. Every number is copied from the facts.json lane D's make facts writes, never computed here. Sentinel-2 at 10 m, not field checked; null means unknown. No file, or a file that is not whole: 503 unavailable, never half. Labels in English for now. No SDK change. Nothing removed.

0.55.0 (2026-10-04) ​

  • A row that is, as a whole, a door name answers its door in POST /v1/clean, behind the door_names feature (PLAN 17 product 4, PLAN 18). Any spelling that folds to the name finds it ("сансар-бэйкери"). The row gets methoddoor_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), empty choices, and match_confidence null. The parsed parts stay as read. The holder org is not shown. Each such row writes one resolution log row. matched counts rows with a building or a named door. An expired name, a retired or restricted door, part of a name, and orgs without the feature get the same rows as before. A name lookup error answers the rows without names, never a 503. No new field, no SDK change. Nothing removed.

0.54.0 (2026-10-04) ​

  • A typed door name answers its door in GET /v1/geocode, behind the door_names feature (PLAN 17 product 4, PLAN 18). When the whole query is a live door name, in any spelling that folds to it ("сансар-бэйкери"), the named door comes first: layer 1, near null, score the best layer 1 score or 1.6 (a whole name), whichever is higher. It is not repeated below, and limit holds. The holder org is not shown. Serving it writes one resolution log row, as GET /v1/door-names/{name} does. Orgs without the feature, and part of a name, get the same results as before. No new field, no SDK change. Nothing removed.

0.53.0 (2026-10-04) ​

  • Door names are transferable, behind the door_names feature (PLAN 17 product 4). The holder makes a code with POST /v1/door-names/{name}/transfer-code (names:write): dnt_ and 24 characters, shown once, works once, for 7 days or until the name expires if sooner. A new code replaces the old one; DELETE on the same path clears it. The other org sends it to POST /v1/door-names/{name}/claim {"transfer_code": "dnt_..."} and holds the name from then, same door, same expires_at. 409 transfer_refused for a wrong, cleared or timed-out code, counted against the door miss cap; 409 already_yours; 422 too_many past 50 names. No org is named to the other. GET /v1/door-names adds transfer_until on each name, null when no code waits. Nothing removed.

0.52.0 (2026-10-04) ​

  • New, behind the door_names feature (403 feature_off until it is open to your org): door names, a spoken name a business holds for its door (PLAN 17 product 4). POST /v1/door-names {"name": "Сансар бэйкери", "code": "7QSVX"} registers it for 365 days, no fee yet; your own name with another code moves it. New scope names:write. GET /v1/door-names lists your names. GET /v1/door-names/{name} (door:read) answers the door as GET /v1/door/{code} does, official address first; the holder is not shown. Names are unique by folded form: case, spaces, punctuation and Latin letters typed for Cyrillic do not make another name. Refused: a phone number, an e-mail, a door code. 409 name_taken, 410 retired_code, 422 too_many past 50 names. A code or name that finds no door counts against the door miss cap. Nothing removed.

0.51.0 (2026-10-04) ​

  • GET /v1/usage adds features: the features open to your org, A to Z, like cargo.eta. A route that belongs to any other feature answers 403 feature_off. Empty when none is open, which is every org today: no route is behind a feature yet. A change shows within 60 s, as a revoked key does. The usage page shows them. Nothing removed.

0.50.0 (2026-10-04) ​

  • GET /v1/route?depart_at=2026-10-05T08:15:00%2B08:00: a driving route's measured reads the speeds of the departure's hour band (GET /v1/traces/roads?band=), by its Ulaanbaatar weekday and hour: am_peak Monday to Friday 07:00 to 09:59, pm_peak Monday to Friday 17:00 to 19:59, off_peak every other hour, Saturday and Sunday whole. The same rule that puts a trip in a band. measured.band names the band read; without depart_at it is null, all hours, as before. depart_at is ISO 8601 with an offset (Z or +08:00; send + as %2B); a time without one is not taken as Ulaanbaatar time but refused, 400 invalid_depart_at. Past or future times both work; the route line and duration_s do not change. Walking and cycling: measured stays null. Empty today: dock's road lines are not loaded yet, so roads_m is 0. Nothing removed.

0.49.0 (2026-10-04) ​

  • GET /v1/traces/roads?band=am_peak: moving speed per road in one hour band (PLAN 9 Phase 7, speed profiles by hour and weekday). A trip counts whole in the band of its first stored point, Ulaanbaatar time: am_peak Monday to Friday 07:00 to 09:59, pm_peak Monday to Friday 17:00 to 19:59, off_peak every other hour, Saturday and Sunday whole. Each road passes the group rule on that band's trips alone (3 or more organizations, 5 or more trips after the biggest), so a road shown for all hours can be left out of a band. Without band: all hours, as before. rule.hours says which hours an answer covers. Any other value: 400 bad_band. Trips matched before this release count in their bands too. Empty today, as all hours: dock's road lines are not loaded yet. Nothing removed.

0.48.0 (2026-10-04) ​

  • GET /v1/route: a driving route now carries measured, the time with all partners' measured moving speed per road (PLAN 9 Phase 7, ETA in the route API). measured.month is the latest month GET /v1/traces/roads publishes (14 days after it ends). Each road of the line with a published speed that month is driven at that speed: the router's seconds on it come out, the measured seconds go in, turn costs stay. measured.roads_m is the meters of the line on such roads; 0 means none, and measured.duration_s equals duration_s. Only roads under the rule of /v1/traces/roads (3 or more organizations, 5 or more trips after the biggest), so no single partner's speed shows. Moving speed leaves out stops under 3 km/h; a month average, not by hour. Walking and cycling: null. Speeds unreadable for one answer: null, the route still answers. duration_s is unchanged, free-flow. Needs only route:read. Empty today: no road has a published speed yet, so roads_m is 0. Nothing removed.

0.47.0 (2026-10-04) ​

  • GET /v1/traces/roads?month=2026-09&bbox=106.7,47.8,107.2,48.0: moving speed per road and month, all partners together (PLAN 18, speed profiles per segment). 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 and it passes the rule of /v1/traces/speeds: 1 to 60 seconds apart, 3 to 130 km/h, its midpoint more than 200 m from the trip's first and last point. km_h is the way's counted meters over its counted seconds, to 0.1. GeoJSON, one feature per way: id the OSM way id as text, the line of dock's current road with that id, km_h, name_mn, name_en, highway. A road shows only under the rule of /v1/traces/cells: 3 or more organizations, and 5 or more trips after taking out the biggest one. Every other road is left out: no road means too few trips, not a slow road. A way dock has no road line for yet, or one crossing a restricted site, is left out; dock has no road lines loaded yet, so the answer is empty until it does. No trip count. Test and internal organizations do not count. Published 14 days after the month ends, month_open before; a trip is matched only before then. Your trace delete takes a trip out; the 90-day erase of the points keeps it. Needs insights:read. The upload answer is unchanged. Nothing removed.

0.46.0 (2026-10-04) ​

  • GET /v1/traces/kinds/own?month=2026-10: your trips per kind of building at the trip end, per month, for your dashboard (PLAN 18, what kind of deliveries). Each trip counts once, by the building whose footprint holds its last stored 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 (trips) and every kind with trips, most first. Same months as /v1/traces/cells/own; closed from 14 days after the month ends. Your trace delete takes a trip out; the 90-day erase of the points keeps it. Your organization only. Needs trace:read. Nothing removed.

0.45.0 (2026-10-04) ​

  • GET /v1/traces/khoroos/own?month=2026-10: your trips per khoroo and hour of arrival, per month, for your dashboard (PLAN 18, where and when). Each trip counts once, in the Ulaanbaatar hour (0 to 23) of its last stored point and the khoroo holding the centre of that point's 500 m cell, as /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), trips ending in no current khoroo polygon (no_khoroo), and per khoroo its total and every hour with trips. There are no khoroo polygons yet: until they are loaded every trip is in no_khoroo. Same months as /v1/traces/cells/own; closed from 14 days after the month ends. Your trace delete takes a trip out; the 90-day erase of the points keeps it. Your organization only. Needs trace:read. Nothing removed.

0.44.0 (2026-10-04) ​

  • GET /v1/traces/speeds?month=2026-09&bbox=106.7,47.8,107.2,48.0: moving speed per 500 m cell and month, all partners together (PLAN 18, congestion hot spots). 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 holding its midpoint. km_h is the cell's counted meters over its counted seconds, to 0.1. A cell shows only under the rule of /v1/traces/cells: 3 or more organizations, and 5 or more trips after taking out the biggest one. Every other cell is left out: no cell means too few trips, not a slow road. No trip count (/v1/traces/cells has one). Per cell, not per road: no map matching yet. Test and internal organizations do not count. Published 14 days after the month ends, month_open before. Needs insights:read. Nothing removed.

0.43.0 (2026-10-04) ​

  • GET /v1/traces/khoroos?month=2026-09: trips per khoroo and hour of arrival, all partners together (PLAN 18, delivery heatmaps by hour and khoroo). Each trip counts once, in the Ulaanbaatar hour (0 to 23) of its last stored point and the khoroo holding the centre of that point's 500 m cell. A khoroo and hour shows only under the rule of /v1/traces/cells: 3 or more organizations, and 5 or more trips after taking out the biggest one. Every other hour is left out: no hour means too few, not zero. No khoroo total. A GeoJSON FeatureCollection, one feature per khoroo with its polygon, district, number and hours. Current khoroo polygons only; there are none yet, so the answer is empty until they are loaded. Counts are kept from today. Test and internal organizations do not count. Published 14 days after the month ends, month_open before. Needs insights:read. Nothing removed.

0.42.0 (2026-10-04) ​

  • GET /v1/traces/cells: test and internal organizations no longer count toward a cell, as GET /v1/traces/hours. They are not one of the 3 organizations, their trips are not in the total, and they are never the biggest one taken out. A cell that showed only because of them is now left out; a cell still shown may have fewer trips. rule has a new key, orgs, that says so. Counts are kept; only what is shown changes. Nothing removed.

0.41.0 (2026-10-04) ​

  • GET /v1/traces/hours?month=2026-09: trips per weekday and hour, all partners together (PLAN 18, road load by hour). 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 shows only under the rule of /v1/traces/cells: 3 or more organizations, and 5 or more trips after taking out the biggest one. Every other slot is left out: no slot means too few, not zero. No total. Test and internal organizations do not count. Published 14 days after the month ends, month_open before. Needs insights:read. Nothing removed.

0.40.0 (2026-10-04) ​

  • GET /v1/traces/hours/own?month=2026-10: your trips per weekday and hour, for your dashboard (PLAN 18, when). Each trip 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. Same months as /v1/traces/cells/own; closed from 14 days after the month ends. Your trace delete takes a trip out; the 90-day erase of the points keeps it. Your organization only. Needs trace:read. Nothing removed.

0.39.0 (2026-10-04) ​

  • GET /v1/traces/arrivals/own?month=2026-10: your trips' arrival minutes per month, for your dashboard (PLAN 18, minutes lost finding the door). For each trip: whole minutes from the first point after it was last more than 200 m from its end, to its end; 60 means 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, so compare months and places with them. Month totals with median, p75 and p90, trips per minute, and up to 100 cells where trips ended, by trips of 5 minutes or more. Same 500 m grid and months as /v1/traces/cells/own; closed from 14 days after the month ends. Your trace delete takes a trip out; the 90-day erase of the points keeps it. Your organization only. Needs trace:read. Nothing removed.

0.38.0 (2026-10-04) ​

  • GET /v1/confirm/own?month=2026-10: your drivers' confirmations and door reports per month, for your dashboard (PLAN 18). 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, most first. 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. No door in a restricted site is listed. No other organization is read. New scope confirm:read, read-only: confirm:write does not read it, and confirm:read cannot confirm or report. Nothing removed.

0.37.0 (2026-10-04) ​

  • GET /v1/traces/cells/own?month=2026-10&bbox=west,south,east,north: your own trips per 500 m cell and month, for your dashboard (PLAN 18). 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 month you are in is answered too; closed is true from 14 days after the month ends, when the counts are final. No other organization is read. New scope trace:read, read-only: trace:write does not read it, and trace:read cannot send or delete. Nothing removed.

0.36.1 (2026-10-03) ​

  • POST /v1/clean: new method number_area_ambiguous. A number in a named area that ties in 2 or 3 places gives a building and choices, with match_confidence null (no test rows yet, not 0). A tie in 4 or more places gives no building. Example: "Улаанхуаран 24" has 2 choices, both in Bayanzurkh; "Сансар 24" (4 places) and "Бага тойрог 10" (12 places) have no answer. Before, such an address came as name_ambiguous with 0.536. No field changed. Nothing removed.

0.36.0 (2026-10-03) ​

  • POST /v1/clean: every row has choices. For method name_ambiguous or number_ambiguous, up to 3 places the address could be, the answer first. Each has building_id, point, district, the nearest area name within 1000 m (area) and street name within 150 m (street), so a person can pick ("Сансар 45, near Tengis or near the 3rd hospital?"). A choice in a restricted site keeps only its district (withheld). Any other method: []. The answer itself is unchanged. On the gold set, 85 of 541 rows carry choices. The upload page adds dock_choices.
  • Each ambiguous answer is logged as its method and building ids only, no address text, to fix area outlines.

JS SDK 0.18.0, Android SDK 0.15.0 (2026-10-03) ​

  • New: basemapStyle(basemap, url, options) gives a whole MapLibre style for a steppe pack's new basemap.pmtiles (OpenMapTiles from OSM, all Mongolia, zoom 0 to 14), from the manifest's basemap block: land cover, water, steppe tracks, roads, borders, buildings, and with glyphs also names in lang (mn or en). One pmtiles:// source with the block's attribution. A block with a poi or housenumber layer is refused. Kotlin also PackBasemap.fromManifest(text). Also BASEMAP_EXCLUDED, and in JS the types PackBasemap, BasemapStyleOptions, BasemapLang. The Swift SDK does not have it yet. Nothing removed.

JS SDK 0.17.0, Swift SDK 0.13.0, Android SDK 0.14.0 (2026-10-03) ​

  • New: poisOnRoute(pois, line, nearM) reads a steppe pack's new pois.geojson: the registered fuel stations and ger camps within nearM (default 2000 m, as on the steppe route pages) of a route line, in route order, with along_m and off_m (alongM, offM) in whole metres, registered name and reg_number (regNumber). Places not matched to the state registry are not in the pack; unlisted (unlistedFuel, unlistedGerCamp) gives their counts for all Mongolia, and registry_checked (registryChecked) false means the registry was not read. The current pack lists 0 places, with 1,268 fuel stations and 175 ger camps counted. New types PackPois, RoutePois, PoiOnRoute. crossingsOnRoute gives the same answers as before. Nothing removed.

JS SDK 0.16.0 (2026-10-03) ​

  • New: attachSuggest(input, client, { onPick }), one address input for a checkout or a pickup form (PLAN 18, entry path 5). Suggestions from the third character, 200 ms after the last key; a newer key cancels the older request. A typed door code is looked up as a door first. Arrows, Enter, click, Escape; ARIA combobox. Each option is label(), so unmatched places say "(unofficial)". Suggester is the same logic with no DOM. Also SUGGEST_MIN_CHARS, SUGGEST_DELAY_MS and the types Suggestion, SuggestOptions, AttachSuggestOptions, SuggestBox, SuggestClient. Nothing removed.
  • The checkout sample uses it.

JS SDK 0.15.0, Swift SDK 0.12.0, Android SDK 0.13.0 (2026-10-03) ​

  • crossingsOnRoute: new field delta on each warning, and on the pack's crossings, for a river crossing on a delta channel: lake (the lake's OSM name, can be null), lake_osm (lakeOsm, as relation/114846) and river. There the river splits into a lake and no model reach says what share a channel carries, so the flow is null: say "delta channel, flow unknown". null/nil elsewhere and in packs built before the rule. In the current pack 3 of 5,525 crossings, all in the Khovd delta into Khar-Us nuur. Nothing removed.

Swift SDK 0.11.0 for CocoaPods (2026-10-03) ​

  • New: Dock.podspec in the sdk/swift folder. A CocoaPods app adds pod 'Dock', :path => '../sdk/swift'. Same sources, platforms (iOS 15, macOS 13) and version as the Swift package. Not on CocoaPods trunk. The SDK did not change.

Android SDK 0.12.0 (2026-10-03) ​

  • New: a map style for MapLibre Native. styleJson(options) gives the dock style (the same as the JS SDK's dockStyle) with its sources keyed, for Style.Builder().fromJson; styleUrl(id) gives a saved style for fromUri. Also StyleOptions, dockStyle, dockSources, dockLayers, UB_CENTER. Nothing removed.
  • The emulator run now draws both styles with MapLibre Native 13.6.1 on Android 16 and checks the colors. MapLibre Native Android 13 needs API 23, so this check does not run on API 21.

Android SDK 0.11.2 (2026-10-03) ​

  • Fixed: with telemetry on, the first geocode() crashed the app on Android with ExceptionInInitializerError (PatternSyntaxException). The search queue split words with a regular expression flag Android does not have. Every earlier Android SDK version had it; on the JVM it worked. Now the same white space is spelled out, so searches are cut into words as before. Found on an Android 16 emulator. Nothing else changed.
  • The emulator run now also checks search telemetry and a trip recorded from the emulator's GPS.

Android SDK 0.11.1 (2026-10-03) ​

  • Fixed: parseDoorCode, and door() which calls it, crashed on Android 5.0 to 6.0 (API 21 to 23) with NoSuchMethodError: codePoints(). That call is Android API 24. Found on an Android 5.0 emulator; same answers on all 74 shared cases now. Nothing else changed.
  • Every release now runs on emulators, API 21 and API 36, from an app built with the Android Gradle Plugin, and the jar is checked against API 21 at build time.

JS SDK 0.14.0, Swift SDK 0.11.0, Android SDK 0.11.0 (2026-10-03) ​

  • crossingsOnRoute: the month's m3/s (month_m3s, monthM3s) is now also null where the flow's confidence is 0.1 or under, not only on a reach marked doubtful. Packs mark 0.1 where no model reach follows the OSM river (snap none_fits) and where the channel follows only a small reach and the river's main stem is taken (main_stem). The steppe dashboard already hid the number there. In the current pack that is 448 of 2,587 river crossings, was 23. The high-water months are still given.
  • New field snap on the flow and on each warning: matched, none_fits, main_stem or nearest; null/nil in packs built before the match. New constant LOW_FLOW_CONFIDENCE (lowFlowConfidence in Swift), 0.1. Nothing removed.

Swift SDK 0.10.0, Android SDK 0.10.0 (2026-10-03) ​

  • New crossingsOnRoute, offline, as in JS SDK 0.13.0: the fords and unbridged crossings a route line passes within 5 m, in route order, from a steppe pack's crossings.geojson (PackCrossings(json:) in Swift, PackCrossings.fromJson(text) in Kotlin). Fields in camel case: alongM, offM, highWater, highMonths, monthM3s (nil/null on a reach marked doubtful). New ubMonth(), the default month. Same answers as JS on the shared test cases and on three real routes. Nothing changed or removed.
  • Android: built as a Maven artifact, mn.dock:dock-android:0.10.0, with a POM, a sources jar and a javadoc jar. The POM lists kotlinx-coroutines and kotlinx-serialization, so an app adds one dependency line. Not on Maven Central yet: your contact gives you the repository folder (Android page, Install). The jar is the same.

JS SDK 0.13.0 (2026-10-03) ​

  • New crossingsOnRoute(crossings, line, month?), offline: the fords and the crossings with no OSM bridge or ford that a route line passes within 5 m, in route order, from a steppe pack's crossings.geojson. Each has along_m, off_m, and on a river high_water for the month (default this month, Ulaanbaatar), high_months and the month's normal flow in m3/s (null on a reach marked doubtful). Show the pack's note and the flow attribution with them. Nothing changed or removed.

JS SDK 0.12.0, Swift SDK 0.9.0, Android SDK 0.9.0 (2026-10-03) ​

  • geocode takes proximity, a point ([lon, lat] in JS, LonLat in Swift and Kotlin), sent as the API 0.35.0 parameter: among names the typed text starts equally well, nearer places come first. Rounded to 7 decimals as route does. Not kept on the device, not sent with telemetry; offline answers do not read it. Without it every request is as before. Nothing removed.

0.35.2 (2026-10-03) ​

  • GET /v1/geocode: of two places with the same score, the one with a matched official address comes first, then partial, then none; then the higher confidence (null and 0 count the same). The tie used to break on id. Today no row has an official address or confidence above 0, so every answer is as 0.35.1. Fields unchanged. Nothing removed.

0.35.1 (2026-10-03) ​

  • GET /v1/geocode, typing a name: a place partner app users picked comes before an equally good match. It counts with 5 or more picks in a year from 2 or more partners. Whole name, then name start, then a later word, as before. Picks come from POST /v1/telemetry/search, sent only after the user opted in. No partner has sent picks yet, so every answer is as 0.35.0 today. Fields unchanged. Nothing removed.

0.35.0 (2026-10-03) ​

  • GET /v1/geocode takes an optional proximity=lon,lat (where the person is, or the map centre). Among names the query starts equally well, the nearer place comes first: whole name, then name start, then a later word, as before, each nearer first. A whole name far away still leads a name start next door. The point is not logged. A bad value is 400 invalid_proximity. Without it, every answer is as 0.34.2. Synthetic set cut to 3 characters, right place in the top 3: 0.385 with no point, 0.431 with the target's district centre, 0.394 with the city centre. Fields unchanged. Nothing removed.

0.34.2 (2026-10-03) ​

  • GET /v1/geocode, typing a name: a place whose name the query starts now comes before places that only look alike. The whole name first, then names that start with the query, then names with a later word that does; a district or area before a building of the same kind of match, then the shorter name. "Бая" gives Bayangol and Bayanzurkh districts, no longer "байр". Synthetic set cut to 3 characters: right place in the top 3 0.330 to 0.385; at 4 characters 0.399 to 0.472; whole queries unchanged (top-1 0.991). score of such a row can reach 1.749. Fields unchanged. Nothing removed.

0.34.1 (2026-10-03) ​

  • GET /v1/geocode, landmark-relative queries: when the query names a kind ("university south of X, kindergarten next door"), places of that kind, or of no known kind, now come before places of another kind; the reranking model orders each group. A query that says only "building" or "place" is ordered as before. score stays the model's. Described-place test sets, 600 queries each: top-1 0.963 to 0.983 (12 better, 0 worse) and 0.978 to 0.983 (3, 0). Fields unchanged. Nothing removed.

JS SDK 0.11.0, Swift SDK 0.8.0, Android SDK 0.8.0 (2026-10-03) ​

  • New offline helper: parseDoorCode(text) reads a typed or pasted door code as the API does, with no request: the code in upper case Latin and what followed it (unit), or why not: empty, length (not 5 characters) or letters (a vowel, I, L, O, U, or a sign). Same Cyrillic lookalike keys and same spaces as the API, checked on the same 74 cases (sdk/testdata/door-code.json). Also DOOR_CODE_ALPHABET and DOOR_CODE_LENGTH (Swift doorCodeAlphabet, doorCodeLength).
  • door() refuses a text that cannot be a code before any request, with the same DockError the API sends: 400 invalid_code and its message. A refused text no longer costs a request. Nothing removed.

Swift SDK 0.7.0, Android SDK 0.7.0 (2026-10-03) ​

  • Offline search, as in JS SDK 0.10.0. Each place passed to pick is kept on the device, and when geocode gets no answer (DockError code network) it answers from them with offline true, most picked first. Places only: not the typed text, time or point. In memory by default, 100 places. placeCache: PlaceCacheOptions(store: UserDefaultsPlaceStore()) (Swift) or placeCache = PlaceCacheOptions(store = ...) (Kotlin, a PlaceStore over SharedPreferences) keeps them across launches; nil or null turns it off. Same key and format as the JS SDK. New places() and forgetPlaces(). pick used to do nothing while telemetry was off; it now keeps the place, and still sends nothing. GeocodeResponse gets offline (false for an API answer, not on the wire). Nothing removed.

JS SDK 0.10.0 (2026-10-03) ​

  • Offline search. Each place passed to pick is kept on the device, and when geocode gets no answer (DockError code network) it answers from them with offline: true, most picked first. Places only: not the typed text, time or point. In memory by default, 100 places; placeCache: { store: localStorage, max } keeps them across page loads, placeCache: false turns it off. New places() and forgetPlaces(). pick used to do nothing while telemetry was off; it now keeps the place, and still sends nothing. API answers are unchanged. Nothing removed.

JS SDK 0.9.0, Swift SDK 0.6.0, Android SDK 0.6.0 (2026-10-02) ​

  • New offline helper: JS closureAt(avoid, point), Swift avoid.closure(at:), Kotlin avoid.closureAt(point). It gives the id of the first closure in an excludePolygons answer whose ring holds the point, or null. Valhalla drops every road inside a ring, so a trip that starts or ends there gets "No path could be found for input": check both ends before asking your own Valhalla. Same rule as the API's 422 end_in_closure on GET /v1/route, checked on the same 700 points. The route() doc comment now lists end_in_closure and closures_over_limit. Nothing removed.

0.34.0 (2026-10-02) ​

  • GET /v1/route: a from or to inside a road closure active today is 422 end_in_closure, and the message names the closure id. Before, the router dropped every road inside the closure and the answer was 422 no_route, "No path could be found for input". Route to a point outside the closure. Nothing removed.

0.33.0 (2026-10-02) ​

  • POST /v1/closures takes an Idempotency-Key header (1 to 64 of letters, digits and ._:-; a uuid fits). A retry after a timeout with the same key and the same body answers the first report, stores nothing and sets Idempotent-Replayed: true. The same key with another body is 422 idempotency_key_reused. Keys are per organization and never expire. Without a key nothing changes. Nothing removed.

0.32.0 (2026-10-02) ​

  • GET /v1/route avoids every road closure active today (Ulaanbaatar), in every mode. New field closures in the answer: as_of (the route day) and avoided (the closure ids kept out). If the day's closures are over the router's limits for one request (10,000 m of ring, 100 vertices), the answer is 422 closures_over_limit: no closure is dropped to give a route. Closures that cannot be read are 503 unavailable. On a day without closures the route is the same as before.

JS SDK 0.8.0, Swift SDK 0.5.0, Android SDK 0.5.0 (2026-10-02) ​

  • JS Route, Swift Route and Kotlin Route have the new closures field (RouteClosures). Nothing removed.

JS SDK 0.7.0, Swift SDK 0.4.0, Android SDK 0.4.0 (2026-10-02) ​

  • closures() reads GET /v1/closures (route:read): road closures not over today, each with its ring. Keep the answer on the phone. excludePolygons(closures, day) works offline: the rings of the closures active on the route day (default today, Ulaanbaatar) for your own Valhalla's exclude_polygons. Over Valhalla's limits for one request it throws (JS ClosuresOverLimitError, Swift ClosuresError.overLimit, Kotlin ClosuresOverLimitException): refuse the route, never drop a closure. A day before the list's as_of throws: fetch again. JS also exports isActive and ubToday. Nothing removed. API still 0.31.0.

0.31.0 (2026-10-02) ​

  • Road closures (PLAN.md 20.14). A partner reports a flooded or washed out stretch, a bridge down, snow, mud or works with POST /v1/closures: a point or a line of up to 3000 m, from and to days, a note. New scope closures:write. The sender is the key's organization. A report is never edited or deleted: lift it with POST /v1/closures/{id}/lift when the road opens. GET /v1/closures/{id} returns your own report, note included.
  • GET /v1/closures (route:read) returns every closure current today or starting later, with the ring to send Valhalla as exclude_polygons. Same shape and same rings as an offline pack's closures block. No partner name, no note. Confidence 0.5: one partner's word. No reports inside a restricted site. A note with a run of 6 or more digits (a phone number) is refused. Nothing removed.

0.30.1 (2026-10-02) ​

  • GET /v1/geocode, landmark-relative queries: the description is now searched with the English word beside its place word ("здание, внутри аптека (pharmacy)"), in Mongolian, Russian, Japanese, Korean and Chinese, as the reranking model already read it. English is read as typed. Kind-only test set, 486 queries: a fitting place first 0.951 to 0.965 (7 better, 0 worse). Described-place test set, 600 queries: top-1 0.952 to 0.953. Fields unchanged. Nothing removed.

0.30.0 (2026-10-02) ​

  • GET /v1/geocode, landmark-relative queries: when the query also describes the place ("building east of Max Mall, pharmacy inside"), the first 10 places found by description are read again side by side with that description by a reranking model and put in its order (PLAN.md 7, layer 3). Those have layer 3 and score is the model's, 0 to 1. New value: layer 3. When the model is slow or down they stay layer 2, as before. Described-place test set, 200 queries: top-1 0.70 to 0.82, top-3 0.84 to 0.91. Nothing removed.

0.29.1 (2026-10-02) ​

  • GET /v1/geocode, landmark-relative queries: a landmark whose name has a comma is read whole ("west of U.S. Embassy - Ulaanbaatar, Mongolia"). Before, only the part after the comma ("Mongolia") was read. The longer reading counts only when a landmark by that name is found; otherwise the landmark stops at the comma, as before. Fields unchanged. Nothing removed.

0.29.0 (2026-10-02) ​

  • Buildings without a name are now searchable too (633,322 of 643,661 buildings have no name). GET /v1/geocode finds them by description (layer 2) and within 150 m of a landmark. GET /v1/reverse returns one only when the point is inside it. GET /v1/address/{id} and GET /v1/changes know them. Names are null, long_mn and long_en give district and area, official_status is none. Name search (layer 1) never returns them. Fields unchanged. Nothing removed.

0.28.1 (2026-10-02) ​

  • GET /v1/geocode, landmark-relative queries: the words that close the relation no longer count as the description. Mongolian тал in a case and нь ("зүүн талд", "баруун талын", "урд нь"), Japanese 側, の, に ("東側の建物"), Korean 에 (있는) ("동쪽에 있는 건물"), Chinese 的 ("东边的建筑"). Fields unchanged. Nothing removed.

0.28.0 (2026-10-02) ​

  • GET /v1/geocode reads landmark-relative queries (PLAN 7): "Хүннү моллын ард", "Max Mall-iin urd", "east of Max Mall", "к востоку от ...", "...の近く", "... 동쪽", "...东边". It finds the landmark by name, then lists first the places within 150 m on the side said (within 55 degrees). Mongolian ар (behind) is north, урд (front) south. They rank on the rest of the query, without the landmark name (layer 2), or by distance when it says nothing more (layer 1).
  • Each result has a new field, near: that landmark (kind, id, names, relation, metres). null for every other result. Name search order is unchanged. Nothing removed.

0.27.0 (2026-10-02) ​

  • GET /v1/geocode: each result has a new field, layer. 1: the words of a name or address matched. 2: the place description matched the meaning (PLAN 7 layer 2), for example "building with a blue roof next to a school". Layer 2 places only fill the slots left when name matches are fewer than limit, after every name match. Compare score only within one layer. Name search order is unchanged. Nothing removed.

0.26.0 (2026-10-02) ​

  • GET /v1/traces/cells?month=2026-09&bbox=west,south,east,north, trips per 500 m cell and month, all partners together (PLAN 19). New scope insights:read. A cell is shown only when 3 or more organizations sent trips through it and 5 or more trips are left after taking out the biggest organization's. Other cells are left out: no cell means too few, not zero. A month is published 14 days after it ends, from trips received by then. Counts stay after the raw points are erased at 90 days. Your DELETE /v1/traces/{id} takes the trip out of the counts too. Nothing else changed.

0.25.0 (2026-10-02) ​

  • POST /v1/oauth/token, OAuth2 client credentials (PLAN 19). Your server trades its key for a dt_ access token that works for an hour with the scopes it asks for, and gives the token to an app, so the key never ships in an app. Tokens go where keys go: ?access_token= or Authorization: Bearer. New 401 errors for tokens only: invalid_token, expired_token. Keys work as before. Nothing removed.

0.24.0 (2026-10-02) ​

  • point in door, geocode and reverse results and in GET /v1/address/{id} is given to 6 decimals (about 0.1 m). The 7th decimal is a mark for your organization (PLAN 19): the same for a point every time, and it moves the point under 0.12 m. The top-level point of a reverse answer comes back as you sent it. No field or type changed.

JS SDK 0.6.0 (2026-10-02) ​

  • Needs maplibre-gl 6.4.1 or later in 6.x (peerDependencies >=6.4.1 <7). Every version up to 6.4.0, all of 4 and 5 included, has a critical XSS in its HTML sanitizer (GHSA-jrc7-96c5-q579). On 4 or 5: npm install maplibre-gl@6 and add the worker lines from Bundlers. The sample apps load maplibre-gl 6.11.2 from unpkg as an ES module. No code change. API still 0.23.0.

Android SDK 0.3.0 (2026-10-02) ​

  • Search telemetry and trip recording in Kotlin, with the JS and Swift consent rules. setTelemetry(on), pick(result), flushTelemetry(), appInBackground(): off by default, final text, result count and pick only, 50 a batch, sent when the app calls appInBackground() from onStop, dropped when the user turns it off. startTrace(Consent.OPT_IN, source), stopTrace(), cancelTrace(), trace(id), deleteTrace(id): points in memory only, [lon, lat, t], split every 20,000 points. The app keeps its own location permission and pushes its fixes into a PositionFeed. Nothing removed. API still 0.23.0, Swift still 0.3.0.

Swift SDK 0.3.0 (2026-10-02) ​

  • Search telemetry and trip recording in Swift, with the JS consent rules. setTelemetry(_:), pick(_:), flushTelemetry(): off by default, final text, result count and pick only, 50 a batch, sent when the app goes to the background, dropped when the user turns it off. startTrace(consent: .optIn, source:), stopTrace(), cancelTrace(), trace(_:), deleteTrace(_:): points in memory only, [lon, lat, t], split every 20,000 points. The app keeps its own location permission and pushes Core Location fixes into a PositionFeed. Nothing removed. API still 0.23.0, Android still 0.2.0.

0.23.0 (2026-10-02) ​

  • The monthly field audit (PLAN 5), for dock staff keys only: GET /v1/audit lists samples, GET /v1/audit/{tag} serves one checklist in walk order with every answer so far, PATCH /v1/audit/{tag}/{seq} records what the surveyor found (found, wrong_location, missing, no_access, or null to clear). found needs the measured point and the method (rtk, imagery_tap, tape). The device position must be within 40 m (150 m for no_access). Every answer is kept. Needs audit:write, which only an internal org can hold. Partner calls are unchanged.

Swift and Android SDK 0.2.0 (2026-10-02) ​

  • label(door, lang) in Swift (label(_:lang:), Lang.mn, .en) and Kotlin (label(place, lang), Lang.MN, EN), same rules as the JS label(): the spoken form, then the name, then the long form, then the code, never the id, and "(unofficial)" / "(албан бус)" when there is no matched official address. Works on Door, RouteDoor, GeocodeResult, ReverseResult and Address. The iOS and Android pages use it. Nothing removed. API still 0.22.0.

iOS and Android SDK pages (2026-10-02) ​

  • Two new pages, iOS SDK and Android SDK, in Mongolian and English: install, every call, errors, tiles, deprecated routes, keys and a reference table. Every code block on them runs in swift test and ./gradlew test on a fake transport. No SDK or API change: Swift and Android 0.1.0, API 0.22.0.

Android SDK 0.1.0 (2026-10-02) ​

  • First cut of the Android SDK, Kotlin in sdk/android (Java 8 bytecode, Android API 21 and up, kotlinx-coroutines and kotlinx-serialization): suspend calls geocode, reverse, door, route, place, changes, confirm, reportDoor, usage, same names and errors (DockError) as the JS and Swift SDKs. The flat type is DoorUnit, so it does not hide Kotlin's Unit. authorizedUrl() adds the key to dock tile URLs only, for MapLibre Native. No map view, telemetry or trip recording yet. Not on Maven yet. No API change, still 0.22.0.

Swift SDK 0.1.0 (2026-10-02) ​

  • First cut of the iOS SDK, a Swift package in sdk/swift (iOS 15, macOS 13, no dependencies): geocode, reverse, door, route, place, changes, confirm, reportDoor, usage, same names and errors (DockError) as the JS SDK. authorizedURL(_:) adds the key to dock tile URLs only, for MapLibre Native. No map view, telemetry or trip recording yet. Not on a package index yet. No API change, still 0.22.0.

Database connections (2026-10-02) ​

  • The API no longer exits when the database drops an idle connection (a restart, for example). It logs a warning and opens a new connection on the next request. On shutdown it waits until every connection has closed. No API change, still 0.22.0.

Change feed watermark (2026-10-01) ​

  • GET /v1/changes: until no longer waits behind database maintenance (autovacuum). Only open writes hold it back, as documented. A change still appears after the next index refresh, never lost. No API change, still 0.22.0.

Confidence levels (2026-10-01) ​

  • Driver confirmations now raise confidence, hourly. One confirmed tap within 5 m of the entrance: 2. Three within 5 m of each other from at least two drivers or orgs: 3. A newer wrong_location or missing holds the level, and so does a confirmed tap over 5 m away, which now waits for review like wrong_location. Levels are only raised here; moving a point drops it one level, as before. No API change, still 0.22.0.

JS SDK 0.5.0 (2026-10-01) ​

  • place(id): GET /v1/address/{id}, a place by id with its parents. changes(since, { limit, cursor }): one page of GET /v1/changes; with a cursor, since is left out; a Date goes as UTC. confirm(confirmation): POST /v1/confirm; TypeScript refuses a confirmed without tap or both code and entrance; observed_at takes a Date. No API change, still 0.22.0.

0.22.0 (2026-10-01) ​

  • POST /v1/confirm: a driver on site confirms a door (by code) or an entrance (by its tile id), or says it is elsewhere (wrong_location) or missing. The driver taps the door; phone GPS (device) only checks the tap and must be within 40 m. driver is your own id for the driver, kept only as a hash; a phone number or e-mail is refused. The same driver saying the same within 24 h counts once. Map data does not change at once. Queued confirmations carry observed_at, up to 7 days old. Needs confirm:write (partner keys).

0.21.0 (2026-10-01) ​

  • GET /v1/changes?since=: ids, kinds, states and times of doors and places changed since a time, so a partner syncs stored places without a full pull. current: fetch it again with /v1/address/{id}; retired or hidden: drop it. The window is [since, until), where until is the last search index refresh, so every current id listed can be fetched. Pages by cursor, limit 1 to 1000. Nothing inside a restricted site is listed. Needs changes:read (partner keys).

0.20.0 (2026-10-01) ​

  • GET /v1/address/{id}: fetch one door or place again 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). Official address first, code after it. A retired door or a demolished building is 410 retired; ids are never reused. Anything inside a restricted site is 404. A place added since the last search index refresh is 404 until the refresh. Needs geocode:read.

0.19.1 (2026-10-01) ​

  • DELETE /v1/traces/{id} works from a browser: CORS now allows DELETE. Before, deleteTrace(id) in a browser failed with a network error and the trip stayed (it worked from Node). New sample app: examples/driver.html, a driver trip with the opt-in box.

0.19.0 (2026-10-01) ​

  • JS SDK 0.4.0: startTrace({ consent: 'opt_in' }), stopTrace(), cancelTrace(), trace(id), deleteTrace(id). Off until started. Records [lon, lat, t] from geolocation in memory only, at most one point a second, fixes over 50 m accuracy and outside Mongolia left out; sends to POST /v1/traces on stop, split every 20,000 points.
  • POST /v1/traces: send one trip a user recorded, for users who opted in, as dock.trace.v1. Exactly three keys: format, consent: "opt_in", points ([lon, lat, t], WGS84, whole seconds UTC, 2 to 20,000 points, inside Mongolia). Any other key (device id, user, accuracy) refuses the whole upload. 11 refusal codes, never a value from the body. Besides the points, only the organization and the day are kept: no key, IP or device. Points inside a restricted site are not stored (restricted). The same trip sent twice is stored once. Raw points are erased after 90 days (purge_after). Bodies up to 2 MB. Needs trace:write.
  • GET /v1/traces/{id}: your trace as stored, GeoJSON. Another organization's id is 404.
  • DELETE /v1/traces/{id}: erase your trace now. Points and times go; the id stays and is never reused.

0.18.0 (2026-10-01) ​

  • POST /v1/telemetry/search: send place searches and what was picked, for users who opted in (consent: "opt_in", else 400 consent_required). Up to 50 searches a request: q, results shown, picked (kind, id, rank). Only the organization and the Ulaanbaatar day are kept: no key, IP, device or time of day. Phone numbers and e-mail addresses are removed before storage. A pick inside a restricted site is dropped whole (dropped). Needs geocode:read; one request is one call. Keep it off by default.
  • JS SDK 0.3.0: telemetry option and setTelemetry(on), off by default. When on, geocode searches (final text only) and pick(result) go to this route in batches of 50, on page hide and on flushTelemetry(). The checkout sample has the consent box.

0.17.0 (2026-10-01) ​

  • POST /v1/clean: each row has numbers: entrance, floor, unit and pavilion as values. "5-р entrance (internal)" is 5, "В1 давхар" is -1 (a basement is negative), "1206-1208 тоот" is "1206", "1207", "1208". Units stay strings ("0-9а"). A gate with no number gives none. Empty lists when absent. The spans as typed stay as they were.

0.16.0 (2026-10-01) ​

  • POST /v1/clean: address cleaning. Up to 100 rows per request, 300 characters each. Per row: district, khoroo, microdistrict, street, building, entrance, floor, unit (as typed), the matched building id and point, the method that found it, that method's share right on test rows (match_confidence), the building source, and the text left over. No state address register is matched yet, so every row is unofficial. A building in a restricted site is withheld (withheld). Needs clean:read; free keys do not carry it. Addresses are not stored or logged.

0.15.0 (2026-10-01) ​

  • POST /v1/alerts: tell me when today's calls, tiles or door_misses pass a share of the daily cap (percent, default 80). Fires at most once per Ulaanbaatar day. Optional webhook_url gets one signed POST per firing (Dock-Signature, HMAC-SHA256; webhook_secret shown once). Any key, not counted. Up to 20 per org (409 alert_limit), one per metric and percent (409 alert_exists). An alert never changes.
  • GET /v1/alerts: your org's alerts and their firings of the last 30 days, with webhook delivery.
  • Usage page: alerts, add one, firings.

0.14.0 (2026-10-01) ​

  • POST /v1/styles: save a map style (name, background, building, building_outline, extrude) and get a style id, st_ and 10 characters. Missing fields take the dock defaults. Needs tiles:read. Up to 100 per org (409 style_limit). A style never changes: to change colors, save a new one.
  • GET /v1/styles: your org's styles, newest first. GET /v1/styles/{id}: the style as MapLibre JSON, sources on this API. Only your org's keys read it; any other id is 404 not_found.
  • JS SDK: createMap(maplibregl, dock, { styleId }) loads a saved style. New options building and buildingOutline. Entrance colors show confidence and stay the same on every map.
  • Style editor page: pick colors, preview them on dock tiles, save, copy the code.

0.13.0 (2026-10-01) ​

  • GET /v1/versions: API versions, deprecated routes, sunset days. No key, not counted.
  • Versioning policy: breaking changes only in /v2. A route is deprecated at least 365 days before its sunset. Its answers carry Deprecation, Sunset and Link headers; from the sunset day it answers 410 sunset. No route is deprecated today.
  • Status page: API and docs site up or down now, share up per day over 90 days, incidents, deprecated routes. Updated every 5 minutes.

0.12.0 (2026-10-01) ​

  • GET /v1/usage adds plan (free or custom) and upgrade. For a free org, upgrade holds the partner plan: 10,000 requests, 100,000 tiles and 200 door misses a day, 1,200 tiles a minute, and the scopes changes:read and confirm:write. null for a partner org. Prices are not published yet.
  • Usage page: your plan, and for a free key its limits next to the partner plan.

0.11.0 (2026-10-01) ​

  • POST /v1/signup: a free key at once. No email, no name, no body. The key is in the answer only; store it then. Free keys read only (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. Three keys per address per day, and a daily cap on free keys for everyone (429 signup_limited, see Retry-After). Both reset at 00:00 Ulaanbaatar time.
  • Playground: a "Get a free key" button fills the key field.

0.10.0 (2026-10-01) ​

  • GET /v1/route?from=&to=&mode=: a route between two points or door codes. A code routes to its entrance, not the building centre. from and to are a code (7QSVX) or lon,lat (106.9301,47.9201). mode: driving (default), walking, cycling. The answer has a GeoJSON line, metres and seconds. The line follows the road network and ends where it is nearest the entrance; the straight distance left to the entrance is to.gap_m and is not drawn as road. Needs route:read, and door:read too when a code is used. An unknown code is 404 and counts against the daily door miss cap. No routes to or from restricted sites (422 not_accepted). Coverage: Ulaanbaatar. No traffic yet.

0.9.0 (2026-10-01) ​

  • CORS: browsers on any site can call the API with a key in the Authorization header. No cookies are used. Preflight requests need no key and are not counted. Retry-After, X-Quota-* and X-Rate-Limit-* are readable from browser code.
  • Usage page: paste a key to see today's calls, tiles and door misses against your caps, and up to 90 days of history. The key is not stored.

0.8.1 (2026-10-01) ​

  • info.version in openapi.json now matches the API release (it was stuck at 0.5.0).

0.8.0 (2026-10-01) ​

  • POST /v1/door/report: report a wrong, missing, duplicate or unmapped door. kind: wrong_location (code, the real point, within 500 m), missing (code), duplicate (code, duplicate_of), unmapped (point only). Answer 202, status: queued. Door data does not change until a person reviews the report. The same report from the same organization within 24 h returns the first one (already_queued). Scope door:read. An unknown code is 404 and counts toward the daily door miss cap. Runs of 6 or more digits and e-mail addresses in note are removed before storing.

0.7.1 (2026-10-01) ​

  • JSON and tiles are compressed, gzip or brotli by Accept-Encoding. Tiles are 2.2x smaller on average, search JSON 4 to 5x. Responses under 1 KB are sent as they are. Map libraries, browsers and curl --compressed decompress on their own. Every response carries Vary: Accept-Encoding.

0.7.0 (2026-10-01) ​

  • GET /v1/usage: your organization's calls, tiles and door misses per Ulaanbaatar day, the caps, and the calling key's scopes and per-minute limits. Any key works. Free: not counted, not stopped by the daily quota. ?days= 1 to 90, default 7. Today includes calls made a moment ago.

0.6.1 (2026-10-01) ​

  • GET /v1/geocode: swapped, missing or extra letters in short names. A word of 4 to 12 letters that is not in the index is corrected to the nearest indexed word (1 or 2 edits; two neighbouring letters swapped count as 1) and searched again. "Банягол" now finds Bayangol district. A corrected hit's score is 0.1 plus 0.05 per edit lower.

0.6.0 (2026-10-01) ​

  • Daily quotas per organization, shared by its keys, reset at 00:00 Ulaanbaatar time. Defaults 10,000 requests and 100,000 tiles a day. Over the quota: 429 quota_exceeded. Headers X-Quota-Limit, -Remaining, -Reset.
  • Tiles have their own per-minute budget per key (default 1,200), apart from the request limit. A map view no longer uses up the request limit.
  • GET /v1/door/{code}: lookups that find no door are capped at 200 a day per organization (code guessing). At the cap: 429 miss_cap until midnight. Other routes keep working.

0.5.1 (2026-10-01) ​

  • GET /v1/geocode: a query equal to a place's name ranks that place above longer names that contain it. "Khan-Uul district" now returns the district first, not "Khan-Uul District Sports Center". score values for exact names are 0.3 higher.

0.5.0 (2026-10-01) ​

  • GET /v1/tiles/{source} (TileJSON) and GET /v1/tiles/{source}/{z}/{x}/{y} (vector tile). Sources building, entrance. Needs tiles:read. Empty tile is 204.
  • Base URL is https://api.dock.mn (was api.map.dock.mn in earlier docs; not live yet).

0.4.0 (2026-10-01) ​

  • GET /v1/geocode?q=: doors, registered businesses, buildings, districts, khoroos, areas, streets and landmarks by name. Cyrillic and Latin, informal spellings, ordinals, one or two typos. Best first, with score.
  • GET /v1/reverse?lat=&lon=: nearest doors, businesses, buildings and landmarks within radius (default 50 m), plus the district and khoroo covering the point.
  • Same result order as a door: official address first, code after it. Both need geocode:read.
  • Nothing inside a restricted site is returned. Query text is not logged.

0.3.0 (2026-10-01) ​

  • GET /v1/door/{code} is live. Returns the door: official address first, then code, natural address (mn, en, short and long), entrance point, entrance kind and number, confidence 0 to 4, source, instructions.
  • 7QSVX 17: the flat is returned when it is on record.
  • 404 not_found for unknown codes, 410 retired_code for retired doors.

0.2.0 (2026-10-01) ​

  • API keys. Every /v1 route needs a key with the route's scope. GET /v1/door/{code} needs door:read.
  • Per-key requests per minute, 429 with Retry-After. Rate limit headers on every keyed response.
  • Daily usage per key and endpoint, by Ulaanbaatar day.

0.1.0 (2026-09-28) ​

  • GET /health, GET /health/ready.
  • GET /v1/door/{code}: validates code format. Lookup returns 501.
  • OpenAPI at /openapi.json.