Documentation menu
API conventions
The cross-cutting rules that every endpoint follows: base URL, authentication,
coordinate order, units, quotas, and the error envelope. Other pages link here
rather than repeating any of it. The machine-readable contract is always
GET /openapi.json on your deployment; where prose and OpenAPI disagree,
OpenAPI wins.
Base URL and availability
The hosted gateway's base URL is https://api.mapmap.ai.
Status, honestly: the hosted gateway is live: sign up for a key and every endpoint on these pages works today. Everything also runs identically against a self-hosted deployment of the same gateway: same endpoints, same error envelope, same keys. Examples throughout the docs use an exported base so they work against either:
export BASE=https://api.mapmap.ai # or your own deployment's origin
export API_KEY=snk_...
Authentication
Metered endpoints take an API key (prefix snk_) either way:
Authorization: Bearer snk_... # preferred
?api_key=snk_... # query parameter, for clients that
# cannot set headers (e.g. tile URLs)
The Authorization header wins when both are present. A missing, malformed,
unknown, revoked or expired key is a 401 problem
(urn:sn-gateway:problem:unauthorized) whose detail says which.
Unauthenticated endpoints (no key needed):
GET /,GET /health,GET /openapi.json,GET /llms.txt,GET /termsPOST /v1/keys(self-serve signup) andGET /v1/keys/verify(magic link)GET /styles,GET /styles/{spec},GET /styles/{spec}/theme: style reads are public so browser map clients can reference them by bare URL (publishing styles is authenticated and metered)GET /fonts/{fontstack}/{range}andGET /sprite/{file}: glyphs and sprites, fetched by bare URL from compiled styles
GET /v1/keys/self is key-authenticated but free: it consumes no quota, no
credit and no rate limit, so agents can poll their own key state.
/admin/* endpoints (self-host only) use a separate static operator token as
a bearer credential, never an snk_ key.
One optional header: SDKs may send X-MapMap-User to meter monthly active
users on identity-owned keys. It is fail-open: MAU billing can never block
or 402 a request.
Keyless demo access (self-host)
A self-hosted deployment can open a small, unauthenticated demo lane with
SN_DEMO_RPM (requests per minute). It is off by default (0), and is
not enabled on the hosted gateway; there, every metered endpoint needs
a key as above.
When the operator sets SN_DEMO_RPM above 0, requests with no API key
to a fixed demo-safe whitelist are admitted instead of 401:
GET /route/v1/*(OSRM-compatible routing, all profiles)GET /geocodeandGET /geocode/reverse
Every other endpoint (and any non-GET method, including POST /route)
still returns 401 without a key. Demo traffic is rate-limited by a token
bucket per client IP (the first X-Forwarded-For hop, since Caddy fronts
the gateway, else the peer address) at SN_DEMO_RPM requests/minute; over the
bucket the response is 429 rate-limited with a Retry-After header. Demo
requests are never metered, billed, or drawn against any quota or prepaid
ledger; they exist purely to let a self-hoster expose a public try-it lane.
Keyed requests are unaffected and are always metered to their own identity.
Coordinates and units
Bare coordinate pairs are always lon,lat: in the compatible URL path
(/route/v1/{profile}/{lon},{lat};{lon},{lat}) and in GeoJSON output
(isochrone contours). JSON request bodies use named fields instead:
{"lat": 51.5, "lon": -0.1}.
| Where | Unit |
|---|---|
GET /route/v1/... responses | distance metres, duration seconds |
POST /matrix responses | durations seconds, distances metres (null = unreachable) |
POST /route responses | time seconds; length in the request's units (kilometres unless you ask for miles) |
POST /isochrone contours | time in minutes or distance in kilometres (request) |
| Vehicle dimensions | metres (height_m, width_m, length_m) |
| Vehicle weights | metric tonnes (gross_weight_t, axle_load_t) |
| Vehicle speed | km/h |
| Offline download allowance | MiB per calendar month |
Route geometry is encoded polyline: precision 6 on the native POST /route
response's shape, precision 5 on the compatible GET /route/v1/...
endpoint (matching each format's convention).
Quotas and rate limits
Quotas are per identity (your email), shared across all of that identity's keys; issuing more keys does not multiply the quota.
| Limit | Value |
|---|---|
| Provisional key (before email verification) | 1,000 calls total, expires after 72 h |
| Verified free tier | 50,000 included calls per calendar month (a premium truck/ADR call draws 20; commercial use allowed) |
| Rate limit (self-served keys) | 60 requests per minute |
| Active keys per identity | 5 |
| Key issuances per IP | 3 per day |
Beyond the free tier, calls draw prepaid credit at a per-call price with two
classes: standard (car/van/bus/bike/pedestrian routing, matrix,
isochrone, map matching; from 0.05p/call) and premium (anything
truck/ADR: "costing": "truck", a top-level adr object, "hazmat": true,
/adr/check, or the truck profile / truck query parameters on the
compatible endpoint; from 1p/call). Included volume (the free tier and
plans) is denominated in standard calls: one premium call draws 20 included
calls, matching the price ratio; pay-as-you-go money is always priced by
each class's own schedule. Requests bill by size where size varies: a
matrix bills one call per started block of 25 elements (sources × targets, max 10,000 per request), an isochrone one call per contour (max
10), an optimisation a flat 10 calls, an along-route search like a matrix
on its detour budget (4 × max_detours elements, max 25 detours; plus one
call when it computes the route). Prices step down with monthly volume;
see /pricing.
Money rule: a request answered with a client error (4xx) is never charged; the debit is refunded. It still counts against the monthly quota.
Hitting a limit gives you one of two 429s, or a 402; see the decision
table below.
Errors
Every JSON endpoint answers errors as RFC 9457 application/problem+json,
with one exception: the compatible GET /route/v1/... endpoint answers its
own routing errors (InvalidQuery, InvalidValue, NoRoute, NoSegment,
always 400) in the OSRM {"code": ...} error envelope, because
compatible clients dispatch on code. Auth, quota and payment errors keep
the shared formats even on the compatible path: 401 and 429 are
problem+json, and beyond-tier 402s use the x402 body.
A real body: the verified free tier exhausted by an identity that never bought credit:
{
"type": "urn:sn-gateway:problem:quota-exceeded",
"title": "Monthly quota exceeded",
"status": 429,
"detail": "monthly quota exceeded (50000/50000)",
"quota": 50000,
"used": 50000
}
type is a stable URN you can dispatch on; detail is for humans. Some
problems carry extra machine-readable fields, noted below.
type (urn:sn-gateway:problem: +) | Status | Meaning | Fix |
|---|---|---|---|
unauthorized | 401 | Missing, malformed, unknown, revoked or expired key | Send Bearer snk_...; reissue at POST /v1/keys |
admin-unauthorized | 401 | Wrong admin token (self-host) | Check the operator token |
forbidden | 403 | Key valid but not permitted, e.g. a provisional key attempting a bulk territory-layer download | Verify your email |
bad-request | 400 | Invalid body or parameters | detail says what |
costing-conflict | 400 | Your own costing_options contradict the adr profile; conflicting_fields lists them | Remove the conflicting fields or make them agree |
tos-not-accepted | 400 | Signup without "accept_tos": true; tos echoes the terms URL | Fetch the terms, re-submit with accept_tos: true |
invalid-email | 400 | Rejected email (syntax or disposable domain) | Use a real address |
not-found | 404 | Unknown resource | Check the id/path |
key-cap | 409 | Identity already has the maximum active keys (max_active_keys) | Revoke one or reuse an existing key |
no-identity-ledger | 409 | Admin credit aimed at an operator-issued key | Credit the identity instead |
invalid-theme | 422 | Style theme failed validation; problems lists each offending key | Correct the named slot/layer and resubmit |
optimisation-too-large | 422 | Problem exceeds the fair-use location cap (max_locations, locations) | Split the problem |
quota-exceeded | 429 | Monthly quota exhausted with no payment path (quota, used) | Verify email, top up credit, or wait for the month |
rate-limited | 429 | Per-minute limit fired; retry_after seconds, also sent as a Retry-After header | Back off and retry |
issuance-velocity | 429 | Too many key issuances from your IP today (limit_per_day) | Come back tomorrow |
internal | 500 | Unexpected failure; detail is logged server-side, never leaked | Retry; report if persistent |
upstream-error | 400/502 | Routing backend (or Photon geocoder) rejected the request (400 for an upstream 4xx, 502 otherwise; upstream_status) | Check the request; else retry |
geocoding-not-enabled | 501 | /geocode on a deployment with neither SN_GEOCODE_DIR nor SN_PHOTON_URL (self-host) | Operator sets one of them |
optimisation-not-enabled | 503 | Deployment has no VROOM sidecar configured (self-host) | Operator sets SN_VROOM_URL |
upstream-unavailable | 503 | Routing backend unreachable | Retry |
upstream-timeout | 504 | Routing backend timed out | Retry |
The two 402s (not problem+json)
Two payment errors deliberately use plain application/json bodies instead
of the problem envelope, because machine clients parse them directly:
-
Payment required: the x402 wire format (
x402Version,accepts,error). Returned when a request beyond the free tier cannot be paid: a provisional key past its 1,000 calls, a credited identity whose balance is below the per-call price, or a failedX-PAYMENTverification. Full shape and payment flows: machine payments. -
Download allowance exceeded: an offline territory-layer download would exceed the identity's monthly MiB allowance:
json{ "code": "download_allowance_exceeded", "allowance_mib": 8192, "used_mib": 2100, "message": "offline map download allowance exhausted; upgrade to a plan or the SDK licence for production downloads", "upgrade": "https://api.mapmap.ai/pricing" }
402 or 429?
| You get | When |
|---|---|
429 quota-exceeded | A free-tier identity that has never bought credit exceeds its monthly quota; or an operator-issued key exceeds its quota |
429 rate-limited | Any key exceeds its per-minute rate limit (Retry-After header set) |
402 (x402 body) | A provisional key past its call allowance; a credited identity with insufficient balance; a rejected X-PAYMENT header |
402 (download body) | An offline layer download would exceed the monthly MiB allowance |
Rule of thumb: 429 means stop or wait; 402 means there is a way to
pay: verify your email, top up, or attach an x402 payment.
ADR and tunnel codes
ADR (the European agreement on carriage of dangerous goods by road; tunnel codes B–E restrict which tunnels a hazmat load may use). This section is the canonical reference the other pages link to.
Routing requests declare the load with a top-level adr object
("costing": "truck" required):
{
"adr": {
"dimensions": {
"height_m": 4.0,
"width_m": 2.55,
"length_m": 16.5,
"gross_weight_t": 40.0,
"axle_load_t": 11.5,
"axle_count": 5
},
"tunnel_code": "D",
"hazmat": true
}
}
axle_load_t and axle_count are optional. On the compatible
GET /route/v1/... endpoint the same facts travel as query parameters:
height, width, length, weight, hazmat, tunnel_code.
Semantics, worst-case by design:
"hazmat": false: the tunnel matrix does not apply; every tunnel is permitted (dimensional limits still apply)."hazmat": truewith notunnel_code: treated as the most restrictive non-quantity code (B), since the load's code is unknown."tunnel_code": "(—)": the ADR "no restriction" entry, explicitly allowed through all tunnels.- Quantity- and tank-conditional codes (
B1000C,B/D, ...) are read worst-case: the API cannot know your net explosive mass or whether the goods travel in tanks, so it assumes the restrictive reading.
If your request's own costing_options.truck values disagree with what the
adr profile implies, the request is rejected with a 400 costing-conflict
listing the fields; set them in one place or make them agree.
Tunnel categories (posted on the tunnel, A least to E most restrictive):
| Category | Restricts |
|---|---|
| A | No restrictions for dangerous goods |
| B | Goods which may lead to a very large explosion |
| C | ... or a large explosion, or a large toxic release |
| D | ... or a large fire |
| E | All dangerous goods except UN 2919, 3291, 3331, 3359 and 3373 |
Tunnel restriction codes (assigned to the load, ADR 8.6.4):
| Code | Passage forbidden through |
|---|---|
B | Categories B, C, D and E |
B1000C | Category B above 1,000 kg total net explosive mass per transport unit; always C, D and E |
B/D | Categories B and C when carried in tanks; always D and E |
B/E | Categories B, C and D when carried in tanks; always E |
C | Categories C, D and E |
C5000D | Category C above 5,000 kg total net explosive mass per transport unit; always D and E |
C/D | Category C when carried in tanks; always D and E |
C/E | Categories C and D when carried in tanks; always E |
D | Categories D and E |
D/E | Category D when carried in bulk or in tanks; always E |
E | Category E |
(—) | Nothing; allowed through all tunnels |
To check a load without routing, POST /adr/check takes the same adr
profile plus a tunnel_category ("A"–"E") and returns the entry
decision for that tunnel; see the API reference.
Next steps
- Quickstart: key to dimensioned truck route in two calls
- API reference: every endpoint, truck and ADR parameters
- Analysis endpoints: isochrone, matrix, map matching
- Machine payments: the 402 flows in full, prepaid credit and x402