MapJet API
MapJet is a shared app of the Substrio stack: geo services (IP location, address
search and geocoding, city centres, time zones) and vector map tiles for any
Substrio account. You call
it with an account key of your own account, scoped to the mapjet app. Every call
counts against your account's MapJet plan.
Get a key
MapJet's owner keeps the app open to other accounts' keys
(account.app.open.set, PUT /v1/_accounts/{id}/apps/mapjet/keys-open with
{"open": true}). With that, any account makes its own key for it:
curl -X POST https://<platform host>/v1/_accounts/<your account id>/keys \
-H "Authorization: Bearer <your account key>" \
-H "Content-Type: application/json" \
-d '{"name": "mapjet-prod", "apps": ["mapjet"], "mode": "live"}'
The answer holds the key's secret once. The key works only at MapJet, as a
customer of the app: it reads nothing else of your account. Give it its own
limits if you want them (requests_per_minute, request_budget; see
account.key.update). MCP: substrio_account_key_create with the same fields.
Call it
Send the key as a bearer token, at MapJet's host or with
X-Substrio-Tenant: mapjet:
curl https://<platform host>/lookup/8.8.8.8 \
-H "X-Substrio-Tenant: mapjet" \
-H "Authorization: Bearer <key>"
| Route | What it answers | Charged |
|---|---|---|
GET /lookup/:ip |
The place of an IPv4 or IPv6 address (country, region, city, coordinates) and its time zone | 1 |
POST /lookup |
{"ips": [...]}, at most 100: each address's place |
1 an address |
GET /timezone/:lat/:lon |
The IANA time zone at a point | 1 |
GET /reverse/:lat/:lon |
The US city nearest a point (its centre, state, distance in km and time zone); 404 NO_PLACE past 2 degrees |
1 |
GET /city/geocode?city=&state= |
A US city's centre | 1 |
GET /address/autocomplete?q=&state=&limit= |
US address suggestions as you type | 1 |
GET /address/geocode?q=&ip= |
A US address's coordinates (ip biases toward the caller's area) |
1 |
GET /tiles/:z/:x/:y |
A vector map tile (Mapbox Vector Tile, gzip), 0 <= z <= 15; 204 where the set has none |
1 tile |
GET /tiles.json |
The tileset as TileJSON 3.0.0: its tile URL, zooms, bounds and attribution | 0 |
GET /styles |
The map themes: id, title, description, accent | 0 |
GET /styles/:theme |
A theme's MapLibre style (light, dark, streets, hybrid, terrain, mono) |
0 |
GET /fonts/:stack/:range |
The styles' glyphs (Noto Sans), as MapLibre asks for them | 0 |
GET /lib/:pkg/:file |
MapLibre GL JS 5.6.0 and MapJet's markers, for a <script> tag (no key) |
0 |
GET /site/tiles/:z/:x/:y, GET /site/fonts/:stack/:range |
The tiles and glyphs of mapjet.net's own map (no key), Las Vegas only; 204 off it |
0 |
GET /maps |
Your account's saved maps | 0 |
POST /maps, PUT /maps/:id, DELETE /maps/:id |
Save a map (a theme, colours, options, a night theme), change it, delete it | 0 |
GET /maps/:id/style, GET /maps/:id/night |
A saved map's day and night styles | 0 |
GET /maps/:id/layers/:layer |
A saved map's data layer, its GeoJSON as saved | 0 |
GET /usage |
Your account, your plan and what you used today and this month | 0 |
A request MapJet refuses (400) is not charged. Tiles count on their own
counter, tiles; every other route counts on requests.
Maps
Give MapLibre GL JS a theme's style and send the key with every request to MapJet:
const MAPJET = 'https://<platform host>';
const headers = { 'X-Substrio-Tenant': 'mapjet', Authorization: 'Bearer <key>' };
const map = new maplibregl.Map({
container: 'map',
style: `${MAPJET}/styles/dark`,
transformRequest: (url) => (url.startsWith(MAPJET) ? { url, headers } : { url }),
});
The themes: light (Daylight), dark (Midnight), streets, hybrid, terrain
and mono (Ink). Every map shows the credit the style carries (MapJet, and "© OpenStreetMap
contributors", which the data's licence requires).
Saved maps
Make a map of your own in the admin's Studio (or by POST /maps), then load it
by its id. A change to the map is what every site that loads the id draws at its
next load, with no deploy of yours.
{ "name": "Fleet", "base": "light", "night": "dark",
"overrides": { "accent": "#00aa66", "highway": "#ff5500" },
"options": { "density": "rich", "language": "en", "buildings": "3d", "pois": true, "rail": true, "boundaries": true } }
base,night: a theme; the night style carries your brand colours (accent,highway,major,park,boundary) over the night theme's own land, water and text.overrides:accent,land,water,park,highway,major,building,text,boundary, each#rrggbb. Labels are held readable (4.5 : 1 on the land) whatever you pick.options.density:none,minimal,normal,rich;language:localoren,es,fr,de,it,pt,nl,pl,ru,tr,ar,hi,ja,ko,zh;buildings:3d,flat,none.layers: your own data over the map, drawn in the order given, each{ "name", "color": "#rrggbb", "clusters": true, "heatmap": true, "data": <GeoJSON> }.datais a FeatureCollection (points, lines, polygons). The save answers each layer'sid; send{ "id", "name", "color", ... }withoutdatato keep a layer as it is and change only how it is drawn, or newdatafor a new layer.clustersgroups nearby points into counted circles as the map zooms out, andheatmapdraws their density; both take points, and both run in the viewer's browser. The style loads each layer fromGET /maps/:id/layers/:layer, once: a layer's URL never changes its data, so the browser keeps it.
Follow the viewer's day or night setting:
const dark = matchMedia('(prefers-color-scheme: dark)');
const styleUrl = () => `${MAPJET}/maps/<id>/${dark.matches ? 'night' : 'style'}`;
const map = new maplibregl.Map({ container: 'map', style: styleUrl(), transformRequest });
dark.addEventListener('change', () => map.setStyle(styleUrl(), { diff: true }));
Markers
MapJet's markers draw a place in the map's theme accent: pin for an exact
place (it drops in and pulses at its foot), approximate for an area a place
may be in, such as an IP's city (a sonar ping at the true radius in metres, at
every zoom). The animations run on the compositor, and stop for a visitor who
asks for reduced motion.
const { pin, approximate } = await import(`${MAPJET}/lib/[email protected]/mapjet-markers.js`);
map.on('load', () => {
pin(map, [-115.1398, 36.1699]);
approximate(map, [-115.14, 36.17], 5000).setRadius(2500);
});
The same module has spotlight(map, [lng, lat], metres, { dim }), which dims
the world around a place, and tour(map, stops, { loop, onEnd }), a camera
flight through { center, zoom, bearing, pitch, duration, hold } stops (each
stop without the flight for a viewer who asks for reduced motion).
Plans
| Plan | Requests | Tiles |
|---|---|---|
free (every account starts here) |
1,000 a day, 10,000 a month | 50,000 a month |
starter |
1,000,000 a month | 5,000,000 a month |
growth |
10,000,000 a month | 50,000,000 a month |
unlimited |
no limit | no limit |
Past your plan, a call answers 429, nothing is counted, and the body says when it
frees:
{"error": "Too Many Requests", "code": "USAGE_LIMIT", "counter": "requests",
"limit": 1000, "used": 1000, "period": "day", "resets_at": 1798761600}
Other answers
| Status | Code | When |
|---|---|---|
401 |
CREDENTIAL_REQUIRED (the platform's) |
No key, or a key that is not an account key (an app's own key) |
401 |
(the platform's) | A key that is revoked or not scoped to MapJet |
401 |
ACCOUNT_KEY_REQUIRED |
A caller the platform lets through with no account behind it (a signed-in person) |
429 |
USAGE_LIMIT |
Past your plan (above) |
429 |
(the platform's) | Past your key's own requests_per_minute or request_budget |
400 |
QUERY_NAME_REPEATED, PATH_NOT_DECODABLE |
A query name sent twice; a path that does not percent-decode |
413 |
BODY_TOO_LARGE |
A request body past 16 MiB (a map save with its layers, a lookup batch); bodies past 1 MiB and chunked ones are read whole up to it |
400 |
TILE_INVALID |
A tile off the grid: z, x, y not integers with 0 <= z <= 15 and 0 <= x, y < 2^z |
404 |
NO_TILESET |
GET /tiles.json before any tiles are imported |
Data
IP places: DB-IP's IP to City Lite (CC BY 4.0, "IP Geolocation by DB-IP"). US addresses: Netsyms' AddressDatabase. Time zones: tz-lookup's tables (built from timezone-boundary-builder). Map tiles: MapJet's own, served from MapJet, built from OpenStreetMap (ODbL, "© OpenStreetMap contributors").