# 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](/pricing)).

**Status:** the npm packages are live:
[`@mapmap/maps`](https://www.npmjs.com/package/@mapmap/maps),
[`@mapmap/core`](https://www.npmjs.com/package/@mapmap/core) and
[`@mapmap/react-native`](https://www.npmjs.com/package/@mapmap/react-native)
install from npm today. The iOS package installs publicly via Swift Package
Manager from the distribution repository
[`Mapmapai/mapmap-ios`](https://github.com/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](/playground); the hosted gateway at `https://api.mapmap.ai` is
live ([sign up](/signup) for a key), and the same gateway runs from the
[self-host distro](/docs/self-host); base URL, auth and quotas are defined
in [conventions](/docs/conventions).

## Compatibility

| SDK | Package | Version | Platform floor | Peer requirements |
| --- | --- | --- | --- | --- |
| Web maps | `@mapmap/maps` | 0.8.0 | Node ≥ 18 to build; ESM only | `maplibre-gl` ≥ 4 < 6, `pmtiles` ≥ 3 < 5 |
| Core (WASM) | `@mapmap/core` | 0.3.0 | Node (the npm artefact is the Node build) | none |
| React Native | `@mapmap/react-native` | 0.3.1 | iOS 16.4, **minSdk 26**; Expo SDK 52+, or bare React Native 0.74+ | `expo-modules-core`, `react`, `react-native`; `expo` ≥ 52 optional |
| Android | `ai.mapmap:core` | 0.3.0 | **minSdk 26**, JDK 17 to build | none |
| iOS | `MapMapKit` ([`Mapmapai/mapmap-ios`](https://github.com/Mapmapai/mapmap-ios)) | 0.6.0 | iOS 16.4, Swift tools 5.9 | none |

> **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](https://www.npmjs.com/package/@mapmap/maps).

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](/docs/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](/docs/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](/docs-videos/directions-multistop.mp4)

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](/docs/maps#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](/docs/api-reference#places-bring-your-own-places).
`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](/docs/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.

### Navigation camera

`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](/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](https://www.npmjs.com/package/@mapmap/core).

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](https://www.npmjs.com/package/@mapmap/react-native), **0.3.1**.

An [Expo module](https://docs.expo.dev/modules/overview/) 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](https://docs.expo.dev/bare/installing-expo-modules/),
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](/docs/android).

> **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](/docs/car) 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`](https://github.com/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](/docs/car) 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](/docs/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](/docs/territories#the-hosted-channels-verifying-key);
it is a public key, safe to embed in any app; [self-host](/docs/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](/pricing).

## Next steps

- [Android API reference](/docs/android): every public class and method of `ai.mapmap:core`
- [File formats](/docs/formats): channel index, package layout, `.drive.jsonl` replay corpus
- [Quickstart](/docs/quickstart): issue an `snk_` key and route a truck in two calls
- [Conventions](/docs/conventions): base URL, auth, error envelope, ADR tunnel codes
- [Playground](/playground): try routing with zero install
- [Self-host](/docs/self-host): run the gateway and map factory yourself
