Skip to content

Calling the API directly

Call any Unmap API directly over HTTPS at https://api.unmap.dev. The @unmap/* packages attach your key, format requests and parse responses, but you can use curl, Python or an HTTP client in any language. Each endpoint defines its HTTP method, parameters and response format.

There is a third path between these two. If you want the packages but not a build step, unmap.dev serves the whole SDK as one script tag; the Quickstart has it.

This page is the whole of what you need to know to do that: (1) the shape of the contract, (2) the same geocode and route as curl, fetch, and Python, (3) rendering a map with a stock MapLibre GL JS and nothing else, and (4) what the packages were doing for you that is now your job. The parameters and response bodies of each endpoint are not repeated here; they live on the Maps API, Layers (including GET /identify and GET /data/query), the Geocoding API, the Routing API, the Transit API, and the Elevation API, and those pages already describe the HTTP contract rather than the SDK. Point-in-polygon is GET /identify / unmap.identify(). Bbox listing against a queryable catalog layer is GET /data/query / unmap.data.query().

The contract in one paragraph

The base URL is https://api.unmap.dev. Unmap's own paths have no version prefix; Optimize's Google-compatible paths also accept /v1/projects/.... Read-only data endpoints usually use GET with query parameters. Batch geocoding, travel-time matrices, map matching, stop-order optimization, fleet optimization, feedback and uploads use POST; deleting an overlay uses DELETE. /route and /isochrone also accept POST with a JSON body, including avoid_areas polygons. The endpoint reference lists every method and request shape. The key travels as Authorization: Bearer <key>, X-API-Key: <key>, X-Goog-Api-Key: <key>, or ?key=<key>, checked in that order. Generated style URLs include query keys so MapLibre can fetch tiles and assets automatically; custom MapLibre integrations can set headers with transformRequest (see Authentication). Geocoding, routing, and style responses are application/json; tiles are vector-tile bytes, glyphs and sprites are the binary and image formats MapLibre expects. Most failures return JSON with an error message and a code. Optimize errors use a structured error object in Google's format. A malformed tile, terrain, contour, glyph, or sprite request answers in plain text instead. An unconfigured terrain or contour archive is JSON 404 not_found. A style whose active basemap release is missing is plain text too: 503 basemap release is misconfigured (see Errors).

The root endpoint needs no key, and describes the rest:

curl https://api.unmap.dev/
{
  "name": "unmap gateway",
  "endpoints": ["/tiles/{z}/{x}/{y}", "/styles/{style}.json", "/geocode/search", /* … */],
  "styles": ["base", "muted", "outdoor", "blueprint", "blush", "orchid", "canopy", "lagoon", "tropic", "sunset", "bold", "pastel", "atlas", "swiss", "winter"],
  "auth": "API key required via 'Authorization: Bearer um_...', 'X-API-Key', 'X-Goog-Api-Key', or '?key=', checked in that order",
  "docs": "https://unmap.dev/docs",
  "openapi": "https://api.unmap.dev/openapi.json",
  "apiCatalog": "https://api.unmap.dev/.well-known/api-catalog",
  /* plus authMd, mcp, a2a, agentCard and protectedResource, and the data attribution */
}

Machine-readable description

If you are wiring up a tool, a code generator, or an agent rather than reading, two more unauthenticated documents describe the same contract in formats built for that:

  • https://api.unmap.dev/openapi.json is an OpenAPI 3.1 description of the gateway data and agent endpoints: parameters, the three key forms as security schemes, and the response and error bodies. Generate a client from it, or hand it to an agent.
  • https://unmap.dev/.well-known/api-catalog (also at api.unmap.dev) is an RFC 9727 API catalog: a linkset that names the API, points at the OpenAPI document, and points back at these docs in English and French. Both homepages and the API root advertise it with a Link header, rel="api-catalog".
curl https://api.unmap.dev/openapi.json | jq '.paths | keys'
curl https://unmap.dev/.well-known/api-catalog

The root response links a few more unauthenticated documents in the same spirit: authMd (/auth.md, how an agent obtains a credential), protectedResource (RFC 9728 metadata), agentCard and mcpServerCard.

The OpenAPI document covers the API-key data plane only. The account API behind the dashboard authenticates with a browser session and is not in it.

The same two requests, three ways

Geocode a query, then drive from the answer. The three tabs are one program: pick the language you work in. fetch is all @unmap/geocoding and @unmap/routing use underneath, and the only things the packages add are URL building, a thrown error on a non-2xx status, and TypeScript types for the response, so the fetch tab is the client library written out by hand, and it runs unchanged in Node, Deno, Bun, Workers, or a browser. Put your key in an environment variable and every tab runs as written.

export UNMAP_API_KEY=um_live_...
 
# Forward geocode. The response is a JSON array, so pipe it to jq to read it.
curl "https://api.unmap.dev/geocode/search?q=Calgary%20Tower&limit=1" \
  -H "Authorization: Bearer $UNMAP_API_KEY" | jq '.[0] | {name, lng, lat}'
 
# Drive between two points. Coordinates are lng,lat, and the comma needs no encoding.
curl "https://api.unmap.dev/route?from=-114.0719,51.0447&to=-113.9871,51.0899&mode=auto" \
  -H "Authorization: Bearer $UNMAP_API_KEY" | jq '{distanceMeters, durationSeconds}'

The first call prints the name and coordinates of the top hit; the second prints a road distance in metres and a duration in seconds. Every other endpoint follows the same pattern: a path, a few query parameters, a header. The Geocoding API and Routing API pages carry a curl and a fetch line for each one.

