UzMap docs

Routing

Three endpoints cover travel: /api/route returns turn-by-turn routes between two or more points, /api/matrix returns travel cost between every source and every target, and /api/isochrone returns the area reachable from a point within a time or distance budget. All three are served by a Valhalla engine behind the Go API server; the server translates between the UzMap vocabulary described here and Valhalla's own, so you never see Valhalla's field names, costing names or units.

Conventions shared by all three endpoints#

TopicRule
Base URLhttps://uzmaps.ndc.uz
Authentication?key=… query parameter or X-API-Key header. Every /api/* path except /api/status is gated, so all three routing endpoints are.
MethodsGET with query parameters, or POST with a JSON body (Content-Type: application/json). Any method other than POST is parsed as a GET, except OPTIONS, which the CORS layer answers with 204 before it reaches the handler.
CoordinatesAlways [lon, lat]. In query strings a point is lon,lat; a list is separated by | or ;.
DistancesMetres.
DurationsSeconds.
Success responseContent-Type: application/json; charset=utf-8, status 200.
Error response{"error": "<message>"} with a 4xx or 5xx status. See Errors.
CORSAccess-Control-Allow-Origin: *; allowed headers are Content-Type and Range only.
BillingCounted against the key's quota as the products routing, matrix and isochrone respectively.

Two consequences of the above are easy to miss:

  • X-API-Key is not in the CORS allow-list, so a cross-origin browser request that sets that header is blocked by the browser at preflight. From browser code, pass ?key= instead.
  • Query-string points that fail to parse as lon,lat are dropped silently, not rejected. A typo in one point therefore shows up as "at least two points required" or as a matrix with fewer rows than you sent, rather than as a parse error.

Why two request forms: GET is what a browser address bar or a curl one-liner reaches for; POST is what survives a large point list. A 50×50 matrix carries 100 coordinates, which is exactly the case a URL cannot hold (matrix.go, file comment).

Travel modes#

mode is accepted by all three endpoints. The server maps it to a Valhalla costing model; an unknown or empty mode falls back to car, and the response echoes the mode it actually used.

modeEngine costingavoid_tolls, avoid_highways, shortest honoured
car (default)autoyes
taxitaxiyes
trucktruckyes
motorcyclemotorcycleyes
busbusyes
footpedestrianno
bikebicycleno

The avoidance flags only change the costing options of the motor modes. For foot the adapter sends a fixed walking_speed of 5.0; for bike it sends a Hybrid bicycle type with use_roads set to 0.4. Sending avoid_tolls=1 with mode=foot is accepted and does nothing.

Costing options the adapter sends, for reference:

CostingOptionDefaultWith flag
motor modes (route and matrix)use_tolls0.50 when avoid_tolls
motor modes (route and matrix)use_highways1.00 when avoid_highways
motor modes (route only)use_ferry0.5— (not settable over HTTP)
motor modes (route and matrix)shortestunsettrue when shortest
pedestrianwalking_speed5.0
pedestrian (route only)use_ferry0.5
bicyclebicycle_typeHybrid
bicycleuse_roads0.4

/api/route#

Returns one recommended route and up to three alternatives between two or more points.

Request#

The GET and POST forms do not accept the same set of fields. Note in particular that the language parameter is lang on GET but language on POST, and that shortest and heading exist only on POST.

FieldGETPOSTTypeDefaultNotes
PointspointspointsGET: lon,lat|lon,lat; POST: [[lon,lat], …]requiredAt least two. The first and last are sent to the engine as stops (break); every intermediate point is sent as a pass-through (through).
ModemodemodestringcarSee Travel modes.
LanguagelanglanguagestringuzGET accepts uz, ru, en, uz-Cyrl; anything else becomes uz. POST is not validated: a value outside uz/ru/en yields English instructions and is echoed back unchanged. See Languages.
Alternativesalternativesalternativesinteger 0–3GET: 2; POST: 0Number of alternative routes to ask for, in addition to the recommended one. A value below 0 or above 3 is replaced by 2. The two forms have different defaults: omitting it on POST asks for no alternatives.
Avoid tollsavoid_tolls=1avoid_tollsGET: literal 1; POST: booleanoffMotor modes only.
Avoid highwaysavoid_highways=1avoid_highwaysGET: literal 1; POST: booleanoffMotor modes only.
ShortestshortestbooleanfalsePrefer the shortest route over the fastest. Motor modes only. Not read on GET.
Headingheadingnumber or nullnoneCompass heading in degrees, applied to the first point only, with a 60° tolerance. Not read on GET.

The adapter has AvoidFerries, DepartAt and per-waypoint Name fields, but neither handler populates them, so ferries cannot be avoided and a departure time cannot be set over HTTP.

The handler gives the engine 25 seconds; the engine client has its own 25-second timeout as well.

Response#

json
{
  "routes": [
    {
      "id": "route-0",
      "mode": "car",
      "distance": 6094,
      "duration": 419,
      "geometry": [[69.2401, 41.2995], [69.2405, 41.2998]],
      "bbox": [69.2401, 41.2995, 69.2870, 41.3131],
      "legs": [
        {
          "distance": 6094,
          "duration": 419,
          "steps": [
            {
              "type": "depart",
              "modifier": "",
              "instruction": "Bunyodkor shoh ko‘chasi bo‘ylab yo‘lga chiqing",
              "verbal": "Bunyodkor shoh ko‘chasi bo‘ylab yo‘lga chiqing",
              "street_names": ["Bunyodkor shoh ko‘chasi"],
              "distance": 320,
              "duration": 41,
              "geometry_start": 0,
              "geometry_end": 7
            },
            {
              "type": "arrive",
              "modifier": "right",
              "instruction": "Manzilga yetib keldingiz, u o‘ng tomonda",
              "verbal": "Manzilga yetib keldingiz, u o‘ng tomonda",
              "distance": 0,
              "duration": 0,
              "geometry_start": 41,
              "geometry_end": 41
            }
          ]
        }
      ],
      "summary": "Bunyodkor shoh ko‘chasi · Kichik halqa yo‘li",
      "has_highway": true,
      "recommended": true
    }
  ],
  "language": "uz",
  "engine": "valhalla"
}

(Abridged: a real geometry array has one entry per shape point, and a real steps array has one entry per manoeuvre.)

Top level:

FieldTypeNotes
routesarray of routeThe recommended route first, then alternatives. Never empty: a request with no route fails with 502 instead.
languagestringThe language the request resolved to, echoed back.
enginestringAlways "valhalla".
warningsarray of stringOmitted when empty.

Route:

FieldTypeNotes
idstringroute-0 for the recommended route, route-1, route-2, … for alternatives in engine order.
modestringThe mode used, after the fallback to car.
distancenumberMetres.
durationnumberSeconds.
geometryarray of [lon, lat]The full route line, all legs concatenated. Steps refer to it by index.
bbox[min_lon, min_lat, max_lon, max_lat]
legsarray of leg
summarystringThe one or two street names the route spends the most distance on, joined with ·. Empty when no step has a street name.
has_toll, has_highway, has_ferrybooleanOmitted when false — absence means false.
recommendedbooleantrue on routes[0] only.

Leg (Leg):

FieldTypeNotes
distancenumberMetres.
durationnumberSeconds.
stepsarray of stepOne per manoeuvre.

Step (Step):

FieldTypeNotes
typestringNormalised manoeuvre type; see the table below. Always present, may be "none".
modifierstringOne of "", straight, left, right, slight_left, slight_right, sharp_left, sharp_right. Always present.
instructionstringText instruction in the requested language.
verbalstringSpoken form. For every language other than uz this is the engine's pre-transition verbal instruction; for uz it is identical to instruction. Omitted when empty.
street_namesarray of stringNames of the road after the manoeuvre; falls back to the road's beginning names when the engine gives none. Omitted when empty.
distancenumberMetres.
durationnumberSeconds.
geometry_start, geometry_endintegerIndices into the route's geometry array. Offsets account for earlier legs, so they index the concatenated line, not the leg.
roundabout_exitintegerWhich exit to take. Omitted when 0.
toll, highway, ferrybooleanOmitted when false.
signobjectOmitted when the engine has no sign data.

Sign (Sign):

FieldType
exit_numberarray of string, omitted when empty
brancharray of string, omitted when empty
towardarray of string, omitted when empty

Manoeuvre types#

The engine's numeric manoeuvre types are normalised to a type and modifier pair by the maneuverMap table in routing/valhalla.go.

typePossible modifier valuesEngine types
none""0
depart"", right, left1–3
arrive"", right, left4–6
continuestraight7, 8
turnslight_right, right, sharp_right, sharp_left, left, slight_left9–11, 14–16
uturnright, left12, 13
rampstraight, right, left17–19
exitright, left20, 21
forkstraight, right, left22–24
merge"", right, left25, 37, 38
roundabout""26
roundabout_exit""27
ferry""28
ferry_exit""29
transit""30–36
elevator""39
steps""40
escalator""41
building_enter""42
building_exit""43

An engine type outside this table produces an empty type and modifier.

Languages#

RequestedEngine localeInstructions
uz (default)en-USGenerated by the server in Latin Uzbek from the normalised manoeuvre, street names and sign data. The engine's English text is discarded.
ruru-RUEngine text.
enen-USEngine text.
anything else (including uz-Cyrl)en-USEngine text in English; the requested value is still echoed in language.

Examples#

Recommended route plus one alternative, Russian instructions:

bash
curl -H "X-API-Key: $KEY" \
  "https://uzmaps.ndc.uz/api/route?points=69.2401,41.2995|69.2870,41.3131&mode=car&lang=ru&alternatives=1"

Walking route with Uzbek instructions, via POST:

bash
curl -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"points":[[69.2401,41.2995],[69.2870,41.3131]],"mode":"foot","language":"uz"}' \
  https://uzmaps.ndc.uz/api/route

Shortest driving route avoiding tolls, with the car's current heading (POST-only fields):

bash
curl -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"points":[[69.2401,41.2995],[69.2870,41.3131]],"mode":"car","shortest":true,"avoid_tolls":true,"heading":90,"alternatives":2}' \
  https://uzmaps.ndc.uz/api/route

/api/matrix#

Returns distance and duration for every source–target pair in one call. A matrix is costed with the same avoidance options as a route (avoid_tolls, avoid_highways, shortest), so that the target you pick from a matrix is the one the route you draw afterwards actually goes to (routing/matrix.go, MatrixRequest comment). The one difference is use_ferry, which the route adapter sends and the matrix adapter does not.

Request#

FieldGETPOSTTypeNotes
SourcessourcessourcesGET: lon,lat|lon,lat; POST: [[lon,lat], …]Required unless points is given.
TargetstargetstargetssameRequired unless points is given.
PointspointspointssameSymmetric shorthand: every point to every point. When non-empty it replaces sources and targets.
ModemodemodestringDefault car.
Avoid tollsavoid_tolls=1avoid_tollsGET: literal 1; POST: booleanMotor modes only.
Avoid highwaysavoid_highways=1avoid_highwaysGET: literal 1; POST: booleanMotor modes only.
Shortestshortest=1shortestGET: literal 1; POST: booleanMotor modes only. Unlike /api/route, this is accepted on GET.

There is no language parameter: a matrix has no instructions.

Size limit. sources × targets must not exceed 2500. The cap is on the product, not on either side, so 1×2500 and 50×50 both fit and 51×50 does not. A request over the cap is rejected with 400 before anything reaches the engine. The reason for a cap: the engine computes a matrix by expanding the graph from every source, so cost grows with the product, and an uncapped endpoint would let one key saturate the engine for everyone (routing/matrix.go, MaxMatrixPairs comment).

The handler allows 60 seconds because a full matrix is many graph expansions; the engine client's own 25-second timeout still applies.

Response#

json
{
  "mode": "car",
  "sources": 1,
  "targets": 2,
  "cells": [
    {"source": 0, "target": 0, "distance": 6094, "duration": 419},
    {"source": 0, "target": 1, "distance": 6695, "duration": 519}
  ],
  "engine": "valhalla",
  "rows": [
    [
      {"source": 0, "target": 0, "distance": 6094, "duration": 419},
      {"source": 0, "target": 1, "distance": 6695, "duration": 519}
    ]
  ]
}
FieldTypeNotes
modestringMode used, after the fallback to car.
sourcesintegerNumber of sources sent.
targetsintegerNumber of targets sent.
cellsarray of cellEvery pair, flat, in row-major order (all targets for source 0, then source 1, …).
rowsarray of array of cellThe same cells grouped by source: rows[i][j] is source i to target j.
enginestringAlways "valhalla".

Cell:

FieldTypeNotes
sourceintegerIndex into the sources you sent.
targetintegerIndex into the targets you sent.
distancenumber or nullMetres.
durationnumber or nullSeconds.

A null distance or duration means no path exists between that pair — an island, a gated area, a point the network cannot reach in the requested mode. It is a real answer, not an error: the rest of the matrix is returned as normal, because a single unreachable pair must not fail the whole request (routing/matrix.go, MatrixCell comment; TestMatrixKeepsUnreachablePairsAsNull in routing/matrix_test.go). The engine reports matrix distance in kilometres; the server multiplies by 1000 so this endpoint agrees with the rest of the API (TestMatrixConvertsKilometresToMetres).

Examples#

One source to two targets:

bash
curl -H "X-API-Key: $KEY" \
  "https://uzmaps.ndc.uz/api/matrix?sources=69.2401,41.2995&targets=69.2870,41.3131|69.22,41.33&mode=car"

Symmetric matrix from a browser (query-string key, because the X-API-Key header is not CORS-allowed):

js
const res = await fetch('https://uzmaps.ndc.uz/api/matrix?key=' + encodeURIComponent(KEY), {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    points: [[69.2401, 41.2995], [69.2870, 41.3131], [69.22, 41.33]],
    mode: 'car',
    avoid_tolls: true,
  }),
});
const matrix = await res.json();
const unreachable = matrix.cells.filter((c) => c.distance === null);

/api/isochrone#

Returns the area reachable from one point within one or more time or distance budgets, as a GeoJSON FeatureCollection with one feature per contour. It is raw GeoJSON rather than a bespoke schema because every mapping client can already draw it (routing/matrix.go, IsochroneResponse comment).

Request#

FieldGETPOSTTypeDefaultNotes
OriginpointpointGET: lon,lat (exactly one); POST: [lon, lat]requiredA point at exactly 0,0 is treated as missing.
ModemodemodestringcarNo avoidance flags; the mode is the only costing input.
MinutesminutesminutesGET: comma-separated numbers; POST: array of numberTime contours.
MetresmetresmetressameDistance contours.
PolygonspolygonspolygonsGET: any value but 0 is true; POST: booleanGET: true; POST: falseFilled areas rather than lines.
Denoisedenoisedenoisenumber (0–1) or nullengine defaultDrops small disconnected islands.
Generalizegeneralizegeneralizenumber or nullengine defaultSimplification tolerance in metres.

The two forms default polygons differently: GET defaults to polygons because a bare browser request wants something it can fill and hit-test, and lines are the specialised choice (matrix.go, handleIsochrone comment); POST defaults to false because that is the JSON zero value. Set it explicitly in POST bodies. Lines are cheaper and suit a heat-style overlay; polygons suit a "can we deliver here" hit test (routing/matrix.go, IsochroneRequest comment).

DepartAt exists in the adapter but is not populated by the handler, so a departure time cannot be set over HTTP.

Contour rules#

RuleStatus on violation
Give minutes or metres, not both.400 contours must be either minutes or metres, not both
Give at least one contour.400 contours required: minutes=5,10,15 or metres=1000,2000
At most 6 contours in total (MaxIsochroneIntervals).400 too many contours: N, limit is 6
Every value must be greater than 0.502 contour minutes must be positive / contour metres must be positive

The six-contour cap exists because each contour is a separate expansion of the graph, so it is the same protection the 2500-pair cap gives the matrix (routing/matrix.go, MaxIsochroneIntervals comment). The non-positive check is made in the adapter rather than the handler, which is why it surfaces as 502 rather than 400.

Distance contours are given in metres and converted to kilometres for the engine on the way in, and back to metres on the way out.

Response#

json
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {"contour": 5, "metric": "time", "fill": "#bf4040", "contour_minutes": 5},
      "geometry": {
        "type": "Polygon",
        "coordinates": [[[69.24, 41.30], [69.25, 41.30], [69.25, 41.31], [69.24, 41.30]]]
      }
    }
  ],
  "mode": "car",
  "engine": "valhalla"
}
FieldTypeNotes
typestringAlways "FeatureCollection".
featuresarray of GeoJSON FeatureOne per contour, passed through from the engine with geometry intact. Coordinates are [lon, lat].
modestringMode used, after the fallback to car.
enginestringAlways "valhalla".

Each feature's properties carries the engine's own metadata plus one field the server adds:

PropertyAdded byNotes
contourengineThe contour value in the engine's units: minutes for a time contour, kilometres for a distance contour.
metricengine"time" or "distance".
contour_minutesserverPresent on time contours. Equal to contour.
contour_metresserverPresent on distance contours. contour × 1000.
anything else (for example fill)enginePassed through untouched.

Read contour_minutes or contour_metres rather than contour, so you never need to know which unit the engine was given. The server decides which one to add from the feature's own metric property, so the label stays correct even if the engine reorders, merges or drops contours; only when a feature carries no recognisable metric does it fall back to whichever axis the request used. A time contour is never also labelled in metres.

If the engine returns no features, the request fails with 502 no reachable area found.

Examples#

Walking reach in 5, 10 and 15 minutes (GET, so polygons by default):

bash
curl -H "X-API-Key: $KEY" \
  "https://uzmaps.ndc.uz/api/isochrone?point=69.2401,41.2995&mode=foot&minutes=5,10,15"

Driving distance rings as lines, with small islands removed (POST, so polygons must be stated):

bash
curl -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"point":[69.2401,41.2995],"mode":"car","metres":[1000,2000,5000],"polygons":false,"denoise":0.5}' \
  https://uzmaps.ndc.uz/api/isochrone

Drawing the result with MapLibre GL, which accepts the response as a GeoJSON source directly:

js
const res = await fetch('https://uzmaps.ndc.uz/api/isochrone?key=' + encodeURIComponent(KEY), {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ point: [69.2401, 41.2995], mode: 'car', minutes: [10, 20], polygons: true }),
});
const isochrone = await res.json();
map.addSource('reach', { type: 'geojson', data: isochrone });
map.addLayer({
  id: 'reach-fill',
  type: 'fill',
  source: 'reach',
  paint: { 'fill-color': ['get', 'fill'], 'fill-opacity': 0.25 },
});

Errors#

Every error is {"error": "<message>"}, except key errors, which the gate writes as {"error", "code", "message", "docs"}.

StatusMessageEndpointCause
400bad jsonallPOST body could not be decoded.
400at least two points required (points=lon,lat|lon,lat)routeFewer than two parseable points.
400sources and targets required (sources=lon,lat|lon,lat&targets=..., or points=... for a symmetric matrix)matrixAn empty side.
400matrix too large: N pairs, limit is 2500matrixsources × targets over the cap.
400point required (point=lon,lat)isochroneMissing or unparseable origin, more than one point on GET, or a point at exactly 0,0.
400contours required: …, contours must be either minutes or metres, not both, too many contours: N, limit is 6isochroneSee Contour rules.
401 / 403 / 429key errorsallMissing, unknown, disabled or origin-restricted key, per-minute rate limit exceeded, or monthly quota exhausted (the quota response also carries Retry-After).
502no route foundrouteThe engine returned no trip.
502no reachable area foundisochroneThe engine returned no features.
502contour minutes must be positive, contour metres must be positiveisochroneA zero or negative contour.
502routing engine unavailable: …allThe engine did not answer (including the 25-second client timeout).
502routing: …, matrix: …, isochrone: …allThe engine rejected the request; the text after the prefix is the engine's own message (for example No suitable edges near location), or HTTP <status> when it gave none.
503routing engine not configuredmatrix, isochroneGuard for a server with no routing client. The constructor always creates one, so this is not reachable through the normal start-up path.

Map data © OpenStreetMap contributors, licensed under the ODbL. © 2026 National Development Community.