# Google Business Profile reviews

> Send Google reviews of your business locations to Dimension Labs with the universal API.

This brings in the Google reviews of your Business Profile locations: each review as a customer message, with its star rating, and your owner reply when there is one. You run it on your side with the [universal API](/api/tracker), using the Google Business Profile API.

## What you need

- A Google Cloud project approved for the Business Profile APIs. Google requires a Business Profile that has been verified and active for 60 days or more, and a website for the business.
- An OAuth 2.0 client ID and secret, and a Google account that is an owner or manager of the locations, to authorize once with the `https://www.googleapis.com/auth/business.manage` scope and get a refresh token.
- A Dimension Labs API key for a new **Universal API** source. See [Your API key](/api/tracker#your-api-key).

## 1. Get API access in Google Business Profile

Google's steps are in [Prerequisites](https://developers.google.com/my-business/content/prereqs) and [Basic setup](https://developers.google.com/my-business/content/basic-setup).

1. In the Google Cloud Console, create or pick a project and copy its **Project Number** from the **Project info** card on the dashboard.
2. Submit the GBP API contact form and choose **Application for Basic API Access**, using an email address that is an owner or manager on the Business Profile. Google says your project is approved when its Business Profile API quota shows 300 QPM instead of 0.
3. Once approved, enable the Business Profile APIs in the **API Library**, including the **Google My Business API**, **My Business Account Management API** and **My Business Business Information API**.
4. Go to **Credentials** and click **Create credentials > OAuth client ID**. Choose **Web application** and add `https://developers.google.com/oauthplayground` as an authorized redirect URI. Copy the client ID and client secret.
5. Get a refresh token once: open the [OAuth 2.0 Playground](https://developers.google.com/oauthplayground), click the settings icon, select **Use your own OAuth credentials** and enter your client ID and secret. Authorize the `https://www.googleapis.com/auth/business.manage` scope with an account that manages the locations, then click **Exchange authorization code for tokens** and copy the refresh token. Keep it in a secrets manager. If your OAuth consent screen is set to **External** and its status is **Testing**, Google expires refresh tokens after 7 days, so set it to **In production** (or use an **Internal** app in a Google Workspace organization).
6. Access tokens last about an hour. The code below uses `google-auth-library` to turn the refresh token into a fresh access token whenever it needs one, and sends it as `Authorization: Bearer ACCESS_TOKEN`.
7. Find your account and location IDs with `GET https://mybusinessaccountmanagement.googleapis.com/v1/accounts` and `GET https://mybusinessbusinessinformation.googleapis.com/v1/accounts/ACCOUNT_ID/locations`.

## 2. Pull new reviews

```bash
curl -H "Authorization: Bearer $ACCESS_TOKEN" \
  "https://mybusiness.googleapis.com/v4/accounts/ACCOUNT_ID/locations/LOCATION_ID/reviews?pageSize=50&orderBy=updateTime%20desc"
```

- `accounts.locations.reviews.list` works only for verified locations. `pageSize` goes up to 50.
- **Incremental:** there is no "updated since" filter. The default order is `updateTime desc`, newest change first, so page until `updateTime` is older than your cursor, then stop.
- **Pagination:** pass `nextPageToken` as `pageToken`. It is missing on the last page.
- The owner reply is in the same review object, under `reviewReply`, so there is no second call.
- Google's [quota page](https://developers.google.com/my-business/content/limits) asks you to spread requests evenly and back off on `429`. It doesn't list a separate limit for reviews.

## 3. Map the fields

Each review is one incoming message under its own `sessionId`. An owner reply is an outgoing message in the same session. Reviews with a star rating but no `comment` have no text to send: skip them.

| Dimension Labs | Google Business Profile | Notes |
|---|---|---|
| `text` | `comment` (incoming), `reviewReply.comment` (outgoing) | Google calls both "plain text with markups". |
| `userId` | `reviewId` | No stable reviewer ID is returned, so use the review ID. Don't send `reviewer.displayName`. |
| `incoming` | | `true` for the review, `false` for the owner reply. |
| `sessionId` | `reviewId` | Replace any colons. |
| `conversationId` | `reviewId` | |
| `dimensionlabs_timestamp` | `createTime` (review), `reviewReply.updateTime` (reply) | RFC 3339. Convert with `toMillis`. If the reply's time is within a second of the review's, the code moves it a second later. |
| `platformJson` | `starRating` (`ONE` to `FIVE`, as a number), location ID, `reviewer.isAnonymous`, `reviewReply.reviewReplyState` | The location ID lets you join reviews to store data. |

## 4. Send it

Install the auth library in your project first: `npm install google-auth-library`.

```js
// google-reviews.mjs. Environment: GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REFRESH_TOKEN,
// GOOGLE_LOCATIONS (comma-separated, each 'accounts/ACCOUNT_ID/locations/LOCATION_ID'), DL_API_KEY.



const HOUR = 60 * 60 * 1000;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const STARS = { ONE: 1, TWO: 2, THREE: 3, FOUR: 4, FIVE: 5 };
const oauth = new OAuth2Client(process.env.GOOGLE_CLIENT_ID, process.env.GOOGLE_CLIENT_SECRET);
oauth.setCredentials({ refresh_token: process.env.GOOGLE_REFRESH_TOKEN });

// Uses a fresh access token on every call (the library refreshes it when it expires),
// and retries Google's rate limits (429) and server errors (5xx) with a growing pause.
async function get(url) {
  for (let attempt = 0; ; attempt++) {
    const { token } = await oauth.getAccessToken();
    const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
    if (res.ok) return res.json();
    if ((res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`Google returned ${res.status}: ${await res.text()}`);
    await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt) * 1000);
  }
}

async function syncLocation(location, since, historical) {
  let pageToken = '', newest = since, done = false;
  while (!done) {
    const q = new URLSearchParams({ pageSize: '50', orderBy: 'updateTime desc', ...(pageToken && { pageToken }) });
    const page = await get(`https://mybusiness.googleapis.com/v4/${location}/reviews?${q}`);
    for (const r of page.reviews ?? []) {
      const updated = toMillis(r.updateTime);
      if (updated < since) { done = true; break; }
      newest = Math.max(newest, updated);
      const id = r.reviewId.replace(/:/g, '-');
      const base = { userId: id, sessionId: id, conversationId: id,
        platformJson: { location, rating: STARS[r.starRating] ?? null, anonymous: r.reviewer?.isAnonymous } };
      const created = toMillis(r.createTime);
      if (r.comment?.trim()) {
        await sendToDimensionLabs({ ...base, text: r.comment.trim(), incoming: true, dimensionlabs_timestamp: created },
          { historical: historical || created < Date.now() - HOUR });
      }
      if (r.reviewReply?.comment?.trim()) {
        const replied = Math.max(toMillis(r.reviewReply.updateTime), created + 1000);
        await sendToDimensionLabs({ ...base, text: r.reviewReply.comment.trim(), incoming: false, dimensionlabs_timestamp: replied,
          platformJson: { ...base.platformJson, replyState: r.reviewReply.reviewReplyState } }, { historical: historical || replied < Date.now() - HOUR });
      }
    }
    pageToken = page.nextPageToken;
    if (!pageToken) done = true;
  }
  return newest;
}

