KurdGeo
DocumentationGetting Started

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
index.html
<!-- 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:

code
Authorization: Bearer kg_live_abc123def456...

You can also use the query parameter ?api_key= for browser-based requests where setting headers is difficult:

code
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:

terminal
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 Authorization headers via transformRequest
  • 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:

code
<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:

ParameterTypeDefaultDescription
apiKeystringRequired. Your KurdGeo API key (e.g. kg_live_...)
apiBasestringhttps://api.kurdgeo.spaceAPI base URL. Override for testing or self-hosted instances.
stylestringstyle-light.jsonStyle filename. See Style switching.
center[lng, lat][44.0, 36.2]Initial map center (longitude, latitude)
zoomnumber10Initial zoom level (0–16)
labelsstringnullLabel mode: default, none, numbers, or all. See Label modes.
example.js
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:

MethodParametersDescription
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?: numberAnimates the map to the given coordinates. If zoom is omitted, zooms in by 2 levels.
setStyle(name, labels?)name: string, labels?: stringSwitches 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: stringChanges label mode at runtime ('default', 'none', 'numbers', 'all'). Re-fetches the current style with the new mode.
on(event, callback)event: string, callback: FunctionRegisters 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():

example.js
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

example.js
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 nameDescription
style.jsonTopographic — full-color map with terrain, roads, and place labels (default)
style-light.jsonLight — minimal clean white/gray theme, roads and labels only (Positron-style)
style-dark.jsonDark — dark background theme for night mode, roads and labels only (Dark Matter)
style-voyager.jsonVoyager — colorful street map with detailed road hierarchy and land use colors
style-2d.json2D Flat — simplified flat topographic style without terrain shading
code
// 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:

ModeValueDescription
Defaultlabels=defaultCity and road names, numeric-only road names hidden (default behavior)
No Labelslabels=noneClean map with no text labels at all
With Numberslabels=numbersCity and road names including numeric road names (e.g. "Highway 1")
All Labelslabels=allEverything — 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:

code
// 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 map
  • numbers — Same as default but also shows numeric road names
  • all — 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.js from 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

index.html
<!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_maps package for a simple widget-based API
  • Manual integration — Use maplibre_gl directly 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:

code
dependencies:
  kurdgeo_maps:
    git:
      url: https://github.com/yad-qasim/kurdgeo.git
      path: flutter/kurdgeo_maps
  maplibre_gl: ^0.16.0

Option B: Manual integration (recommended)

code
dependencies:
  maplibre_gl: ^0.16.0
  http: ^1.0.0
  geolocator: ^12.0.0

Android setup: Add to android/app/src/main/AndroidManifest.xml:

code
<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:

code
<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)

example.dart
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
# 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:

example.dart
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:

request
# 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:

code
// 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).

request
# 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_KEY

Glyphs (font PBF files) are served with font glyph IDs matching the font's internal layout, compatible with MapLibre GL Native's HarfBuzz text shaping:

request
# 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_KEY

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

example.dart
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:

example.dart
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:

code
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:

code
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:

example.dart
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.

request
GET /api/v1/gateway/routing/route/v1/driving/{lng1},{lat1};{lng2},{lat2}?overview=full&geometries=geojson&steps=true
terminal
curl "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.

response.json
{
  "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.

terminal
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:

  • overviewfull, simplified, or false (geometry detail)
  • geometriesgeojson, polyline, or polyline6
  • stepstrue to include turn-by-turn instructions
  • alternativestrue to return alternative routes
  • continue_straighttrue to force continuation
  • annotationsduration, 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.

request
GET /api/v1/gateway/routing/nearest/v1/driving/{lng},{lat}?number=3
terminal
curl "https://api.kurdgeo.space/api/v1/gateway/routing/nearest/v1/driving/44.01,36.19?number=3" \
  -H "Authorization: Bearer kg_live_..."
response.json
{
  "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.

request
GET /api/v1/gateway/routing/table/v1/driving/{lng1},{lat1};{lng2},{lat2};{lng3},{lat3}
terminal
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_..."
response.json
{
  "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)
  • annotationsduration, 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.

request
GET /api/v1/gateway/routing/trip/v1/driving/{lng1},{lat1};{lng2},{lat2};{lng3},{lat3}?roundtrip=true&geometries=geojson&overview=full
terminal
curl "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_..."
response.json
{
  "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:

  • roundtriptrue returns to start, false ends at last waypoint
  • sourcefirst, last, or any (fixed start/end)
  • destinationfirst, last, or any

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.

request
GET /api/v1/gateway/routing/match/v1/driving/{lng1},{lat1};{lng2},{lat2};{lng3},{lat3}?geometries=geojson&overview=full&steps=true
request
# 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_..."
response.json
{
  "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:

  • stepstrue for turn-by-turn instructions
  • geometriesgeojson, polyline, or polyline6
  • overviewfull, simplified, or false
  • timestamps — Semicolon-separated Unix timestamps for each point
  • radiuses — Max GPS accuracy radius in meters per point
  • gapsignore, split, or bridge for missing data
  • tidytrue to 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 successfully
  • NoMatch — Could not match any points to roads
  • TooFewCoordinates — Need at least 2 coordinates

MapLibre Integration — Draw routes

Draw a route line on a KurdGeo map using the routing API response.

code
// 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.

code
// 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.

code
// With timestamps for better accuracy
const timestamps = gpsPoints.map((_, i) => startTime + i * interval);
const tsStr = timestamps.join(";");
// Add &timestamps=1234567890;1234567891;1234567892 to the URL

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

code
https://api.kurdgeo.space/api/v1/gateway/maps/kurdistan.pmtiles

Requires Authorization: Bearer header. Supports HTTP Range requests for partial content (PMTiles protocol).

Styles

Available style JSONs (require auth to fetch):

code
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.json

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

request
# 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=all

The 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:

request
GET /api/v1/gateway/satellite/tile/{z}/{x}/{y}.png?api_key=YOUR_KEY

The satellite style JSON is also available for easy integration:

request
# Fetch the satellite style JSON
GET /api/v1/gateway/satellite/style-satellite.json?api_key=YOUR_KEY

Using 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:

example.js
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:

PlanCreditsDurationDescription
test2,000UnlimitedFor evaluation and testing
paid_75k75,0001 monthStandard plan for production apps
paid_750k750,0001 monthHigh-volume plan for scaling apps
customConfigurableConfigurableCustom 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 key
  • PLAN_EXPIRED — time limit reached
  • CREDITS_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, sprites
  • routing: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:

response.json
{
  "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:

response.json
{
  "data": { ... },
  "meta": { "requestId": "..." }
}

Rate limit headers

Every gateway response includes these headers:

  • X-RateLimit-Limit — max requests per second
  • X-RateLimit-Remaining — remaining requests in current window
  • X-RateLimit-Reset — epoch seconds until window resets
  • X-Daily-Quota-Remaining — remaining daily quota