
Whatever page told you "no API key required for v3" was written before June 22, 2016. Google removed keyless access on that date, and in June 2018 added the rule that the project behind the key must have a billing account. You already have an account and a key, so the JavaScript error is one of a short list Google prints in the browser console, and each one names its own fix. The current documentation is at developers.google.com/maps/documentation/javascript, and the two pages that matter are linked at the end.
Press F12 and Read the Error Name, Not the Grey Map
A broken key never says “bad key” on the page. You get a dark grey map with a “For development purposes only” watermark, or a box saying “This page can’t load Google Maps correctly”, or nothing at all. The real message is in the console:
- Open the page, press F12, click the Console tab.
- Reload the page.
- Look for a line starting
Google Maps JavaScript API error:orGoogle Maps JavaScript API warning:. The word after the colon is the code.
| Code in the console | What it means | Fix |
|---|---|---|
NoApiKeys | The script tag has no key= at all. This is the one you get when following the old “no key needed” docs. | Add key=YOUR_KEY to the script URL. Code below. |
InvalidKeyMapError | There is a key, but Google has no record of it. Usually a typo, a trailing space, or a key from a deleted project. | Copy the key again from Cloud Console → Credentials. |
ApiNotActivatedMapError | The key is real, but “Maps JavaScript API” isn’t switched on in that project. | Cloud Console → APIs & Services → Library → Maps JavaScript API → Enable. |
BillingNotEnabledMapError | The project has no billing account. The map shows the watermark. | Cloud Console → Billing → link an account. You still get the monthly free usage. |
RefererNotAllowedMapError | The key is restricted to certain websites and yours isn’t on the list. Common when a key made for the live site is tested on localhost. | Add your URL pattern to the key’s “Website restrictions”. Patterns below. |
ApiTargetBlockedMapError | The key’s “API restrictions” list doesn’t include Maps JavaScript API. | Edit the key → API restrictions → tick Maps JavaScript API. |
ApiProjectMapError | Google couldn’t match the key to a project. Sometimes temporary right after creating a key. | Wait five minutes. If it stays, make a new key. |
Load the API the Way the Current Docs Show It
Two forms are documented right now. The first is the one Google recommends: a small bootstrap block that loads only the libraries you ask for. Put your key where it says YOUR_API_KEY.
<div id="map" style="height: 400px"></div>
<script>
(g => {{ var h, a, k, p = "The Google Maps JavaScript API", c = "google", l = "importLibrary", q = "__ib__", m = document, b = window; b = b[c] || (b[c] = {{}}); var d = b.maps || (b.maps = {{}}), r = new Set, e = new URLSearchParams, u = () => h || (h = new Promise(async (f, n) => {{ await (a = m.createElement("script")); e.set("libraries", [...r] + ""); for (k in g) e.set(k.replace(/[A-Z]/g, t => "_" + t[0].toLowerCase()), g[k]); e.set("callback", c + ".maps." + q); a.src = `https://maps.${{c}}apis.com/maps/api/js?` + e; d[q] = f; a.onerror = () => h = n(Error(p + " could not load.")); a.nonce = m.querySelector("script[nonce]")?.nonce || ""; m.head.append(a) }})); d[l] ? console.warn(p + " only loads once. Ignoring:", g) : d[l] = (f, ...n) => r.add(f) && u().then(() => d[l](f, ...n)) }})({{
key: "YOUR_API_KEY",
v: "weekly"
}});
</script>
<script>
async function initMap() {{
const {{ Map }} = await google.maps.importLibrary("maps");
new Map(document.getElementById("map"), {{
center: {{ lat: 51.5072, lng: -0.1276 }},
zoom: 10
}});
}}
initMap();
</script>
The second form is the plain script tag most tutorials still show. It works, and it’s fine for an existing page. Keep loading=async on it, or the console adds a warning about blocking the page.
<div id="map" style="height: 400px"></div>
<script>
function initMap() {{
new google.maps.Map(document.getElementById("map"), {{
center: {{ lat: 51.5072, lng: -0.1276 }},
zoom: 10
}});
}}
</script>
<script async
src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&loading=async&callback=initMap">
</script>
- The
<div>needs a height. A map in a 0 px tall box loads with no error and shows nothing. v=weeklygives you the current release. Pinningv=3.5xworks for a year, then Google retires that number.- Don’t paste the key into a public GitHub repo. Restrict it (next step) so a copied key is useless elsewhere.
Catch the Key Failure in Your Own Code
Google calls a global function named gm_authFailure whenever the key is rejected for any reason. Define it before the API loads and you get a plain message on the page instead of a grey box, which is handy on a client’s site where nobody opens the console.
window.gm_authFailure = function () {{
const box = document.getElementById("map");
box.innerHTML =
"<p style='padding:1em;font-family:sans-serif'>" +
"The map key was rejected. Open the browser console (F12) for the error code." +
"</p>";
console.error("Google Maps rejected the API key. See the line above this one for the code.");
}};
A second check worth having while you set things up: print which key the page actually sent. Half the “invalid key” reports come from a build step or a CMS field that quietly replaced the key with an old one.
[...document.scripts]
.map(s => s.src)
.filter(src => src.includes("maps.googleapis.com"))
.forEach(src => console.log(new URL(src).searchParams.get("key")));
What you should see
AIzaSyB...your-key-ending...Q4
If that prints null, the page is loading the API with no key, and the console is about to say NoApiKeys.
Set the Website Restriction So localhost and the Live Site Both Work
In Cloud Console → APIs & Services → Credentials → your key → Application restrictions → Websites, add one line per pattern:
http://localhost:*/* http://127.0.0.1:*/* https://example.com/* https://*.example.com/*
- The port wildcard
:*matters on localhost, where the dev server picks 3000, 5173, 8080 and so on. - Changes take a few minutes to apply. Hard-reload (Ctrl+Shift+R) after waiting.
- Under API restrictions, tick only Maps JavaScript API and whatever else the page uses (Places, Geocoding). Leaving it unrestricted works, but a leaked key then bills every Google service.
You Still Have to Link Billing, Even for Free Usage
This is the step that catches people with an account and a key who still see the watermark. Since June 2018 the project needs a billing account attached, and Google then applies free monthly usage to it. As of 2026 that’s a per-SKU free quota (10,000 map loads a month on the Dynamic Maps SKU at the time of writing) before anything is charged. A card on file with zero spend is the normal state for a small site. Check it in Cloud Console → Billing: if the project shows “No billing account”, that’s the error.