Proxying your API calls

A proxy is a small service of your own between the browser and api.tomtom.com. The browser calls it, the proxy adds the API key and forwards the request, and the response comes back the same way. The key never reaches the page.

The SDK has a built-in proxy mode, so your application code barely changes. This guide shows how to turn it on and what the proxy must do, in any stack.

BrowserYour proxy (holds the key)api.tomtom.comYour app (SDK in proxy mode, no key)MapLibre tile workers service calls, session cookietiles, sprites, glyphs key added here response, key echoed in style.jsonresponse, key stripped out

A proxy is only for what the browser fetches from TomTom itself: style documents, map tiles, sprites and glyphs. A Node.js script or backend needs none of this. A browser app that only calls services can move those calls into your own backend. If a proxy isn’t worth it yet, cheaper options range from a domain whitelist to per-key limits. Where your API key runs covers all three.

Pointing the SDK at a proxy

To turn on the SDK’s proxy-credentials mode, set commonBaseURL to your proxy and leave apiKey unset:

import { TomTomConfig } from '@tomtom-org/maps-sdk/core';
TomTomConfig.instance.put({
commonBaseURL: 'https://proxy.example.com/map',
// No apiKey at all. Its absence, together with a non-TomTom
// commonBaseURL, is what switches the SDK into proxy mode.
});

With no apiKey and a commonBaseURL other than https://api.tomtom.com, three things change:

  1. Every TomTom URL is rewritten to your proxy. This covers service calls and the tile, sprite and glyph URLs in the style document, which MapLibre fetches itself.
  2. No credential leaves the browser. Service calls carry neither key= nor the TomTom-Api-Key header, and the SDK removes any key= from a style-document URL before the request goes out.
  3. Requests go out with credentials: 'include', so your proxy’s session cookie travels with them, including on the tile requests MapLibre makes from its web workers.

The session cookie is yours, not the SDK’s, so list it in your cookie notice with the rest of what the SDK sends.

Setting commonBaseURL while keeping an apiKey is a different mode, for infrastructure you own with a key that is yours to send. The SDK rewrites URLs and keeps the key in the request, but attaches no session cookie. The commonBaseURL table compares the modes.

commonBaseURL is the only base URL the SDK takes. Every service and map request goes through it, on the endpoint path and API version the SDK picks (Per-call overrides). Serve your proxy over https:. The SDK refuses any http: base URL other than a loopback one, with the code INSECURE_BASE_URL (commonBaseURL).

Establish the session before the first request

If your proxy issues a session cookie, the cookie must exist before the SDK’s first call. MapLibre fetches tiles from web workers, so wrapping fetch on the main thread does not hold them back. A tile request sent before the cookie is set gets a 401 that the worker cannot recover from.

Start the session first, and create the map once it resolves:

import { TomTomConfig } from '@tomtom-org/maps-sdk/core';
import { TomTomMap } from '@tomtom-org/maps-sdk/map';
TomTomConfig.instance.put({ commonBaseURL: 'https://proxy.example.com/map' });
// Ask your proxy for a session cookie and wait for it. The cookie is HttpOnly,
// so this response completing is the only signal the page gets that it exists.
// What the request carries is up to your proxy: nothing at all, or a token from
// whatever challenge you put in front of it.
await fetch('https://proxy.example.com/session', {
method: 'POST',
credentials: 'include',
});
const map = new TomTomMap({ mapLibre: { container: 'map' } });

If your sessions are short-lived, renew them on a timer rather than in response to a 401. By the time a worker sees one, the tile it was fetching is already gone.

What your proxy has to do

Two routes are enough:

