50,000 free calls a month, card-free. Get an API key →

Documentation menu
docs / sdks · raw .md

SDKs & installation

Five SDKs over one Rust core. The mobile packages ship from tagged releases (sdk-v*); the npm packages can also be cut on their own between tags, so their versions run ahead. Fleet customers licence the on-device SDK per vehicle; app developers can meter per monthly active user instead (see pricing).

Status: the npm packages are live: @mapmap/maps, @mapmap/core and @mapmap/react-native install from npm today. The iOS package installs publicly via Swift Package Manager from the distribution repository Mapmapai/mapmap-ios (current release 0.6.0). The Android AAR publishes to Maven Central (current release 0.3.0), which needs no account and no token. The zero-install way in is the playground; the hosted gateway at https://api.mapmap.ai is live (sign up for a key), and the same gateway runs from the self-host distro; base URL, auth and quotas are defined in conventions.

Compatibility

SDKPackageVersionPlatform floorPeer requirements
Web maps@mapmap/maps0.8.0Node ≥ 18 to build; ESM onlymaplibre-gl ≥ 4 < 6, pmtiles ≥ 3 < 5
Core (WASM)@mapmap/core0.3.0Node (the npm artefact is the Node build)none
React Native@mapmap/react-native0.3.1iOS 16.4, minSdk 26; Expo SDK 52+, or bare React Native 0.74+expo-modules-core, react, react-native; expo ≥ 52 optional
Androidai.mapmap:core0.3.0minSdk 26, JDK 17 to buildnone
iOSMapMapKit (Mapmapai/mapmap-ios)0.6.0iOS 16.4, Swift tools 5.9none

iOS: never pin 0.2.0. The 0.2.0 iOS release shipped without its generated Swift bindings and did not compile; 0.2.1 is the packaging fix (same checksum-pinned binary core).

Web maps: @mapmap/maps

Availability: live on npm.

A thin TypeScript wrapper over MapLibre GL JS that gives you a MapMap map with our tiles, styles and routing wired in. You need an snk_ API key; issue one card-free in the quickstart.

bash
npm install @mapmap/maps maplibre-gl pmtiles

The map needs MapLibre's stylesheet and a container with a real height:

html
<div id="map" style="height: 480px"></div>

The example below routes a hazmat truck. ADR (the European agreement on carriage of dangerous goods by road; tunnel codes B–E restrict which tunnels a hazmat load may use) is a first-class routing parameter; the full tunnel-code table is in conventions.

ts
import "maplibre-gl/dist/maplibre-gl.css";
import { createMap, RouteLayer } from "@mapmap/maps";

const map = createMap({
  container: "map",
  apiKey: "snk_...", // from the quickstart
  // baseUrl defaults to https://api.mapmap.ai
});
await map.whenReady();

const routes = new RouteLayer(map);
const route = await routes.route(
  { lon: -0.1278, lat: 51.5074 }, // London
  { lon: -1.8904, lat: 52.4862 }, // Birmingham
  { profile: "truck", truck: { heightM: 4.0, weightT: 40, hazmat: true, tunnelCode: "C" } },
);

route.distanceM; // total distance, metres
route.durationS; // total duration, seconds
route.geometry;  // GeoJSON LineString ([lng, lat] positions), already drawn on the map
route.raw;       // the raw routes[0] object, for leg/step detail

Alternatives, ferry legs and geometry you already hold

Offering a choice of routes is table stakes for a navigation UI, so RouteLayer draws the unselected ones dimmer and beneath, clickable to switch. The selected route keeps everything else the layer does: progress, manoeuvre arrow, alert corridors.

ts
routes.drawAlternatives(parsedRoutes, selectedIndex);
routes.onSelectAlternative((i) => routes.drawAlternatives(parsedRoutes, i));

// Ferry legs as dashes over the selected line, so a water crossing does
// not read as driving. Supplied explicitly, because line-dasharray cannot
// be data-driven and only you know which legs are ferries.
routes.setFerrySegments([[[lng, lat], ...]]);

// A line you already have, with no ParsedRoute to build:
routes.drawGeometry([[lng, lat], ...]);

// Runtime paint over the design, e.g. marking a route provisional or
// straight-line-approximated. Survives style reloads; `{}` clears it.
routes.setLineStyle({ color: "#9096a2", dash: [1.6, 1.4], casingOpacity: 0 });
Multi-stop routing drawn live with RouteLayer · © OpenStreetMap contributors © OpenMapTiles

