๐Ÿ—พ Branded Static Maps

Spatialized founder Jozef Sorocin
Book a consultation โ†—
12 min read  โ€ข  Updated 07/13/2025

Remember the last time you took a trip with Uber/Lyft/Bolt? Just as the trip ended, you probably got an email containing the receipt and trip summary. This trip summary then contained the route โ€œscreenshotโ€:

Sample Uber trip summary
Sample Uber trip summary
Sample Uber trip summary

The map โ€œscreenshotโ€ is what we call a static map.

In contrast to dynamic maps which you can zoom, pan, and move , static maps are image snapshots (GIF, PNG, or JPEG) of a given viewport.

As you remember from the chapter on map tiles , dynamic raster maps are themselves composed of styled image tiles. The Static API lets you request the very same tiles โ€” on demand.

In this chapter, you'll learn how to leverage the Google Maps Static API to:

  • generate map images consistent with your brand,
  • correctly place custom markers,
  • and efficiently draw semi-transparent polylines.

For my ride-hailing app I'm working on an email that contains an image snapshot of the route. I'd like to visually differentiate between the origin and the destination. Plus, I need to make sure the map pins are correctly positioned:

A static map consistent with my brand displaying visually distinct markers
A static map consistent with my brand displaying visually distinct markers
A static map consistent with my brand displaying visually distinct markers

First off, turn on billing and enable the Maps Static API for your API key.

After that, you'll be requesting the

https://maps.googleapis.com/maps/api/staticmap?key=AIza...

endpoint with various query parameters.

The most basic query parameter is the size :

Basic static map with the default zoom of 0 and `?size=312x358`

Basic static map with the default zoom of 0 and
?size=312x358

Basic static map with the default zoom of 0 and `?size=312x358`

Just like with dynamic maps , static maps can be styled using Map IDs by passing the map_id attribute:

Basic static map with a `map_id` and `?size=312x358&map_id=cf19af61093c176a`

Basic static map with a map_id and
?size=312x358 &map_id=cf19af61093c176a

Basic static map with a `map_id` and `?size=312x358&map_id=cf19af61093c176a`

Static maps can be centered either using geocodeable address strings like โ€œVienna, Austriaโ€โ€ฆ

Static map centered on Vienna, Austria and `?size=312x358&map_id=cf19af61093c176a&center=Vienna,Austria`

Static map centered on Vienna, Austria and
?size=312x358 &map_id=cf19af61093c176a
&center=Vienna,Austria

Static map centered on Vienna, Austria and `?size=312x358&map_id=cf19af61093c176a&center=Vienna,Austria`

โ€ฆor by passing a latitude/longitude pair and an optional zoom level.

To refresh your understanding of coordinates and learn how to obtain them, go back a few chapters.

Static map centered on Vienna, Austria and `?size=312x358&map_id=cf19af61093c176a&center=48.2082,16.3738&zoom=13`

Static map centered on Vienna, Austria and
?size=312x358
&map_id=cf19af61093c176a
&center=48.2082,16.3738
&zoom=13

Static map centered on Vienna, Austria and `?size=312x358&map_id=cf19af61093c176a&center=48.2082,16.3738&zoom=13`

To place a custom marker on the static map, you'll need a marker URL (e.g. this one from Wikimedia ) and its position.

With these two pieces of information, you'll construct a query parameter of the form:

&markers=|icon:...|lat,lng

that is:

&markers=|icon:https://upload.wikimedia.org/wikipedia/commons/thumb/d/d1/Google_Maps_pin.svg/20px-Google_Maps_pin.svg.png|48.2082,16.3738

resulting in โ†’

Static map with a custom marker icon

Static map with a custom marker icon

Static map with a custom marker icon

Unlike with dynamic maps' custom marker labels , static map marker labels cannot be SVGs โ€” neither standard .svgs nor encoded data URLs like data:image/svg+xml.

Only PNGs, JPEGs, or GIFs are permitted โ€” though PNGs are recommended.

One more restriction applies: icons may only be up to 4096 pixels in size (64x64px for square images). Any invalid marker icon will fall back to the default, red Google pin.

Oftentimes, invalid parameters (marker-related or not) will show a yellow โ€œMap errorโ€ warning in the top right corner. To inspect the error message closer, look for the x-staticmap-api-warning response header :

