Authentication
Every gateway request (maps, geocoding, routing) needs an unmap API key. There is no unauthenticated tier: even the free plan works by key, it just never gets billed for dev traffic (see Plans & Limits).
Key forms
A key looks like um_live_... or um_test_.... The prefix is checked before anything else: a
string that does not start with one of the two is rejected without a lookup, so a stray value
never even reaches key storage.
um_live_is your production key.um_test_is for local development and staging.@unmap/sdk's quickstart ships a publicum_test_demo key that is rate-limited and safe to embed for trying the API before you have your own.
Both forms are checked the same way at the gateway: authenticated, rate-limited, and (outside of
dev-origin traffic) metered against your plan. The prefix identifies the key's environment; it
does not by itself exempt a request from billing. Whether a request counts as free "dev traffic"
depends on the request's Origin. See Plans & Limits for the exact
rule.
Presenting the key
Three forms are accepted:
Authorization: Bearer <key>is the normal form for server-side calls.X-API-Key: <key>is for clients that reserve theAuthorizationheader for something else.?key=<key>is a query parameter. Required for tile, glyph, and sprite requests, since MapLibre GL cannot attach custom headers to the requests it fires for those. The@unmap/*clients use this form for every request, tiles and JSON alike, so with the SDK you never present the key yourself; the header forms are for calling the gateway directly.
# Authorization header
curl "https://api.unmap.dev/geocode/search?q=Calgary+Tower" \
-H "Authorization: Bearer $UNMAP_API_KEY"
# X-API-Key header
curl "https://api.unmap.dev/geocode/search?q=Calgary+Tower" \
-H "X-API-Key: $UNMAP_API_KEY"
# Query parameter, the form MapLibre uses for the tile, glyph and sprite requests it fires
curl "https://api.unmap.dev/tiles/11/375/685.mvt?key=$UNMAP_API_KEY" -o tile.mvt// Authorization header
await fetch("https://api.unmap.dev/geocode/search?q=Calgary+Tower", {
headers: { Authorization: `Bearer ${key}` },
});
// X-API-Key header
await fetch("https://api.unmap.dev/geocode/search?q=Calgary+Tower", {
headers: { "X-API-Key": key },
});
// Query parameter, the form MapLibre uses for the tile, glyph and sprite requests it fires
await fetch(`https://api.unmap.dev/tiles/11/375/685.mvt?key=${key}`);If more than one form is present, the header wins: Authorization is checked first, then
X-API-Key, then ?key=.
The gateway strips ?key= before it forwards a request to a backend or files a response in the
edge cache, so a cached tile or geocoding response is shared across customers and never carries
anyone's key. What it does keep is a hash of the key, counted per endpoint for billing.
Can the key go in the browser?
Yes, and it has to. The browser fetches the tiles itself, so the key travels in URLs anyone can read in the network panel. A maps key is a public credential in the same sense as a Stripe publishable key: finding it is not an exploit, and there is no server-only variant that would let a map render without it.
Secrecy is therefore not the control. Two other things are, and both are in the dashboard:
- Allowed origins, set per key. List the exact origins a key may be presented from, one per
line, on the key itself. A key with a list is strict: requests from anywhere else are refused
with a
403and the codeorigin_not_allowed, and so are requests with noOriginorRefererat all. A key with an empty list is unrestricted, which is what a server key should be. Dev origins are not implicit: listhttp://localhost:3000if that is where you develop. - The spend cap, which starts at
$0on every paid plan. Someone using a key they found cannot run up an overage you did not opt into. They could still spend your included calls, which is what the origin list is for.
Use two keys, not one: a scoped key for the browser and an unscoped one for your backend. A
backend sends no Origin, so scoping the key your server uses would refuse your own server.
Be honest about what this buys. Origin and Referer are set by the browser, and anything that
is not a browser can send whatever it likes. An allowed-origins list stops a key lifted off your
page from working on someone else's page. It does not stop a script. If you think a key has been
taken, revoke it from the dashboard and create another. Full rules on
Plans & Limits.
Getting a key
Sign in at unmap.dev/signin with a magic-link email or GitHub, then create a key from the dashboard. That sign-in flow is a browser session (a cookie, not an API key). It is how you manage your account, not how your application authenticates. Your application always uses the key itself, presented one of the three ways above.
What a bad key looks like
A missing or unrecognised key gets a 401:
// no key presented
{ "error": "missing API key", "code": "unauthorized" }
// key does not exist, or has been revoked
{ "error": "invalid or disabled API key", "code": "unauthorized" }Keys are looked up by a hash of the presented value, not stored in plaintext, so there is nothing to leak from key storage itself beyond the hash.
Going over the rate limit (1,000 requests per 60 seconds per key, per Cloudflare location) gets a
429 with a Retry-After header:
{ "error": "rate limit exceeded", "code": "rate_limited" }See Errors for the full error contract, including the plan-enforcement codes
(quota_exceeded, spend_cap, billing_required, origin_not_allowed) that a request can hit
once it is past authentication.