RoutePurpose
POST /sessionVerify a challenge token, set the session cookie, return its expiry
ALL /map/* (or whatever prefix you point commonBaseURL at)Check the caller, inject the key, forward to api.tomtom.com, stream the response back

Most of the work is injecting the key and forwarding. The surprises are in the TomTom-specific part below.

Inject the key in exactly one place

The SDK sends the key in one of two ways, depending on the request:

CredentialRequests
TomTom-Api-Key header, nothing on the URLRouting, reachable ranges, traffic incident details, and every places service: search, geometry search, along-route search, geocoding, reverse geocoding, search suggestions, POI categories, place details and geometry data
key= query parameterEvery map resource (style documents, tiles, sprites and glyphs) and traffic area analytics

So set both on the server, from an environment variable or secret store. A proxy that only sets key= serves tiles but fails every search and routing request, which looks like an SDK bug rather than a proxy one.

Drop any TomTom-Api-Key header that arrives from the browser before you set your own. Otherwise a caller can supply that header themselves and your proxy will forward it.

Those two lines should be the only place in your codebase where the key appears.

Strip the key back out of the responses

This step is the easiest to get wrong, and it quietly undoes the rest.

TomTom echoes the key you injected inside the documents it returns. The sprite, glyphs and tile URL templates in style.json come back with key=<your key>, and so does the sprite index. If you forward them unchanged, the key is back in the browser on the map’s first request.

Nothing fails visibly. The map still renders, and the only symptom is the key in the network tab, so check there.

Remove the key from every application/json or text/* response body on the way out, with a string replacement. If you walk the fields of TomTom’s documents instead, you can miss one.

Stream binary bodies, read only textual ones

Tiles, sprite images and glyph ranges are the large majority of your traffic and can be megabytes each. Pipe them through without reading, buffering or re-encoding. Read only json and text/* bodies into memory, since only they need the sanitisation above.

Filter headers in both directions

Don’t pass headers through untouched in either direction.

Do not forward upstream:

  • Cookie, because your session is not TomTom’s business
  • Authorization
  • Origin
  • Referer, because forwarding the browser’s referrer both discloses your users’ page URLs and lets any client satisfy a referrer restriction on your key
  • Host

Do not return to the browser:

  • Set-Cookie from upstream
  • The upstream’s CORS headers, since yours should be the only ones. Otherwise the browser sees an ambiguous response
  • Content-Encoding and Content-Length, which are both wrong if your HTTP client transparently decompressed the body

Get CORS right

Your proxy is cross-origin from your app unless you arrange otherwise, and it needs credentials. For that combination, Access-Control-Allow-Origin must name a specific origin, never *, together with Access-Control-Allow-Credentials: true. Browsers reject a credentialed response with a wildcard, so if you get this wrong, every tile fails.

Echo back the request’s origin only after checking it against your allowlist. Reflecting it unconditionally is the same as *, except that it looks careful. Add Vary: Origin so a shared cache never serves one origin’s response to another.

You also have to answer preflights. The SDK sets a custom tomtom-user-agent header, which makes its requests non-simple, so the browser sends an OPTIONS request first.

Allow every header the SDK sends, or the preflight fails and the request never happens:

HeaderSent on
tomtom-user-agentEvery service call, and every map request except images (sprite sheets and raster tiles), unless sendUserAgentHeader is false
Content-TypeAny POST: routing, reachable ranges, geometry search, along-route search, traffic area analytics, and traffic incident details by more than five IDs
TomTom-Api-KeyRouting, reachable ranges, traffic incident details and every places service, outside proxy mode
TomTom-Api-VersionThe same requests, in proxy mode too
Accept-LanguageRouting, reachable ranges, reverse geocoding and traffic incident details, when a language is set
AttributesRouting, reachable ranges, reverse geocoding and traffic incident details

So answer with Access-Control-Allow-Headers: tomtom-user-agent, Content-Type, TomTom-Api-Key, TomTom-Api-Version, Accept-Language, Attributes (plus anything your own code adds), the allowed methods, and the same origin and credentials headers as above. Listing only tomtom-user-agent is the same trap as injecting only key=. Tiles work, but search and routing do not.

If your proxy handles GET but returns 404 for OPTIONS, the map fails with only CORS errors in the console.

Serve it on your app’s origin if you can

Because of that custom header, every vector tile request costs a preflight, one per distinct tile URL, since each URL has its own preflight cache entry. Access-Control-Max-Age only helps when the browser fetches the same URL again, which mostly doesn’t happen for tiles.

Serving the proxy behind a path on the app’s own hostname (https://example.com/map/*) makes those requests same-origin. This removes preflights entirely and lets the session cookie be SameSite=Lax instead of None.

The order things happen in

Any language or runtime works, but the order of the steps matters:

  1. Check where the request claims to come from. This is the cheapest rejection, so it goes first. Cross-origin requests carry Origin and same-origin ones do not. So fall back to Referer, and refuse only when both are missing. A check that insists on Origin rejects every same-origin tile.
  2. Answer OPTIONS preflights, before any session or rate-limit check. A preflight carries no cookie, so a proxy that demands a session first rejects the preflight and the real request never follows.
  3. Check the session, then the rate limit. This order stops an unauthenticated caller from consuming a real user’s allowance.
  4. Inject the key as the query parameter and the header, and forward the request.
  5. Sanitise the response. Strip the key from json and text/* bodies, stream everything else untouched, and set your own CORS headers rather than passing the upstream’s through.

Hardening your proxy covers steps 1 to 3 in detail.

Next steps