Telemap Docs

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.

One base URL for everything. Every request — routing, search, live traffic, the SDK — goes to 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.

SDK reference →

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.

  1. 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.

  2. Call the API

    Every request goes to https://map.geomate.com.np with your key in an apikey header (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/invalid profile, malformed coordinates, or a point too far from any mapped road returns 400 — see each endpoint's own error list below.

  3. Watch your rate limit

    Standard keys get 60 requests/minute; sandbox keys get 10/minute. Every response carries RateLimit-* and Retry-After headers, and going over the limit returns 429.

  4. Pick a vehicle profile

    Every routing call needs a profile: car, bus, motorcycle, or truck (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 bumpBreaking — 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}
Heads up: 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
}
Not a route optimizer. This is plain multi-waypoint routing — stops are visited in the given order, not optimized for shortest total time. Full reference: GET /route/stops.

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.

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"
Zones themselves are read-only through the API today — contact your Geomate representative to have zones added or changed. Events are a bounded recent-history log, not a durable archive. Full reference: GET /geofences · GET /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.

ConcernDetail
AuthEvery gated request needs an apikey header (or query param). No key or a bad one → 401.
Rate limit60 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 keyNo self-serve signup yet — contact your Geomate representative. Ask for a sandbox-tier key to try things out first.
Usage & billingUsage is metered per key and billed on a usage basis above a free monthly allowance; sandbox-tier keys are never billed.
CORSEnabled 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'tPOST /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.

ParamTypeRequiredNotes
profilestringyescar / bus / motorcycle / truck
fromstringyesorigin, "lon,lat"
tostringyesdestination, "lon,lat"
geometryboolnoinclude route polyline as GeoJSON (default false)
Example
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.

ParamTypeRequiredNotes
profilestringyesone of the four profiles
waypointsstringyes2–25 "lon,lat" stops, ;-separated, in visit order
geometryboolnoinclude route polyline as GeoJSON (default false)
Example
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.

ParamTypeRequiredNotes
profilestringyesone of the four profiles
fromstringyesorigin, "lon,lat"
tostringyesdestination, "lon,lat"
Example
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).

ParamTypeRequiredNotes
profilestring (query)yesone of the four profiles
geometrybool (query)nomatched-segment polylines as GeoJSON (default false)
annotationsbool (query)noper-road-segment distance/duration/speed, timestamped (default false)
bodyJSON arrayyes≥2 pings, timestamps strictly increasing: {lon, lat, timestamp, accuracy_m?}
Example
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.

ParamTypeRequiredNotes
qstringyesfree-text query
limitintno1–50, default 10
langstringnoen or ne, default en
Example
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.

ParamTypeRequiredNotes
latfloatyes-90 to 90
lonfloatyes-180 to 180
langstringnodefault en
Example
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.

ParamTypeRequiredNotes
vehicle_idstring (path)yesany identifier; tracked automatically on first ping
profilestring (query)noone of the four profiles, default car
bodyJSON objectyes{lat, lon, timestamp, accuracy_m?}
Example
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.

Example
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.

ParamTypeRequiredNotes
minutesintnohow far back to look, 1–60, default 5
Example
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.

Example
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.

ParamTypeRequiredNotes
limitintno1–200, default 50
Example
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",
});
MethodReturnsWraps
searchTelemap.Search.ResponseGET /search
reverseTelemap.Reverse.ResponseGET /reverse
routeTelemap.Route.PlanGET /route
routeStopsTelemap.Route.MultiStopPlanGET /route/stops
etaTelemap.EtaGET /eta
trafficSegmentsTelemap.Traffic.Segment[]GET /traffic/segments
vehicles / vehicleTelemap.Vehicle.StateGET /vehicles
geofences / geofenceEventsTelemap.Geofence.Zone[] / Event[]GET /geofences
ingestvoidPOST /ingest
addPoi / listPois / getPoi / deletePoiTelemap.Poi.Itemcustom 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.