Coordinates are accepted as [lng, lat] arrays, { lng, lat } or { lon, lat } objects. Profiles: driving (the default), walking and truck; the truck options (heightM, widthM, lengthM, weightT, hazmat, tunnelCode) only apply to the truck profile. AdrCheck (a typed client for the gateway's POST /adr/check compliance endpoint) and the pmtiles protocol are wired up for you.

Label language is a style option: buildStyle({ labelLanguage: "ja" }) renders labels in that language with local-name fallback ("local" shows every place in its own language); see label languages. PlacesLayer renders your own places (pins, clustering, popups, on-map labels, a different icon per place, nearest by straight line or by drive time) and pairs with the hosted /places store-finder API. MarkersLayer draws the markers a designer placed in Studio's Markers tab and shipped with the theme, and new RouteLayer(map, { endpoints: true }) adds branded start, destination and numbered waypoint markers to a drawn route.

The SDK runs from any origin: the gateway answers with Access-Control-Allow-Origin: *, so browser calls work from ordinary web apps and from null-origin contexts alike: Figma plugins, Electron apps, file:// pages. Keys travel as bearer headers, never cookies, which is what makes the open policy safe.

Errors

route() throws a plain Error. Gateway errors (401 bad key, 429 rate limit; RFC 9457 problem+json on the wire) surface as route request failed: HTTP <status> (<title> - <detail>); routing-engine failures (OSRM-shaped bodies with a code) surface as OSRM routing failed: <code> - <message>. The full error envelope, including 402-vs-429, is in conventions.

ts
try {
  await routes.route(from, to, { profile: "truck" });
} catch (err) {
  // e.g. "route request failed: HTTP 401 (...)"; check your snk_ key
  // e.g. "OSRM routing failed: NoRoute - ...";   no legal route exists
  console.error(err);
}

Turn-by-turn guidance

Request voice and banner instructions on the route, then use the guidance helpers:

ts
import { extractGuidance, speak, GuidanceBanner } from "@mapmap/maps";

const route = await routes.route(from, to, {
  profile: "truck",
  voice: true,    // spoken voiceInstructions on each step
  banner: true,   // visual bannerInstructions, with lane data
  language: "en-GB",
});

const steps = extractGuidance(route);          // one StepGuidance per step
const banner = new GuidanceBanner(document.body, map.navDesign?.banner);
banner.update(steps[0]?.banners[0] ?? null);   // render the next maneuver
if (steps[0]?.voice[0]) speak(steps[0].voice[0], { lang: "en-GB" });

Each step's voice array is ordered by descending trigger distance; speak each instruction once as its distance is crossed; de-duplication is the caller's responsibility.

NavigationCamera is the turnkey chase cam: tilted (pitch 60, clamped 0–85), course-up, zoom 17, with the puck anchored low-centre (anchorY 0.72) so the camera looks up the road. Feed it from the same loop that drives your guidance:

ts
import { NavigationCamera, PositionPuck } from "@mapmap/maps";

const camera = new NavigationCamera(map, { pitch: 60, zoom: 17 });
camera.attachPuck(new PositionPuck(map)); // one call now moves both
navigator.geolocation.watchPosition(({ coords }) =>
  camera.follow({ lat: coords.latitude, lon: coords.longitude }, coords.heading ?? undefined),
);

Every follow(fix, courseDeg?) glides with an interruptible linear ease (duration easeMs, default 900 ms, capped by the observed fix interval), sets the bearing to the course (or holds it when courseDeg is omitted), and co-drives an attached puck. Any user drag / rotate / pitch / zoom switches the camera to "free" mode and it recentres itself after autoRecentreMs idle (default 6 s; 0 disables; call resume()). overview(route.geometry) fits the whole route top-down and resume() returns to the chase cam; camera.mode reports "follow" | "overview" | "free", and destroy() removes the listeners. NavigationCamera.isSupported(map) returns false under the globe projection, whose camera geometry breaks the low-anchor offset maths; switch the map to mercator before navigating.

The PositionPuck interpolates between fixes by default: it glides along the shortest arc from its rendered position to each new fix over a duration matched to the observed fix interval, so the puck and camera arrive together instead of the puck teleporting once a second. Pass { interpolate: false } to snap instead.

Progress line & manoeuvre arrows

Dim the travelled part of the route (the "vanishing route line") and drop a rotated arrow at the next turn; both survive a light/dark style swap:

ts
routes.setProgress(0.42);              // 0–1 of the route length, travelled
routes.setManeuver([lon, lat], 135);   // arrow at the next turn, bearing°
// …later
routes.setProgress(0);                 // restore the plain line
routes.clearManeuver();

setProgress renders a line-gradient over the route source (built with lineMetrics); pass a progressColor in the layer options to change the dimmed colour. Derive the fraction from the guidance module's distance-remaining.

Day/night & label language

Switch light/dark automatically by sun position, and labels by language; both dependency-free and offline-capable (territory packages ship paired light/dark styles and the OpenMapTiles multilingual name:* fields):

ts
import { ThemeScheduler, setMapLanguage } from "@mapmap/maps";

// Flip light ↔ dark at local sunrise/sunset, re-armed each boundary.
const scheduler = new ThemeScheduler({
  lat: 51.5, lng: -0.13,
  onLight: () => map.setStyle("light"),
  onDark:  () => map.setStyle("dark"),
});
// scheduler.dispose() to stop.

setMapLanguage(map, "de");   // relabel in German; null restores the default

ThemeScheduler handles polar day/night; resolveTheme(date, lat, lng) is the one-shot form. setMapLanguage rewrites only name-label layers (road shields and house numbers are left alone) and validates the tag. For right-to-left scripts, also install MapLibre's RTL text plugin in your app.

Using your Studio design

A theme designed in Studio carries its navigation look: route line, position puck, banner and safety alerts, under extra.nav in the theme JSON you download or copy. Pass that theme file to createMap and the navigation UI styles itself:

ts
import { createMap, RouteLayer, GuidanceBanner, PositionPuck } from "@mapmap/maps";
import theme from "./midnight-fleet.theme.json"; // downloaded from Studio

const map = createMap({ container: "map", apiKey: "snk_...", style: theme });
await map.whenReady();

const routes = new RouteLayer(map);   // line colour/width/opacity + casing
const banner = new GuidanceBanner(document.body, map.navDesign?.banner);
const puck = new PositionPuck(map);   // dot + heading arrow, or custom image
puck.setLocation({ lat: 51.5074, lon: -0.1278 }, 45); // heading in degrees

The alerts block (optional since v1) styles safety-camera alerts: CameraAlertChip renders the chip in its calm and escalated states with per-kind icons, and RouteLayer.setAlertCorridors tints average-speed corridors on the route line.

ts
import { CameraAlertChip } from "@mapmap/maps";

const chip = new CameraAlertChip(document.body, map.navDesign?.alerts);
chip.update({ kind: "fixed", limitKph: 30, distanceM: 240 }, { speeding: false });

The visual alert is unconditional and calm; audio is the escalation channel, so a driver within the limit still sees the chip and hears it only when the kind's alertMode says so. Chip positions are a fixed set of slots rather than free coordinates, so an alert can never cover the speed or the limit. On CarPlay and Android Auto, alerts render from the platform's own template, so read alertChipContent for the title, subtitle and icon and let the colours degrade gracefully.

Whether cameras may be shown at all is a server-side jurisdiction policy applied per country before the response leaves the gateway. The design styles what policy permits and cannot widen it.

map.navDesign is the parsed block (navDesignFromTheme / parseNavDesign are exported too); RouteLayer and PositionPuck pick it up automatically, and without a nav block everything keeps the SDK's built-in look.

The block also survives hosted publishing: it travels with the theme document, stored on publish, served back from GET /styles/{id}/theme (public, uncached), while the compiled style.json never carries it. Fetch it from a hosted style with navDesignFromThemeUrl:

ts
import { navDesignFromThemeUrl } from "@mapmap/maps";

const design = await navDesignFromThemeUrl(
  "https://api.mapmap.ai/styles/midnight-fleet-a1b2c3/theme",
);
const banner = new GuidanceBanner(document.body, design?.banner);

The map renders the attribution required by our data licence and the MapMap mark, and neither can be removed by configuration. As of 0.8.0, mapOptions: { attributionControl: false } and logo: false are accepted, ignored and reported with one console.warn per map rather than honoured. What still works: attributionControl: { compact: true } collapses the credit, { customAttribution } adds your own alongside the OpenStreetMap one, and logo: { position, href } moves or relinks the mark.

The package has over 200 named exports, types included: map, routing, ADR, guidance, markers, places, styling and coordinate helpers. This page shows the main ones; a generated TypeDoc reference ships with the npm release.

Core (WASM): @mapmap/core

Availability: live on npm.

The navigation core compiled to WebAssembly, for offline or worker-side use without a map: ADR tunnel compliance, offline route-request building, route parsing, polyline codecs, and the same Ferrostar-derived guidance state machine the mobile SDKs run. The npm artefact is the Node build.

bash
npm install @mapmap/core
js
const { checkTunnel, buildOfflineRouteRequest } = require("@mapmap/core");

// May this load use a category D tunnel? (ADR 8.6.4, worst-case reading)
checkTunnel({ hazmat: true, tunnelCode: "C" }, "D");
// => { status: "blocked", reason: "..." }  or  { status: "allowed" }

// Build a turn-by-turn request any Valhalla endpoint accepts;
// hosted API, self-hosted server, or an on-device engine:
const body = buildOfflineRouteRequest(
  [{ lat: 51.1279, lon: 1.3134 }, { lat: 52.4862, lon: -1.8904 }],
  "truck",
  { heightM: 4.0, grossWeightT: 40, hazmat: true, tunnelCode: "D" },
);

The full surface: version(), checkTunnel(profile, category), forbiddenCategories(profile), buildOfflineRouteRequest(locations, costing, adrProfile), parseRouteSummary(json) (returns { distanceM, durationS, hasToll, hasHighway, hasFerry }), polylineDecode / polylineEncode (precision 5 or 6), and GuidanceSession: construct it from a route response JSON, stream location fixes in with updateLocation(lat, lon, timestampMs, …), and each call returns the updated { state: "navigating" | "arrived" | "offRoute", … }.

React Native: @mapmap/react-native

Availability: live on npm, 0.3.1.

An Expo module over the same native SDKs: signed territory install and lifecycle, on-device routing with typed geometry and manoeuvres, a typed guidance event stream, and voice guidance.

sh
npx expo install @mapmap/react-native

Register the config plugin in app.json or app.config.js. It writes the NSLocation*UsageDescription keys the SDK's location provider needs (the SDK declares none of its own) and enables the location and audio background modes, so guidance and voice keep running with the screen off:

jsonc
{
  "expo": {
    "plugins": [
      ["@mapmap/react-native", {
        "locationWhenInUsePermission": "Shown when asking for foreground location.",
        "locationAlwaysPermission": "Shown when asking for background location."
      }],
      ["expo-build-properties", { "android": { "minSdkVersion": 26 } }]
    ]
  }
}

Then build a dev client with npx expo run:ios or npx expo run:android.

minSdk 26 again. The same floor as the Android SDK, and above Expo's default, so the expo-build-properties line above is not optional.

Not available in Expo Go. The native module needs a dev client. Pass true to getMapmapNav for a pure TypeScript simulator with an identical surface, which is what Expo Go, unit tests and Storybook-style UI work should use. Importing the package never requires the native module: the native handle resolves lazily, on first use.

ts
import { getMapmapNav, addGuidanceListener } from "@mapmap/react-native";

const nav = getMapmapNav(false); // true → the mock
await nav.init("snk_…");

await nav.installTerritory("uk"); // signed, verify-then-promote
const { routes } = await nav.computeRoute({
  profile: "car",
  origin: { latitude: 51.5074, longitude: -0.1278 },
  destination: { latitude: 51.5081, longitude: -0.0759 },
});

On bare React Native, install expo-modules-core alongside it (that peer is required; the expo package itself is optional and never imported at runtime), follow Expo's bare install guide, set minSdkVersion 26, and apply the Info.plist keys and background modes by hand, since the config plugin only runs under Expo prebuild.

Android (Kotlin)

Availability: ai.mapmap:core 0.3.0, published to Maven Central; no account and no token needed. The 0.2.x releases remain on GitHub Packages only. Full class-by-class API on the Android API reference.

minSdk 26. The SDK requires minSdk 26 (Android 8.0). That is above the default of many app templates; Expo projects in particular default lower, so adding the SDK forces a minSdkVersion bump in your app (an app.json / expo-build-properties change on Expo).

0.3.0 resolves from mavenCentral() with no credentials. Only the 0.2.x releases still need GitHub Packages, which requires authentication even for public packages: a GitHub account with access to the repository and a personal access token with the read:packages scope, supplied via ~/.gradle/gradle.properties (gpr.user / gpr.token) or environment variables.

Declare the repository at the root of the build, not in a module. Projects created from current Android/Gradle templates use dependencyResolutionManagement in settings.gradle(.kts); once that block exists, a repositories {} block inside a module's build.gradle(.kts) is silently ignored (unless the project opts into FAIL_ON_PROJECT_REPOS), and dependency resolution never searches your repo. This especially bites library/RN-module authors: your module cannot supply the repository; the host app's root project must declare it.

kotlin
// settings.gradle.kts: root project, inside dependencyResolutionManagement
dependencyResolutionManagement {
  repositories {
    google()
    mavenCentral() // 0.3.0 and later; nothing else to add
    // Only needed to resolve an 0.2.x release:
    // maven {
    //   url = uri("https://maven.pkg.github.com/Mapmapai/mapmap")
    //   credentials {
    //     username = providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_ACTOR")
    //     password = providers.gradleProperty("gpr.token").orNull ?: System.getenv("GITHUB_TOKEN")
    //   }
    // }
  }
}
kotlin
// app/build.gradle.kts
dependencies {
  implementation("ai.mapmap:core:0.3.0")
}

The offline territory store takes the pinned verifying key at construction (see "The verifying key" below):

kotlin
import ai.mapmap.territory.TerritoryStore

// 64-hex factory verifying key, baked in at build time (e.g. BuildConfig);
// never fetched at runtime.
val store = TerritoryStore(
    rootDir = File(context.filesDir, "territories"),
    verifyingKeyHex = BuildConfig.MAPMAP_FACTORY_PUBKEY_HEX,
)

Every install is verify-then-promote: a package that fails signature verification leaves no trace on disk.

Android Auto

The :car and :car-maplibre modules put the same guidance state on the car's own display: a NavigationManagerBridge for the Car App Library's NavigationManager, plus a CarMapViewHost that projects your MapLibre MapView onto the car surface. Beta; see CarPlay & Android Auto for the entitlement/Play Console gates, a full quickstart and DHU testing.

iOS (Swift)

Availability: installs publicly via Swift Package Manager from the distribution repository Mapmapai/mapmap-ios. Current release 0.6.0; pin from: "0.6.0", never 0.2.0 (0.2.0 shipped without its generated Swift bindings and did not compile; 0.2.1 is the fix, same binary core).

The SDK is MapMapKit: a Swift package whose FFI layer is a prebuilt binary XCFramework (UniFFI bindings over the same Rust core as Android), referenced as a checksummed binaryTarget pinned per release. Add the MapMapKit product to your target; an optional MapMapValhalla product adds the on-device Valhalla routing engine (iOS 16.4+).

swift
// Package.swift
dependencies: [
  .package(url: "https://github.com/Mapmapai/mapmap-ios", from: "0.6.0"),
],
// In your target's dependencies:
.product(name: "MapMapKit", package: "mapmap-ios"),
.product(name: "MapMapValhalla", package: "mapmap-ios"), // opt-in on-device routing
swift
import MapMapKit

let territoryDir = FileManager.default
    .urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
    .appendingPathComponent("territories")

// 64-hex factory verifying key, pinned at build time.
let store = TerritoryStore(rootDir: territoryDir, verifyingKeyHex: mapMapFactoryPubkeyHex)

Requires iOS 16.4 and Swift tools 5.9.

CarPlay

The optional MapMapCarPlay product orchestrates the CarPlay side end to end: the CPMapTemplate root, browse → preview → navigate activity states and the CPNavigationSession lifecycle, driven from the same NavigationEngine output. Beta; see CarPlay & Android Auto for the required Apple entitlement, a full quickstart and CarPlay Simulator testing.

The verifying key: trust anchor for offline packages

Territory packages (the offline maps the mobile SDKs consume; see territories) are ed25519-signed by the factory signing key, and each app pins the factory's 64-hex verifying key (the public half of that pair) so a tampered or substituted package is rejected on device. The pinning call is the TerritoryStore constructor shown in the Android and iOS sections above: bake the key in at build time (a build config field or bundled resource); never fetch it over the same channel as the packages it validates.

Where the key comes from: the hosted channel's verifying key is published in the territories docs; it is a public key, safe to embed in any app; self-host operators generate their own key pair with snfactory keygen and pin their own public key, which is exactly what makes a deployment yours: it runs entirely on your infrastructure.

The web SDK streams tiles from the API rather than verifying packages in the browser, so pinning applies to the mobile SDKs.

Licensing: per vehicle or per MAU

The on-device SDK is licensed per vehicle per month for fleets. If you are building an app rather than running a fleet, the same SDKs can be metered per monthly active user instead: your app sends an opaque X-MapMap-User header on its gateway requests (8–128 printable ASCII characters; hash an install id; never send PII, and the gateway hashes the value again before storing it), and the gateway counts distinct identifiers per calendar month. No header means no MAU counting, which is the normal state for server-to-server API use. The SDKs do not yet set this header automatically; send it from your app's networking layer:

sh
curl -fsS -H "Authorization: Bearer $API_KEY" \
  -H "X-MapMap-User: 9f2c4a1e8b7d3f60" \
  "$BASE/route/v1/truck/1.3134,51.1279;-1.8904,52.4862"

Billing is fail-open by design: when a new user cannot be covered by your prepaid balance the request is never refused; the account is marked mau_overdrawn for the month (visible on GET /v1/keys/self) and the gateway keeps serving, so your users are never interrupted by your account's billing state. Numbers on pricing.

Next steps