Skip to main content

Signed Tile Tokens

Why​

An API key embedded in a mobile app can always be extracted from the binary or sniffed from traffic, and it never expires. Signed tile tokens solve this: your backend holds a signing secret and mints short-lived tokens for your app; your app fetches tiles with ?token= instead of ?api_key=.

  • A leaked token expires on its own (recommended lifetime: 24 h, hard maximum: 7 days).
  • The signing secret never leaves your servers, and you can revoke it from the dashboard at any time; every outstanding token dies instantly.
  • Token traffic draws from the same tile quota and rate limits as your plan; nothing new to think about for billing.
Your app ──(your existing auth)──► Your backend
◄──── signed token (JWT, exp ≤ 7 d) ─────┘
│
└──► https://tiles.geocodeme.net/streets/{z}/{x}/{y}.png?token=eyJ...
└─► geocodeme verifies the signature, expiry & your plan limits

1. Create a signing secret​

Dashboard → API Keys → Tile signing secrets → Generate signing secret.

You get two values:

ValueExampleRole
Key id (kid)tsk_9f27ab31c04dPublic identifier, goes in the JWT header.
Secretgm_tss_...HMAC-SHA256 signing key. Shown once: store it in your backend's secret manager, never in an app.

Up to two secrets can be active at once, so you can rotate with zero downtime: generate the new secret, deploy your backend with it, then revoke the old one. Apps refetch tokens automatically (see step 3), so rotation needs no app release.

2. Mint tokens in your backend​

A token is a standard JWT (HS256) with your kid in the header:

PartFieldValue
HeaderalgHS256 (only accepted algorithm)
HeaderkidYour key id (tsk_...), required
Claimsscope"tiles", required
ClaimsexpUnix expiry, required, at most 7 days ahead
ClaimsiatIssued-at, recommended
ClaimssubOptional opaque reference (e.g. your user id) for your own tracing
import os
import time
import jwt # pip install pyjwt

TILE_KID = os.environ["GEOCODEME_TILE_KID"] # tsk_... (public id)
TILE_SECRET = os.environ["GEOCODEME_TILE_SIGNING_SECRET"] # gm_tss_...

def mint_tile_token(lifetime_seconds: int = 86_400) -> str:
now = int(time.time())
return jwt.encode(
{"scope": "tiles", "iat": now, "exp": now + lifetime_seconds},
TILE_SECRET,
algorithm="HS256",
headers={"kid": TILE_KID},
)

Expose the token through an authenticated endpoint of your API, e.g.:

GET /map/tile-token → { "token": "eyJ...", "expires_at": 1789300000 }
CDN-friendly tokens

The tile CDN caches by full URL, so a different token per user fragments the edge cache. Mint one shared token per day (memoize it server-side and hand the same token to every authenticated user); you keep the short-lived security model and a hot CDN cache.

3. Use tokens in your app​

Fetch a token at map start, and refetch when a tile request returns 401 (expired token):

// 1. Get a token from YOUR backend (authenticated call)
final tileToken = await api.getTileToken();

// 2. Use it in the tile URL
TileLayer(
urlTemplate:
'https://tiles.geocodeme.net/streets/{z}/{x}/{y}{r}.png?token=$tileToken',
retinaMode: RetinaMode.isHighDensity(context),
userAgentPackageName: 'com.example.app',
),
RichAttributionWidget(
attributions: [TextSourceAttribution('© OpenMapTiles © OpenStreetMap contributors')],
),

// 3. On 401 (token expired): refetch the token and rebuild the TileLayer
// (e.g. keep the token in a state notifier the TileLayer depends on).

Errors​

StatusCauseWhat to do
401Missing/expired token, bad signature, wrong scope, unknown or revoked kid, exp more than 7 days aheadRefetch a fresh token from your backend. If it persists, check the secret/kid pair.
429Tile rate limit or monthly tile quota of your plan exceededBack off; see Map Tiles for plan limits.

Security model, honestly​

Tokens are bearer credentials: someone extracting the current token from their own device can fetch tiles with it until it expires. What the design guarantees is that the damage is bounded (by the token lifetime, by your plan's tile rate/quota, and by per-IP limits at our edge), and that recovery is instant: revoke the secret in the dashboard and every token signed with it stops working immediately. This is the same model used by the temporary tokens of major map providers, without giving up self-served rotation.