# Migrating from Mapbox: what changes and what does not

> **Note, updated 3 August 2026.** Three things below have changed. Geocoding is live on the hosted gateway, so `/geocode` and `/geocode/reverse` no longer answer `501`. `GET /geocode` also takes a hard `bbox` filter now. And the MCP server exposed twelve tools when this was written; it exposes **32** today. See [/docs/api-reference](/docs/api-reference) and [/docs/mcp](/docs/mcp) for the current surface.

Most of a Mapbox Directions integration moves to MapMap by changing a base URL and a key. Our compatible endpoint is `GET /route/v1/{profile}/{coordinates}` and it answers in the OSRM route shape. Ask for `steps=true`, `voice_instructions=true` and `banner_instructions=true` and each step carries `voiceInstructions` and `bannerInstructions` in the Mapbox shape, so a navigation UI written against those field names parses our JSON with no change to its parsing layer. The work that is left is narrow and knowable: a different error model, different tile and style URLs, and a map client swap if you are not already on MapLibre. This post is the honest version, limits included.

## What moves with a URL swap and a key swap

Routing, matrices, isochrones, map matching and optimisation each have a direct counterpart on the MapMap gateway, and authentication is the only header that changes.

| What you use today | MapMap endpoint |
|---|---|
| Directions | `GET /route/v1/{profile}/{coordinates}` (compatible) or `POST /route` (native) |
| Matrix | `POST /matrix` |
| Isochrone | `POST /isochrone` |
| Map Matching | `POST /trace_route`, `POST /trace_attributes` |
| Geocoding | `GET /geocode`, `GET /geocode/reverse` |
| Optimized Trips | `POST /optimise` (alias `/optimize`) |
| Styles | `GET`/`POST /styles`, `GET /styles/{id}.json` |
| Tiles | `GET /tiles/{territory}/{z}/{x}/{y}.mvt` |

Keys are self-served in one call, card-free, with no sales conversation in front of them:

```bash
curl -fsS -X POST https://api.mapmap.ai/v1/keys \
  -H "Content-Type: application/json" \
  -d '{"email": "you@example.com", "accept_tos": true}'
```

That returns `201` with a one-time `snk_` key: provisional, 1,000 calls, valid 72 hours. Verifying the email upgrades the same key to the free tier at 50,000 included calls per calendar month, commercial use allowed. Then route:

```bash
export BASE=https://api.mapmap.ai
export API_KEY=snk_...

curl -fsS "$BASE/route/v1/driving/-0.1276,51.5072;-1.8904,52.4862?steps=true&overview=full" \
  -H "Authorization: Bearer $API_KEY"
```

Bare coordinate pairs are `lon,lat`, distances are metres and durations are seconds. The key travels as `Authorization: Bearer snk_…`, or as `?api_key=snk_…` for URL-only contexts like a style URL handed to MapLibre. The response is the OSRM shape you already parse: `code`, `routes[]` with `distance`, `duration`, `geometry` and `legs`, and `waypoints[]` with `[lon, lat]` locations. Geometry is an encoded polyline at precision 5 on this endpoint unless you ask for `geometries=geojson`.

## Do voice and banner instructions still survive the move?

Yes, and that is the single thing that saves the most migration time. Ask for them with three query parameters, all of which require `steps=true`:

| Param | Effect |
|---|---|
| `steps=true` | Required. Per-step manoeuvres, the thing instructions attach to |
| `voice_instructions=true` | Adds `voiceInstructions`: plain text, an SSML form, a distance trigger |
| `banner_instructions=true` | Adds `bannerInstructions`, with lane diagrams where OSM carries `turn:lanes` |
| `language` | BCP 47 narration, for example `en-GB`. Also sets the route's `voiceLocale` |

The field names are Mapbox's on purpose: `distanceAlongGeometry`, `announcement`, `ssmlAnnouncement`, `primary`, `secondary`, `sub`, `components`. Match on those literal strings. The reasoning behind copying the shape rather than improving it is in [voice and banner instructions in the Mapbox shape](/news/turn-by-turn-instructions), which also names the one gap: we emit the pure OSRM shape, so `maneuver.instruction` is not populated. If your UI reads that field directly it will find nothing there, and it is the most common thing to trip a port.

## What actually changes: the error model

Errors are RFC 9457 `application/problem+json` with a stable `type` URN, `urn:sn-gateway:problem:<slug>`, and there are two deliberate exceptions you must code for.

```json
{
  "type": "urn:sn-gateway:problem:quota-exceeded",
  "title": "Monthly quota exceeded",
  "status": 429,
  "detail": "monthly quota exceeded (50000/50000)",
  "quota": 50000,
  "used": 50000
}
```

Dispatch on `type`, never on `detail`. There is no `instance` field. The exceptions:

- **The compatible URL endpoint keeps its own envelope.** Routing errors there are the OSRM `{"code": …}` shape (`InvalidQuery`, `InvalidValue`, `NoRoute`, `NoSegment`, always `400`), because compatible clients dispatch on `code` and breaking that would defeat the point of the endpoint. Auth and quota errors on that path still use `problem+json`.
- **`402` is not problem+json.** It is plain JSON: either the x402 machine-payment wire format (`{x402Version, error, accepts[], instructions}`), or a download-allowance pointer on territory layer downloads.

The rule of thumb is worth internalising: `429` means stop or wait, `402` means there is a way to pay. A 4xx response is never charged, though it still counts against the monthly quota. The full slug table is in the [API reference](/docs/api-reference).

## Where do tiles and styles move to?

Tiles move to `/tiles/{territory}/...` and styles to `/styles`, and both are MapLibre-native, so the client-side change is a URL.

