You have a web app — a store locator, a delivery tracker, an asset dashboard — and you need a map in it. Not a screenshot of a map, but a real one: pan, zoom, a pin you can drop at a latitude and longitude, a popup when someone clicks it. The instinct is to reach for whatever map library you saw in a tutorial, paste a key, and move on. Then the bill arrives, or the key leaks, or the map renders blank in production, and you wish someone had walked you through it properly the first time. Azure Maps is Microsoft’s geospatial platform — a set of REST services (tiles, search, routing, geolocation, traffic, weather) plus a browser Web SDK that renders an interactive, WebGL-accelerated map — and this article is that proper walk-through.
We are doing one concrete thing: adding an interactive map with a single marker to a web page, backed by a real Azure Maps account, the right way. “The right way” means you understand the two pieces you create (an Azure Maps account and the credential it issues), you choose the pricing tier and authentication mode deliberately, and you can reproduce the whole thing three ways — the Azure portal, the az CLI, and Bicep so it lives in source control. By the end you will have a map on screen with a pin on it, and you will know exactly which knob does what.
This is deliberately a first map — not a routing engine or a tour of every REST endpoint, but the smallest end-to-end slice that is still real and production-aware. The habits you form here (don’t ship a key in client-side code, pick the cheapest tier that works, tag and budget the resource) are the ones you keep when the map grows up.
What problem this solves
Every “where” feature needs the same plumbing: a rendered basemap, a coordinate system, the ability to place and style points, and a way to react to clicks. Building that yourself — sourcing tiles, handling projections, writing pan/zoom math — is months of work and a legal minefield around map-data licensing. A managed platform hands you all of it behind one credential and one SDK. Azure Maps solves “I need a commercially-licensed map this week, billed on the same invoice and using the same identity model as the rest of my Azure estate.”
What breaks without doing this deliberately is predictable. People embed a subscription key in a public page, where anyone with DevTools can read it and a scraper can run up the bill. People pick the wrong pricing tier and either lack a feature or pay for one they don’t use. People see a blank grey rectangle because the SDK script didn’t load, the container <div> has zero height, or the key is wrong — three causes, one symptom. None are hard once you’ve seen them; all cost an afternoon the first time.
Who hits this: front-end and full-stack developers adding a location feature, solo builders who want something that “just works” without tile servers, and architects standardising on Azure who want maps on the same identity (Microsoft Entra ID), governance and billing as everything else. If you’ve ever pasted a maps key into a <script> tag and hoped, this article is the antidote.
Learning objectives
By the end of this article you can:
- Explain what an Azure Maps account is, what the Web SDK does, and how the two relate.
- Choose between the Gen2 (S0/S1) pricing tiers and between shared-key and Microsoft Entra ID authentication for your scenario, and say why.
- Create an Azure Maps account three ways — Azure portal,
az maps accountCLI, and Bicep — and retrieve its credentials safely. - Load the Azure Maps Web SDK in an HTML page, initialise a
Map, set its centre and zoom, and confirm it renders. - Place your first marker with an
HtmlMarker, attach a popup, and react to a click event. - Identify and fix the four blank-map failure modes (script not loaded, zero-height container, bad key, wrong domain authorization).
- Apply the baseline security, cost and best-practice rules — and tear it all down — so your first map never becomes an incident or a surprise on next month’s bill.
Prerequisites & where this fits
You need an Azure subscription (a free trial is enough — Azure Maps has a generous free grant, see Cost & sizing) and the ability to run the az CLI, locally or in Azure Cloud Shell (which has az and Bicep pre-installed — the fastest start). You should be comfortable editing a small HTML file and serving it over a local web server (python3 -m http.server; the Web SDK needs http:///https://, not file://). Basic JavaScript helps; no GIS, cartography or WebGL knowledge is assumed.
Where this fits: Azure Maps is a developer service your application calls — the map runs in the browser, the account and credential live in your subscription. Where the app hosting the map runs is the Azure App Service vs Container Apps vs AKS decision; Azure Maps is agnostic to it, since the SDK is just static JS and CSS, and the account sits in your Azure resource hierarchy like any other resource. When you graduate from a raw key to a managed identity and a secrets store, you’ll lean on Azure Key Vault and the patterns in Azure App Configuration and Key Vault references. Once real users hit the map, you’ll watch usage and errors through Azure Monitor and Application Insights.
A quick orientation to the moving parts before the deep dive:
| Piece | What it is | Where it lives | You touch it… |
|---|---|---|---|
| Azure Maps account | The billable resource that exposes the Maps services | Your subscription / resource group | Once, at setup |
| Pricing tier (SKU) | S0 or S1 — sets features, limits and billing model |
A property of the account | At create time (changeable) |
| Credential | A shared key, an SAS token, or an Entra token | Issued by the account | Every map load |
| Web SDK | Browser JS/CSS library that renders the map | A CDN or your node_modules |
In your front-end code |
| REST services | Search, Route, Render, Geolocation, etc. | Microsoft-hosted endpoints | The SDK calls them for you |
Core concepts
Five ideas make everything that follows obvious.
The account is the resource; the SDK is the code. An Azure Maps account is a lightweight Azure resource — a name, a resource group, a pricing tier, and credentials it issues — that does almost nothing by itself. The visible, interactive map is produced entirely in the browser by the Web SDK (the atlas JavaScript library), which authenticates to the Maps REST services using a credential the account issued. You create one account, then write code that points at it; one account can back many maps and apps.
A “map” is a WebGL canvas the SDK manages. When you call new atlas.Map(...), the SDK creates a WebGL canvas inside a <div> you give it, fetches vector map tiles from the Render service, and handles pan and zoom. You position it with a camera: a center ([longitude, latitude], longitude first) and a zoom level (roughly 0 = whole world, 22 = building-level). Markers, lines and popups layer on this canvas.
Coordinates are [lon, lat], not [lat, lon]. This trips up nearly everyone. Azure Maps follows the GeoJSON standard — longitude first. A human says “latitude, longitude” (Bengaluru is 12.97 N, 77.59 E), but the SDK wants [77.59, 12.97]. Get it backwards and your marker lands in the ocean off Africa, not in India. Memorise: X then Y, lon then lat.
A marker is a styled point you attach to the map. The simplest pin is an HtmlMarker — an HTML element (a teardrop pin by default) anchored to a coordinate. You give it a position (and optional color/text), add it to the map, and it tracks the camera as you pan and zoom. Attach a Popup and wire a click event to open it — the whole “first marker” experience.
The credential is the thing you must protect. The account issues a shared key (primary and secondary, for rotation) granting full access to the Maps services — anyone with it can spend your quota. In client-side code, which is public by definition, don’t ship a raw shared key beyond a throwaway demo; the grown-up options are Microsoft Entra ID with a runtime token, or an SAS token with a tight expiry and rate cap. We’ll start with a key for speed, then harden it.
The vocabulary in one table
| Term | One-line definition | Why it matters for your first map |
|---|---|---|
| Azure Maps account | The Azure resource exposing Maps services | The thing you create and bill |
| Pricing tier / SKU | S0 or S1 (Gen2) — feature & billing band |
Decides cost model and which APIs you can call |
| Shared key | A long secret string granting full access | Fastest auth; unsafe to ship in public JS |
| SAS token | A scoped, time-limited, rate-capped token | Safer client-side credential than a raw key |
| Microsoft Entra ID auth | Token-based auth via Entra + RBAC | The production-grade auth model |
Web SDK (atlas) |
Browser JS/CSS that renders the map | The code that draws the interactive map |
Map |
The WebGL canvas object | The map itself |
| Camera | center + zoom (+ bearing/pitch) |
Where the map is looking |
HtmlMarker |
An HTML pin anchored to a coordinate | Your first pin |
Popup |
An info bubble | The “click the pin” experience |
| GeoJSON | The [lon, lat] coordinate standard |
Why coordinate order is lon-first |
Choosing your pricing tier
Azure Maps bills on the Gen2 model, exposed as two SKUs on the account; you pick one at create time and can change it later. The difference is the billing model and which capabilities are surfaced, not map quality — the basemap looks the same either way.
| Tier | Billing model | Best for | Notes |
|---|---|---|---|
| Gen2 S0 | Pay-as-you-go per transaction, with a monthly free grant | Most apps, including production, that pay per use | The default starting point; you pay for what you call |
| Gen2 S1 | Higher-throughput / committed-style band for heavy, sustained use | High-volume workloads needing elevated limits | Choose only when S0 limits or throttling bite |
For a first map — and most apps — Gen2 S0 is the correct choice: a free monthly allowance (see Cost & sizing), pay-as-you-go beyond that, and full support for the Web SDK, markers and the common services. Reach for S1 only when you’ve measured sustained high volume and need its elevated throughput; don’t pick the bigger tier “to be safe.”
A note on the transaction, because it drives the bill: panning/zooming pulls map tiles, metered in batches (render transactions count per a set number of tiles, not per tile); each search or routing call is its own transaction.
Choosing your authentication
This is the decision that separates a safe map from a leaked credential. Three ways the Web SDK can authenticate to the Maps services:
| Auth method | How it works | Safe in public client JS? | Setup effort | Use it for |
|---|---|---|---|---|
| Shared key | A static secret string sent with each call | No — it’s visible to anyone | Lowest | Local demos, server-to-server calls |
| SAS token | A signed, time-boxed, rate-limited token minted server-side | Yes, if short-lived and scoped | Medium | Public web apps without full Entra wiring |
| Microsoft Entra ID | A bearer token from Entra; access via Azure RBAC | Yes — the recommended model | Higher | Production apps and enterprises |
The honest pattern for learning: use a shared key to get the map on screen in five minutes, then never ship that key in a public page — a key in a page anyone can open is your account password, published. The lab’s local-demo key is fine because the page runs only on your machine; the moment it’s public, switch to SAS or Entra.
How the credential reaches the SDK differs only in the authOptions.authType value — 'subscriptionKey', 'sas', or 'aad'. The lab uses the shared-key version; Part 6 shows the exact Entra and SAS variants. The map code is identical across all three; only the authOptions block differs.
Loading the Web SDK
Two ways to get the atlas library into your page. Option A — the CDN (fastest, what the lab uses): two <link>/<script> tags at Microsoft’s Azure Maps CDN give you the latest v3 SDK with no build step — ideal for learning. Option B — the npm package (npm install azure-maps-control) bundles atlas with Vite/webpack, pinning a version and fitting a component framework (React/Vue/Angular); move to it once the map is part of a real front end. Either way, load both SDK assets:
| Asset | What it is | CDN tag |
|---|---|---|
atlas.min.css |
The SDK’s stylesheet (controls, markers, popups) | <link rel="stylesheet" href="https://atlas.microsoft.com/sdk/javascript/mapcontrol/3/atlas.min.css"> |
atlas.min.js |
The SDK script that defines the atlas namespace |
<script src="https://atlas.microsoft.com/sdk/javascript/mapcontrol/3/atlas.min.js"></script> |
Forgetting the CSS is a classic first mistake: the map works but markers and controls look broken because their styles never loaded.
Placing your first marker
Once the map exists, a marker is three lines: create it with a position, add it to the map, and wire a popup to a click. The HtmlMarker options you’ll use on day one:
| Option | What it sets | Example | Default |
|---|---|---|---|
position |
The [lon, lat] the pin sits at |
[77.5946, 12.9716] |
required |
color |
The pin fill colour | '#E81123' |
a default blue |
text |
A short label drawn on the pin | '1' |
none |
draggable |
Whether the user can drag the pin | true |
false |
htmlContent |
Custom HTML instead of the default pin | '<div class="dot"></div>' |
the teardrop pin |
And the three SDK calls that put it on screen and make it interactive:
| Call | What it does |
|---|---|
new atlas.HtmlMarker({ position, color, text }) |
Creates the pin object |
map.markers.add(marker) |
Renders it on the map |
map.events.add('click', marker, handler) |
Runs handler on click (e.g. open a popup) |
The lab assembles these into one working file.
Architecture at a glance
Picture three layers with one credential threading through them. In the browser, your HTML page loads the atlas Web SDK (CSS + JS) and calls new atlas.Map(...), handing it the <div> to draw in and an authOptions block. Across the network, the SDK authenticates with that credential and requests vector map tiles from the Azure Maps Render service over HTTPS, streaming them in as you pan and zoom; search and routing go to their own REST endpoints the same way. In your subscription sits the Azure Maps account — the resource that validates the credential, meters transactions for billing, and issued that credential in the first place.
The flow is a loop: the page asks the SDK for a map, the SDK requests tiles using your credential, the account validates it and counts the usage, tiles render, and your HtmlMarker is drawn on top. The marker and popup are pure client-side — no server round-trip — so clicking a pin is instant. The model to keep: the account authorises and bills; the SDK draws; the credential is the bridge. The lab makes those three layers talk; Security notes makes the bridge safe to cross in public.
Real-world scenario
Cafezure, a 40-store coffee chain in Bengaluru, wanted a store-locator page: a map of every outlet, click a pin for address and hours. A junior developer shipped it in two days with the Azure Maps Web SDK and a shared subscription key pasted straight into the page’s JavaScript. It demoed great and went live on a Friday.
By Monday, two things had gone wrong. First, a competitor’s scraper had found the key in the page source (it’s right there in DevTools) and was hammering the Render and Search services to harvest store data, pushing the transaction count up sharply — not yet a huge bill on S0’s free grant, but a clear abuse trend and a leaked credential that now had to be rotated. Second, the map intermittently rendered as a blank grey box for some users. The developer assumed it was the key; it wasn’t. The page’s CSS framework set the map container’s height to auto, which computed to zero — so the WebGL canvas was there but had no pixels to fill. Where another element forced the container open it worked; elsewhere, grey.
The architect’s fix took an afternoon. The shared key was rotated immediately and removed from client code. Authentication moved to a short-lived SAS token minted by a tiny serverless endpoint, scoped to Render and Search with a rate cap and a one-hour expiry — so even a leaked token expires fast and can’t exceed the cap. The blank-map bug was fixed by giving the container an explicit height (#map { height: 480px; }) and confirming the SDK CSS loaded. They fronted the token endpoint with an origin check, and turned on diagnostic logging to Log Analytics so transaction spikes raise an alert.
The lesson this whole article teaches: the map was never the hard part — it rendered in two days. The hard parts were the credential (don’t ship a raw key publicly) and the container (a WebGL map needs an explicit, non-zero height). Get those two right on day one and the map is boring, in the best way. Cafezure now treats the maps key like any other secret — rotated on a schedule, never in client code, monitored for anomalies — and the locator has run clean since.
Advantages and disadvantages
| Advantages | Disadvantages |
|---|---|
| Managed, commercially-licensed map data — no tile servers to run | Another service and credential to govern and secure |
| First-party Azure resource: same billing, RBAC, tagging, regions | Smaller third-party plugin ecosystem than some rival map libraries |
| Generous free monthly grant covers learning and small apps | Pay-per-transaction can surprise you if a key leaks or a loop runs |
| Modern WebGL Web SDK (vector tiles, smooth pan/zoom) | WebGL dependency: very old browsers/devices degrade |
| Microsoft Entra ID + SAS auth for proper credential hygiene | Doing auth right (Entra/SAS) is more setup than pasting a key |
| Rich service family to grow into (search, route, traffic, weather) | Easy to over-provision the tier or call services you don’t need |
The advantages matter most when you’re already on Azure and want maps on the same identity, governance and invoice, with licensed data and a free on-ramp that scales to production. The disadvantages bite on a tiny static site where any free widget would do — or for a team with no appetite to manage secrets, who will leak a key, so invest the hour in SAS/Entra first.
Hands-on lab
This is the centrepiece. You will create an Azure Maps account (three ways), build a page with an interactive map, drop a marker, validate it, then tear it all down. Total Azure cost: effectively zero — you’ll stay inside the free grant.
What you’ll build: a single HTML page showing a map centred on Bengaluru with one clickable, popup-bearing marker, authenticated with a shared key for local development.
Prerequisites checklist:
| Need | How to get it | Verify |
|---|---|---|
| Azure subscription | Free trial at azure.com | az account show returns a subscription |
az CLI + Bicep |
Use Cloud Shell, or install locally | az version shows azure-cli and bicep |
| A text editor | Any | — |
| A local web server | python3 -m http.server (built in) |
python3 --version works |
| A modern browser | Edge/Chrome/Firefox/Safari | WebGL enabled (default) |
Part 1 — Create the account in the Azure portal
- Sign in to the Azure portal (
portal.azure.com). - In the top search bar, type Azure Maps and select Azure Maps Accounts.
- Click + Create.
- On the Basics tab, fill in:
- Subscription: your subscription.
- Resource group: click Create new, name it
rg-maps-lab. - Name:
maps-lab-kv(globally meaningful, lowercase). - Pricing tier: select Gen2 (this maps to the S0 pay-as-you-go model).
- Region: pick one near you (e.g. Central India) — this is the metadata region.
- Tick the box acknowledging the terms (it confirms you accept the Azure Maps licensing).
- Click Review + create, then Create.
- Expected output: a deployment-succeeded notification within ~30 seconds. Click Go to resource.
- In the account’s left menu, open Authentication (or Settings → Authentication). You’ll see Shared Key Authentication with a Primary Key and Secondary Key. Click the copy icon on the Primary Key and keep it handy for Part 4.
Validation: the account’s Overview blade shows status as available and the pricing tier as Gen2/S0. You now have a working Maps account and a key.
Part 2 — Create the same account with the az CLI
If you prefer the command line (or want it reproducible), do this instead of — or in addition to — Part 1. Open Cloud Shell (the >_ icon in the portal) or a local terminal.
- Sign in and select your subscription (skip in Cloud Shell, you’re already signed in):
az login
az account set --subscription "<your-subscription-name-or-id>"
- Create the resource group:
az group create --name rg-maps-lab --location centralindia
Expected output: JSON with "provisioningState": "Succeeded".
- Register the Maps resource provider (only needed once per subscription; harmless if already done):
az provider register --namespace Microsoft.Maps
- Create the Azure Maps account on the Gen2 S0 tier:
az maps account create \
--name maps-lab-cli \
--resource-group rg-maps-lab \
--sku S0 \
--kind Gen2 \
--accept-tos
Expected output: JSON describing the account, including "name": "maps-lab-cli" and the SKU. The --accept-tos flag is your programmatic acceptance of the licensing terms — the create fails without it.
- Retrieve the keys (don’t print these into shared logs):
az maps account keys list \
--name maps-lab-cli \
--resource-group rg-maps-lab \
--query "{primary:primaryKey, secondary:secondaryKey}" -o json
Expected output: a JSON object with primary and secondary key strings. Copy the primary for the lab page.
The az maps account commands you’ll use in this lab and beyond:
| Command | What it does |
|---|---|
az maps account create |
Creates the account (needs --sku, --kind, --accept-tos) |
az maps account show |
Shows account properties |
az maps account list |
Lists accounts in a group/subscription |
az maps account keys list |
Reads the primary/secondary shared keys |
az maps account keys renew |
Rotates a key (--key primary or --key secondary) |
az maps account update |
Changes tier or other properties |
az maps account delete |
Deletes the account |
Part 3 — Declare it as Bicep (infrastructure as code)
For anything you intend to keep, put it in source control. Create a file main.bicep:
@description('Name of the Azure Maps account (globally meaningful, lowercase).')
param mapsAccountName string = 'maps-lab-bicep'
@description('Location for the resource group; Maps metadata is global.')
param location string = resourceGroup().location
resource maps 'Microsoft.Maps/accounts@2023-06-01' = {
name: mapsAccountName
location: 'global' // Azure Maps account location is 'global'
sku: {
name: 'S0' // Gen2 pay-as-you-go; use 'S1' only for sustained high volume
}
kind: 'Gen2'
properties: {
disableLocalAuth: false // true would turn OFF shared-key auth (Entra-only) — keep false for the lab
}
tags: {
purpose: 'maps-first-map-lab'
owner: 'vinod'
}
}
output mapsAccountId string = maps.id
output mapsAccountName string = maps.name
Deploy it into the existing resource group:
az deployment group create \
--resource-group rg-maps-lab \
--template-file main.bicep
Expected output: "provisioningState": "Succeeded" and the two outputs (mapsAccountId, mapsAccountName). You now have the same account, but reproducibly, reviewable in a pull request. Note disableLocalAuth: leaving it false keeps shared-key auth on (what the lab uses); flipping it to true is the one-line way to enforce Entra-only authentication in production.
The key Bicep properties and what each controls:
| Property | Values | Meaning |
|---|---|---|
location |
'global' |
Azure Maps accounts deploy as global resources |
sku.name |
'S0' | 'S1' |
Gen2 pricing tier |
kind |
'Gen2' |
The current generation |
properties.disableLocalAuth |
false | true |
true disables shared keys → Entra-only |
tags |
any map | Cost-allocation and ownership metadata |
Part 4 — Build the map page and drop a marker
Create a file named index.html. This is the whole app. Replace YOUR_AZURE_MAPS_KEY with the primary key you copied.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>My First Azure Map</title>
<!-- 1) Azure Maps Web SDK: CSS + JS (both required) -->
<link rel="stylesheet"
href="https://atlas.microsoft.com/sdk/javascript/mapcontrol/3/atlas.min.css" />
<script src="https://atlas.microsoft.com/sdk/javascript/mapcontrol/3/atlas.min.js"></script>
<style>
html, body { margin: 0; height: 100%; }
/* 2) The map container MUST have an explicit, non-zero height */
#map { width: 100%; height: 480px; }
</style>
</head>
<body>
<div id="map"></div>
<script>
// 3) Create the map, authenticating with a shared key (LOCAL DEMO ONLY)
const map = new atlas.Map('map', {
center: [77.5946, 12.9716], // [longitude, latitude] — Bengaluru. lon FIRST.
zoom: 11,
authOptions: {
authType: 'subscriptionKey',
subscriptionKey: 'YOUR_AZURE_MAPS_KEY'
}
});
// 4) Wait until the map is ready, then add a marker + popup
map.events.add('ready', function () {
const marker = new atlas.HtmlMarker({
position: [77.5946, 12.9716], // same [lon, lat]
color: '#E81123',
text: '1'
});
map.markers.add(marker);
const popup = new atlas.Popup({
content: '<div style="padding:8px">Hello from Bengaluru!</div>',
pixelOffset: [0, -30]
});
// 5) Open the popup when the marker is clicked
map.events.add('click', marker, function () {
popup.setOptions({ position: marker.getOptions().position });
popup.open(map);
});
});
</script>
</body>
</html>
Serve it (the SDK won’t run from a file:// path — it needs a web origin):
# from the folder containing index.html
python3 -m http.server 8080
Then open http://localhost:8080 in your browser.
Expected output at each step:
| Step | What you should see |
|---|---|
| Page loads | An interactive map centred on Bengaluru |
| Pan/zoom | Smooth movement, labels in view |
| The marker | A red teardrop pin labelled “1” downtown |
| Click the pin | A popup reading “Hello from Bengaluru!” |
| DevTools → Console | No red 401/403 or atlas is not defined errors |
| DevTools → Network | 200 responses from atlas.microsoft.com tile requests |
Part 5 — Validate it properly
- Visual: the pin sits downtown, not in the sea (in the ocean off Africa means you swapped lat/lon — use
[77.59, 12.97]). - Network tab: filtered on
atlas.microsoft.com, tile requests return 200 (a 401/403 means a bad or unauthorized key). - Console: no
atlas is not defined(SDK script didn’t load) and no auth errors. - CLI cross-check: confirm the account is live and on the right tier:
az maps account show --name maps-lab-cli --resource-group rg-maps-lab \
--query "{name:name, sku:sku.name, kind:kind}" -o table
Expected output: a one-row table showing your account, S0, Gen2.
Part 6 — (Optional) See the Entra and SAS variants
You don’t deploy these, but here’s exactly what changes for the safe auth modes — only the authOptions block differs; the map and marker code is identical.
Microsoft Entra ID:
authOptions: {
authType: 'aad',
aadAppId: '<your-entra-app-client-id>',
aadTenant: '<your-entra-tenant-id>',
clientId: '<the-maps-account-client-id>' // from the account's Authentication blade
}
SAS token (the token is minted server-side and fetched at runtime):
authOptions: {
authType: 'sas',
getToken: function (resolve, reject, map) {
fetch('/api/maps-token') // your tiny server endpoint returns a short-lived SAS string
.then(r => r.text())
.then(resolve)
.catch(reject);
}
}
Part 7 — Teardown (do this when done)
A learning resource that lingers becomes a line item. Delete the whole resource group — it removes every account you created in one shot:
az group delete --name rg-maps-lab --yes --no-wait
Expected output: the command returns immediately (--no-wait); deletion completes in the background. Verify after a minute:
az group exists --name rg-maps-lab
# -> false (once deletion finishes)
In the portal, the equivalent is rg-maps-lab → Delete resource group → type the name to confirm. Either way, your bill returns to zero. To keep one account instead, delete the others individually with az maps account delete --name <name> --resource-group rg-maps-lab.
Common mistakes & troubleshooting
The “blank grey box” is the single most reported first-map problem, with at least four distinct causes that look identical. Work this table top to bottom — confirm with the exact check, then apply the fix.
| # | Symptom | Root cause | How to confirm | Fix |
|---|---|---|---|---|
| 1 | Blank grey box, atlas is not defined in console |
SDK <script> didn’t load / loaded after your code |
Console error names atlas; Network shows atlas.min.js missing or 404 |
Ensure the SDK <script> is in <head> and loads before your map code |
| 2 | Blank box, no console error | Container <div> has zero height |
Inspect #map in DevTools → computed height is 0px |
Give #map an explicit height, e.g. height: 480px |
| 3 | Map area present but 401/403 on tile requests |
Bad, wrong, or unauthorized key | Network tab: tile requests return 401/403 |
Re-copy the primary key; confirm it’s the right account |
| 4 | Markers/controls look broken or invisible | SDK CSS not loaded | No atlas.min.css in Network; controls unstyled |
Add the <link> for atlas.min.css |
| 5 | Pin lands in the ocean off Africa | Coordinates swapped to [lat, lon] |
Pin near 0,0-ish / Gulf of Guinea |
Use [lon, lat] — longitude first |
| 6 | Map shows but Failed to fetch / CORS errors |
Page opened via file:// |
URL bar shows file:///... |
Serve over http:// (python3 -m http.server) |
| 7 | Marker code runs but nothing appears | Added the marker before the map was ready | No marker; no error | Add markers inside the 'ready' event handler |
| 8 | Works locally, 403 once deployed |
Public key blocked / domain not allowed | 403 only from the deployed origin |
Move to SAS/Entra; don’t ship raw keys publicly |
| 9 | Intermittent 429 Too Many Requests |
Throttled — too many transactions | Network tab shows 429 |
Reduce call rate; cache; consider S1 if sustained |
| 10 | az maps account create fails immediately |
Missing terms acceptance | Error mentions terms / tos |
Add --accept-tos to the create command |
| 11 | Bicep deploy fails on location |
Used a region instead of global |
Error on the location property |
Set location: 'global' for the Maps account |
The one distinction that saves the most time: the blank-box symptom has three causes that look identical, but the tool tells them apart — the Console naming atlas means a script-order/load problem, a computed height of 0 means the container, and a 401/403 in Network means the key.
Best practices
- Never ship a raw shared key in public client code. Use SAS (short-lived, rate-capped) or Microsoft Entra ID for any page real users can open. A key in a page is a published password.
- Start on Gen2 S0. It has a free monthly grant and pay-as-you-go. Move to S1 only when measured volume forces it.
- Always give the map container an explicit, non-zero height. A WebGL canvas with
height: autoresolving to0is the number-one blank-map cause. - Load both SDK assets —
atlas.min.cssandatlas.min.js. CSS-less maps render with broken controls. - Remember
[lon, lat]. Longitude first, everywhere. Wrap a tiny helper if your data islat,lon. - Add markers inside the
'ready'event. Touchingmap.markersbefore the map initialises silently no-ops. - Pin the SDK version for production. The lab uses the rolling
/3/path; in a real app, installazure-maps-controlvia npm and pin a version so a CDN update can’t change behaviour under you. - Rotate keys on a schedule and immediately if one leaks (
az maps account keys renew --key primary). Two keys exist precisely so you can rotate without downtime. - Tag the account (
owner,purpose, cost-centre) so it surfaces correctly in cost reports. - Tear down learning resources — delete the resource group when done so a demo doesn’t bill forever.
Security notes
The map is harmless; the credential is the entire attack surface, so treat it like one.
- Shared keys are full-access and static. Anyone holding one can use your account until you rotate it — keep them out of client code, git and screenshots. If you must use a key, use it server-side only.
- Prefer Microsoft Entra ID for production: it replaces the static secret with a short-lived bearer token and grants access via Azure RBAC (e.g. Azure Maps Data Reader) to a managed identity, so there’s no secret to leak. Enforce it with
disableLocalAuth: true. - If you can’t do full Entra yet, use SAS tokens — minted server-side with a tight expiry, a rate limit and scope, so a leaked token self-destructs and can’t run up an unbounded bill.
- Store secrets in Azure Key Vault and reference them via Azure App Configuration and Key Vault references — never in code or committed config.
- Front your token endpoint with an origin/referer check so only your own site can request a credential, even on top of SAS expiry.
- Monitor for abuse via Azure Monitor and Application Insights: alert on sudden transaction spikes, the signature of a leaked key being scraped. Apply least privilege — a read-only map page needs only read access to the services it calls.
Cost & sizing
Azure Maps is metered by transaction, and a first map costs effectively nothing because Gen2 includes a free monthly grant before any pay-as-you-go charges. The free quantities differ per service (renders, searches, route calls each have their own allotment and per-unit price) and are on the Azure Maps pricing page — check current figures before you scale, but for learning and small apps you’ll stay inside the free tier. What drives the bill, and how to keep it small:
| Cost driver | What it is | How to control it |
|---|---|---|
| Map render transactions | Tile/render usage as users pan/zoom | Sensible default zoom; don’t auto-refresh the map |
| Search transactions | Geocoding / place lookups | Cache results; debounce search-as-you-type |
| Route / traffic / weather calls | Each call is its own transaction | Only call what the feature needs |
| Leaked-key abuse | A scraper hammering your account | SAS/Entra auth + spike alerts (the big one) |
| Wrong tier | Paying for S1 headroom you don’t use | Start S0; upgrade only on measured need |
A rough sense of scale: a store-locator that a few thousand monthly visitors load once or twice each generates a modest number of render transactions — typically inside the free grant, so ₹0 / $0 in practice. The cost risk here is almost never legitimate traffic; it’s an exposed key being abused. Size the tier at S0, protect the credential, set a spike alert, and revisit only when real usage tells you to.
Interview & exam questions
1. What is an Azure Maps account, and what does it actually do? It’s the billable Azure resource that exposes the Maps REST services and issues credentials (shared keys, SAS, Entra access). It doesn’t render maps itself — the browser Web SDK does, authenticating with a credential the account issued.
2. Why are coordinates [longitude, latitude] and not [latitude, longitude]?
Azure Maps follows the GeoJSON standard, which orders coordinates X-then-Y, i.e. longitude first. Humans say “lat, long,” so swapping them is a classic bug that lands markers in the wrong place.
3. Why should you not put a shared key in client-side JavaScript? Client code is public — anyone can read the key in DevTools and use your account, spending your quota or running up a bill. Production apps use Microsoft Entra ID tokens or short-lived, rate-capped SAS tokens instead.
4. What’s the difference between Gen2 S0 and S1? Both are the current Gen2 generation; S0 is pay-as-you-go with a free monthly grant and suits most apps, while S1 offers elevated throughput for sustained high-volume workloads. Start on S0 and upgrade only on measured need.
5. A user reports a blank grey box where the map should be. Name three distinct causes.
The SDK script didn’t load (atlas is not defined), the container <div> has zero height, or the key is wrong/unauthorized (401/403 on tiles). You distinguish them via the console, computed height, and the Network tab respectively.
6. Which two SDK assets must you load, and what breaks if you forget the CSS?
atlas.min.js (the library) and atlas.min.css (the styles). Without the CSS the map may render but markers, popups and controls look broken or invisible because their styles never loaded.
7. What is disableLocalAuth on a Maps account?
A property that, when set to true, turns off shared-key authentication so the account accepts only Microsoft Entra ID tokens — the cleanest way to enforce keyless, identity-based access in production.
8. How do you rotate a shared key without downtime?
The account has primary and secondary keys; switch your apps to the secondary, regenerate the primary (az maps account keys renew --key primary), then optionally rotate again. Two keys exist precisely to allow zero-downtime rotation.
9. What is a transaction in Azure Maps billing, and why does it matter? A metered unit of usage — render/tile batches, a search, a route call each count. Cost scales with transactions, so a leaked key being scraped, or a runaway loop, directly increases the bill.
10. Where do you add a marker so it actually appears, and why?
Inside the map’s 'ready' event handler. Adding to map.markers before the map initialises silently does nothing, because the rendering surface isn’t ready yet.
11. Your map works locally but returns 403 in production. What’s the fix? A raw shared key that was fine locally is blocked or simply must not be shipped publicly. Move to SAS or Entra authentication and serve the credential from a protected server endpoint.
Quick check
- In Azure Maps coordinates, which comes first — latitude or longitude?
- Name the two SDK files you must include in the page.
- Which authentication method is unsafe to ship in public client-side code, and what should you use instead?
- You see a blank grey box and the console says
atlas is not defined. What’s the cause? - What’s the safest way to make a learning Maps account stop costing money?
Answers
- Longitude first — coordinates are
[lon, lat](GeoJSON order). atlas.min.cssandatlas.min.js— both are required; missing CSS breaks marker/control styling.- A shared subscription key; use Microsoft Entra ID or a short-lived SAS token instead.
- The SDK
<script>didn’t load (or loaded after your code) — put theatlas.min.jstag in<head>. - Delete the resource group (
az group delete --name rg-maps-lab --yes) — it removes the account and returns the bill to zero.
Glossary
- Azure Maps account: the Azure resource that exposes the Maps services and issues credentials; the billable unit.
- Web SDK (
atlas): the browser JS/CSS library that renders the WebGL map and calls the Maps services. - Pricing tier / SKU:
S0orS1on the Gen2 model — sets the billing model and limits. - Shared key: a static secret (primary/secondary) granting full account access; server-side only.
- SAS token: a signed, time-limited, rate-capped credential minted server-side — safer for client use.
- Microsoft Entra ID auth: token-based authentication with Azure RBAC; the production-grade model (enforced by
disableLocalAuth: true). Map/ camera: the SDK’s WebGL canvas and its viewpoint —center([lon, lat]) pluszoom.HtmlMarker: an HTML-element pin anchored to a coordinate.Popup: an info bubble opened on a marker click.- GeoJSON: the coordinate standard Azure Maps follows — longitude-first.
- Transaction: the metered unit of Maps usage that drives billing.
Next steps
- Decide where the page runs with Azure App Service vs Container Apps vs AKS.
- Move the credential out of client code with Azure Key Vault and Azure App Configuration and Key Vault references.
- Place the account in context with Azure resource hierarchy explained.
- Watch usage and catch leaked-key abuse with Azure Monitor and Application Insights.