KurdGeo Documentation
Everything you need to integrate KurdGeo's mapping and routing APIs into your app.
Quickstart
KurdGeo provides two services: a Maps SDK for embedding interactive maps, and a Routing API for turn-by-turn directions. Both are accessed through api.kurdgeo.space.
Sign up at /register to get a free trial with 1,000 requests per day.
To embed a map on your website, you need exactly 3 things:
- A container
<div>with a height - The SDK script tag (no auth needed to load the SDK itself)
KurdGeoMap.init()with your API key
<!-- 1. Container with explicit height -->
<div id="map" style="width: 100%; height: 500px;"></div>
<!-- 2. Load the SDK (no auth required, it's just code) -->
<script src="https://api.kurdgeo.space/api/v1/gateway/maps/kurdgeo-maps.js"></script>
<!-- 3. Initialize with your API key -->
<script>
KurdGeoMap.init('map', {
apiKey: 'kg_live_abc123def456...',
style: 'style-light.json',
center: [44.0, 36.2],
zoom: 10,
labels: 'all' // show all labels (POIs, shops, hospitals...)
});
</script>That's it. MapLibre GL, PMTiles, the RTL text plugin with Kurdish letter shaping, authentication, and URL rewriting are all handled automatically by the SDK.
Authentication
KurdGeo uses Bearer token authentication. Pass your API key in the Authorization header on every request:
Authorization: Bearer kg_live_abc123def456...You can also use the query parameter ?api_key= for browser-based requests where setting headers is difficult:
https://api.kurdgeo.space/api/v1/gateway/routing/route/v1/driving/44.0,36.2;44.1,36.3?api_key=kg_live_...Your API key is shown only once at creation. Store it securely — it cannot be recovered. If lost, rotate the key to generate a new one.
Note: The SDK script itself (kurdgeo-maps.js) does notrequire auth to load — it's just code. The API key is required when calling KurdGeoMap.init() to fetch styles, tiles, glyphs, and the RTL plugin.
Your first request
Make a routing request between two coordinates:
curl "https://api.kurdgeo.space/api/v1/gateway/routing/route/v1/driving/44.009,36.193;44.012,36.195?overview=full&geometries=geojson" \
-H "Authorization: Bearer kg_live_..."See the full API docs for all routing endpoints.
Maps SDK — Overview
The KurdGeo Maps SDK (kurdgeo-maps.js) is a self-contained JavaScript library that lets you embed interactive Kurdistan maps on any website with minimal code.
What the SDK handles for you:
- Loads MapLibre GL JS v5.13 and PMTiles v4.4 from CDN
- Fetches and injects the RTL text plugin with Kurdish Arabic shaping fixes
- Authenticates all tile, style, glyph, and sprite requests through the gateway
- Rewrites PMTiles URLs to route through
api.kurdgeo.space - Injects
Authorizationheaders viatransformRequest - Queues event listeners, markers, and calls made before the map finishes loading
What you need to provide:a container element and your API key. That's it.
Installation
No npm install. No build step. Just add one script tag to your HTML:
<script src="https://api.kurdgeo.space/api/v1/gateway/maps/kurdgeo-maps.js"></script>The SDK is served without authentication — it's just code, not data. The global KurdGeoMap object is available after the script loads.
Important: Your page must be served over HTTP or HTTPS. Opening the HTML file directly (file://) will not work because the RTL plugin uses blob URLs in a Web Worker, which browsers block under the file: protocol.
Options
KurdGeoMap.init(containerId, options) accepts the following options:
| Parameter | Type | Default | Description |
|---|---|---|---|
apiKey | string | — | Required. Your KurdGeo API key (e.g. kg_live_...) |
apiBase | string | https://api.kurdgeo.space | API base URL. Override for testing or self-hosted instances. |
style | string | style-light.json | Style filename. See Style switching. |
center | [lng, lat] | [44.0, 36.2] | Initial map center (longitude, latitude) |
zoom | number | 10 | Initial zoom level (0–16) |
labels | string | null | Label mode: default, none, numbers, or all. See Label modes. |
var map = KurdGeoMap.init('map', {
apiKey: 'kg_live_abc123def456...',
apiBase: 'https://api.kurdgeo.space', // optional
style: 'style-dark.json', // optional
center: [44.0, 36.2], // optional [lng, lat]
zoom: 12 // optional
});Methods
The object returned by init() exposes these methods:
| Method | Parameters | Description |
|---|---|---|
addMarker(lat, lng, opts) | lat: number, lng: number, opts?: {color?, title?, description?} | Adds a pin marker. opts.color sets pin color (default #c84e6a). If title or description is provided, a popup is attached. |
clearMarkers() | — | Removes all markers added via addMarker(). |
flyTo(lat, lng, zoom?) | lat: number, lng: number, zoom?: number | Animates the map to the given coordinates. If zoom is omitted, zooms in by 2 levels. |
setStyle(name, labels?) | name: string, labels?: string | Switches the map style (e.g. 'style-dark.json'). Optional second argument sets label mode. Fetches the new style through the gateway with auth. |
setLabels(mode) | mode: string | Changes label mode at runtime ('default', 'none', 'numbers', 'all'). Re-fetches the current style with the new mode. |
on(event, callback) | event: string, callback: Function | Registers a MapLibre event listener. Safe to call before map loads — listeners are queued and applied on load. |
getMap() | — | Returns the underlying MapLibre GL Map instance (or null if not yet created). |
destroy() | — | Removes the map, clears all markers, and empties the container. Call when unmounting in SPA frameworks. |
Events
Use on()to listen for MapLibre GL events. The SDK queues listeners if the map isn't ready yet, so you can call this immediately after init():
var map = KurdGeoMap.init('map', { apiKey: 'kg_live_...' });
map.on('load', function () {
console.log('Map is ready!');
});
map.on('click', function (e) {
console.log('Clicked at:', e.lngLat);
});
map.on('error', function (e) {
console.error('Map error:', e.error?.message || e.type);
});Common events: load, click, moveend, zoomend, error.
Markers & popups
var map = KurdGeoMap.init('map', { apiKey: 'kg_live_...' });
map.on('load', function () {
// Simple marker
map.addMarker(36.193, 44.009);
// Colored marker with popup
map.addMarker(36.195, 44.012, {
color: '#3b82f6',
title: 'Erbil',
description: 'Capital of Kurdistan Region'
});
// Clear all markers
map.clearMarkers();
});Style switching
Available style files:
| Style name | Description |
|---|---|
style.json | Topographic — full-color map with terrain, roads, and place labels (default) |
style-light.json | Light — minimal clean white/gray theme, roads and labels only (Positron-style) |
style-dark.json | Dark — dark background theme for night mode, roads and labels only (Dark Matter) |
style-voyager.json | Voyager — colorful street map with detailed road hierarchy and land use colors |
style-2d.json | 2D Flat — simplified flat topographic style without terrain shading |
// Switch to dark style at runtime
map.setStyle('style-dark.json');Label modes
Control how many labels appear on the map by appending ?labels= to any style request. Four modes are available:
| Mode | Value | Description |
|---|---|---|
| Default | labels=default | City and road names, numeric-only road names hidden (default behavior) |
| No Labels | labels=none | Clean map with no text labels at all |
| With Numbers | labels=numbers | City and road names including numeric road names (e.g. "Highway 1") |
| All Labels | labels=all | Everything — POIs (schools, hospitals, shops, restaurants), water names, house numbers, mountain peaks, airports. Like Google Maps. |
Usage with the SDK:
Pass labels in the init options, or call setLabels() at runtime:
// Option 1: Set label mode at init
var map = KurdGeoMap.init('map', {
apiKey: 'kg_live_...',
style: 'style-light.json',
labels: 'all' // show everything (POIs, shops, etc.)
});
// Option 2: Change label mode at runtime
map.setLabels('none'); // hide all labels
map.setLabels('all'); // show all labels (POIs, shops, hospitals...)
map.setLabels('default'); // back to normal
// Option 3: Set label mode when switching styles
map.setStyle('style-dark.json', 'all');Label mode reference:
default— City/town/village names + road names (numeric-only names hidden)none— No text labels anywhere on the mapnumbers— Same as default but also shows numeric road namesall— Everything: POIs (hospitals, schools, shops, restaurants, pharmacies, banks, etc.), water body names, house numbers, mountain peaks, airport names, plus lower zoom thresholds for more labels
The all mode injects 8 additional layers: POI labels at 3 importance tiers (zoom 12/13/14+), shop labels (zoom 15+), water names, house numbers, mountain peaks, and aerodrome labels.
Kurdish text (RTL)
Kurdish text on the map is automatically displayed right-to-left (RTL) with properly connected Arabic script letters. This is handled by a custom RTL text plugin that is fetched and injected by the SDK — you don't need to do anything.
What the SDK does under the hood:
- Fetches
rtl-text.jsfrom the gateway with your API key - Creates a blob URL (Web Workers can't send auth headers via
importScripts) - Registers the plugin with MapLibre GL via
setRTLTextPlugin - Shapes Kurdish letters that ICU misses: U+06D5 (ە), U+06B5 (ڵ), U+06CE (ێ), U+06CC (ی)
- Fixes adjacent presentation forms for correct letter connections
The RTL plugin loads before the map is created, so Kurdish text is shaped correctly from the first render.
Full example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My App with KurdGeo Maps</title>
</head>
<body>
<!-- Container with explicit height -->
<div id="map" style="width: 100%; height: 500px;"></div>
<!-- Buttons for style switching & label modes -->
<button onclick="map.setStyle('style-light.json')">Light</button>
<button onclick="map.setStyle('style-dark.json')">Dark</button>
<button onclick="map.setLabels('all')">All Labels</button>
<button onclick="map.setLabels('none')">No Labels</button>
<button onclick="map.flyTo(36.193, 44.009, 14)">Fly to Erbil</button>
<!-- Load the SDK -->
<script src="https://api.kurdgeo.space/api/v1/gateway/maps/kurdgeo-maps.js"></script>
<!-- Initialize -->
<script>
var map = KurdGeoMap.init('map', {
apiKey: 'kg_live_abc123def456...',
style: 'style-light.json',
center: [44.0, 36.2],
zoom: 10,
labels: 'all' // show POIs, shops, hospitals...
});
// Add markers (queued until map loads)
map.addMarker(36.193, 44.009, {
title: 'Erbil',
description: 'Capital of Kurdistan Region',
color: '#c84e6a'
});
map.addMarker(36.161, 43.993, {
title: 'Another location',
color: '#3b82f6'
});
// Listen for events
map.on('load', function () {
console.log('Map loaded!');
});
map.on('click', function (e) {
console.log('Clicked:', e.lngLat);
});
</script>
</body>
</html>Flutter SDK — Overview
The KurdGeo Flutter SDK lets you embed interactive Kurdistan maps in your Flutter app using MapLibre GL Native. It supports multiple map styles, vector tiles, Kurdish fonts, routing (OSRM), and user location.
There are two ways to integrate KurdGeo maps in Flutter:
- SDK package — Use the
kurdgeo_mapspackage for a simple widget-based API - Manual integration — Use
maplibre_gldirectly with KurdGeo gateway URLs (recommended for full control)
The gateway automatically rewrites style JSON for Flutter requests. When you append?flutter=1 to a style URL, the server:
- Converts
pmtiles://sources to tile proxy URLs - Injects the API key into tile and glyph URLs
- Returns absolute URLs that MapLibre GL Native can fetch directly
Installation
Option A: Using the SDK package
Add to your pubspec.yaml:
dependencies:
kurdgeo_maps:
git:
url: https://github.com/yad-qasim/kurdgeo.git
path: flutter/kurdgeo_maps
maplibre_gl: ^0.16.0Option B: Manual integration (recommended)
dependencies:
maplibre_gl: ^0.16.0
http: ^1.0.0
geolocator: ^12.0.0Android setup: Add to android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />Set minSdk 21 in android/app/build.gradle.
iOS setup: Add to ios/Runner/Info.plist:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Show your location on the map</string>Run cd ios && pod install.
Usage (SDK package)
import 'package:flutter/material.dart';
import 'package:kurdgeo_maps/kurdgeo_maps.dart';
class MapScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Kurdistan Map')),
body: KurdGeoMap(
options: KurdGeoMapOptions(
apiKey: 'kg_live_your_api_key_here',
style: KurdGeoStyle.topo,
center: LatLng(36.193, 44.009), // Erbil
zoom: 12,
),
markers: [
KurdGeoMarker(
lat: 36.193,
lng: 44.009,
title: 'Erbil',
),
],
),
);
}
}Available styles: KurdGeoStyle.topo, .light, .dark, .voyager, .flat2d.
Manual integration
For full control over the map (custom markers, routing, user location), use maplibre_gl directly with KurdGeo gateway URLs. This is the approach used in production apps like Bdozawa.
Key concept: Server-side style rewriting
When you request a style JSON with ?flutter=1, the KurdGeo server rewrites the style for Flutter compatibility:
# Request (with flutter=1, _api_key, and base_url):
GET /api/v1/gateway/maps/style-light.json?api_key=YOUR_KEY&_api_key=YOUR_KEY&flutter=1&base_url=https://api.kurdgeo.space/api/v1/gateway/maps/
# Server returns rewritten style with:
# - pmtiles:// converted to absolute tile proxy URLs
# - API key injected into tile and glyph URLs
# - All URLs are absolute (MapLibre GL Native needs absolute URLs)Why three API key parameters?
api_key— Used by the gateway for authentication (stripped before forwarding to upstream)_api_key— Passed through to the maps server to inject into tile/glyph URLs (not stripped by gateway)base_url— Tells the server the gateway base path so it can construct absolute URLs
Basic setup:
import 'package:maplibre_gl/maplibre_gl.dart';
const String apiKey = 'kg_live_your_api_key_here';
const String mapsGatewayUrl = 'https://api.kurdgeo.space/api/v1/gateway/maps';
const String mapsGatewayBaseUrl = '$mapsGatewayUrl/';
// Build the style URL with all required params
final styleUrl = '$mapsGatewayUrl/style-light.json'
'?api_key=$apiKey'
'&_api_key=$apiKey'
'&flutter=1'
'&base_url=${Uri.encodeComponent(mapsGatewayBaseUrl)}';
MapLibreMap(
styleString: styleUrl,
initialCameraPosition: const CameraPosition(
target: LatLng(36.1911, 44.0092),
zoom: 8,
),
onMapCreated: (controller) {
// Store controller for later use
},
onStyleLoadedCallback: () {
// Add markers, lines, etc.
},
)Map styles
Available style JSONs through the gateway:
# Topographic (default)
$mapsGatewayUrl/style.json?api_key=KEY&_api_key=KEY&flutter=1&base_url=...
# Light (Positron)
$mapsGatewayUrl/style-light.json?api_key=KEY&_api_key=KEY&flutter=1&base_url=...
# Dark (Dark Matter)
$mapsGatewayUrl/style-dark.json?api_key=KEY&_api_key=KEY&flutter=1&base_url=...
# Voyager
$mapsGatewayUrl/style-voyager.json?api_key=KEY&_api_key=KEY&flutter=1&base_url=...Switching styles at runtime:
// Change the styleString and rebuild the widget
setState(() {
_currentStyleUrl = '$mapsGatewayUrl/style-dark.json'
'?api_key=$apiKey&_api_key=$apiKey&flutter=1&base_url=$mapsGatewayBaseUrl';
});
// After the new style loads, re-add your markers
void onStyleLoaded() {
_addMarkers();
_addUserLocation();
}Tiles & glyphs
Vector tiles are served as individual .pbf files through the tile proxy endpoint. The server extracts tiles from a PMTiles archive and decompresses them before sending (the gateway strips Content-Encoding headers, so the server sends raw protobuf).
# Tile endpoint
GET /api/v1/gateway/maps/tiles/{z}/{x}/{y}.pbf?api_key=YOUR_KEY
# Example: tile at zoom 6, x=39, y=25
https://api.kurdgeo.space/api/v1/gateway/maps/tiles/6/39/25.pbf?api_key=YOUR_KEYGlyphs (font PBF files) are served with font glyph IDs matching the font's internal layout, compatible with MapLibre GL Native's HarfBuzz text shaping:
# Glyph endpoint
GET /api/v1/gateway/maps/glyphs/{fontstack}/{range}.pbf?api_key=YOUR_KEY
# Example: Rabar_014 font, range 0-255
https://api.kurdgeo.space/api/v1/gateway/maps/glyphs/Rabar_014/0-255.pbf?api_key=YOUR_KEYAvailable fonts: Rabar_014, Rabar_038, NotoSansArabic.
You don't need to construct these URLs manually — the server injects them into the style JSON when flutter=1 is set.
Routing (OSRM)
The routing API is powered by OSRM and accessed through the gateway at /api/v1/gateway/routing/. The gateway automatically strips the api_key parameter before forwarding to OSRM.
import 'package:http/http.dart' as http;
import 'dart:convert';
Future<void> drawRoute(MaplibreMapController controller, LatLng userLocation, LatLng destination) async {
final url = 'https://api.kurdgeo.space/api/v1/gateway/routing/route/v1/driving/'
'${userLocation.longitude},${userLocation.latitude};'
'${destination.longitude},${destination.latitude}'
'?overview=full&geometries=geojson&api_key=YOUR_KEY';
final res = await http.get(Uri.parse(url));
if (res.statusCode != 200) return;
final data = json.decode(res.body);
if (data['routes'] == null || (data['routes'] as List).isEmpty) return;
final coords = data['routes'][0]['geometry']['coordinates'] as List;
final lineCoords = coords
.map((c) => LatLng((c[1] as num).toDouble(), (c[0] as num).toDouble()))
.toList();
await controller.addLine(
LineOptions(
geometry: lineCoords,
lineColor: '#3b82f6',
lineWidth: 4.0,
lineOpacity: 0.9,
),
);
}The response includes distance (meters),duration (seconds), andgeometry (GeoJSON LineString coordinates).
Available OSRM endpoints:
/route/v1/driving/— Get a route between coordinates/nearest/v1/driving/— Find nearest road point/table/v1/driving/— Duration/distance matrix/trip/v1/driving/— Traveling salesman solver
User location
To show the user's live location on the map, use the geolocator package and add a Circle annotation:
import 'package:geolocator/geolocator.dart';
// Start listening to location updates
final positionStream = Geolocator.getPositionStream(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 10,
),
);
positionStream.listen((Position position) {
final userLatLng = LatLng(position.latitude, position.longitude);
// Update or add the blue dot
if (_userDotCircle != null) {
_mapController!.updateCircle(
_userDotCircle!,
CircleOptions(geometry: userLatLng),
);
} else {
_mapController!.addCircle(
CircleOptions(
geometry: userLatLng,
circleRadius: 8,
circleColor: '#3b82f6',
circleStrokeWidth: 2,
circleStrokeColor: '#ffffff',
),
).then((circle) => _userDotCircle = circle);
}
});Always request location permissions before starting the stream. Handle the case where GPS is disabled at system level.
Markers & pins
Markers are added as Symbol annotations after the style loads:
Future<void> addMarker(MaplibreMapController controller, double lat, double lng, String title) async {
await controller.addSymbol(
SymbolOptions(
geometry: LatLng(lat, lng),
iconImage: 'marker-15',
iconSize: 1.5,
textField: title,
textSize: 12,
textOffset: const [0, 1.5],
),
);
}
// Handle tap on marker
controller.onSymbolTapped.add((symbol) {
// Show popup with place details
});For custom marker icons, register an image on the map controller first:
final bytes = await rootBundle.load('assets/images/pin.png');
await controller.addImage('custom-pin', bytes.buffer.asUint8List());
await controller.addSymbol(
SymbolOptions(
geometry: LatLng(lat, lng),
iconImage: 'custom-pin',
iconSize: 1.0,
),
);Kurdish text
MapLibre GL Native uses HarfBuzz for text shaping, which handles Arabic script including Kurdish-specific letters (ڕ, ڵ, ێ, ی). The Rabar font family is optimized for Kurdish text.
Glyph PBF files are generated with font-specific glyph IDs (not Unicode codepoints) to match HarfBuzz's output. This ensures that shaped letter forms (initial, medial, final, isolated) are rendered correctly.
The font is referenced in the style JSON and fetched automatically through the gateway. No additional configuration is needed.
Full example
Complete integration with map, markers, user location, and routing:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
import 'package:http/http.dart' as http;
import 'package:geolocator/geolocator.dart';
class MapPage extends StatefulWidget {
@override
State<MapPage> createState() => _MapPageState();
}
class _MapPageState extends State<MapPage> {
static const apiKey = 'kg_live_your_api_key_here';
static const mapsUrl = 'https://api.kurdgeo.space/api/v1/gateway/maps';
static const mapsBase = '$mapsUrl/';
static const routingUrl = 'https://api.kurdgeo.space/api/v1/gateway/routing';
MaplibreMapController? _mapController;
LatLng? _userLocation;
Circle? _userDot;
final styleUrl = '$mapsUrl/style.json'
'?api_key=$apiKey&_api_key=$apiKey&flutter=1'
'&base_url=${Uri.encodeComponent(mapsBase)}';
@override
void initState() {
super.initState();
_startLocation();
}
void _startLocation() {
Geolocator.getPositionStream(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 10,
),
).listen((pos) {
final latLng = LatLng(pos.latitude, pos.longitude);
setState(() => _userLocation = latLng);
if (_mapController != null) {
if (_userDot != null) {
_mapController!.updateCircle(
_userDot!, CircleOptions(geometry: latLng));
} else {
_mapController!.addCircle(CircleOptions(
geometry: latLng,
circleRadius: 8,
circleColor: '#3b82f6',
circleStrokeWidth: 2,
circleStrokeColor: '#ffffff',
)).then((c) => _userDot = c);
}
}
});
}
Future<void> _drawRoute(LatLng destination) async {
if (_mapController == null || _userLocation == null) return;
final url = '$routingUrl/route/v1/driving/'
'${_userLocation!.longitude},${_userLocation!.latitude};'
'${destination.longitude},${destination.latitude}'
'?overview=full&geometries=geojson&api_key=$apiKey';
final res = await http.get(Uri.parse(url));
if (res.statusCode != 200) return;
final data = json.decode(res.body);
if (data['routes'] == null) return;
final coords = data['routes'][0]['geometry']['coordinates'] as List;
final line = coords.map((c) =>
LatLng((c[1] as num).toDouble(), (c[0] as num).toDouble())).toList();
await _mapController!.addLine(LineOptions(
geometry: line, lineColor: '#3b82f6', lineWidth: 4.0));
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: MapLibreMap(
styleString: styleUrl,
initialCameraPosition: const CameraPosition(
target: LatLng(36.1911, 44.0092),
zoom: 8,
),
onMapCreated: (c) => _mapController = c,
onStyleLoadedCallback: () {
// Add markers here
},
),
);
}
}Routing API — Overview
The Routing API is powered by OSRM (Open Source Routing Machine) with the full Iraq road network from OpenStreetMap. All endpoints are accessed through the gateway proxy at /api/v1/gateway/routing/.
Coverage: All of Iraq — from Duhok to Basra, Baghdad to Erbil. The routing graph is built from the complete Geofabrik Iraq OSM extract.
Available profiles: driving, walking, cycling.
Note: Our OSRM server currently has driving data only. Walking and cycling routes use the same road network.
Coordinates are passed as semicolon-separated longitude,latitude pairs.
Capabilities:
- Route — Turn-by-turn driving directions between waypoints
- Nearest — Snap coordinates to nearest road
- Table — Distance/duration matrix between all coordinate pairs
- Trip — Traveling Salesman Problem solver for optimized delivery routes
- Match — GPS trace map-matching for live vehicle tracking
Supports up to 1,000 coordinates per request for table and trip endpoints.
Route — Driving directions
Returns the fastest route between two or more coordinates with full geometry and optional turn-by-turn steps.
GET /api/v1/gateway/routing/route/v1/driving/{lng1},{lat1};{lng2},{lat2}?overview=full&geometries=geojson&steps=truecurl "https://api.kurdgeo.space/api/v1/gateway/routing/route/v1/driving/44.009,36.193;45.85,35.47?overview=full&geometries=geojson&steps=true" \
-H "Authorization: Bearer kg_live_..."Response includes distance (meters), duration (seconds), geometry (GeoJSON LineString), and legs with per-step maneuvers when steps=true.
{
"code": "Ok",
"routes": [{
"geometry": {
"type": "LineString",
"coordinates": [[44.01, 36.19], [44.02, 36.20], ...]
},
"distance": 180000.0,
"duration": 7200.0,
"legs": [{
"distance": 180000.0,
"duration": 7200.0,
"steps": [{
"geometry": { "type": "LineString", "coordinates": [...] },
"maneuver": {
"type": "turn",
"modifier": "left",
"location": [44.01, 36.19],
"bearing_before": 0,
"bearing_after": 270
},
"name": "Road Name",
"distance": 500.0,
"duration": 30.0,
"mode": "driving"
}]
}]
}],
"waypoints": [
{ "location": [44.01, 36.19], "name": "Road Name" },
{ "location": [45.85, 35.47], "name": "Another Road" }
]
}Multi-waypoint routes: Add more coordinates separated by semicolons.
curl "https://api.kurdgeo.space/api/v1/gateway/routing/route/v1/driving/44.01,36.19;44.35,36.05;45.85,35.47?overview=full&geometries=geojson" \
-H "Authorization: Bearer kg_live_..."Query parameters:
overview—full,simplified, orfalse(geometry detail)geometries—geojson,polyline, orpolyline6steps—trueto include turn-by-turn instructionsalternatives—trueto return alternative routescontinue_straight—trueto force continuationannotations—duration,distance,nodes,speed
Nearest — Snap to road
Finds the nearest snapped point on the road network for each coordinate. Useful for placing markers on roads.
GET /api/v1/gateway/routing/nearest/v1/driving/{lng},{lat}?number=3curl "https://api.kurdgeo.space/api/v1/gateway/routing/nearest/v1/driving/44.01,36.19?number=3" \
-H "Authorization: Bearer kg_live_..."{
"code": "Ok",
"waypoints": [{
"location": [44.0098, 36.1902],
"name": "Street Name",
"hint": "..."
}]
}Query parameters:
number— Number of nearest results (default 1)
Table — Distance matrix
Computes durations and distances between all pairs of given coordinates. Ideal for ETA calculations, dispatch optimization, and reachability analysis.
GET /api/v1/gateway/routing/table/v1/driving/{lng1},{lat1};{lng2},{lat2};{lng3},{lat3}curl "https://api.kurdgeo.space/api/v1/gateway/routing/table/v1/driving/44.01,36.19;45.85,35.47;43.80,36.20" \
-H "Authorization: Bearer kg_live_..."{
"code": "Ok",
"distances": [
[0, 180000, 25000],
[180000, 0, 200000],
[25000, 200000, 0]
],
"durations": [
[0, 7200, 900],
[7200, 0, 8000],
[900, 8000, 0]
]
}Query parameters:
sources— Indexes of source coordinates (default: all)destinations— Indexes of destination coordinates (default: all)annotations—duration,distance, or both (default: both)fallback_speed— Speed (km/h) for disconnected components
Supports up to 1,000 coordinates per request.
Trip — TSP solver
Solves the Traveling Salesman Problem for the given waypoints. Returns an optimized route order — useful for delivery drivers and multi-stop planning.
GET /api/v1/gateway/routing/trip/v1/driving/{lng1},{lat1};{lng2},{lat2};{lng3},{lat3}?roundtrip=true&geometries=geojson&overview=fullcurl "https://api.kurdgeo.space/api/v1/gateway/routing/trip/v1/driving/44.01,36.19;45.85,35.47;43.80,36.20?roundtrip=true&geometries=geojson&overview=full" \
-H "Authorization: Bearer kg_live_..."{
"code": "Ok",
"trips": [{
"geometry": { "type": "LineString", "coordinates": [...] },
"distance": 400000.0,
"duration": 16000.0,
"legs": [...]
}],
"waypoints": [
{ "waypoint_index": 0, "location": [44.01, 36.19] },
{ "waypoint_index": 2, "location": [43.80, 36.20] },
{ "waypoint_index": 1, "location": [45.85, 35.47] }
]
}Query parameters:
roundtrip—truereturns to start,falseends at last waypointsource—first,last, orany(fixed start/end)destination—first,last, orany
Supports up to 1,000 waypoints per request.
Match — Live GPS tracking
Map-matches a sequence of GPS coordinates to the road network. This is the core endpoint for live vehicle tracking — it snaps noisy GPS pings to actual roads, reconstructs the driven path, and returns clean geometry.
GET /api/v1/gateway/routing/match/v1/driving/{lng1},{lat1};{lng2},{lat2};{lng3},{lat3}?geometries=geojson&overview=full&steps=true# Snap a GPS trace to roads
curl "https://api.kurdgeo.space/api/v1/gateway/routing/match/v1/driving/44.01,36.19;44.02,36.20;44.03,36.21;44.04,36.22?geometries=geojson&overview=full" \
-H "Authorization: Bearer kg_live_..."{
"code": "Ok",
"tracepoints": [
{ "location": [44.01, 36.19], "name": "Road Name", "matchings_index": 0 },
{ "location": [44.02, 36.20], "name": "Road Name", "matchings_index": 0 },
{ "location": [44.03, 36.21], "name": "Road Name", "matchings_index": 0 }
],
"matchings": [{
"geometry": { "type": "LineString", "coordinates": [...] },
"distance": 3000.0,
"duration": 120.0
}]
}Query parameters:
steps—truefor turn-by-turn instructionsgeometries—geojson,polyline, orpolyline6overview—full,simplified, orfalsetimestamps— Semicolon-separated Unix timestamps for each pointradiuses— Max GPS accuracy radius in meters per pointgaps—ignore,split, orbridgefor missing datatidy—trueto remove outliers
Use cases:
- Live vehicle tracking — Send GPS pings as the vehicle moves, get snapped road positions
- GPS trace cleanup — Fix noisy/off-road GPS points from logged data
- Route reconstruction — Recover the actual road path from raw GPS data
- Distance validation — Compare matched distance vs GPS-reported distance
Response codes:
Ok— All points matched successfullyNoMatch— Could not match any points to roadsTooFewCoordinates— Need at least 2 coordinates
MapLibre Integration — Draw routes
Draw a route line on a KurdGeo map using the routing API response.
// Fetch route from the routing API
const response = await fetch(
"https://api.kurdgeo.space/api/v1/gateway/routing/route/v1/driving/44.01,36.19;45.85,35.47?overview=full&geometries=geojson&steps=true",
{ headers: { Authorization: "Bearer kg_live_..." } }
);
const json = await response.json();
// Add the route geometry to the map
map.addSource("route", {
type: "geojson",
data: {
type: "Feature",
geometry: json.routes[0].geometry,
},
});
map.addLayer({
id: "route-line",
type: "line",
source: "route",
layout: {
"line-join": "round",
"line-cap": "round",
},
paint: {
"line-color": "#2563eb",
"line-width": 5,
},
});
// Fit map to route bounds
const coords = json.routes[0].geometry.coordinates;
const bounds = coords.reduce(
(b, c) => b.extend(c),
new maplibregl.LngLatBounds(coords[0], coords[0])
);
map.fitBounds(bounds, { padding: 50 });Live Tracking — Map Matching
Use the /match endpoint to snap GPS points to roads in real-time. Send coordinates as the vehicle moves and update the map with the snapped path.
// Collect GPS points as the vehicle moves
const gpsPoints = [[44.01, 36.19], [44.02, 36.20], [44.03, 36.21]];
const coordString = gpsPoints.map(([lng, lat]) => `${lng},${lat}`).join(";");
const response = await fetch(
`https://api.kurdgeo.space/api/v1/gateway/routing/match/v1/driving/${coordString}?geometries=geojson&overview=full`,
{ headers: { Authorization: "Bearer kg_live_..." } }
);
const json = await response.json();
// Draw the snapped road path
if (json.matchings && json.matchings.length > 0) {
map.getSource("live-track").setData({
type: "Feature",
geometry: json.matchings[0].geometry,
});
}Tip: Use the timestamps parameter to pass Unix timestamps for each GPS point. This helps OSRM determine direction and speed, improving match accuracy.
// With timestamps for better accuracy
const timestamps = gpsPoints.map((_, i) => startTime + i * interval);
const tsStr = timestamps.join(";");
// Add ×tamps=1234567890;1234567891;1234567892 to the URLTile service
Vector tiles are served as PMTiles (single-file archive) through the gateway. The SDK handles PMTiles loading automatically — this section is for advanced users who want to use MapLibre GL directly without the SDK.
https://api.kurdgeo.space/api/v1/gateway/maps/kurdistan.pmtilesRequires Authorization: Bearer header. Supports HTTP Range requests for partial content (PMTiles protocol).
Styles
Available style JSONs (require auth to fetch):
https://api.kurdgeo.space/api/v1/gateway/maps/style.json
https://api.kurdgeo.space/api/v1/gateway/maps/style-light.json
https://api.kurdgeo.space/api/v1/gateway/maps/style-dark.json
https://api.kurdgeo.space/api/v1/gateway/maps/style-voyager.json
https://api.kurdgeo.space/api/v1/gateway/maps/style-2d.jsonStyle JSONs use relative paths for glyphs and sprites, and pmtiles://kurdistan.pmtiles for the tile source. The SDK rewrites these to absolute gateway URLs at runtime.
Label modes
All style endpoints accept an optional ?labels= query parameter that controls which labels are rendered on the map. This is processed server-side — the gateway modifies the style JSON before returning it.
# Default (city & road names, no numbers)
GET /api/v1/gateway/maps/style.json?api_key=YOUR_KEY
# No labels at all
GET /api/v1/gateway/maps/style.json?api_key=YOUR_KEY&labels=none
# City & road names including numbers
GET /api/v1/gateway/maps/style.json?api_key=YOUR_KEY&labels=numbers
# All labels — POIs, shops, water names, house numbers, peaks, airports
GET /api/v1/gateway/maps/style.json?api_key=YOUR_KEY&labels=allThe all mode adds POI layers (hospitals, schools, restaurants, shops, pharmacies, banks, and more), water body names, house numbers, mountain peaks, and airport labels. It also lowers the minzoom on place and road name layers to show more labels at lower zoom levels.
Works with all style files (style.json, style-light.json, style-dark.json, style-voyager.json, style-2d.json) and the satellite style (style-satellite.json).
Sprites & glyphs
Sprite sheets and glyph fonts (PBF) are served alongside the tile service. Style JSONs reference them with relative paths like glyphs/{fontstack}/{range}.pbf and the SDK rewrites them to gateway URLs automatically.
Available font: Rabar_038 (Kurdish-optimized).
Satellite Imagery — Overview
KurdGeo provides high-resolution satellite imagery for the Kurdistan region, seamlessly integrated into the same gateway as our vector maps. Satellite tiles are served through a dedicated endpoint and authenticated with the same API keys.
Imagery is sourced from premium third-party providers and cached on our infrastructure for low-latency delivery. All satellite requests are border-locked to the Kurdistan region, just like our vector tiles.
Key features:
- High-resolution aerial imagery with regular updates
- Same API key workflow as vector maps — no separate authentication
- Border-locked to Kurdistan region for security and compliance
- Credit-based usage with flexible plans (Test, 75K, 750K, Custom)
- Automatic deactivation when credits or time limits are exhausted
- Real-time usage analytics in your dashboard
Satellite keys are managed by administrators and assigned to users. When a key is assigned to you, it automatically appears in your dashboard with full analytics.
Using satellite tiles
Satellite imagery is accessed through the satellite gateway endpoint. You can use it with MapLibre GL JS, the KurdGeo SDK, or any compatible map renderer that supports raster tiles.
Gateway endpoint:
GET /api/v1/gateway/satellite/tile/{z}/{x}/{y}.png?api_key=YOUR_KEYThe satellite style JSON is also available for easy integration:
# Fetch the satellite style JSON
GET /api/v1/gateway/satellite/style-satellite.json?api_key=YOUR_KEYUsing with the Maps SDK:
The KurdGeo playground supports switching between vector and satellite styles. In your own app, you can fetch the satellite style JSON and pass it to MapLibre GL:
var map = KurdGeoMap.init('map', {
apiKey: 'kg_live_...',
style: 'style-satellite.json',
center: [44.0, 36.2],
zoom: 12
});Note: Satellite requests consume credits from your satellite plan. Each tile request counts as one credit. When your credits are exhausted, the API returns HTTP 403 with a CREDITS_EXHAUSTED error code.
Plans & credits
Satellite imagery plans are credit-based and time-limited. Administrators can create keys with the following plan types:
| Plan | Credits | Duration | Description |
|---|---|---|---|
test | 2,000 | Unlimited | For evaluation and testing |
paid_75k | 75,000 | 1 month | Standard plan for production apps |
paid_750k | 750,000 | 1 month | High-volume plan for scaling apps |
custom | Configurable | Configurable | Custom credits and duration (0 = unlimited) |
How credits work:
- Each successful satellite tile request consumes 1 credit
- Credits are tracked per API key, not per user
- When
creditsUsed >= creditsLimit, the key is automatically deactivated - Time-based plans expire after the configured number of months from the start date
- Administrators can top up credits or extend duration at any time
You can view your remaining credits and usage in the API keys dashboard.
Activation & deactivation
Satellite API keys can be activated and deactivated by administrators. This provides fine-grained control over access to satellite imagery.
Automatic deactivation occurs when:
- Credits are exhausted (
creditsUsed >= creditsLimit) - Plan expires (
planExpiresAt < now) - Administrator manually deactivates the key
When a key is deactivated, all satellite requests with that key return HTTP 403 with an appropriate error code:
KEY_DEACTIVATED— admin disabled the keyPLAN_EXPIRED— time limit reachedCREDITS_EXHAUSTED— no remaining credits
Administrators can reactivate keys, top up credits, or extend duration from the admin panel at any time.
User assignment:
Satellite keys can be assigned to one or more users. Assigned users see the key in their dashboard with full analytics — request counts, credit usage, and plan status. Multiple users can share a single key, and all requests consume from the same credit pool.
Managing keys
Create and manage API keys from the dashboard. Each key belongs to a project and inherits that project's rate limits.
Keys are stored as HMAC-SHA256 hashes — the plaintext is shown only once at creation.
Scopes & permissions
Each API key has scopes that determine which services it can access:
maps:read— access map tiles, styles, glyphs, spritesrouting:read— access routing endpoints*— full access to all services
Keys can also be restricted by origin (CORS) and IP address.
Rate limits & quotas
Each plan has configurable limits:
- RPS — requests per second (sliding window)
- Daily quota — requests per UTC day
- Monthly quota — requests per calendar month
When limits are exceeded, the API returns HTTP 429 with a QUOTA_EXCEEDED error code.
Rotation
Rotating a key generates a new secret and immediately invalidates the old one. This is useful if a key is compromised or you want to cycle keys periodically.
Error codes
All errors follow a consistent JSON structure:
{
"error": {
"code": "QUOTA_EXCEEDED",
"message": "Your daily quota has been exceeded.",
"contact": "yadvader888@gmail.com",
"upgradeUrl": "https://app.kurdgeo.site/pricing"
},
"meta": { "requestId": "..." }
}Common error codes: INVALID_API_KEY, QUOTA_EXCEEDED, RATE_LIMITED, UPSTREAM_ERROR, VALIDATION_ERROR.
Response format
All successful responses are wrapped in a standard envelope:
{
"data": { ... },
"meta": { "requestId": "..." }
}Rate limit headers
Every gateway response includes these headers:
X-RateLimit-Limit— max requests per secondX-RateLimit-Remaining— remaining requests in current windowX-RateLimit-Reset— epoch seconds until window resetsX-Daily-Quota-Remaining— remaining daily quota