Google Place Details and Place Photos API

Add place photos, reviews and ratings to your website with the Google Maps Place Details API and Place Photos API.

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!

The autocomplete app we'll enhance with the Google Place Details API / Place Photos API
The autocomplete app we'll enhance with the Google Place Details API / Place Photos API

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 Place details used to provide information for local search
Google Place details used to provide information for local search

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.

Fields from the Google Place Details API response
Fields from the Google Place Details API response

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_componentsadr_addressbusiness_statusformatted_addressgeometry,  iconicon_mask_base_uriicon_background_colorname,  permanently_closed , photo,  place_idplus_codetypeurlutc_offset,  vicinitywheelchair_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_pickupdeliverydine_ineditorial_summaryprice_levelratingreservablereviewsserves_beerserves_breakfastserves_brunchserves_dinnerserves_lunchserves_vegetarian_foodserves_winetakeoutuser_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.

The Google Place Photos API uses the same data from Google Map search
The Google Place Photos API uses the same data from Google Map search

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 response from the Google Place Details API is an image
The response from the Google Place Details API is an image

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.

🚧
It worked in your browser, but if you try calling the same API in your app you'll get a CORS No 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.

Adding Place Photos and Place Details to the autocomplete demo app
Adding Place Photos and Place Details to the autocomplete demo app

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:

  1. Install required dependencies
    In terminal, navigate to the /backend folder and run npm install axios cors dotenv express.
  2. 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 line GOOGLE_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.
  3. Run backend + frontend
    The final step to get our demo app working is to run npm start in /backend in one tab followed by npm start in /frontend on another. If everything works as it should, you should see these two messages :

/backend

> [email protected] 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.

Use the Places Details / Place Photos demo app to explore the world
Use the Places Details / Place Photos demo app to explore the world

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.

Next: Part 4: Google Nearby Search API