How to Pull Google Business Profile Insights via API
Read Search and Maps impressions, website clicks, call clicks, direction requests, bookings, and food orders for every Google Business Profile location as a timeseries — one endpoint, no Business Profile Performance API setup.
What Google exposes
Business Profile performance data comes from Google’s Business Profile Performance API, which replaced the Insights section of the old My Business API. It reports per location, per day, on these metrics:
| Metric | Key in Postproxy | Meaning |
|---|---|---|
| Search impressions, desktop | business_impressions_desktop_search | Listing shown in Google Search on desktop |
| Search impressions, mobile | business_impressions_mobile_search | Same, on mobile |
| Maps impressions, desktop | business_impressions_desktop_maps | Listing shown in Google Maps on desktop |
| Maps impressions, mobile | business_impressions_mobile_maps | Same, on mobile |
| All impressions | views | The four impression metrics summed by Postproxy |
| Website clicks | website_clicks | Clicks on the website button |
| Call clicks | call_clicks | Clicks on the call button |
| Direction requests | business_direction_requests | Clicks on Directions |
| Conversations | business_conversations | Message threads started from the listing |
| Bookings | business_bookings | Bookings made through the listing |
| Food orders | business_food_orders | Orders placed through the listing |
| Menu clicks | business_food_menu_clicks | Clicks on the menu |
Google counts impressions per unique user per day, so a customer who sees the listing three times in one day is one impression. Metrics that a location has never triggered (a bakery with no bookings) are absent rather than zero.
Not available anywhere in Google’s API, and therefore not in Postproxy: a follower count (Business Profiles have none) and per-post analytics for local posts.
Get the latest numbers for every location at once
GET /api/profiles/:id returns the newest snapshot for each location the profile manages, plus a sum across them:
curl "https://api.postproxy.dev/api/profiles/prof_abc123" \ -H "Authorization: Bearer YOUR_API_KEY"{ "id": "prof_abc123", "platform": "google_business", "latest_stats": [ { "placement_id": "accounts/113344/locations/558899", "recorded_at": "2026-09-16T03:10:22Z", "stats": { "views": 18432, "business_impressions_desktop_search": 2210, "business_impressions_mobile_search": 9871, "business_impressions_desktop_maps": 1104, "business_impressions_mobile_maps": 5247, "website_clicks": 612, "call_clicks": 288, "business_direction_requests": 941, "business_conversations": 37 } }, { "placement_id": "accounts/113344/locations/558900", "recorded_at": "2026-09-16T03:10:24Z", "stats": { "views": 7310, "website_clicks": 201, "call_clicks": 95, "business_direction_requests": 388 } } ], "summary_stats": { "recorded_at": "2026-09-16T03:10:24Z", "stats": { "views": 25742, "website_clicks": 813, "call_clicks": 383, "business_direction_requests": 1329, "business_conversations": 37 } }}Each snapshot is a trailing 30-day total as of recorded_at. summary_stats sums the numeric keys across every location, which is the number a multi-location dashboard’s headline tile wants.
Get the timeseries for one location
Snapshots are captured roughly every 23 hours and kept. The raw series is on GET /api/profiles/:id/stats with the location’s location_id as placement_id:
curl "https://api.postproxy.dev/api/profiles/prof_abc123/stats?placement_id=accounts/113344/locations/558899&from=2026-08-01T00:00:00Z" \ -H "Authorization: Bearer YOUR_API_KEY"{ "data": { "profile_id": "prof_abc123", "placement_id": "accounts/113344/locations/558899", "records": [ { "recorded_at": "2026-08-01T03:08:11Z", "stats": { "views": 16901, "website_clicks": 540, "call_clicks": 251 } }, { "recorded_at": "2026-08-02T03:09:40Z", "stats": { "views": 16988, "website_clicks": 544, "call_clicks": 255 } } ] }}placement_id is required for Google Business; a call without it returns 400. from and to are ISO 8601 timestamps.
Because each record is a rolling 30-day window, the series answers “how is the last month trending” directly. Two records 30 days apart are non-overlapping windows and can be compared as periods.
Get one record per day
For charts and period comparisons, daily_stats returns one clean record per calendar day:
curl "https://api.postproxy.dev/api/profiles/prof_abc123/daily_stats?placement_id=accounts/113344/locations/558899&from=2026-08-17&to=2026-09-15" \ -H "Authorization: Bearer YOUR_API_KEY"{ "data": { "profile_id": "prof_abc123", "platform": "google_business", "placement_id": "accounts/113344/locations/558899", "from": "2026-08-17", "to": "2026-09-15", "data_timezone": "America/Los_Angeles", "records": [ { "date": "2026-08-17", "views": 17244, "platform_stats": { "website_clicks": 548, "call_clicks": 259, "business_direction_requests": 902 } } ], "metrics": { "views": { "supported": true, "source": "native", "additive": false }, "followers": { "supported": false } } }}The range is capped at 366 days, and to defaults to yesterday. The metrics map tells a generic dashboard which keys to expect for this network without hard-coding platform rules.
Build a multi-location report
A weekly report for a chain or an agency’s client is a loop over placements, reading the latest snapshot for each:
const BASE = "https://api.postproxy.dev";const headers = { Authorization: `Bearer ${process.env.POSTPROXY_API_KEY}` };
const profile = await fetch(`${BASE}/api/profiles/${PROFILE}`, { headers }).then((r) => r.json());const { data: placements } = await fetch(`${BASE}/api/profiles/${PROFILE}/placements`, { headers }) .then((r) => r.json());
const names = Object.fromEntries(placements.map((p) => [p.id, p.name]));
const rows = profile.latest_stats .map(({ placement_id, stats }) => ({ location: names[placement_id], views: stats.views ?? 0, calls: stats.call_clicks ?? 0, directions: stats.business_direction_requests ?? 0, website: stats.website_clicks ?? 0, actions: (stats.call_clicks ?? 0) + (stats.business_direction_requests ?? 0) + (stats.website_clicks ?? 0), })) .sort((a, b) => b.actions - a.actions);
console.table(rows);actions per location divided by views is the listing’s conversion rate, the number most useful for spotting a location whose profile is being seen but not acted on.
Get notified instead of polling
A profile.stats webhook fires each time a new snapshot is recorded, carrying the profile ID and the placement. Subscribe to it to push the numbers into a warehouse as they land rather than scheduling a nightly pull.
Errors
| Status | Cause |
|---|---|
400 | placement_id missing on a Google Business profile, or a from/to range over 366 days |
404 | Unknown profile, or a placement_id the profile does not manage |
latest_stats is an empty array for a profile connected less than a day ago; the first snapshot lands within the first polling cycle. Full field tables are on the profile stats reference and the Google Business API reference.