Telemap Developer Docs
Geomate's map platform for Nepal — routing, geocoding, and live traffic, built directly on OpenStreetMap data rather than a third-party map vendor. This page covers the full API, how each feature works, and the JS SDK for embedding a map in your own app.
https://map.geomate.com.np with an apikey
header. There's no separate host per feature.
What's available
Routing live
Route planning, realistic ETAs, multi-stop routes, and GPS map-matching across four vehicle profiles (car / bus / motorcycle / truck), each with real vehicle-dimension constraints.
Routing reference →Geocoding live
Address search, typeahead, and reverse geocoding over OpenStreetMap plus Geomate's own corporate and landmark POIs — Nepal-only, English and Nepali.
Geocoding reference →Live traffic live
Continuous GPS ingest, map-matching into road-segment speeds, a historical speed model, and a live traffic overlay on the map.
Live tracking reference →Geofencing live
Real circular geofence zones checked against every GPS ping, with enter/exit events — for location-triggered campaigns or alerts.
Geofencing guide →JS SDK live
TelemapClient and TelemapMap — a JavaScript
SDK for the API and an embeddable map with the live traffic overlay built in.
Getting started
Three things you need before making a real call: an API key, the base URL, and a vehicle profile (for routing calls). This section walks through all three.
-
Get an API key
There's no self-serve signup yet — contact your Geomate representative to request an API key. Ask for a free sandbox-tier key if you just want to try things out before committing to a plan.
-
Call the API
Every request goes to
https://map.geomate.com.npwith your key in anapikeyheader (or query param):curl -H "apikey: YOUR_KEY" \ "https://map.geomate.com.np/route?profile=car&from=85.3240,27.6710&to=85.4298,27.6710"No key or a bad one returns
401. A missing/invalidprofile, malformed coordinates, or a point too far from any mapped road returns400— see each endpoint's own error list below. -
Watch your rate limit
Standard keys get 60 requests/minute; sandbox keys get 10/minute. Every response carries
RateLimit-*andRetry-Afterheaders, and going over the limit returns429. -
Pick a vehicle profile
Every routing call needs a
profile:car,bus,motorcycle, ortruck(no pedestrian/walking profile yet). Each is backed by its own routing graph with real vehicle dimensions (height/width/length/weight) checked against road data — a bus and a car can legitimately get routed onto different roads, not just scored with different ETAs on the same route.
API versioning
Every endpoint on this page is implicitly v1 — unprefixed paths, no version header to set.
| Backward-compatible — no bump | Breaking — needs a version bump |
|---|---|
|
A new endpoint. A new optional query param. A new optional response field. A field appended to a response, existing fields untouched. |
Removing or renaming a response field or endpoint. Changing a field's type or meaning. Tightening validation so a previously-valid request starts failing. Changing an existing param's default behavior. |
When a breaking change is actually needed, it lands as a new path prefix (e.g. /v2/route)
introduced alongside the existing v1 endpoint, which stays live through a deprecation window — never a
retroactive rename or an abrupt cutoff.
Guides
Task-oriented walkthroughs for each feature. For the exhaustive param-by-param contract, see the API reference below — these guides link into it.
Plan a route
GET /route plans between exactly two points for one vehicle profile:
curl -H "apikey: YOUR_KEY" \
"https://map.geomate.com.np/route?profile=car&from=85.3240,27.6710&to=85.4298,27.6710"
{"profile": "car", "distance_km": 11.906, "duration_min": 12.57, "geometry": null}
duration_min comes straight from legal/design speeds
for each road class, which run well above real mixed-traffic travel time. Pass geometry=true
to get the route polyline back as GeoJSON for drawing on a map. For a realistic time estimate, use
/eta instead. Full reference:
GET /route.
Get a realistic ETA
GET /eta takes the same inputs as /route but blends
real historical speeds (where the road segment has been driven before) with a static per-profile average as
fallback — instead of an optimistic legal-speed duration:
curl -H "apikey: YOUR_KEY" \
"https://map.geomate.com.np/eta?profile=bus&from=85.3240,27.6710&to=85.4298,27.6710"
{
"profile": "bus", "distance_km": 11.938, "osrm_duration_min": 14.52,
"eta_min": 43.03, "static_eta_min": 44.77,
"average_speed_kmh": 16.0, "historical_coverage_pct": 5.9
}
historical_coverage_pct tells you how much of the route's distance was actually
backed by real driven history (vs. the static fallback) — a route with zero coverage returns
eta_min == static_eta_min exactly. All three numbers (eta_min,
static_eta_min, osrm_duration_min) are returned together
so you can see how far apart they are. Full reference: GET /eta.
Multi-stop routes
For a route through more than two points — an office shuttle's pickup list, a freight pickup-then-delivery
run — use GET /route/stops with 2–25 waypoints, visited in the order you
give them:
curl -H "apikey: YOUR_KEY" \
"https://map.geomate.com.np/route/stops?profile=bus&waypoints=85.3157,27.6997;85.2803,27.6939;85.3127,27.7167"
{
"profile": "bus", "stop_count": 3, "distance_km": 15.916, "duration_min": 20.07,
"legs": [
{"from_index": 0, "to_index": 1, "distance_km": 9.1, "duration_min": 11.59},
{"from_index": 1, "to_index": 2, "distance_km": 6.816, "duration_min": 8.48}
],
"geometry": null
}
Map-match a GPS trace
POST /match snaps a sequence of noisy GPS pings onto the road network — the
reverse problem from /route: given where a vehicle actually was, figure out which
roads it drove.
curl -X POST "https://map.geomate.com.np/match?profile=car" \
-H "apikey: YOUR_KEY" -H "Content-Type: application/json" -d '[
{"lon": 85.3240, "lat": 27.6710, "timestamp": 1700000000},
{"lon": 85.3300, "lat": 27.6720, "timestamp": 1700000015},
{"lon": 85.3360, "lat": 27.6730, "timestamp": 1700000030}
]'
A ping too far from any mapped road comes back "matched": false instead of
forcing a bad snap. A trace can split into multiple matchings (e.g. across a large
time gap), each with its own confidence score. Pass annotations=true to also get
per-road-segment distance/duration/speed, timestamped by interpolation. Full reference:
POST /match.
Search & geocode
Forward search (free text → places) and reverse geocoding (coordinate → nearest place), over OpenStreetMap plus Geomate's own POIs:
curl -H "apikey: YOUR_KEY" "https://map.geomate.com.np/search?q=Thamel&limit=5"
curl -H "apikey: YOUR_KEY" "https://map.geomate.com.np/reverse?lat=27.7172&lon=85.3240"
Every result carries source: "osm" or "custom" —
that's how you tell a Geomate-added POI (corporate offices, depots, landmarks not in OSM) apart from plain OSM
data. /reverse returns result: null (not a 404) when
nothing is found nearby — a normal outcome, not an error. Full reference:
GET /search · GET /reverse.
Live vehicle tracking
A GPS tracker (your own device or backend) posts pings one at a time to
POST /ingest/{vehicle_id}. Unlike every other endpoint on this page, ingest doesn't
require an apikey header:
curl -X POST "https://map.geomate.com.np/ingest/bus-14?profile=bus" \
-H "Content-Type: application/json" \
-d '{"lat": 27.7000, "lon": 85.3200, "timestamp": 1786524200}'
Pings buffer per-vehicle for a short window, then get map-matched onto the road network automatically and folded into that vehicle's live position and the traffic model. Check a vehicle's live status (buffer size, last match outcome) with:
curl -H "apikey: YOUR_KEY" "https://map.geomate.com.np/vehicles/bus-14"
Full reference: POST /ingest/{vehicle_id} · GET /vehicles.
Live traffic overlay
Recent matched road segments, as GeoJSON, straight off the live-tracking history — this is what a map polls to draw a color-coded live traffic layer:
curl -H "apikey: YOUR_KEY" "https://map.geomate.com.np/traffic/segments?minutes=5"
Each feature carries speed_kmh — color it red under 15 km/h, orange 15–30, green
above 30 (the same bands the JS SDK's TelemapMap uses). The
JS SDK wraps this whole flow (polling + rendering) in one call. Full reference:
GET /traffic/segments.
Geofencing for location triggers
Circular zones checked against every ingested ping (a real meter-radius check, not a flat-degrees approximation) — enter/exit transitions are logged, so you can trigger something (an ad, an alert, a notification) the moment a tracked vehicle crosses into or out of a zone.
curl -H "apikey: YOUR_KEY" "https://map.geomate.com.np/geofences"
curl -H "apikey: YOUR_KEY" "https://map.geomate.com.np/geofences/events"
Embed the map (JS SDK)
The fastest path to a working map with search, routing, and live traffic in your own app:
import { TelemapClient, TelemapMap } from "@telemap/sdk";
const client = new TelemapClient({ apiKey: "YOUR_KEY", baseUrl: "https://map.geomate.com.np" });
const results = await client.search("Thamel", { limit: 5 });
const telemap = new TelemapMap({
apiKey: "YOUR_KEY",
container: "map",
client,
styleBaseUrl: "https://map.geomate.com.np",
});
telemap.traffic.setLiveEnabled(true); // polls trafficSegments() every 15s
No map-engine import, no engine instance to configure — see the full SDK reference below, or the live demo for a working page with search and live traffic already wired up.
API reference
The complete contract for every endpoint. routing and
geocoding calls are stateless; live tracking calls
are not.
Authentication & limits
Base URL: https://map.geomate.com.np — this is the only host you should ever
call.
| Concern | Detail |
|---|---|
| Auth | Every gated request needs an apikey header (or query param). No key or a bad one → 401. |
| Rate limit | 60 req/min per key, standard tier. 10 req/min on sandbox-tier keys. RateLimit-*/Retry-After headers on every response; 429 past the limit. |
| Getting a key | No self-serve signup yet — contact your Geomate representative. Ask for a sandbox-tier key to try things out first. |
| Usage & billing | Usage is metered per key and billed on a usage basis above a free monthly allowance; sandbox-tier keys are never billed. |
| CORS | Enabled for all origins, so the JS SDK works directly from a browser without a server-side proxy. |
| What needs a key | /route, /route/stops, /eta, /match, /search, /reverse, /traffic/segments, /vehicles, /geofences, /geofences/events. |
| What doesn't | POST /ingest — GPS pings come from your own tracker/backend, not a browser session with a key on hand. |
Routing
One vehicle profile per request — car, bus,
motorcycle, or truck — each backed by its own routing
graph with real vehicle dimensions checked against road data.
GET/route
Plan a route between two points.
| Param | Type | Required | Notes |
|---|---|---|---|
profile | string | yes | car / bus / motorcycle / truck |
from | string | yes | origin, "lon,lat" |
to | string | yes | destination, "lon,lat" |
geometry | bool | no | include route polyline as GeoJSON (default false) |
curl -H "apikey: YOUR_KEY" "https://map.geomate.com.np/route?profile=car&from=85.3240,27.6710&to=85.4298,27.6710"
{"profile": "car", "distance_km": 11.906, "duration_min": 12.57, "geometry": null}
Errors: 400 unknown profile, malformed/out-of-range coordinate, or a coordinate >20km from the nearest known road. 404 no route found.
GET/route/stops
Route through 2–25 points, visited in the given order.
| Param | Type | Required | Notes |
|---|---|---|---|
profile | string | yes | one of the four profiles |
waypoints | string | yes | 2–25 "lon,lat" stops, ;-separated, in visit order |
geometry | bool | no | include route polyline as GeoJSON (default false) |
curl -H "apikey: YOUR_KEY" "https://map.geomate.com.np/route/stops?profile=bus&waypoints=85.3157,27.6997;85.2803,27.6939;85.3127,27.7167"
{
"profile": "bus", "stop_count": 3, "distance_km": 15.916, "duration_min": 20.07,
"legs": [
{"from_index": 0, "to_index": 1, "distance_km": 9.1, "duration_min": 11.59},
{"from_index": 1, "to_index": 2, "distance_km": 6.816, "duration_min": 8.48}
],
"geometry": null
}
legs[i] is the hop from waypoints[from_index] to
waypoints[to_index]; top-level distance_km/duration_min
sum across all legs.
Errors: 400 unknown profile, fewer than 2 or more than 25 waypoints, a malformed waypoint, or any waypoint >20km from a known road. 404 no route found.
GET/eta
Same inputs as /route (minus geometry) — returns a travel-time estimate blending real historical speeds with a static fallback.
| Param | Type | Required | Notes |
|---|---|---|---|
profile | string | yes | one of the four profiles |
from | string | yes | origin, "lon,lat" |
to | string | yes | destination, "lon,lat" |
curl -H "apikey: YOUR_KEY" "https://map.geomate.com.np/eta?profile=bus&from=85.3240,27.6710&to=85.4298,27.6710"
{
"profile": "bus", "distance_km": 11.938, "osrm_duration_min": 14.52,
"eta_min": 43.03, "static_eta_min": 44.77,
"average_speed_kmh": 16.0, "historical_coverage_pct": 5.9
}
eta_min walks the route in small segments and, per segment, uses a real
historical speed where one exists, the static per-profile average (car 22,
bus 16, motorcycle 26, truck
18 km/h) otherwise. historical_coverage_pct (0–100) is the share of route
distance actually backed by real history.
Errors: same as /route.
POST/match
Snap a sequence of live GPS pings onto the road network (map-matching).
| Param | Type | Required | Notes |
|---|---|---|---|
profile | string (query) | yes | one of the four profiles |
geometry | bool (query) | no | matched-segment polylines as GeoJSON (default false) |
annotations | bool (query) | no | per-road-segment distance/duration/speed, timestamped (default false) |
| body | JSON array | yes | ≥2 pings, timestamps strictly increasing: {lon, lat, timestamp, accuracy_m?} |
curl -X POST "https://map.geomate.com.np/match?profile=car" -H "apikey: YOUR_KEY" -H "Content-Type: application/json" -d '[
{"lon": 85.3240, "lat": 27.6710, "timestamp": 1700000000},
{"lon": 85.3300, "lat": 27.6720, "timestamp": 1700000015},
{"lon": 85.3360, "lat": 27.6730, "timestamp": 1700000030}
]'
{
"profile": "car",
"points": [
{"input_index": 0, "matched": true, "lon": 85.324206, "lat": 27.670902, "matching_index": 0},
{"input_index": 1, "matched": true, "lon": 85.329716, "lat": 27.672237, "matching_index": 0},
{"input_index": 2, "matched": true, "lon": 85.335616, "lat": 27.673113, "matching_index": 0}
],
"matchings": [
{"index": 0, "confidence": 0.62, "distance_km": 1.67, "duration_min": 3.56, "geometry": null, "segments": null}
]
}
A ping too far from any mapped road comes back "matched": false". A trace can
split into multiple matchings across a large time gap, each with its own
confidence (0–1). With annotations=true, each matching also gets a
segments list, one entry per road microsegment:
{"from_node": 1832396853, "to_node": 1496663340, "distance_m": 44.11, "duration_s": 2.9, "speed_kmh": 54.72, "observed_at": 1786524870.4}
observed_at is interpolated by apportioning the timespan between bounding
pings across that leg's microsegments.
Errors: 400 unknown profile, fewer than 2 pings, or non-increasing timestamps. 404 no match found.
Geocoding
Forward and reverse geocoding over OpenStreetMap plus Geomate's own corporate/landmark POIs.
GET/search
Forward geocoding / autocomplete.
| Param | Type | Required | Notes |
|---|---|---|---|
q | string | yes | free-text query |
limit | int | no | 1–50, default 10 |
lang | string | no | en or ne, default en |
curl -H "apikey: YOUR_KEY" "https://map.geomate.com.np/search?q=Thamel&limit=2"
{
"query": "Thamel",
"results": [
{
"name": "Thamel", "lat": 27.7166578, "lon": 85.3127015,
"osm_type": "place", "osm_value": "neighbourhood", "place_type": "locality",
"source": "osm",
"address": {"city": "Kathmandu", "state": "Bagmati Province", "country": "Nepal", "countrycode": "NP"}
}
]
}
source is "osm" for OpenStreetMap data or
"custom" for a Geomate-added POI.
Errors: 422 empty q or limit out of range.
GET/reverse
Coordinate → nearest place.
| Param | Type | Required | Notes |
|---|---|---|---|
lat | float | yes | -90 to 90 |
lon | float | yes | -180 to 180 |
lang | string | no | default en |
curl -H "apikey: YOUR_KEY" "https://map.geomate.com.np/reverse?lat=27.7172&lon=85.3240"
{
"lat": 27.7172, "lon": 85.324,
"result": {
"name": "Nano Hana Garden &Thakali Chulo", "lat": 27.7176566, "lon": 85.3242651,
"osm_type": "amenity", "osm_value": "restaurant", "place_type": "house",
"source": "osm", "address": {"city": "Kathmandu", "country": "Nepal"}
}
}
result is null (not a 404) if nothing is found near the coordinate — a normal outcome, not an error.
Errors: 422 lat/lon out of range.
Live tracking
A GPS tracker (or your own backend standing in for one) posts a continuous stream of pings per vehicle, which get buffered briefly and periodically map-matched onto the road network.
POST/ingest/{vehicle_id}no key required
Record one GPS ping for a vehicle.
| Param | Type | Required | Notes |
|---|---|---|---|
vehicle_id | string (path) | yes | any identifier; tracked automatically on first ping |
profile | string (query) | no | one of the four profiles, default car |
| body | JSON object | yes | {lat, lon, timestamp, accuracy_m?} |
curl -X POST "https://map.geomate.com.np/ingest/demo-car-1?profile=car" \
-H "Content-Type: application/json" \
-d '{"lat": 27.7000, "lon": 85.3200, "timestamp": 1786524200}'
{"vehicle_id": "demo-car-1", "buffered_pings": 1}
Pings aren't matched synchronously — they're matched onto the road network shortly after arrival, once a
vehicle has at least 2 buffered pings. Check /vehicles/{id} for last-match
visibility.
GET/vehicles /vehicles/{vehicle_id}
Current state for one or all of your tracked vehicles — buffer size and last match outcome.
curl -H "apikey: YOUR_KEY" "https://map.geomate.com.np/vehicles/demo-car-1"
{"profile": "car", "buffered_pings": 1, "last_flush_at": null, "last_match_confidence": null, "last_match_error": null, "current_geofences": []}
current_geofences is the list of geofence zone names this vehicle's most recent ping fell inside.
Errors: 404 from /vehicles/{vehicle_id} if that vehicle has never sent a ping.
GET/traffic/segments
Recent matched road segments as GeoJSON — what a live traffic overlay polls. Capped at 5000 features.
| Param | Type | Required | Notes |
|---|---|---|---|
minutes | int | no | how far back to look, 1–60, default 5 |
curl -H "apikey: YOUR_KEY" "https://map.geomate.com.np/traffic/segments?minutes=5"
{
"type": "FeatureCollection",
"features": [{
"type": "Feature",
"geometry": {"type": "LineString", "coordinates": [[85.400175, 27.674118], [85.400201, 27.674111]]},
"properties": {"speed_kmh": 24.84, "profile": "car", "vehicle_id": "sim-car-1", "observed_at": "2026-08-12T10:55:47.9+00:00"}
}]
}
GET/geofences
Configured geofence zones. Read-only through the API today.
curl -H "apikey: YOUR_KEY" "https://map.geomate.com.np/geofences"
[{"id": 1, "name": "Geomate Head Office", "lon": 85.3157, "lat": 27.6997, "radius_m": 200.0, "metadata": {}}]
GET/geofences/events
Recent zone enter/exit transitions, most recent first.
| Param | Type | Required | Notes |
|---|---|---|---|
limit | int | no | 1–200, default 50 |
curl -H "apikey: YOUR_KEY" "https://map.geomate.com.np/geofences/events"
[{"ts": 1786554678.1, "vehicle_id": "ad-geofence-demo", "zone_id": 1, "zone_name": "Geomate Head Office", "event": "exit"}]
Bounded to recent events — a live feed, not persisted history.
JS SDK
A TypeScript SDK, distributed as an ESM build for your own bundler or a self-contained browser
<script> build. The underlying map engine is never exposed —
nothing in the public API accepts, returns, or requires a map-engine-specific type or instance; every
argument/return value is a named Telemap.* type. That's deliberate: the engine
underneath can change later without breaking anything built against this SDK.
TelemapClient
A thin, fully-typed fetch wrapper over the API.
import { TelemapClient, type Telemap } from "@telemap/sdk";
const client = new TelemapClient({
apiKey: "YOUR_KEY",
baseUrl: "https://map.geomate.com.np",
});
| Method | Returns | Wraps |
|---|---|---|
search | Telemap.Search.Response | GET /search |
reverse | Telemap.Reverse.Response | GET /reverse |
route | Telemap.Route.Plan | GET /route |
routeStops | Telemap.Route.MultiStopPlan | GET /route/stops |
eta | Telemap.Eta | GET /eta |
trafficSegments | Telemap.Traffic.Segment[] | GET /traffic/segments |
vehicles / vehicle | Telemap.Vehicle.State | GET /vehicles |
geofences / geofenceEvents | Telemap.Geofence.Zone[] / Event[] | GET /geofences |
ingest | void | POST /ingest |
addPoi / listPois / getPoi / deletePoi | Telemap.Poi.Item | custom POI management — public by default, private needs an entitled plan |
Every call rejects with a TelemapApiError (carrying status
and body) on a non-2xx response. Field names are camelCase throughout
(distanceKm, not distance_km) — the client translates
the wire format for you.
TelemapMap
A map pre-configured with Telemap's own style, plus telemap.pins (markers — no
map-engine knowledge required), telemap.traffic (the live overlay),
telemap.routes (drawing a planned route's geometry),
telemap.vehicles (live tracking + animation), and
telemap.geofences (zone rendering + enter/exit events).
import { TelemapMap, type Telemap } from "@telemap/sdk";
const telemap = new TelemapMap({
apiKey: "YOUR_KEY",
container: "map", // element id or HTMLElement
center: [85.3240, 27.7172], // default: Kathmandu
zoom: 12,
theme: "light", // "light" | "dark"
styleBaseUrl: "https://map.geomate.com.np",
});
const pin: Telemap.Pin = { id: "hq", lon: 85.3157, lat: 27.6997, label: "Geomate HQ" };
telemap.pins.add(pin, { onClick: (p) => console.log("clicked", p.id) });
await telemap.traffic.setLiveEnabled(true, { minutes: 5, pollMs: 15000 });
// Needs geometry: true, or draw() throws a clear error instead of drawing nothing.
const client = new TelemapClient({ apiKey: "YOUR_KEY", baseUrl: "https://map.geomate.com.np" });
const plan = await client.route("car", [85.3157, 27.6997], [85.2803, 27.6939], { geometry: true });
telemap.routes.draw("hq-to-depot", plan); // fits the camera to it by default
// telemap.vehicles: bring-your-own-data — no Telemap backend involved,
// the SDK just renders it, animating between hops and rotating to face travel.
telemap.vehicles.update("bus-14", { lat: 27.6997, lon: 85.3157, timestamp: Date.now() / 1000 });
// telemap.vehicles: Telemap-managed — pings ingested via POST /ingest elsewhere,
// polled back and fed into the same update().
telemap.vehicles.track("bus-14", { pollMs: 3000 });
// telemap.geofences: zones are global, read-only data -- one fixed overlay of
// real geodesic circles, plus a delivery hook for enter/exit events.
await telemap.geofences.show();
telemap.geofences.onEvent((e) => console.log(e.event, e.vehicleId, e.zoneName));
// ... later
telemap.destroy();
telemap.traffic.setLiveEnabled polls trafficSegments()
on the given interval while enabled and color-codes each segment by speed (red <15, orange 15–30, green
>30 km/h). telemap.routes.draw() is id-keyed like pins (show more than one route
at once) and accepts the direct result of client.route() or
client.routeStops(). telemap.vehicles.update() is the
shared rendering primitive for both tracking paths — bring-your-own-data calls it directly with zero backend
involvement, and telemap.vehicles.track(vehicleId) (the Telemap-managed path) polls
client.vehicle() and feeds each result into the same
update(). Animation (position interpolation, heading) is handled by the SDK, not
something you write. telemap.geofences.show() renders every zone from
GET /geofences as one fixed overlay (real geodesic circles, not a zoom-dependent
pixel radius). telemap.geofences.onEvent() is a delivery hook, not a rendering
feature: it polls geofenceEvents() and calls your handler for genuinely new
enter/exit transitions.
Plain <script>, zero build step for the browser
build: <script src="telemap-sdk.iife.js"></script> is the only tag
needed. Exposes a global Telemap object
(const { TelemapClient, TelemapMap } = Telemap;).
See the live demo for a working page: search flies the map to a result, and the traffic toggle renders live segments.