Let's Connect

Analytics and location data dashboard on a laptop screen

The catch with google places api business data is not the rate quota; it is the Terms of Service. The API will happily return names, addresses, ratings, hours and reviews, and it is tempting to dump all of that into a `businesses` table and call it your own dataset. That is exactly what Google's policy forbids. You generally may not pre-fetch, cache, or permanently store most Place fields. The one durable identifier you are allowed to keep is the `place_id`. The legal pattern is: store the `place_id`, cache the rest only briefly, and re-fetch details by ID when you need them again. I shipped this on a directory product, and the architecture below is what kept us compliant without hammering billing.

Which endpoints actually return business data?

On the Places API (New) you mostly use two operations. Text Search (`POST https://places.googleapis.com/v1/places:searchText`) finds places from a query string like "plumbers in Sydney" and returns a list of candidates, each carrying an `id` (the place ID). Place Details (`GET https://places.googleapis.com/v1/places/{PLACE_ID}`) returns the record for one place by its ID. The normal flow is search once to discover IDs, then call Details by `place_id` whenever you need fresh information about a specific business.

The single most important request detail in the new API: the `X-Goog-FieldMask` header is mandatory. Omit it and the call returns an error. The mask is also how you control cost, so it is not boilerplate you copy blindly.

text-search.sh
# Find businesses. Returns place IDs you are allowed to keep.
curl -X POST 'https://places.googleapis.com/v1/places:searchText' \
  -H 'Content-Type: application/json' \
  -H "X-Goog-Api-Key: $PLACES_API_KEY" \
  -H 'X-Goog-FieldMask: places.id,places.displayName,places.formattedAddress' \
  -d '{ "textQuery": "emergency plumbers in Sydney, Australia" }'

# Get details for ONE place by its durable place_id.
curl -X GET 'https://places.googleapis.com/v1/places/ChIJN1t_tDeuEmsRUsoyG83frY4' \
  -H "X-Goog-Api-Key: $PLACES_API_KEY" \
  -H 'X-Goog-FieldMask: id,displayName,formattedAddress,rating,regularOpeningHours,websiteUri'

Why does the field mask decide your bill?

Pricing on the new API is per-field-tier, not flat per request. Fields are grouped into SKUs, and the most expensive field you request sets the SKU for that call. Requesting `reviews` is in a far higher tier than requesting only `id` and `formattedAddress`. So the field mask is a billing control surface, not a formatting preference.

  • Essentials (IDs Only): `id`, `photos`, `attributions` — cheapest, ideal for resolving or refreshing place IDs.
  • Essentials: `addressComponents`, `formattedAddress`, `location`, `types`.
  • Pro: `displayName`, `businessStatus`, `primaryType`.
  • Enterprise: `rating`, `userRatingCount`, `regularOpeningHours`, `websiteUri`, `internationalPhoneNumber`.
  • Enterprise + Atmosphere (priciest): `reviews`, `paymentOptions`, `parkingOptions`.

This is why the masks in my examples land on the Enterprise SKU: the moment you add `rating` or `regularOpeningHours`, you are billed at Enterprise regardless of how many cheap fields sit alongside them. Practical rule: never use the wildcard `*` mask outside local development. It pulls every field and bills you at the top tier on every call. Request the narrowest mask that satisfies the screen you are rendering.

Pins marking multiple business locations on a city map
Place Search returns candidates with a place_id each; that ID is the only field you are licensed to keep long-term.

What does "legally" actually mean here?

This is the part teams skip. The Places API policy states you may not pre-fetch, cache, index, or store Places content, with limited exceptions. The headline exception is `place_id`: it is explicitly exempt from the caching restriction, so you may store it indefinitely and use it to refresh data later. Google does recommend refreshing any place ID older than 12 months, since IDs can change as the underlying record is updated. Everything else — names, ratings, hours, reviews — is licensed content you display, not data you own. Building a permanent scraped mirror of business records is a Terms of Service violation, regardless of how politely you stayed under the quota.

Store the place_id, treat every other field as a short-lived cache entry you refresh from Google, and refresh the IDs themselves once they pass a year old.Md Raihan Hasan

You also carry attribution obligations. You must show the "Google Maps" attribution wherever you display this data, surface any third-party attributions from the response, and display the per-photo `authorAttributions` and per-review author names. You cannot hide or restyle the Google Maps mark.

How do you cache without breaking the ToS?

The compliant pattern: persist only `place_id` in your database, and keep the rendered detail payload in a short-lived cache keyed by that ID. When the cache misses or expires, re-fetch from Place Details. This is the same disposable-cache discipline I cover in caching third-party API responses — the difference here is that the short TTL is doing double duty: a freshness optimization that also keeps you clear of the no-permanent-store rule. Below is the Laravel version I run in production, wrapped so the cache and the refresh logic live in one place.

app/Services/PlacesDirectory.php
<?php

namespace App\Services;

use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;

class PlacesDirectory
{
    // place_id is the ONLY durable identifier. Persist this; refetch the rest.
    public function details(string $placeId): array
    {
        // Short TTL: this is a transient cache, not a permanent store of Place data.
        return Cache::remember("place:{$placeId}", now()->addHours(24), function () use ($placeId) {
            $response = Http::withHeaders([
                'X-Goog-Api-Key'    => config('services.google_places.key'),
                // Narrowest mask that still renders the page (these fields bill at Enterprise).
                'X-Goog-FieldMask'  => 'id,displayName,formattedAddress,rating,regularOpeningHours,websiteUri',
            ])
            ->retry(3, 200, throw: false) // transient 5xx / network blips
            ->get("https://places.googleapis.com/v1/places/{$placeId}");

            $response->throw();

            return $response->json();
        });
    }
}

Three things make this defensible. The database row only ever holds `place_id` (plus your own metadata, like which user bookmarked it) — never a frozen copy of Google's fields. The cache TTL is short and disposable, so a purge leaves you with no Place content, only IDs. And every read goes back through Place Details, so what users see is refreshed from Google rather than served from a stale private mirror. The `retry()` here covers transient blips; for full circuit-breaking and backoff on a flaky upstream, see my resilient third-party API client write-up.

What setup keeps the key and budget safe?

Lock the key down before you ship. In Google Cloud Console, restrict the API key to the Places API only, and add an Application restriction — HTTP referrers for browser keys, or an IP allowlist for a server-side key. A server key in your backend should never be referrer-restricted; it should be IP-restricted to your egress addresses. Set a billing budget with email alerts, because per-field pricing means one accidental wildcard mask in a loop can run up real money fast. The official policy and SKU breakdown live in Google's own docs under developers.google.com/maps/documentation/places/web-service — read them before launch, not after the invoice.

  • Restrict the key to the Places API and to your server IPs (or referrers for client keys).
  • Set a Cloud Billing budget with alert thresholds at 50/90/100 percent.
  • Keep `X-Goog-FieldMask` minimal in production; ban the wildcard in code review.
  • Store `place_id` only; cache rendered details with a short TTL and refresh on miss.
  • Render the Google Maps attribution and pass through photo and review author attributions.

The core constraint is simple once you accept it: Google licenses you a window onto its data, not a copy of it. Keep the `place_id`, refresh everything else, request the narrowest field set the page needs, and show the attributions. Do that and your directory stays fast, cheap, and on the right side of the Terms of Service — which is the only version of "using the google places api" that survives an audit.