Google Place Details and Place Photos API
In this post, we’ll take an in-depth look at the Google Place Details and Place Photos APIs. We’ll explore how each API works, the kind of information they provide, and how you can easily add them to your own project. At the end of this post, we'll enhance the Place Autocomplete Demo App introduced in the previous blog post to include a photo of the selected place along with its aggregated user rating (demo / source code). Whether you're building a travel app, a local guide, or just want to share cool places, the Place Details and Place Photos APIs makes it easy for users to access important data like reviews, photos, hours of operation, and much more - all in one place!
Part 1: Finding the right place with the Google Places API
Part 2: Google address autocomplete with the Places API
Part 3: Google Place Details and Place Photos API (this article)
Part 4: Google Nearby Search API
Part 5: Google Places Text Search
Place details, place photos and Google's economic moat
In September 2024, Google lost a landmark antitrust case against the U.S. Department of Justice, with a federal judge ruling that the tech giant had built an illegal monopoly around its dominance in online search. While Google's 88.01% market share in search is already impressive, its dominance in local search is even more so. An estimated 46% of all Google searches are location-based, with nearly half of these queries focused on finding local businesses, services, or information. This kind of search is particularly valuable due to the strong buying intent associated with it - 88% of users who perform a local search on their smartphones will either visit or contact a business within 24 hours.
Google Maps, integrated into Google Search, plays a crucial role in maintaining Google's search market share by providing users with detailed business listings, directions, reviews, and more, directly within the search results. This information is mostly user generated, and creates a virtuous cycle that strengthens its economic moat (a competitive advantage that is hard for rivals to replicate). The more content users contribute, the richer the data Google can surface when someone searches for a local business or point of interest.
Businesses are incentivized to maintain a strong presence on Google Maps because positive user-generated content (like high ratings and good reviews) can directly drive foot traffic and sales. This motivates businesses to engage with users on the platform, whether by responding to reviews or updating their Google My Business profiles.
The Google Place Details and Place Photos APIs allow developers to integrate this source of rich, user submitted data into their own applications, thereby enhancing the user experience, engagement, and the overall value of their app.
What is the Place Details API?
The Place Details API provides a wide range of data about a place, including its address, phone number, website URL, user reviews, business hours, and more. It takes a {place_id} - usually obtained from a Place Autocomplete or Text Search API call, and returns a JSON object with detailed place data.
Google Place Details API example
Here's how you make a call to the Place Details API:
Method: GET
https://maps.googleapis.com/maps/api/place/details/json
?fields={fields}&
place_id={place_id}&
key={YOUR_API_KEY}
{fields} is a comma-separated list of place data types to return. For example, if you wanted to retrieve the name, address, coordinates, rating, rating count, and reviews for a place, you'd use name%2Cformatted_address%2Cgeometry%2Crating%2C user_ratings_total%2Creviews
(the %2C
string is the encoded form for the comma "," character). For a list of supported data types, see the official docs.
{place_id} uniquely identifies a place in the Google Places database (docs). You get one by making a Place Autocomplete or Text Search API call, or by using Google's Place ID Finder.
{YOUR_API_KEY} is a valid Google Maps API key with the Places API enabled.
Here’s an example of a Place Details API call for Stanley Park in Vancouver (Place ID: ChIJo-QmrYxxhlQRFuIJtJ1jSjY):
Method: GET
https://maps.googleapis.com/maps/api/place/details/json
?fields=name%2Cformatted_address%2Cgeometry%2Crating%2Cuser_ratings_total%2Creviews%2Ctypes&
place_id=ChIJo-QmrYxxhlQRFuIJtJ1jSjY&
key={YOUR_API_KEY}
Response
{
"html_attributions": [],
"result": {
"formatted_address": "Vancouver, BC V6G 1Z4, Canada",
"geometry": {
"location": {
"lat": 49.30425839999999,
"lng": -123.1442523
},
"viewport": {
"northeast": {
"lat": 49.32017484999999,
"lng": -123.13024895
},
"southwest": {
"lat": 49.28048825,
"lng": -123.15088955
}
}
},
"name": "Stanley Park",
"rating": 4.8,
"reviews": [
{
"author_name": "kerry hallatt",
"author_url": "https://www.google.com/maps/contrib/104594965526885792260/reviews",
"language": "en",
"original_language": "en",
"profile_photo_url": "https://lh3.googleusercontent.com/a/ACg8ocKIucdQsMTRZmn7WIrtw8ZXYy_d_A__5EqgnHFGoFN3XBZbHQ=s128-c0x00000000-cc-rp-mo-ba4",
"rating": 5,
"relative_time_description": "2 weeks ago",
"text": "We had a great day at this park so much to do. The Aquarium was awesome and food was good. There is a lovely beach area to take a picnic. If you have kids they will love it. The walks are interesting you won't regret going. Take lots of water",
"time": 1728096313,
"translated": false
},
// 4 more reviews
],
"types": [
"park",
"tourist_attraction",
"point_of_interest",
"establishment"
],
"user_ratings_total": 48740
},
"status": "OK"
}
In the result
object:
formatted_address
is a human-readable, properly formatted address of the place.
geometry.location
gives us its lat
(latitude) and lng
(longitude).
name
is the generally accepted place name.
rating
is the average rating based on aggregated user reviews. In North America, it's very rare for a place's rating to go below 3.0.
reviews
is an array of the last five user reviews.
types
is an array of place types that Google uses to categorize this place. The first element is the primary type and the remaining ones are secondary types e.g. for Stanley Park, the primary type is "park" and the secondary ones are "tourist_attraction", "point_of_interest" and "establishment".
user_ratings_total
represents the total number of reviews for a place. Generally, the more reviews a place has, the more reliable its overall rating
tends to be.
Place Details API pricing
Pricing for the Place Details API starts at $17 CPM (cost per thousand requests) plus an additional charge depending on the type of data requested in the fields
parameter (see About Places Data SKUs for more information).
Basic ($0): The Basic category includes the following fields: address_components
, adr_address
, business_status
, formatted_address
, geometry
, icon
, icon_mask_base_uri
, icon_background_color
, name
, permanently_closed
, photo
, place_id
, plus_code
, type
, url
, utc_offset
, vicinity
, wheelchair_accessible_entrance
.
Contact ($3 CPM): The Contact category includes the following fields current_opening_hours
, formatted_phone_number
, international_phone_number
, opening_hours
, secondary_opening_hours
, website
.
Atmosphere ($5 CPM): The Atmosphere category includes the following fields: curbside_pickup
, delivery
, dine_in
, editorial_summary
, price_level
, rating
, reservable
, reviews
, serves_beer
, serves_breakfast
, serves_brunch
, serves_dinner
, serves_lunch
, serves_vegetarian_food
, serves_wine
, takeout
, user_ratings_total
.
Let’s revisit the Place Details API call from the previous section, where we requested details about Stanley Park, Vancouver. We asked for basic information like the name
, formatted_address
, geometry
, and type
, as well as atmosphere-related data such as rating
, user_ratings_total
, and reviews
. Since we included both basic data and atmosphere-related fields (rating
, user_ratings_total
, and reviews)
, the total cost is $17 CPM + $5 CPM, which equals $22 CPM, or approximately $0.022 (about two cents) for this request.
What is the Place Photos API?
The Google Place Photos API gives developers access to the millions of photos stored in the Google Maps database. It works by taking a photo reference obtained from other Google Places API responses (such as the Place Details, Nearby Search or Text Search APIs) and using it to retrieve images of the corresponding location.
Google Place Photos API example
Using the same Stanley Park example from earlier, let's make another Place Details API call but this time include photo
as the only field we want returned.
Method: GET
https://maps.googleapis.com/maps/api/place/details/json
?fields=photo&
place_id=ChIJo-QmrYxxhlQRFuIJtJ1jSjY&
key={YOUR_API_KEY}
Response
{
"html_attributions": [],
"result": {
"photos": [
{
"height": 3072,
"html_attributions": [
"<a href=\"https://maps.google.com/maps/contrib/116055760197341416813\">John Ryan</a>"
],
"photo_reference": "AdCG2DNrBTDTfgY2rgQymqU7l1ftVo0TH-DtBqWSq-lsU7ADE6qvVL0P9_mvgVY8phx92dJTom8KCf_VWuyQoh_aFl3P1-W0bxqnf90xWrx4ioqIrVOsJMAJtMhQtKi6_6qwqNR4p7ggmcNJ93JW6WLureXm7otpAb9qLN5v3hMAuoG9hL9Y",
"width": 4080
},
// 9 more entries
]
},
"status": "OK"
}
The photos
array contains up to ten photos (represented by a unique photo_reference
), returned in the same order that they appear on Google Maps. Google doesn't explicitly document the criteria for ranking photos in the array, but they are often organized by relevance and quality, with higher-quality, clearer images appearing first.
Next, we are going to call the Place Photos API using the photo_reference
from the first photo in the result.photos
array, AdCG2DNrBTD...G9hL9Y. Copy the URL below into your browser address bar, swap in your Google Maps API key and press [Return].
Method: GET
https://maps.googleapis.com/maps/api/place/photo?maxwidth=800&photoreference=AdCG2DNrBTDTfgY2rgQymqU7l1ftVo0TH-DtBqWSq-lsU7ADE6qvVL0P9_mvgVY8phx92dJTom8KCf_VWuyQoh_aFl3P1-W0bxqnf90xWrx4ioqIrVOsJMAJtMhQtKi6_6qwqNR4p7ggmcNJ93JW6WLureXm7otpAb9qLN5v3hMAuoG9hL9Y&key={YOUR_API_KEY}
Response
The Place Photos API returns the photo associated with the given {photoreference}, sized to the 800px {maxwidth} specified in the API call. It's also the very first photo of Stanley Park that shows up when you search for "Stanley Park" in Google Maps.
access-control-allow-origin
error. To fix this, we need to call the Place Photos API using a proxy backend.Place Photos API pricing
Pricing for the Place Photos API is straightforward - it costs $7.00 per 1,000 requests (CPM). For discount pricing, you can contact a Google Maps Partner .
Adding Place Details and Place Photos to an app
In the previous blog post, we built an app that uses Google Places Autocomplete to help users quickly find locations and display them on a map (live demo: https://autocomplete-place.afi.dev). In this section, we'll enhance places_api_autocomplete_demo by including a photo of the selected place along with its aggregated user rating. As always, you can find full source code at the Github repository here: places_api_place_details_demo.
How our code is organized
places_api_place_details_demo is a React app built on top of places_api_autocomplete_demo. To include a photo and the aggregated user rating for the searched place, we'll do two things. First, we’ll set up a proxy backend to handle calls to the Google Place Photos API using the photo_reference
returned by the Place Details API. This helps avoid CORS errors related to access-control-allow-origin restrictions. Second, we’ll update the code to display the photo, aggregated user rating, and total number of reviews within the <InfoWindow/>
component.
The simplest way to follow along is to clone the places_api_autocomplete_demo project and update the code based on the instructions below.
App.jsx
In App.jsx
, remove the existing handlePlaceSelection()
method and replace it with the following:
/* frontend/components/App.jsx */
const handlePlaceSelection = async (placeResult) => {
try {
const locationName = placeResult.name;
const locationPlaceID = placeResult.place_id
const response = await axios.get(
`${process.env.REACT_APP_BACKEND_URI}/place-details`,
{
params: { locationName, locationPlaceID },
}
);
const details = response.data;
if (details) {
setSelectedPlace({
name: details.name,
rating: details.rating,
reviewsCount: details.reviewsCount,
category: details.category,
imageUrl: details.imageUrl,
address: placeResult.formatted_address,
latitude: placeResult.geometry?.location?.lat(),
longitude: placeResult.geometry?.location?.lng(),
});
}
} catch (error) {
console.error("Error fetching place details:", error);
}
};
Rather than just using the name
, formatted_address
, and geometry coordinates (geometry.location.lat
and geometry.location.lng
) returned by the Place Autocomplete API, we'll send the selected place's name
and place_id
to our proxy backend via the /place-details
endpoint. There, we'll use these to call the Google Place Details and Place Photos APIs to retrieve ratings, types and photos associated with the selected place.
InfoWindow.jsx
We'll update the InfoWindow.jsx
component to display additional details, including the place photo, aggregate rating, total number of reviews, and place categories.
/* frontend/components/InfoWindow.jsx */
import React from "react";
import { InfoWindow as GInfoWindow } from "@vis.gl/react-google-maps";
import "./InfoWindow.scss";
const InfoWindow = ({ place, anchor, onCloseClick }) => {
const { name, rating, reviewsCount, category, address, imageUrl } = place;
return (
<GInfoWindow anchor={anchor} onCloseClick={onCloseClick}>
<div className="info-window">
<div className="info-window-content">
{imageUrl && (
<img src={imageUrl} alt={name} className="info-window-image" />
)}
<div className="info-window-details">
<h2 className="info-window-title">{name}</h2>
{rating && (
<div className="info-window-rating">
<span>{rating}</span>
<span className="info-window-stars">
{Array.from({ length: 5 }, (_, index) => (
<i
key={index}
className={`star ${
index < Math.round(rating) ? "filled" : "unfilled"
}`}
/>
))}
</span>
<span className="info-window-reviews">({reviewsCount})</span>
</div>
)}
<div className="info-window-category">{category}</div>
</div>
</div>
<div className="info-window-address">{address}</div>
</div>
</GInfoWindow>
);
};
export default InfoWindow;
Fixing the Google Places API CORS error with a proxy backend
If you've experimented with the Google Place Details and Place Photos APIs before, you may have tried making a request directly from your app's frontend and encountered a CORS error, which might look like this:
Access to fetch at 'https://maps.googleapis.com/maps/api/place/details/json?parameters' from origin 'http://yourdomain.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
A CORS (Cross-Origin Resource Sharing) error occurs when a frontend web application tries to make a request to a resource (like an API) hosted on a different domain or port without the server allowing it. Since Google restricts access to many of its APIs from client-side requests without proper handling, you'll need to use a proxy backend to make the requests on behalf of your frontend app and avoid CORS errors. Here's what the backend for places_api_place_details_demo looks like:
src
├── /frontend
├── /backend
│ ├── /controllers
│ │ ├── placeController.js
│ ├── /routes
│ │ ├── placeDetails.jsx
│ ├── server.js
│ ├── .env
server.js
To keep things simple, we'll use Express to run a basic web server. In the root folder of places_api_autocomplete_demo, create a new folder called /backend
. In that folder, create a new file called server.js
.
/* backend/server.js */
require("dotenv").config();
const express = require("express");
const cors = require("cors");
const placeDetailsRoute = require("./routes/placeDetails");
const app = express();
const PORT = process.env.PORT || 3001;
app.use(cors());
app.use(express.json());
// Use the place details route
app.use("/api/place-details", placeDetailsRoute);
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
The code in server.js
sets up Express to run on port 3001 and creates a new route, /api/place-details
which points to /routes/placeDetails.js
.
placeDetails.js
placeDetails.js
is an an Express.js router that calls the getPlaceDetails()
function whenever a GET
request is made to the root path ("/"
). getPlaceDetails()
points to /controllers/placeControler.js
, which is where the pass through calls to the Google Place Details and Place Photos APIs are made.
/* backend/routes/placeDetails.js */
const express = require("express");
const { getPlaceDetails } = require("../controllers/placeController");
const router = express.Router();
router.get("/", getPlaceDetails);
module.exports = router;
placeController.js
This is where the magic happens. First, we take the locationPlaceID
passed to the backend from App.js
in the frontend and use it to make a call to the Google Place Detail API. The response is stored in placeDetails
.
/* backend/controllers/placeController.js */
const placeDetailsUrl = `https://maps.googleapis.com/maps/api/place/details/json?place_id=${locationPlaceID}&key=${apiKey}`;
Second, we extract the name
, rating
, user_ratings_total
,types
and formatted_address
from placeDetails
and store it in placeInfo
. This is what's returned to the frontend.
/* backend/controllers/placeController.js */
const placeInfo = {
name: placeDetails.name,
rating: placeDetails.rating,
reviewsCount: placeDetails.user_ratings_total,
category: placeDetails.types ? placeDetails.types.join(", ") : "",
address: placeDetails.formatted_address
}
Third, we use the photo_reference
of the first photo included in the response and use it to call the Place Photos API. The URL of the photo is stored in placeInfo
.
/* backend/controllers/placeController.js */
const getPhotoUrl = (photoReference, apiKey) => {
return `https://maps.googleapis.com/maps/api/place/photo?maxwidth=400&photoreference=${photoReference}&key=${apiKey}`;
};
Here's the code for placeController.js
in full:
/* backend/controllers/placeController.js */
const axios = require("axios");
const getPlaceDetails = async (req, res) => {
const { locationPlaceID } = req.query;
const apiKey = process.env.GOOGLE_API_KEY;
try {
// Step 1: Retrieve place details using Place ID
const placeDetailsUrl = `https://maps.googleapis.com/maps/api/place/details/json?place_id=${locationPlaceID}&key=${apiKey}`;
const placeDetailsResponse = await axios.get(placeDetailsUrl);
const placeDetails = placeDetailsResponse.data.result;
// Step 2: Extract the necessary information
const placeInfo = {
name: placeDetails.name,
rating: placeDetails.rating,
reviewsCount: placeDetails.user_ratings_total,
category: placeDetails.types ? placeDetails.types.join(", ") : "",
address: placeDetails.formatted_address,
imageUrl: placeDetails.photos
? getPhotoUrl(placeDetails.photos[0].photo_reference, apiKey)
: "",
};
res.json(placeInfo);
} catch (error) {
console.error("Error fetching place details:", error);
res.status(500).json({ message: "Internal server error" });
}
};
// Helper function to get the photo URL
const getPhotoUrl = (photoReference, apiKey) => {
return `https://maps.googleapis.com/maps/api/place/photo?maxwidth=400&photoreference=${photoReference}&key=${apiKey}`;
};
module.exports = { getPlaceDetails };
Running the proxy backend
To get our proxy backend working, we still need to do a few things:
- Install required dependencies
In terminal, navigate to the/backend
folder and runnpm install axios cors dotenv express
. - Configure .env files
We need a .env file to store our credentials. In the/backend
folder, create a new file called.env
and add the lineGOOGLE_API_KEY={YOUR_API_KEY}
(replace {YOUR_API_KEY} with your Google Maps API key (make sure you have the Places API enabled). In the/frontend
folder, insert this statement -REACT_APP_BACKEND_URI="http://localhost:3001/api"
to the.env
file there. - Run backend + frontend
The final step to get our demo app working is to runnpm start
in/backend
in one tab followed bynpm start
in/frontend
on another. If everything works as it should, you should see these two messages :
/backend
> backend@1.0.0 start
> node server.js
Server is running on port 3001
/frontend
You can now view autocomplete-place-demo in the browser.
Local: http://localhost:3000
On Your Network: http://172.20.10.3:3000
Note that the development build is not optimized.
To create a production build, use npm run build.
Launch the app in your browser and start exploring! Try searching for different places and types of locations to see the data returned by the Place Details API.
Parting thoughts
One way to think about the competitive advantage that Google Maps has over its competition is to imagine what it would cost to build a similar service. Even if you started with a pretty good database of business locations and points of interest, it would take you hundreds of millions of dollars each year to keep these listings up to date and send people to review and rate them on a regular basis (don't laugh - this is what Lonely Planet used to do with hotels and restaurants). Google Maps gets its users to do this for free.
👋 As always, if you have any questions or suggestions for me, please reach out or say hello on LinkedIn.