```html
<!-- In production, pin an exact maplibre-gl version and add SRI
     (integrity="sha384-…" crossorigin="anonymous"), or bundle it via npm. -->
<link href="https://unpkg.com/maplibre-gl@5/dist/maplibre-gl.css" rel="stylesheet" />
<script src="https://unpkg.com/maplibre-gl@5/dist/maplibre-gl.js"></script>

<!-- The container needs an explicit height, or the map renders zero pixels tall. -->
<div id="map" style="height: 100vh"></div>
<script>
  const map = new maplibregl.Map({
    container: "map",
    style: "https://api.mapmap.ai/tiles/uk/style.json?api_key=snk_…",
    center: [-1.5, 52.6], // lon, lat
    zoom: 6,
  });
</script>
```

`GET /tiles/{territory}/tiles.json` gives you TileJSON 3.0, and `GET /tiles/{territory}/style.json` a ready-to-render MapLibre style. All three are metered at the Standard price class. Hosted styles are versioned and immutable: `POST /styles` creates version 1 and returns a `style_url` you can cache forever, `POST /styles/{id}` publishes the next version. Style reads are public and unmetered so browser clients can reference them by bare URL. Custom Mapbox styles do not port automatically. You rebuild the look as a theme document, either by hand or in Studio, against 19 palette slots and a fixed set of layer ids. The full reference is in the [maps guide](/docs/maps).

One thing that is not optional: map data derives from OpenStreetMap, so the Mapbox wordmark comes out and "© OpenStreetMap contributors" goes in. Our compiled styles carry the attribution structurally, and theme validation rejects any theme that tries to drop it.

## How much does the map client itself change?

If you are already on MapLibre GL JS, very little. `@mapmap/maps` is a wrapper over MapLibre GL JS, not a fork: MapLibre does every pixel of the rendering, `maplibre-gl` stays a peer dependency your app owns, and `map.map` hands you the raw `maplibregl.Map` when our helpers stop being enough.

```sh
npm install @mapmap/maps maplibre-gl pmtiles
```

The wrapper adds the MapMap style, the `pmtiles://` protocol handler, typed routing (`RouteLayer`), a course-up chase cam (`NavigationCamera`), a position puck and guidance banners. Everything it adds is additive. Delete the wrapper and you still have a working MapLibre app, which is the property you want in anything you are migrating onto.

## Why teams actually move

Four reasons, and none of them is the endpoint compatibility. Compatibility is what makes the move cheap, not what makes it worth doing.

| Reason | The specific |
|---|---|
| Self-host | The identical stack runs on your hardware from one Docker Compose file, air-gap capable, on a commercial licence with no sales cycle |
| Truck and ADR | Height, width, length, weight, `hazmat` and `tunnel_code` enforced in costing rather than post-filtered, plus `POST /adr/check` for the ADR 8.6.4 decision with no routing |
| Agent-native | An agent can discover, price and key itself with no human in the loop, and 402 responses already speak the x402 machine-payment wire format. Twelve MCP tools at `https://mcp.mapmap.ai/mcp` |
| Prepaid-only pricing | Standard calls at 0.05p, 0.03p beyond 1M/month, 0.02p beyond 10M. Premium (truck/ADR) at 1p, 0.7p, 0.4p across the same bands. No credit means `402` or `429`, never a surprise invoice |

A matrix bills one call per started block of 25 elements, capped at 10,000 elements per request. An isochrone bills one call per contour, maximum 10. An optimisation is a flat 10 calls. As of July 2026 we are the only navigation API offering self-serve ADR tunnel compliance at published per-call prices. `/pricing.json` is always authoritative over any price quoted here. The self-hosting case, including what it costs you in ops burden, is argued in [sovereign navigation](/news/sovereign-navigation).

## What Mapbox has that we do not

Server-side raster rendering and live geocoding on the hosted gateway are the two real gaps today. Read this before you commit a sprint, because a gap found halfway through a migration has already cost you the week.

- **No server-side raster or static image rendering.** Clients render the vector tiles themselves. If you generate static map PNGs on a server today, there is no counterpart here and you will need to render them yourself.
- **Geocoding is not live on the hosted gateway.** `/geocode` and `/geocode/reverse` answer `501` (`urn:sn-gateway:problem:geocoding-not-enabled`) until a territory index is wired in. Self-hosters can enable it now with `SN_GEOCODE_DIR` or a `SN_PHOTON_URL` proxy. Even then, `GET /geocode` exposes `q`, `limit`, `lang` and `bias` and nothing else, so there is no counterpart to a Mapbox search-box autocomplete session.
- **`POST /optimise` is enabled per deployment** and answers `503` where the operator has not configured a VROOM sidecar. That is deliberate, not a bug.
- **ADR restrictions apply only on `POST /route` and `POST /optimise`.** The analysis endpoints ignore an `adr` object, so do not lean on `/matrix` or `/isochrone` for dangerous-goods compliance.
- **No self-serve key revoke endpoint yet.** Self-hosted deployments revoke via `DELETE /admin/keys/{id}`; on the hosted gateway, ask us.
- **We are in beta.** Things will change, and we will tell you when they do.

## Try it

Point your existing OSRM-shaped client at `https://api.mapmap.ai` with a key from `POST /v1/keys` and see what breaks, which for most Directions integrations is nothing. If a coding agent is doing the port, install the skill that carries this knowledge as context:

```sh
npx skills add Mapmapai/mapmap-agent-skills --skill mapmap-migrate-from-mapbox
```

`mapmap-migrate-from-mapbox` covers the endpoint mapping, `pk.…` tokens becoming `snk_` keys, the GL JS to MapLibre swap and the same honest gap list above. The other nine modules are described on the [agent skills page](/docs/skills).

Routing derives from OpenStreetMap, so credit © OpenStreetMap contributors when you render it.
