The problem
A local business owner asked me: "How do I send customers a link that opens the Google review box straight away?"
Google supports this with a direct review link:
https://search.google.com/local/writereview?placeid=ChIJb5qX9LW9cEgRVzYV0mRGxVw To build it you need the business's Place ID (the ChIJ… string). The official route is the Places API, which means:
- a Google Cloud project
- an API key and billing details
- a server to keep the key private
There's a simpler way. The Place ID is already inside every Google Maps link, just encoded differently. In this post we'll build a Place ID finder around one small function (about 10 lines) that pulls it out, entirely in the browser.
TL;DR
| Step | What you do |
|---|---|
| 1 | Find the feature ID in the Maps URL: !1s0x…:0x… |
| 2 | Pack its two 64-bit numbers into 20 bytes of protobuf |
| 3 | Base64url-encode the bytes. That's the Place ID. |
Step 1: Find the feature ID in the URL
Open a business on Google Maps and look at the full URL. In the data= part you'll see something like:
!1s0x4870bdb5f4979a6f:0x5cc54664d2153657 This is the feature ID (FID): two 64-bit hex numbers joined by a colon.
- First half:
0x4870bdb5f4979a6f - Second half:
0x5cc54664d2153657. This is the CID, the number used inmaps.google.com/?cid=…links.
The Place ID for the same business is:
ChIJb5qX9LW9cEgRVzYV0mRGxVw They look unrelated, but they contain exactly the same data.
Step 2: Understand the Place ID format
Base64-decode the Place ID and you get 20 bytes:
0a 12 09 6f 9a 97 f4 b5 bd 70 48 11 57 36 15 d2 64 46 c5 5c These bytes are a small protocol buffers message:
| Bytes | Meaning |
|---|---|
0a 12 | Field 1: a nested message, 18 bytes long |
09 | Inner field 1, type fixed64 |
6f 9a 97 f4 b5 bd 70 48 | 0x4870bdb5f4979a6f, first half of the FID (little-endian) |
11 | Inner field 2, type fixed64 |
57 36 15 d2 64 46 c5 5c | 0x5cc54664d2153657, second half of the FID (little-endian) |
In short, a Place ID is { 1: { 1: fixed64 a, 2: fixed64 b } }, serialised and base64url-encoded.
💡 That's also why every Place ID starts with ChIJ: it's the base64 form of the fixed header bytes 0a 12 09.
Step 3: The finder: feature ID → Place ID
function placeIdFromFid(fid) { const [a, b] = fid.split(':').map(h => BigInt(h)); const bytes = new Uint8Array(20); const dv = new DataView(bytes.buffer); bytes[0] = 0x0a; bytes[1] = 0x12; bytes[2] = 0x09; // header dv.setBigUint64(3, a, true); // first half, little-endian bytes[11] = 0x11; // field 2 tag dv.setBigUint64(12, b, true); // second half, little-endian let s = ''; bytes.forEach(x => { s += String.fromCharCode(x); }); return btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); } placeIdFromFid('0x4870bdb5f4979a6f:0x5cc54664d2153657'); // → "ChIJb5qX9LW9cEgRVzYV0mRGxVw" ✅ BigInt and DataView.setBigUint64 work in all modern browsers, so this runs fully client-side. No key, no server, no quota.
Step 4: Build the review link
const reviewUrl = pid => `https://search.google.com/local/writereview?placeid=${encodeURIComponent(pid)}`; Common pitfalls (and how to fix them)
| Problem | Why it happens | Fix |
|---|---|---|
| Wrong business returned | A URL can contain several feature IDs: the building, nearby places, directions points | Match in priority order (see below) |
First half is 0x0 | The link points to a single review, not the business | Show a message asking for the business link |
Short links (maps.app.goo.gl) | The browser can't follow cross-origin redirects | Resolve them with a small server endpoint |
share.google/… links | These come from Google Search and contain a Knowledge Graph ID, not a feature ID | Ask the user to share from Google Maps instead |
| Missing business name | /maps/place/<Name>/ is URL-encoded and sometimes absent on mobile | Treat the name as optional |
Picking the right feature ID
Match the URL patterns in this order:
-
!3m<n>!1s<FID>: the place panel that's open (most reliable) -
!1s<FID>: usually the place, but can be a directions point -
ftid=<FID>: older link formats - any
0x…:0x…: last resort
!5s… usually refers to a nearby place, so it's never picked first.
const FID = '(0x[0-9a-f]{1,16}:0x[0-9a-f]{1,16})'; const fid = ( url.match(new RegExp('!3m\\d+!1s' + FID, 'i')) || url.match(new RegExp('!1s' + FID, 'i')) || url.match(new RegExp('ftid=' + FID, 'i')) || url.match(new RegExp(FID, 'i')) || [] )[1]; Resolving short links safely
My server endpoint for maps.app.goo.gl links:
- only contacts Google hosts (allowlist)
- follows redirects one at a time and stops once it finds a feature ID
- handles the
consent.google.com?continue=…page served to UK and EU visitors - is rate-limited per hashed IP
How I tested it
I compared the output with Place IDs from the official Places API for several UK businesses, using:
- long desktop URLs
-
maps.app.goo.glshort links - mobile share links
- directions URLs
Every result matched byte for byte. To check your own, reverse the process in Node:
const b = Buffer.from(pid.replace(/-/g, '+').replace(/_/g, '/'), 'base64'); console.log('0x' + b.readBigUInt64LE(3).toString(16) + ':0x' + b.readBigUInt64LE(12).toString(16)); What I built with it
I turned this into a free tool for small businesses. Paste a Google Maps link and it gives you:
- ✅ the business's Place ID, ready to copy
- ✅ the direct review link
- ✅ a QR code (PNG)
- ✅ a printable A6 counter card, drawn on a
<canvas> - ✅ ready-to-send WhatsApp, text and email messages
It needs no sign-up, stores nothing, and is plain JavaScript plus the MIT-licensed qrcode-generator.
👉 Try it: Free Google review link generator and Place ID finder
Limitations
This format isn't documented by Google and could change. It has been stable for years, but if you rely on it, keep a fallback such as the Places API, or let users paste a Place ID directly.
Over to you
Have you found Google Maps URL patterns that break this, or other hidden IDs in Google URLs worth decoding? Tell me in the comments and I'll make the parser more robust. 👇