By default, the Static Maps API will position the markers bottom center. If you need to align the markers differently (i.e. to center them on the x/y axes), you can use one of the predefined alignments: top, bottom, left, right, center, topleft, topright, bottomleft, or bottomright.

The anchor URL query parameter also accepts x,y values. See this anchoring deep dive section for more info.

To verify that you positioned the marker correctly, place an extra marker for testing purposes:

Verifying marker positioning
Verifying marker positioning

Note that order matters โ€” the &markers= attributes are rendered in the reverse order they appear in the request URL. So, to place a marker โ€œon topโ€ of another marker, pass it before the other markers parameters.

In the chapter on lines, you learned how to work with ordered sets of coordinates to construct polylines.

The Maps Static API also lets you draw polylines. You only need a list of lat/long coordinates separated by the pipe symbol (|):

&path=|48.26491,16.398|48.26408000000001,16.397640000000003|48.263130000000004,16.40098|48.265150000000006,16.40229|48.266070000000006,16.397920000000003|48.26594,16.396510000000003|48.26561,16.39609|48.261570000000006,16.39431|48.2603,16.39292|48.25309000000001,16.375210000000003|48.25213,16.37348|48.250260000000004,16.37141|48.247890000000005,16.37002|48.241640000000004,16.36448|48.235780000000005,16.36089|48.23377000000001,16.36046|48.232110000000006,16.3609|48.22854,16.36286|48.22576,16.366200000000003|48.22482,16.36694|48.22108,16.367420000000003|48.219260000000006,16.368080000000003|48.21585,16.36069|48.21437,16.359260000000003|48.21425000000001,16.35734|48.21186,16.35661|48.21177,16.355600000000003|48.20864,16.354940000000003|48.206770000000006,16.355700000000002|48.205670000000005,16.357100000000003|48.205290000000005,16.352230000000002|48.20543000000001,16.34915|48.204240000000006,16.348940000000002|48.199870000000004,16.34928|48.19973,16.34814

which would produce a blue line with the default width of 5px:

Default, blue, 5px polyline `&path=48.26491,16.398|...`

Default, blue, 5px polyline &path=48.26491,16.398|...

Default, blue, 5px polyline `&path=48.26491,16.398|...`

To adjust its width, pass a weight attribute:

A 3px polyline `&path=weight:3|48.26491,16.398|...`

A 3px polyline &path=weight:3|48.26491,16.398|...

A 3px polyline `&path=weight:3|48.26491,16.398|...`

To change the line's color, use the color attribute.

Since polyines are 50% transparent by default , append the opacity as an ff to the 32-bit hexadecimal value to make the polyline โ€œfullyโ€ black:

Pitch-black 4px polyline `&path=color:0x000000ff|weight:4|48.26491,...`

Pitch-black 4px polyline &path=color:0x000000ff|weight:4|48.26491,...

Pitch-black 4px polyline `&path=color:0x000000ff|weight:4|48.26491,...`

Sorry, 32-bit what?

You're probably used to traditional, 24-bit HEX color strings, e.g. #FFFFFF for white and #000000 for black. Such strings, when deconstructed, specify the RGB (red/green/blue) channels:

# FF FF FF
  ^^ red
     ^^ green
        ^^ blue

In hexadecimal notation , FF represents 255, i.e. the highest available channel value. Then, 8 bits per color amount to 24 bits in total.

In Google Maps Static API, you'll prepend the hex color strings with an 0x instead of a #.

Time for a slight detour. As I'm writing this chapter and inspect Notion's background color, #191919, the RGB channels are equally set to the hexadecimal 19, which, in RGB-speak is 25 out of 255:

The last channel (100) is the opacity. We'll come back to it momentarily.

Notion's native app is built with Chromium โ€” so you can toggle the Chrome DevTools directly in the app (run ctrl + alt + i / โŒ˜ + โŒฅ + i). You can then use DevTools' color picker to inspect and modify colors:

With RBG, opacity is usually expressed as a decimal value between 0 and 1. This transparency (โ€œalphaโ€) channel is appended to the color definition. For a semi-transparent black, you'd use rgb(0, 0, 0, 0.5).

