Hosted vector tiles and styles, scoped to territories
The MapMap gateway serves Mapbox Vector Tiles at GET /tiles/{territory}/{z}/{x}/{y}.mvt, a TileJSON 3.0 document at /tiles/{territory}/tiles.json and a ready-to-render MapLibre style at /tiles/{territory}/style.json. All three are metered per request at the Standard price class, 0.05p per call. The tiles are cut from the same per-territory archives the routing engine and the signed offline packages are built from, so the map you render and the route you get back agree about the world.
What do the tile endpoints return?
Three endpoints, one territory archive behind them.
| Method | Path | What it returns |
|---|---|---|
| GET | /tiles/{territory}/{z}/{x}/{y}.mvt | One Mapbox Vector Tile (.pbf also accepted), gzip-encoded, strong ETag, immutable caching. An in-range tile with no data is 204 |
| GET | /tiles/{territory}/tiles.json | TileJSON 3.0: zoom range, bounds and centre read from the territory archive, plus OSM attribution |
| GET | /tiles/{territory}/style.json | A MapLibre GL style over the territory's vector source |
Territory slugs come from GET /territories. Start with tiles.json, because it tells you the zoom range and bounds before you render anything:
export BASE=https://api.mapmap.ai
export API_KEY=snk_…
curl -fsS "$BASE/tiles/uk/tiles.json" -H "Authorization: Bearer $API_KEY"
{
"tilejson": "3.0.0",
"name": "uk",
"scheme": "xyz",
"tiles": ["https://api.mapmap.ai/tiles/uk/{z}/{x}/{y}.mvt?v=1720981132"],
"minzoom": 0,
"maxzoom": 14,
"bounds": [-8.65, 49.86, 1.77, 60.86],
"center": [-2.0, 54.0, 6.0],
"attribution": "© OpenStreetMap contributors"
}
Keep the ?v= discriminator in any URL you pass through. Tile bytes are served immutable for 24 hours but the URLs are not content-addressed, so a republished archive would otherwise race stale caches. The discriminator busts every cache atomically when the operator swaps the archive.
Individual tiles carry a strong BLAKE3 ETag ("b3-…"), so a conditional GET with If-None-Match short-circuits to 304. A tile that is in range but holds no data returns 204 No Content, which is the empty-tile convention MapLibre renders as blank. Unknown territory, out-of-range coordinates, a bad extension or a deployment with no tiles staged all return 404.
Why are tiles scoped to a territory rather than a global mesh?
Because a territory is the unit we build, sign and ship, and pretending otherwise would mean two sources of truth. Our map factory builds one archive per territory from OSM extracts, and that archive feeds three consumers: the routing engine, the signed offline package a device downloads, and these tile routes.
The gateway reads tiles from a plain, seekable PMTiles v3 archive staged at SN_TILES_DIR/<territory>.pmtiles. That is deliberately not the update-channel tree that territory packages use. The channel wraps each PMTiles inside a .pmtiles.tar.zst layer blob so devices download and verify one atomic signed artefact, and a compressed tarball is not randomly seekable. Per-tile serving needs random access, so the operator stages the same Planetiler output uncompressed alongside it. Same bytes, two access patterns, one build.
The practical consequence for you is that a territory scope is honest about coverage. If GET /territories does not list it, there are no tiles for it, and you find that out at 404 rather than by rendering a map with a hole in it. Territory base maps do include ocean and land at all zooms: the factory stages the global sources (OSM ocean water polygons, Natural Earth, lake centrelines) into every territory build, so a coastline does not stop at the extract boundary.
How do you render a map from this?
Point MapLibre GL at the style URL and you are done. The container div must have an explicit height, or the map renders zero pixels tall and the page looks silently blank:
<!-- 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>
<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>
Metered endpoints take the key as Authorization: Bearer (preferred) or as an ?api_key= query parameter. The query form exists for URL-only contexts exactly like a style URL handed to MapLibre, which has nowhere to put a header. For a typed wrapper with routing helpers, use @mapmap/maps, covered in the web maps SDK post.
Where do glyphs and sprites come from?
From the gateway, public and unmetered, with immutable caching. Labels and icons are not billed.
| Path | What it serves |
|---|---|
GET /fonts/{fontstack}/{range}.pbf | SDF glyph ranges for the bundled fontstacks |
GET /sprite/sprite[@2x].json and .png | The first-party sprite sheet: marker, dot, arrow, lane diagrams, road shields, tunnel-restricted |
The bundled fontstacks are Noto Sans Regular (plus Bold and Italic), Barlow Regular, Fira Sans Regular, IBM Plex Sans Regular, Inter Regular, Lato Regular, Montserrat Regular, Noto Serif Regular, Nunito Regular, Open Sans Regular, Rubik Regular, Source Sans 3 Regular and Work Sans Regular, all SIL Open Font Licence, static Regular instances. Keep a theme's fonts.regular on one of these or the labels silently drop. If you write your own style by hand instead of compiling a theme, point its glyphs and sprite fields at these same routes.
One gotcha worth stating plainly: compiled styles default their glyph URL to https://fonts.mapmap.ai/{fontstack}/{range}.pbf. That is live and serving, and it is also a call to our infrastructure. On a self-hosted or air-gapped deployment, set the theme's glyphs field to your own gateway's route, $BASE/fonts/{fontstack}/{range}.pbf, or your labels will quietly depend on us. Those routes serve when SN_MAP_ASSETS_DIR is set; tiles need SN_TILES_DIR and hosted styles need SN_STYLES_DIR.
Which hosted style calls are metered?
Reads are free, writes are metered. Styles are versioned and immutable: publishing writes a new version, and a versioned style URL can be cached forever.
| Method | Path | Auth | Metered |
|---|---|---|---|
| GET | /styles | none | no |
| GET | /styles/{id}.json | none | no |
| GET | /styles/{id}@{version}.json | none | no |
| GET | /styles/{id}/theme | none | no |
| POST | /styles | key | yes, Standard class |
| POST | /styles/{id} | key | yes, Standard class |
Reads being public and unmetered is the point: a compiled style can be referenced by bare URL from a browser map client without a key in the page. Creating a style is one call:
curl -fsS -X POST "$BASE/styles" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Midnight Fleet",
"theme": {
"name": "midnight-fleet",
"base": "dark",
"palette": { "water": "#0b2038" }
}
}'
{
"id": "midnight-fleet-9f3a2c",
"version": 1,
"style_url": "https://api.mapmap.ai/styles/midnight-fleet-9f3a2c@1.json",
"theme": { "name": "midnight-fleet", "base": "dark", "palette": { "water": "#0b2038" } }
}
The id is the name's slug plus six hex characters, so same-named styles never collide. POST /styles/{id} publishes the next version, and its body is a wrapper with the full theme document under "theme", not a bare theme and not a diff. POSTing a bare theme is a 400. An invalid theme is a 422 whose problems list names the offending slot or layer and the accepted values, so an agent can self-correct without a human reading the docs. The theme document itself, its 19 palette slots and the browser editor over them are covered in the Map Studio post.
What this costs
Tiles, tiles.json, style.json and the two style publishes all bill 0.05p a call at the Standard class. Glyphs, sprites and every /styles read are free.
| Item | Price |
|---|---|
Each tile, tiles.json or style.json request | 0.05p (Standard class) |
POST /styles and POST /styles/{id} | 0.05p (Standard class) |
Glyphs, sprites, all /styles reads | free, unmetered |
| Standard class beyond 1M calls/month | 0.03p |
| Standard class beyond 10M calls/month | 0.02p |
| Free tier | 50,000 calls/month after email verification, commercial use allowed |
Tile requests are not charged against the offline download byte allowance. That gate applies only to bulk territory-package downloads.
What this does not do
Four real limits, stated before you find them.
- Every tile is a call. Metering is per request, so a map browsing session bills for each tile it pulls, not once per page view. The 24-hour immutable caching and the
304path onIf-None-Matchblunt this a lot on repeat views, but the first load of a busy pan bills honestly. Budget in tiles, not in sessions. - No server-side raster rendering. There is no
/tiles/{territory}/{z}/{x}/{y}.png. It needs a headless GL stack (styling, glyph and sprite atlases, GPU or Mesa software rendering) that dwarfs the gateway, and it would be a separate service. Clients render the vector tiles themselves. - These routes are enabled per deployment. They answer
404until the operator stages the tile and asset directories. The hosted gateway has them enabled, with worldwide coverage from the planet build; GET /territories lists exactly what a deployment publishes. - Compiled styles default their glyphs to our domain. Self-hosting is not complete until you override the
glyphsfield, as above.
Attribution is structural, not a checkbox
Every compiled style carries © OpenStreetMap contributors © OpenMapTiles on its tile source, and validation rejects any theme that drops either. There is no theme field that can remove them. The OSM credit covers the data under ODbL, the OpenMapTiles credit covers the tile schema under CC-BY 4.0. Anything you render or republish must credit © OpenStreetMap contributors with a link to openstreetmap.org/copyright. The compiled styles carry this for you, which is the intended outcome: nobody has to remember.
Try it
Get a key, then fetch tiles.json for uk and paste the style URL into MapLibre. The full reference, including the theme document, the palette slots, the styleable layer ids and the 3D buildings caveat on mobile, lives in the maps documentation. Every call above works identically against a self-hosted gateway.