// Sends every review created or changed at or after `since` (Unix ms), for each location. Returns the new `since`.
export async function syncGoogleReviews({ since, historical = false }) {
  let newest = since;
  for (const location of process.env.GOOGLE_LOCATIONS.split(',').map((x) => x.trim()).filter(Boolean)) {
    newest = Math.max(newest, await syncLocation(location, since, historical));
  }
  return newest;
}
```

A review that is edited, or gets a new reply, comes back with a newer `updateTime` and is sent again under the same `sessionId`. Anything unchanged is a duplicate, and is removed automatically. A new reply to an old review goes to `/track`; the old review itself goes to `/trackhistorical` because of its age. One Dimension Labs source can hold all your locations: the location goes in `platformJson.location`.

## 5. Run it

Save the code as `google-reviews.mjs` in a project set up as in [Set up your project](/api/tracker#set-up-your-project), and run it with the [scheduled sync](/api/scheduled-sync) runner: `import { syncGoogleReviews as sync } from './google-reviews.mjs';`.

- **History first:** `node run.mjs 2020-01-01` pages back through every review since that date and sends it to `/trackhistorical`. Then start a [backfill](/dimensions/backfill) from the **Backfill** tab.
- **Then every 15 minutes:** the scheduled job sends new reviews and replies to `/track`, and they are enriched automatically every day. Anything more than an hour old when the job sees it goes to `/trackhistorical`, and needs a backfill.
- **Re-runs are safe:** duplicates are removed automatically.

If you have reviews in a file instead, [upload it](/data/upload-a-file) and check it first with the [file checker](/data/check-a-file), then run a backfill.

## Google Business Profile reference

- [Prerequisites](https://developers.google.com/my-business/content/prereqs)
- [Basic setup](https://developers.google.com/my-business/content/basic-setup)
- [Work with review data](https://developers.google.com/my-business/content/review-data)
- [Method: accounts.locations.reviews.list](https://developers.google.com/my-business/reference/rest/v4/accounts.locations.reviews/list)
- [REST Resource: accounts.locations.reviews](https://developers.google.com/my-business/reference/rest/v4/accounts.locations.reviews)
- [Method: accounts.list](https://developers.google.com/my-business/reference/accountmanagement/rest/v1/accounts/list)
- [Method: accounts.locations.list](https://developers.google.com/my-business/reference/businessinformation/rest/v1/accounts.locations/list)
- [Quota limits](https://developers.google.com/my-business/content/limits)
- [Using OAuth 2.0 to access Google APIs (refresh token expiration)](https://developers.google.com/identity/protocols/oauth2)
- [Latest updates](https://developers.google.com/my-business/content/latest-updates)

Checked against Google's documentation on 2026-10-13.