To convert these 50% to a hex value, remember that the maximum channel value is 255. 50% of 255 is roughly 127, and 127 converted to hexadecimal is 7F. So, to color your polyline with semi-transparent black, you'd pass 0x0000007F โ€” a 32-bit hexadecimal value.

Google Maps Static API puts a hard limit on the request URL length โ€” namely 8192 characters.

But your polylines might be longer than that.

Even if the coordinates are rounded to 5 decimal places and assuming all coordinates are negative and double digit (e.g. -12.12345), that's 9 characters per coordinate, 18 per pair, and 19 including the pipe symbol |. Such sets of coordinates add up quickly and may not fully fit into the request URL.

In instances like theses, it's better to compress the polyline to ensure it fits inside the URL.

Google requires you to use their own compression algorithm . Without getting into the specifics too much, it's good to know that you can:

Assuming your geo points comprise a list of [lat, lng] pairs:

[
  [48.26491, 16.398],
  [48.26408, 16.39764],
  [48.26313, 16.40098],
  [48.26515, 16.40229],
  [48.26607, 16.39792],
  [48.26594, 16.39651],
  [48.26561, 16.39609],
  [48.26157, 16.39431],
  [48.2603, 16.39292],
  [48.25309, 16.37521],
  [48.25213, 16.37348],
  [48.25026, 16.37141],
  [48.24789, 16.37002],
  [48.24164, 16.36448],
  [48.23578, 16.36089],
  [48.23377, 16.36046],
  [48.23211, 16.3609],
  [48.22854, 16.36286],
  [48.22576, 16.3662],
  [48.22482, 16.36694],
  [48.22108, 16.36742],
  [48.21926, 16.36808],
  [48.21585, 16.36069],
  [48.21437, 16.35926],
  [48.21425, 16.35734],
  [48.21186, 16.35661],
  [48.21177, 16.3556],
  [48.20864, 16.35494],
  [48.20677, 16.3557],
  [48.20567, 16.3571],
  [48.20529, 16.35223],
  [48.20543, 16.34915],
  [48.20424, 16.34894],
  [48.19987, 16.34928],
  [48.19973, 16.34814],
];

You can encode this polyline with:

import { encode } from '@googlemaps/polyline-codec';

const points = [...];
const encodedPath = encode(points);
// returns "uvqeHovacBdDfA|D{SsKeGwDhZXxG`ArAfXbJ|FtG`l@tmB~DxItJ|KxMtG`f@ra@rc@lUpKtAjIwAhUgKjP{SzDsCjV_BjJcChTdm@fH|GV~J|MpCPhEpRbCtJwCzEwGjAl][fRlFh@hZcAZbF"

To pass this encoded polyline to the Static Map endpoint, you'd prefix the path with enc::

path=color:0x00000078|weight:4|enc:uvqeHovacBdDfA|D{SsKeGwDhZXxG`ArAfXbJ|FtG`l@tmB~DxItJ|KxMtG`f@ra@rc@lUpKtAjIwAhUgKjP{SzDsCjV_BjJcChTdm@fH|GV~J|MpCPhEpRbCtJwCzEwGjAl][fRlFh@hZcAZbF
															 ^^^^

which is equivalent to the traditional, much longer string of:

path=color:0x00000078|weight:4|48.26491,16.398|48.26408,16.39764|48.26313,16.40098|48.26515,16.40229|48.26607,16.39792|48.26594,16.39651|48.26561,16.39609|48.26157,16.39431|48.2603,16.39292|48.25309,16.37521|48.25213,16.37348|48.25026,16.37141|48.24789,16.37002|48.24164,16.36448|48.23578,16.36089|48.23377,16.36046|48.23211,16.3609|48.22854,16.36286|48.22576,16.3662|48.22482,16.36694|48.22108,16.36742|48.21926,16.36808|48.21585,16.36069|48.21437,16.35926|48.21425,16.35734|48.21186,16.35661|48.21177,16.3556|48.20864,16.35494|48.20677,16.3557|48.20567,16.3571|48.20529,16.35223|48.20543,16.34915|48.20424,16.34894|48.19987,16.34928|48.19973,16.34814
import { encode } from "@googlemaps/polyline-codec";

const key = "AIza...";
const base = "https://maps.googleapis.com/maps/api/staticmap?";

const generateStaticMapURL = ({
  origin,
  destination,
  polyLineCoords,
}: {
  origin: google.maps.LatLngLiteral;
  destination: google.maps.LatLngLiteral;
  polyLineCoords: number[][];
}) => {
  const q = [
    "size=312x358",
    `markers=|anchor:center|icon:https://storage.googleapis.com/gmaps-handbook/public/common/6/6.2/circle-8px.png|${origin.lat},${origin.lng}`,
    `markers=|anchor:bottom|icon:https://storage.googleapis.com/gmaps-handbook/public/common/6/6.2/google-maps-pin-wikimedia.png|${destination.lat},${destination.lng}`,
    `path=color:0x00000078|weight:4|enc:${encode(polyLineCoords)}`,
    "map_id=cf19af61093c176a",
  ];

  return base + q.join("&") + `&key=${key}`;
};

