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

Documentation menu
docs / conventions · raw .md

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:

sh
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:

ini
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 /terms
  • POST /v1/keys (self-serve signup) and GET /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} and GET /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 /geocode and GET /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}.

WhereUnit
GET /route/v1/... responsesdistance metres, duration seconds
POST /matrix responsesdurations seconds, distances metres (null = unreachable)
POST /route responsestime seconds; length in the request's units (kilometres unless you ask for miles)
POST /isochrone contourstime in minutes or distance in kilometres (request)
Vehicle dimensionsmetres (height_m, width_m, length_m)
Vehicle weightsmetric tonnes (gross_weight_t, axle_load_t)
Vehicle speedkm/h
Offline download allowanceMiB 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.

LimitValue
Provisional key (before email verification)1,000 calls total, expires after 72 h
Verified free tier50,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 identity5
Key issuances per IP3 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:

json
{
  "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: +)StatusMeaningFix
unauthorized401Missing, malformed, unknown, revoked or expired keySend Bearer snk_...; reissue at POST /v1/keys
admin-unauthorized401Wrong admin token (self-host)Check the operator token
forbidden403Key valid but not permitted, e.g. a provisional key attempting a bulk territory-layer downloadVerify your email
bad-request400Invalid body or parametersdetail says what
costing-conflict400Your own costing_options contradict the adr profile; conflicting_fields lists themRemove the conflicting fields or make them agree
tos-not-accepted400Signup without "accept_tos": true; tos echoes the terms URLFetch the terms, re-submit with accept_tos: true
invalid-email400Rejected email (syntax or disposable domain)Use a real address
not-found404Unknown resourceCheck the id/path
key-cap409Identity already has the maximum active keys (max_active_keys)Revoke one or reuse an existing key
no-identity-ledger409Admin credit aimed at an operator-issued keyCredit the identity instead
invalid-theme422Style theme failed validation; problems lists each offending keyCorrect the named slot/layer and resubmit
optimisation-too-large422Problem exceeds the fair-use location cap (max_locations, locations)Split the problem
quota-exceeded429Monthly quota exhausted with no payment path (quota, used)Verify email, top up credit, or wait for the month
rate-limited429Per-minute limit fired; retry_after seconds, also sent as a Retry-After headerBack off and retry
issuance-velocity429Too many key issuances from your IP today (limit_per_day)Come back tomorrow
internal500Unexpected failure; detail is logged server-side, never leakedRetry; report if persistent
upstream-error400/502Routing backend (or Photon geocoder) rejected the request (400 for an upstream 4xx, 502 otherwise; upstream_status)Check the request; else retry
geocoding-not-enabled501/geocode on a deployment with neither SN_GEOCODE_DIR nor SN_PHOTON_URL (self-host)Operator sets one of them
optimisation-not-enabled503Deployment has no VROOM sidecar configured (self-host)Operator sets SN_VROOM_URL
upstream-unavailable503Routing backend unreachableRetry
upstream-timeout504Routing backend timed outRetry

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:

  1. 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 failed X-PAYMENT verification. Full shape and payment flows: machine payments.

  2. 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 getWhen
429 quota-exceededA free-tier identity that has never bought credit exceeds its monthly quota; or an operator-issued key exceeds its quota
429 rate-limitedAny 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):

json
{
  "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": true with no tunnel_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):

CategoryRestricts
ANo restrictions for dangerous goods
BGoods which may lead to a very large explosion
C... or a large explosion, or a large toxic release
D... or a large fire
EAll dangerous goods except UN 2919, 3291, 3331, 3359 and 3373

Tunnel restriction codes (assigned to the load, ADR 8.6.4):

CodePassage forbidden through
BCategories B, C, D and E
B1000CCategory B above 1,000 kg total net explosive mass per transport unit; always C, D and E
B/DCategories B and C when carried in tanks; always D and E
B/ECategories B, C and D when carried in tanks; always E
CCategories C, D and E
C5000DCategory C above 5,000 kg total net explosive mass per transport unit; always D and E
C/DCategory C when carried in tanks; always D and E
C/ECategories C and D when carried in tanks; always E
DCategories D and E
D/ECategory D when carried in bulk or in tanks; always E
ECategory 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