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

news / migrating-from-mapbox · raw .md
Platform7 min readMapMap engineering
Two networks of identical road geometry side by side in different line conventions, with registration marks bridging between them as one overlays the other.

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 and /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 todayMapMap endpoint
DirectionsGET /route/v1/{profile}/{coordinates} (compatible) or POST /route (native)
MatrixPOST /matrix
IsochronePOST /isochrone
Map MatchingPOST /trace_route, POST /trace_attributes
GeocodingGET /geocode, GET /geocode/reverse
Optimized TripsPOST /optimise (alias /optimize)
StylesGET/POST /styles, GET /styles/{id}.json
TilesGET /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:

ParamEffect
steps=trueRequired. Per-step manoeuvres, the thing instructions attach to
voice_instructions=trueAdds voiceInstructions: plain text, an SSML form, a distance trigger
banner_instructions=trueAdds bannerInstructions, with lane diagrams where OSM carries turn:lanes
languageBCP 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, 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.

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.

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.

ReasonThe specific
Self-hostThe identical stack runs on your hardware from one Docker Compose file, air-gap capable, on a commercial licence with no sales cycle
Truck and ADRHeight, 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-nativeAn 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 pricingStandard 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.

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.

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