Telemap Developer Docs
Geomate's own OSM/OSRM-based map platform for Nepal — routing, geocoding, and live traffic, self-hosted end to end, no third-party map vendor. This page covers everything built so far: how to call the API, how each feature works, and the JS SDK for embedding it in your own app.
/docs) for exploring requests directly against its own dev port — unauthenticated,
local-only. Real integration traffic goes through the gateway instead, at
http://localhost:8010, with an apikey header. Every
example below uses whichever path is actually correct for that call.
What's running
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-api reference →Geocoding live
Address search, typeahead, and reverse geocoding over OpenStreetMap plus Geomate's own corporate/landmark POIs — Nepal-only, English and Nepali.
geocoding-api 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 — built for Module C's location-triggered ad campaigns.
Geofencing guide →Gateway & billing live
API keys, per-consumer rate limiting, usage metering, usage-based billing, and a free sandbox tier — the customer-facing layer in front of everything above.
Gateway reference →JS SDK live
TelemapClient and TelemapMap — a zero-build
JavaScript wrapper for the gateway and a MapLibre map with the live traffic overlay built in.
| Service | Direct dev port | Gated behind gateway | Wraps |
|---|---|---|---|
| routing-api | http://localhost:8000 | /route, /route/stops, /eta, /match | OSRM (car/bus/motorcycle/truck graphs) |
| geocoding-api | http://localhost:8001 | /search, /reverse | Photon → Nominatim |
| live-tracking | http://localhost:8002 | /traffic/segments, /vehicles, /geofences, /geofences/events | routing-api's /match |
| gateway (Kong) | http://localhost:8010 — the actual customer-facing entrypoint | all three above | |
Not gated by the gateway on any service: fleet-side ingest
(POST /ingest — a GPS tracker isn't a "customer") and ops endpoints
(/health, /version, /alerts) —
those stay reachable only on the internal Docker network or direct host ports.
Getting started
Three things you need before making a real call: a base URL, an API key, and a vehicle profile (for routing calls). This section walks through all three.
-
Get an API key
There's no self-serve signup yet — keys are issued by running a script against a live stack:
./scripts/issue-api-key.py acme-corp # standard tier, 60 req/min ./scripts/issue-api-key.py acme-corp-trial --tier sandbox # free trial tier, 10 req/min, never billedThis appends a new consumer to
gateway/kong.ymland hot-reloads Kong's config — no restart needed. Just trying things out? Ask for a--tier sandboxkey. -
Call the gateway, not a direct service port
Every real request goes to
http://localhost:8010with your key in anapikeyheader (or query param):curl -H "apikey: YOUR_KEY" \ "http://localhost:8010/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 returns400from the underlying service — 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. Each is backed by its own OSRM graph with real vehicle dimensions (height/width/length/weight) checked against OSM tags — a bus and a car can legitimately get routed onto different roads, not just scored with different ETAs on the same route. -
Explore interactively (optional, dev-only)
Each backend service serves its own Swagger UI directly, unauthenticated, for poking at requests without writing any code:
routing-api
localhost:8000/docs →geocoding-api
localhost:8001/docs →live-tracking
localhost:8002/docs →These talk straight to the backend, bypassing the gateway entirely — fine for local exploration, not the path a real integration should use.
API versioning
Every endpoint on this page is implicitly v1 — unprefixed paths, no version header. Check
which version a service is serving with GET /version on any of the three backends:
curl "http://localhost:8000/version"
# {"service": "routing-api", "api_version": "v1"}
| 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" \
"http://localhost:8010/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 OSRM's way-class
legal/design speeds, 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 OSRM's optimistic legal-speed duration:
curl -H "apikey: YOUR_KEY" \
"http://localhost:8010/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" \
"http://localhost:8010/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 "http://localhost:8010/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 — this is exactly what
live-tracking uses to build the traffic history behind /eta.
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" "http://localhost:8010/search?q=Thamel&limit=5"
curl -H "apikey: YOUR_KEY" "http://localhost:8010/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. Custom POIs aren't writable through this API yet — they're seeded in Postgres and synced into search via
a script, read-only from the API's point of view. /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 (or a simulator standing in for one) posts pings one at a time to
POST /ingest/{vehicle_id} — this endpoint is fleet-side, not gated by the
gateway, so it's called directly against live-tracking's own port:
curl -X POST "http://localhost:8002/ingest/bus-14?profile=bus" \
-H "Content-Type: application/json" \
-d '{"lat": 27.7000, "lon": 85.3200, "timestamp": 1786524200}'
Pings buffer per-vehicle in memory; a background loop flushes each vehicle's window to
/match every ~20 seconds once it has at least 2 pings, and persists every matched
road segment to Postgres. Check a vehicle's live status (buffer size, last flush outcome) through the gateway:
curl -H "apikey: YOUR_KEY" "http://localhost:8010/vehicles/bus-14"
Full reference: POST /ingest/{vehicle_id} · GET /vehicles.
Live traffic overlay
Recent matched road segments, as GeoJSON, straight off the persisted match history — this is what a map viewer polls to draw a color-coded live traffic layer:
curl -H "apikey: YOUR_KEY" "http://localhost:8010/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 reference map viewer and the JS SDK's TelemapMap use).
The JS SDK wraps this whole flow (polling + rendering) in one call. Full reference:
GET /traffic/segments.
Geofencing for ad triggers
Circular zones defined in Postgres — every ingested ping is checked against all of them (a real meter-radius check, not a flat-degrees approximation), and enter/exit transitions are logged. Built for Module C's location-triggered in-vehicle ad campaigns (e.g. "play this campaign when the bus is within 200m of the office").
curl -H "apikey: YOUR_KEY" "http://localhost:8010/geofences"
curl -H "apikey: YOUR_KEY" "http://localhost:8010/geofences/events"
enter event (play that zone's
campaign) — this service only detects and logs the transition. Zones themselves are read-only through the
API; define/edit them directly in Postgres. Events are a bounded in-memory log (last 200), not durable
history. Full reference: GET /geofences ·
GET /geofences/events.
Monitor pipeline health
GET /alerts flags three kinds of gap in the live-tracking pipeline: dead trackers
(a vehicle that's stopped sending pings), match errors (a vehicle whose most recent flush to routing-api
failed), and a stalled pipeline (nothing new persisted anywhere, independent of any one vehicle). It's an
ops endpoint — not gated by the gateway, and detection-only, no built-in alerting channel:
curl "http://localhost:8002/alerts"
Wire the non-zero exit code of scripts/traffic-gap-check.py into cron or a
monitoring system. Full reference: GET /alerts.
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" });
const results = await client.search("Thamel", { limit: 5 });
const telemap = new TelemapMap({ apiKey: "YOUR_KEY", container: "map", client });
telemap.traffic.setLiveEnabled(true); // polls trafficSegments() every 15s
No MapLibre import, no engine instance to configure — see the full SDK reference below,
or the working example at sdk/example/ in the repo.
API reference
The complete contract for every endpoint across the three backend services, plus the gateway
that sits in front of them. All three backends are FastAPI — interactive Swagger docs are auto-served at
/docs on each direct port. routing-api and
geocoding-api are stateless; live-tracking is not.
Gateway & auth
Source: gateway/kong.yml — Kong
in DB-less/declarative mode, port 8010 (proxy). This is the actual customer-facing
entrypoint; everything documented under "routing-api" / "geocoding-api" / "live-tracking" below describes each
service's own contract, reached in production through this gateway, not its direct port.
| 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 consumer, standard tier. 10 req/min on sandbox-tier keys. RateLimit-*/Retry-After headers on every response; 429 past the limit. |
| Issuing a key | ./scripts/issue-api-key.py <name> [--tier sandbox] — appends a consumer to kong.yml and hot-reloads Kong, no restart. |
| Usage metering | Every gated request logged as one JSON line (consumer, route, status, latency). scripts/usage-report.py aggregates it per consumer. |
| Billing | scripts/billing-report.py turns the usage log into a per-consumer bill — free monthly allowance, flat rate above it, sandbox never billed. |
| Sandbox tier | --tier sandbox on key issuance — free, 10 req/min, excluded from billing. Same mechanism as a standard key, just a per-consumer rate-limit override. |
| CORS | Wide open (*) — no browser-based customer auth flow to protect yet. |
| What's gated | /route, /route/stops, /eta, /match, /search, /reverse, /traffic/segments, /vehicles, /geofences, /geofences/events. |
| What's not gated | POST /ingest (fleet-side), /health, /version, /alerts (ops) — internal Docker network / direct host ports only. |
routing-api
Source: services/routing/. Base URL (direct): http://localhost:8000.
One vehicle profile per request — car, bus,
motorcycle, or truck — each backed by its own OSRM
graph with real vehicle dimensions checked against OSM tags.
GET/routegated
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 "http://localhost:8000/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. 502 OSRM backend unreachable.
GET/route/stopsgated
Route through 2–25 points, visited in the given order — not stop-order optimization.
| 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 "http://localhost:8000/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. 502 backend unreachable.
GET/etagated
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 "http://localhost:8000/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's OSM node-pair microsegments 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/matchgated
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 "http://localhost:8000/match?profile=car" -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 OSM node-to-node 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. This is what live-tracking uses to build
traffic history.
Errors: 400 unknown profile, fewer than 2 pings, or non-increasing timestamps. 404 no match found. 502 backend unreachable.
GET/healthungated
Liveness check → {"status": "ok"}. Same shape on all three backends.
GET/versionungated
→ {"service": "routing-api", "api_version": "v1"}. See API versioning.
geocoding-api
Source: services/geocoding/. Base URL (direct): http://localhost:8001.
Wraps Photon (which sits on Nominatim). Every query is logged for a weekly relevance-tuning report — nothing a
caller needs to do.
GET/searchgated
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 "http://localhost:8001/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. Custom POIs aren't writable through this API —
added to Postgres and synced in separately; this is search/read only.
Errors: 422 empty q or limit out of range. 502 Photon unreachable.
GET/reversegated
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 "http://localhost:8001/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. 502 Photon unreachable.
GET/healthungated
Liveness check.
GET/versionungated
See API versioning.
live-tracking
Source: services/live-tracking/. Base URL (direct): http://localhost:8002.
Different shape from the two above: this ingests a continuous stream of GPS pings per vehicle, buffers
them in memory, and periodically hands each vehicle's window to routing-api's POST /match.
No real GeoMate fleet feed is wired up yet — a simulator stands in.
POST/ingest/{vehicle_id}ungated · fleet-side
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 "http://localhost:8002/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 — a background loop flushes each vehicle's buffer to
/match every 20s (default), once it has at least 2 pings. Check
/vehicles/{id} for last-flush visibility.
GET/vehicles /vehicles/{vehicle_id}gated
Current state for one or all tracked vehicles — buffer size and last flush outcome. Debug/demo visibility, not a stable data API.
curl "http://localhost:8002/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/segmentsgated
Recent matched road segments as GeoJSON — the map viewer's live traffic overlay polls this. Capped at 5000 features.
| Param | Type | Required | Notes |
|---|---|---|---|
minutes | int | no | how far back to look, 1–60, default 5 |
curl "http://localhost:8002/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/alertsungated · ops
Pipeline-gap detection: dead trackers, match errors, and a stalled pipeline. Detection only, no alerting channel.
curl "http://localhost:8002/alerts"
{
"checked_at": 1786552424.08, "gap_threshold_seconds": 90.0,
"dead_trackers": [{"vehicle_id": "sim-car-1", "last_ingest_at": 1786552330.1, "seconds_since_ingest": 93.9}],
"match_errors": [],
"pipeline": {"last_observation_at": 1786552409.9, "seconds_since_last_observation": 14.2, "stale": false},
"healthy": false
}
GET/geofencesgated
Configured geofence zones, for Module C's location-targeted ad triggers. Read-only — define/edit rows directly in Postgres.
curl "http://localhost:8002/geofences"
[{"id": 1, "name": "GeoMate Head Office", "lon": 85.3157, "lat": 27.6997, "radius_m": 200.0, "metadata": {"ad_campaign": "hq-local-diner-promo"}}]
GET/geofences/eventsgated
Recent zone enter/exit transitions, most recent first.
| Param | Type | Required | Notes |
|---|---|---|---|
limit | int | no | 1–200, default 50 |
curl "http://localhost:8002/geofences/events"
[{"ts": 1786554678.1, "vehicle_id": "ad-geofence-demo", "zone_id": 1, "zone_name": "GeoMate Head Office", "event": "exit"}]
Bounded to the last 200 events in memory — a live feed, not persisted history.
GET/healthungated
Liveness check.
GET/versionungated
See API versioning.
JS SDK
Source: the sibling telemap-sdk repo, not this one. TypeScript, built with esbuild into two bundles — an ESM build
for your own bundler and a self-contained browser IIFE build. The underlying map engine (MapLibre GL
JS today) is never exposed — nothing in the public API accepts, returns, or requires a MapLibre type
or instance; every argument/return value is a named type under the Telemap
namespace (Telemap.Pin, Telemap.Route.Plan,
Telemap.Vehicle.Position, …), never a plain object. That's deliberate: it means
the engine can be swapped later without breaking anything built against this SDK.
TelemapClient
A thin, fully-typed fetch wrapper over the gateway contract.
import { TelemapClient, type Telemap } from "@telemap/sdk";
const client = new TelemapClient({
apiKey: "YOUR_KEY",
baseUrl: "http://localhost:8010", // default
});
| 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 |
whoami | Telemap.Whoami | GET /whoami |
addPoi / listPois / getPoi / deletePoi | Telemap.Poi.Item | POI write API — public by default, visibility: "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, so the TypeScript-facing surface reads like a real SDK, not a JSON pass-through.
TelemapMap
A map pre-configured with Telemap's own style, plus telemap.pins (markers — no
MapLibre 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"
});
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 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 (path 1) — 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 (path 2) — pings ingested elsewhere via
// client.ingest()/POST /ingest, 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) — the same live overlay verified in the reference map viewer, just re-platformed behind the
adapter. telemap.routes.draw() is id-keyed like pins (a dispatch-style caller can
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 paths in the vehicle-tracking model — 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 engine-agnostic
logic in the SDK itself, not the map engine. telemap.geofences.show() renders
every zone from GET /geofences as one fixed overlay (real geodesic circles, not
a zoom-dependent pixel radius) — global read-only data, so it's a single toggle like
telemap.traffic, not id-keyed like pins/routes/vehicles.
telemap.geofences.onEvent() is a delivery hook, not a rendering feature: it polls
geofenceEvents() and calls your handler for genuinely new enter/exit transitions
(the first poll seeds a baseline rather than replaying history). All five modules named in the original SDK
v2 plan are now built.
Plain <script>, zero build step: <script src="telemap-sdk.iife.js"></script>
is the only tag needed — no separate MapLibre script or stylesheet, both are inlined and injected
automatically. Exposes a global Telemap object (const { TelemapClient, TelemapMap } = Telemap;).
TelemapMap works today against the local dev stack
only, not something a real external app could point at from elsewhere yet.
Full working example: sdk/example/index.html — search flies the map to a
result, the traffic toggle renders live segments, a pin toggle demonstrates telemap.pins,
two vehicle toggles demonstrate both tracking paths side by side, and a geofence toggle renders the real
zones already configured in the stack with a live enter/exit event log.
Known limitations
Real gaps, documented rather than hidden — what this platform can't do yet.
| Area | Gap |
|---|---|
| Onboarding | No self-serve API key signup UI — keys are issued by running a script against a live stack. |
| Billing | No live Stripe integration — billing-report.py computes exactly what a real metered-billing call would need and stops there. |
| Map assets | Tiles/style/glyphs aren't behind the gateway or a public CDN; fonts/sprites are pulled from public third-party hosts, not self-hosted. |
| Routing | No pedestrian/walking profile. No alternative or connecting routes (no GTFS data). |
| Multi-stop | /route/stops is not a VRP solver — no stop-order optimization. |
| Live data | Poll-only — no WebSocket/MQTT/GTFS-Realtime push feed. No real GeoMate fleet feed wired up yet (a simulator stands in). |
| Geofence events | Bounded in-memory log (last 200), not durable Proof-of-Play-style analytics storage. |
| Custom POIs | No PATCH /pois/{id} yet (create/read/delete only); /reverse doesn't merge private POIs the way /search does. |
| Dogfooding | No real production traffic or paid third-party (Google Maps/Mapbox) account exists yet to benchmark accuracy against — genuinely blocked on a real module launching, not a missing feature. |
Build status by leg
| Leg | Focus | Status |
|---|---|---|
| 01 | Local dev environment, PostGIS, Nepal OSM import | Done |
| 02 | Custom vector tiles & cartography | Done |
| 03 | OSRM routing, vehicle profiles, map-matching, ETA, load test | Done |
| 04 | Geocoding — Nominatim, Photon, synonyms, custom POIs, relevance loop | Done |
| 05 | Live GPS ingest, traffic history model, live overlay, gap alerting | Done |
| 06 | Module A/B/C integration reference clients, geofencing, versioning | Done — dogfooding period genuinely blocked on a real module launch |
| 07 | API gateway, auth, billing, developer docs, JS SDK, sandbox tier | Done |
See MAP_ENGINE_ROADMAP.md and each
leg's section in README.md for the full detail behind each line.