// usage
const imageURL = generateStaticMapURL({
  origin: { lat: 48.26491, lng: 16.398 },
  destination: { lat: 48.19974, lng: 16.34814 },
  polyLineCoords: [
    [48.26491, 16.398],
    [48.26408, 16.39764],
    [48.26313, 16.40098],
    [48.26515, 16.40229],
    [48.26607, 16.39792],
    [48.26594, 16.39651],
    [48.26561, 16.39609],
    [48.26157, 16.39431],
    [48.2603, 16.39292],
    [48.25309, 16.37521],
    [48.25213, 16.37348],
    [48.25026, 16.37141],
    [48.24789, 16.37002],
    [48.24164, 16.36448],
    [48.23578, 16.36089],
    [48.23377, 16.36046],
    [48.23211, 16.3609],
    [48.22854, 16.36286],
    [48.22576, 16.3662],
    [48.22482, 16.36694],
    [48.22108, 16.36742],
    [48.21926, 16.36808],
    [48.21585, 16.36069],
    [48.21437, 16.35926],
    [48.21425, 16.35734],
    [48.21186, 16.35661],
    [48.21177, 16.3556],
    [48.20864, 16.35494],
    [48.20677, 16.3557],
    [48.20567, 16.3571],
    [48.20529, 16.35223],
    [48.20543, 16.34915],
    [48.20424, 16.34894],
    [48.19987, 16.34928],
    [48.19973, 16.34814],
  ],
});

// returns
// https://maps.googleapis.com/maps/api/staticmap?size=312x358&markers=|anchor:center|icon:https://storage.googleapis.com/gmaps-handbook/public/common/6/6.2/circle-8px.png|48.26491,16.398&markers=|anchor:bottom|icon:https://storage.googleapis.com/gmaps-handbook/public/common/6/6.2/google-maps-pin-wikimedia.png|48.19974,16.34814&path=color:0x00000078|weight:4|enc:uvqeHovacBdDfA|D{SsKeGwDhZXxG`ArAfXbJ|FtG`l@tmB~DxItJ|KxMtG`f@ra@rc@lUpKtAjIwAhUgKjP{SzDsCjV_BjJcChTdm@fH|GV~J|MpCPhEpRbCtJwCzEwGjAl][fRlFh@hZcAZbF&map_id=cf19af61093c176a&key=AIza...

You could display two variants of this static map โ€” light/dark โ€” depending on the time of the day the trip took place. You'd then simply swap the map_id as well as the colors of the path & markers.

If your Maps Static API usage will exceed 25,000 requests per day, you'll need to โ€œsignโ€ the request URLs. More info here .

Due to Google's icon restriction of 64x64px, it is not possible to directly create a static map like this one:

Custom static map, courtesy of Garages Near Me

Custom static map, courtesy of Garages Near Me

Custom static map, courtesy of Garages Near Me

So how was the above map created?

In short, it's a supercomposition of two images: a static google basemap and a semi-transparent, centrally-positioned PNG logo.

jimp , the image manipulation library, exposes the handy composite utility and the implementation goes something like this:

The core idea here is to let Google handle as many of the geometry and lat/lng โ†’ x,y image conversions as possible. Though, you could very well draw a polyline yourself on the jimp canvas by:


Done! You're now able to create outstanding branded static maps.

In the next chapter we'll dive into the nuances of geocoding .