Qartas Authentication

The API key

Your credential is a single API key:

cmk_live_<32 base64url characters>        e.g. cmk_live_uvgADMdlIfH4GPLJ8YtQqOQkAMEo_wgR

Presenting the key

Send it as a header on any endpoint:

X-API-Key: cmk_live_...

That covers every JSON endpoint and — for callers that can set headers — the tile family too. Requests with neither a valid key nor a JWT get 401 (MISSING_TOKEN on the JSON endpoints, AUTH_REQUIRED on the tile family).

Handling guidelines:

Tile tokens (?t=)

Map renderers fetch tiles by URL template and often can't attach headers, so the tile-serving family (/maps/v1/tiles, /style, /glyphs, /sprites) also accepts a short-lived token as a query parameter:

GET /maps/v1/tiles/12/2456/1608@2x.png?t=<token>

Minting. POST /maps/v1/tile-token (authenticated with your key, no body) returns:

{"token": "bWFwc2tleTpkYTRk...", "expires_at": "2026-07-31T22:24:16Z"}

Properties (all enforced server-side, verified live):

Property Value
Format base64url of callerID\|expiry-unix\|HMAC-SHA256-signature — opaque; do not parse or construct it yourself.
Lifetime 24 hours by default (expires_at tells you exactly; the operator can tune it via CHAFMAPS_TILE_TOKEN_TTL).
Binding The callerID is mapskey:<your key's ID> — the token is cryptographically bound to the key that minted it. A token not bound to an API key is rejected (401 TOKEN_NOT_KEY_BOUND).
Scope Tile family only. A leaked tile token cannot call autocomplete/geocode/directions/places or mint further tokens.
Statelessness HMAC-verified per request — minting costs no DB work, so mint freely (one token per session is the intended pattern; it covers unlimited tile fetches while valid).

Refresh. Mint a new token before expires_at. The Dart SDK's client.tiles does this automatically — it caches the token and re-mints at ~80% of its lifetime; you never see a token unless you use the advanced URL- template path (client.tiles.tokenManager.getToken()).

Failure modes:

Response Meaning What to do
401 TOKEN_EXPIRED Past expires_at. Mint a new token.
401 INVALID_TOKEN Malformed or bad signature. Re-mint; check you passed it unmodified.
401 TOKEN_NOT_KEY_BOUND Token wasn't minted from an API key. Mint via POST /maps/v1/tile-token with your key.
401 API_KEY_REVOKED The key behind the token is no longer active. Contact the operator.
503 MAPS_AUTH_UNAVAILABLE Server-side auth misconfiguration. Retry later; report if persistent.

What happens on revoke

When the operator revokes a key (DELETE /admin/keys/{id}), the key row is soft-deleted (revoked_at set, active=false). Effects, in order:

  1. Header auth dies immediately-to-promptly. The JSON endpoints check Postgres per request — revoked keys 401 immediately there. The tile family checks a short-TTL Redis cache (30 s default, env CHAFMAPS_API_KEY_CACHE_TTL_SECONDS) which the revoke also proactively invalidates — so in the common case tiles 401 within seconds, and in the worst case within the cache TTL.
  2. All previously-minted tile tokens die with the key. Token validation re-checks that the bound key is usable right now; a revoked key's tokens answer 401 API_KEY_REVOKED on the same timeline as (1) — even though the token's own 24 h expiry hasn't passed. (Verified live: pre-existing tokens stopped working seconds after revoke.)
  3. CDN edge caveat. Tiles already fetched before the revoke can keep being served by an intermediary CDN edge cache for up to that tile's max-age (1 h) without touching the origin. This affects only exact tile URLs already fetched; anything new hits the origin and 401s. It is expected behavior, not a revocation bug.

Expiry (expires_at on the key) behaves the same way, with 401 API_KEY_EXPIRED on the header path.

Security posture

Layer Posture
Transport HTTPS only; HSTS (max-age=31536000; includeSubDomains; preload). The public host sits behind Cloudflare.
Response headers X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: strict-origin-when-cross-origin, Permissions-Policy (camera/mic/geo denied) on every response.
Key storage SHA-256 hash only; plaintext never stored or logged.
Key scoping Per-key: scopes (maps only today), optional expiry, per-minute and daily limits, plan label; every request is attributed and metered to its key (tiles included).
Tile access No anonymous access anywhere. Tiles require the key or a key-bound HMAC token; every tile request is attributed to a key.
Abuse backstops Per-IP rate caps on the tile family run before auth, so credential-stuffing floods are capped regardless of auth outcome; request bodies capped at 1 MB; 30 s request timeout.
Error hygiene All 5xx bodies are sanitized to a generic message + request_id — internal details are logged server-side, never sent on the wire.
Server-fault isolation Rate limiting and metering fail open on infrastructure blips (a Redis hiccup can't take the API down), while token/secret verification fails closed (no configured secret ⇒ no tokens validate).

Quick reference — which credential where

Surface X-API-Key header ?t= tile token JWT bearer
/maps/v1 JSON endpoints + /staticmap ✅ (operator-internal)
/maps/v1/tiles, /vector, /style, /glyphs, /sprites ❌ (a JWT-minted tile token is rejected: TOKEN_NOT_KEY_BOUND)
/admin/* super_admin only
/livez, /healthz not required

Verified against internal/api/{auth,tileauth,jwtauth,admin,keycache}.go, internal/engine/tiles.go, and live probes (mint → use → revoke → observe 401s) on 2026-07-31. Not verified from code: none for this document.

← Documentation