Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild

The Mapbox API: Adding Maps to an App Without the Bill Surprise

Sean

Platform Writer

Aug 10, 2026
8 min read

A Mapbox map is a container element, a stylesheet, a script, and about eight lines of JavaScript. The two things worth understanding before you ship it are what counts as a billable map load and which of your two token types is safe to put in a browser bundle — because the answer to the second one is only the public one, scoped correctly.

The Mapbox API: Adding Maps to an App Without the Bill Surprise

Adding a map is genuinely quick. The interesting parts are the billing model, which surprises people at the end of a month, and the token handling, which is where most map integrations have a quiet security flaw.

Table of contents

The minimum map

<link href="https://api.mapbox.com/mapbox-gl-js/v3.28.0/mapbox-gl.css" rel="stylesheet">
<script src="https://api.mapbox.com/mapbox-gl-js/v3.28.0/mapbox-gl.js"></script>

<div id="map" style="height: 480px"></div>

<script>
  const map = new mapboxgl.Map({
    accessToken: 'pk.your_public_token',
    container: 'map',
    style: 'mapbox://styles/mapbox/standard',
    center: [-0.1276, 51.5072],   // [longitude, latitude] -- lng first
    zoom: 11,
  });

  map.addControl(new mapboxgl.NavigationControl());
</script>

Coordinates are [longitude, latitude], in that order. This catches nearly everyone, because most other contexts — including how people say them aloud — put latitude first. A map that lands in the ocean off West Africa means you have them swapped; that is where [0, 0] is.

The container needs an explicit height. A div with no height renders a map zero pixels tall, which looks like the script failed.

For a bundled app, install the package instead of using the CDN:

import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';

mapboxgl.accessToken = import.meta.env.VITE_MAPBOX_TOKEN;

Pin the version. Map libraries change rendering behaviour between minor releases, and a floating version means your map can change appearance without a deploy.

Tokens: which one goes where

There are two prefixes and the distinction is the security-relevant part of this whole integration.

  • pk. — public token. Designed for client-side use. It will be visible in your bundle and that is expected.
  • sk. — secret token. Server-side only. Grants account-level access including token management and billing data.

Never put an sk. token in frontend code. It is not a stricter version of the public token; it is an account credential. In a browser bundle it is readable by anyone who opens DevTools.

Public tokens are visible by design, so the protection is scoping rather than secrecy:

  1. Create a token with only the scopes you need — usually styles:read, fonts:read, and tilesets:read.
  2. Add URL restrictions listing exactly the origins allowed to use it. This is the control that stops someone lifting your token and running up your bill on their own site.
  3. Use separate tokens for development and production, so you can rotate one without breaking the other.
  4. Set a usage alert on the account.
// Server-side only -- geocoding through your own endpoint
// keeps the secret token off the client entirely
export async function GET(request) {
  const q = new URL(request.url).searchParams.get('q');
  if (!q) return Response.json({ error: 'missing q' }, { status: 400 });

  const url = new URL('https://api.mapbox.com/search/geocode/v6/forward');
  url.searchParams.set('q', q);
  url.searchParams.set('limit', '5');
  url.searchParams.set('access_token', process.env.MAPBOX_SECRET_TOKEN);

  const res = await fetch(url);
  return Response.json(await res.json());
}

Environment variables on the service are where both tokens belong — the services documentation covers where those are set, and it means rotating a token is a configuration change rather than a commit.

What actually costs money

This is the part worth reading before you ship, because the billing unit is not obvious.

A map load is billed when you instantiate a Map object. Not per tile, not per pan, not per zoom. Once the map exists, the user can explore freely at no extra cost.

The consequences are practical:

  • Do not create a new map on every render. A React component that instantiates a map in a re-rendering effect can bill dozens of loads per user session. Create once and keep the instance.
  • Lazy-load maps below the fold. A map instantiated on a page nobody scrolls to is a load you paid for and nobody saw.
  • Static images are cheaper for non-interactive uses. A store locator thumbnail does not need an interactive map — the Static Images API returns a PNG.
  • Geocoding is billed per request. Autocomplete firing on every keystroke is expensive; debounce it and use session tokens where the API supports them.
// React: create once, never on re-render
useEffect(() => {
  if (mapRef.current) return;          // already created
  mapRef.current = new mapboxgl.Map({ /* ... */ });
  return () => { mapRef.current?.remove(); mapRef.current = null; };
}, []);                                 // empty deps -- runs once
// Lazy-load: only instantiate when the container is visible
const io = new IntersectionObserver(([entry], obs) => {
  if (!entry.isIntersecting) return;
  obs.disconnect();
  new mapboxgl.Map({ container: entry.target, /* ... */ });
}, { rootMargin: '200px' });