Two details in those tabs are worth carrying into your own code. First, let the library encode the query: URL and URLSearchParams in the fetch tab, params= in the Python tab. That is what makes a query like rue Sainte-Catherine, or one written in syllabics, arrive intact. Reading the body on failure is worth the extra line too: the message names the parameter at fault, and the code tells a 429 for the rate limit apart from a 429 for a plan quota, and a 422 outside_coverage apart from a 422 no_route.

Second, set a timeout, as the Python tab does. Geocoding and /route answer in a few hundred milliseconds, but an isochrone is several costing passes inside the routing container and measured around 700 ms warm on 2026-09-02, so a client that has no timeout at all will one day wait on it.

Rendering a map with a stock MapLibre

A map needs no @unmap code either. The style endpoint returns a complete MapLibre style document whose tile, glyph, and sprite URLs already point back at the gateway with your key in their query strings, so a plain MapLibre GL JS pointed at that one URL discovers everything else itself. This is the whole page, with MapLibre loaded from a CDN and no build step:

<link
  href="https://unpkg.com/maplibre-gl@6.13.0/dist/maplibre-gl.css"
  rel="stylesheet"
  integrity="sha384-yFNc3bs28S14jOJo1ZO9SnLiAmtyY2a3H9be8GkLMpDGlO5r1zZNP8fYu+fOCE/w"
  crossorigin="anonymous"
/>
 
<div id="map" style="height: 400px"></div>
 
<script type="module">
  import { Map } from "https://unpkg.com/maplibre-gl@6.13.0/dist/maplibre-gl.mjs";
 
  new Map({
    container: "map",
    style: "https://api.unmap.dev/styles/base.json?mode=light&key=um_live_...",
    center: [-114.0719, 51.0447],
    zoom: 11,
  });
</script>

MapLibre 6 is ESM-only. There is no UMD build and no maplibregl global, so the script is a module and the pieces you want are imported by name. The stylesheet still carries an integrity hash; the module cannot, because an import statement has no attribute to carry one. That is the second reason the CDN here is a demonstration and not a recommendation: in an application, install maplibre-gl and serve it from your own site, as the Quickstart does.

The key must be in the style URL as ?key=, because that is the one form the tile, glyph, and sprite requests MapLibre fires can carry, and the gateway copies whatever key fetched the style into the URLs it writes. Everything the SDK's style, mode, lang, and theme options do is a query parameter on this same URL, so /styles/outdoor.json?mode=dark&lang=fr is a dark outdoor map with French labels and ?theme=<code> applies a theme from /create. The full list of styles and parameters is on the Maps API.

The same URL works anywhere a MapLibre style is accepted: MapLibre Native on iOS and Android, the MapLibre Leaflet plugin, or any renderer that reads a style specification version 8 document. You are installing our cartography, not our code.

Calling from a browser

The API answers cross-origin requests from any origin: GET, HEAD, OPTIONS, and POST (the last only reaches /mcp, the JSON-RPC endpoint; everything on this page is a GET), with the Authorization and X-API-Key headers permitted. Both header forms therefore work from a browser fetch, but ?key= is the better choice there. A custom header turns each request into a preflighted one, an extra OPTIONS round trip before the GET, while a query parameter is a simple request that goes straight through, and it is the mechanism the map's own tile requests already use.

What a browser sends that a server does not is an Origin header, and the gateway reads it. A request from a dev origin is counted but never billed: localhost, 127.0.0.1, [::1], any *.localhost host, and any *.pages.dev or *.workers.dev preview, provided the key allows that origin. A request from any other origin is billed normally. If the key carries an allowed-origins list, anything not on it is a 403 with the code origin_not_allowed, including a request that sends no Origin at all. A server-side call carries no Origin and always counts as production traffic, whichever key it uses, so give your backend a key with no allowed origins. The exact rules are on Plans & Limits.

What the packages did for you

Going without them means picking up a few responsibilities the clients handled. None is hard; they are listed so nothing is a surprise:

  • Coordinate pairs are lng,lat, longitude first. A point is one comma-separated pair: from and to on /route, near on /geocode/reverse, /isochrone and the other endpoints that take an anchor. The clients take [lng, lat] tuples and write the HTTP parameters for you. /timezone uses separate lat and lon parameters; /identify uses at=lng,lat. JSON request bodies use arrays, such as "near": [lng, lat] on POST /isochrone.
  • Encode the query. q=Calgary Tower must go over the wire as Calgary%20Tower. Every HTTP library does this when you pass parameters as a map rather than concatenating a string.
  • Check the status yourself. The clients throw on any non-2xx response. Over HTTP, a 400 still has a JSON body, so read it; the message names the parameter at fault.
  • Honour Retry-After. A 429 carries one unless its code is billing_required. Back off for that many seconds instead of retrying immediately; the burst limit is 1,000 requests per 60 seconds per key, per Cloudflare location. A 502 has none: the client retries that status on its own backoff.
  • Let the cache headers work. A geocoding response is cached at our edge for 24 hours, a route or isochrone is private, max-age=3600 (your client may reuse it, a shared cache must not), and a tile that has bytes is public, max-age=86400. An empty 204 does not send that header. A browser respects all of this automatically. A server-side client only does if it has an HTTP cache, and a repeated route request without one is a repeated billable call.
  • Keep a um_live_ key out of anything you ship to strangers, or list the origins it may be used from so a copied key is useless elsewhere. On the server, an environment variable is enough, and that key should carry no allowed origins at all.

Next steps