io.observe(document.getElementById('map'));

The React double-instantiation bug is the most common cause of an unexpected bill. An effect with a changing dependency creates a map on every change, and in development React’s strict mode mounts effects twice. Guard with a ref.

Markers, popups, and data layers

// A handful of markers -- fine as DOM elements
new mapboxgl.Marker({ color: '#7f62f4' })
  .setLngLat([-0.1276, 51.5072])
  .setPopup(new mapboxgl.Popup().setHTML('<strong>London</strong>'))
  .addTo(map);

Markers are DOM elements, and hundreds of them will make the page stutter. Past roughly fifty, switch to a GeoJSON source with a symbol or circle layer, which renders on the GPU.

map.on('load', () => {
  map.addSource('stores', {
    type: 'geojson',
    data: '/api/stores.geojson',
    cluster: true,
    clusterRadius: 50,
  });

  map.addLayer({
    id: 'clusters',
    type: 'circle',
    source: 'stores',
    filter: ['has', 'point_count'],
    paint: {
      'circle-color': '#7f62f4',
      'circle-radius': ['step', ['get', 'point_count'], 16, 20, 22, 100, 30],
    },
  });

  map.addLayer({
    id: 'unclustered',
    type: 'circle',
    source: 'stores',
    filter: ['!', ['has', 'point_count']],
    paint: { 'circle-color': '#3b82f6', 'circle-radius': 6 },
  });
});

Everything must go inside map.on('load'). Adding a source or layer before the style has loaded throws Style is not done loading, which is the second-most-common error after the coordinate order.

Clustering is built in and handles thousands of points without custom code. It is worth reaching for early rather than after the map has become slow.

Accessibility and the alternatives

Maps are difficult for screen reader users and for anyone navigating by keyboard. A map alone is not an accessible way to present location information.

  • Always provide the same data as text. A list of addresses beside the map serves everyone, including people who just want to copy an address.
  • Keyboard navigation works in Mapbox GL JS, but tab order and focus indicators need checking.
  • Do not put essential information only in a popup reachable by clicking a marker.
  • Respect prefers-reduced-motion — set fly animations to instant, since a map that animates across the world can cause discomfort.

It is also worth asking whether you need an interactive map at all. For a single office location, a static image with an address and a link to directions is faster, cheaper, more accessible, and adds no JavaScript to the page.

<!-- Static image: no library, no map load billing, works everywhere -->
<img src="https://api.mapbox.com/styles/v1/mapbox/streets-v12/static/pin-l+7f62f4(-0.1276,51.5072)/-0.1276,51.5072,13/600x400@2x?access_token=pk.your_token"
     width="600" height="400" alt="Map showing our office on Baker Street, London" />

That is the right answer more often than it gets used. An interactive map earns its place when users need to explore — comparing locations, drawing an area, following a route. For here is where we are, an image and an address do the job.

How this fits the rest of the stack

Instantiate the map once, keep the secret token on the server and scope the public one by URL, and remember that billing is per map load rather than per interaction. Switch from markers to a GeoJSON layer past about fifty points, and use a static image when the map is decorative rather than exploratory. If you are building an app with a server-side proxy for geocoding and want the service and database as separate line items, the RunxBuild hosting calculator shows them apart.

Useful related references:

FAQ

What is the difference between a pk and sk Mapbox token?

A pk token is public and designed for browser use — restrict it by URL and scope. An sk token is a secret account credential granting access to token management and billing, and must never appear in frontend code.

How does Mapbox billing work?

The main unit is a map load, billed when you instantiate a Map object. Panning, zooming, and tile fetches within that session are not billed again. Geocoding and other API calls are billed per request.

Why is my Mapbox map in the wrong place?

Almost certainly swapped coordinates. Mapbox takes [longitude, latitude], while most other contexts state latitude first. A map appearing in the Atlantic off West Africa means it is rendering at [0, 0].

Why do I get Style is not done loading?

You added a source or layer before the map style finished loading. Wrap those calls in map.on(‘load’, …) so they run after the style is ready.

Do I need an interactive map for a single location?

Usually not. A static map image with the address and a directions link is faster, cheaper, more accessible, and adds no JavaScript. Interactive maps earn their place when users need to explore or compare locations.

#mapbox api#mapbox gl js#geocoding#access tokens#web maps