The Google Business Reviews API lets you programmatically manage customer reviews across Google Business Profile locations. Whether you're building a reputation management dashboard, automating review responses, or aggregating feedback across multiple locations, this API gives you the tools to do it at scale.
This guide covers everything you need: initial setup, OAuth authentication, fetching reviews, replying programmatically, and the edge cases you'll hit in production.
Introduction to Google Business Profile API
Google's business APIs use a combination of endpoints:
- My Business Account Management API (
mybusinessaccountmanagement.googleapis.com/v1): Manages accounts and permissions - My Business Business Information API (
mybusinessbusinessinformation.googleapis.com/v1): Handles location data - My Business API v4 (
mybusiness.googleapis.com/v4): Manages reviews and local posts (still active for these features)
The Google My Business API reviews functionality lives in the v4 API. Google has migrated many features to newer v1 endpoints, but review management stays on v4 with no announced deprecation date.
Note: Google frequently updates their API structure. The v4 endpoints for reviews are stable, but always check the official documentation for the latest changes.
What You Can Do with the Reviews API
| Operation | HTTP Method | Endpoint |
|---|---|---|
| List reviews | GET | /accounts/{id}/locations/{id}/reviews |
| Get single review | GET | /accounts/{id}/locations/{id}/reviews/{reviewId} |
| Reply to review | PUT | /accounts/{id}/locations/{id}/reviews/{reviewId}/reply |
| Update reply | PUT | /accounts/{id}/locations/{id}/reviews/{reviewId}/reply |
| Delete reply | DELETE | /accounts/{id}/locations/{id}/reviews/{reviewId}/reply |
You can't delete customer reviews through the API. Only the reviewer or Google (through policy violations) can remove them.
Setting Up Google Cloud Project
Step 1: Create a Google Cloud Project
- Navigate to Google Cloud Console
- Create a new project or select an existing one
- Note your project ID for later use
Step 2: Enable Required APIs
# Using gcloud CLI
gcloud services enable mybusinessaccountmanagement.googleapis.com
gcloud services enable mybusinessbusinessinformation.googleapis.com
gcloud services enable mybusiness.googleapis.com
Or enable them through the Cloud Console:
- My Business Account Management API
- My Business Business Information API
- Google My Business API
Step 3: Configure OAuth Consent Screen
- Go to APIs & Services > OAuth consent screen
- Select External user type (unless you have a Google Workspace organization)
- Fill in the required fields: app name, user support email, developer contact email
- Add the required scope:
https://www.googleapis.com/auth/business.manage - Add test users (required while in testing mode)
Step 4: Create OAuth Credentials
- Go to APIs & Services > Credentials
- Click Create Credentials > OAuth client ID
- Select Web application
- Add your authorized redirect URIs
- Save your Client ID and Client Secret
// Store these in environment variables
const config = {
clientId: process.env.GOOGLE_BUSINESS_CLIENT_ID,
clientSecret: process.env.GOOGLE_BUSINESS_CLIENT_SECRET,
redirectUri: process.env.GOOGLE_BUSINESS_REDIRECT_URI,
};
OAuth 2.0 for Google Business
Google Business Profile requires OAuth 2.0 with offline access to get refresh tokens. This lets your app access the API without users re-authenticating every hour.
Generating the Authorization URL
function getGoogleBusinessAuthUrl(state?: string): string {
const scopes = [
'https://www.googleapis.com/auth/business.manage',
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/userinfo.email'
];
const params = new URLSearchParams({
client_id: process.env.GOOGLE_BUSINESS_CLIENT_ID!,
redirect_uri: process.env.GOOGLE_BUSINESS_REDIRECT_URI!,
scope: scopes.join(' '),
response_type: 'code',
access_type: 'offline',
prompt: 'consent', // Forces consent screen to ensure refresh token
});
if (state) {
params.append('state', state);
}
return `https://accounts.google.com/o/oauth2/auth?${params.toString()}`;
}
The prompt: 'consent' parameter is critical. Without it, Google may not return a refresh token on subsequent authorizations.
Exchanging the Authorization Code
interface TokenResponse {
access_token: string;
refresh_token?: string;
expires_in: number;
token_type: string;
scope: string;
}
async function exchangeCodeForToken(code: string): Promise<TokenResponse> {
const response = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
code,
client_id: process.env.GOOGLE_BUSINESS_CLIENT_ID!,
client_secret: process.env.GOOGLE_BUSINESS_CLIENT_SECRET!,
redirect_uri: process.env.GOOGLE_BUSINESS_REDIRECT_URI!,
grant_type: 'authorization_code',
}),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Token exchange failed: ${response.status} ${errorText}`);
}
return response.json();
}
Note: Store the refresh token securely. It's only returned on the initial authorization (or when using
prompt: 'consent').
Account and Location Structure
The GBP API is hierarchical: users have accounts, accounts contain locations. You need both IDs to access reviews.
Fetching User Accounts
interface GBPAccount {
name: string; // Format: "accounts/123456789"
accountName: string;
type: string;
role: string;
state: { status: string };
}
async function getAccounts(accessToken: string): Promise<GBPAccount[]> {
const allAccounts: GBPAccount[] = [];
let pageToken: string | undefined;
do {
const url = new URL('https://mybusinessaccountmanagement.googleapis.com/v1/accounts');
url.searchParams.set('pageSize', '100');
if (pageToken) {
url.searchParams.set('pageToken', pageToken);
}
const response = await fetch(url.toString(), {
headers: { Authorization: `Bearer ${accessToken}` },
});
if (!response.ok) {
throw new Error(`Failed to fetch accounts: ${response.status}`);
}
const data = await response.json();
if (data.accounts) {
allAccounts.push(...data.accounts);
}
pageToken = data.nextPageToken;
} while (pageToken);
return allAccounts;
}
Fetching Locations for an Account
interface GBPLocation {
name: string; // Format: "locations/987654321"
title: string;
storefrontAddress?: {
addressLines: string[];
locality: string;
administrativeArea: string;
postalCode: string;
regionCode: string;
};
websiteUri?: string;
}
async function getLocations(
accessToken: string,
accountId: string
): Promise<GBPLocation[]> {
const allLocations: GBPLocation[] = [];
let pageToken: string | undefined;
do {
const url = new URL(
`https://mybusinessbusinessinformation.googleapis.com/v1/${accountId}/locations`
);
url.searchParams.set('readMask', 'name,title,storefrontAddress,websiteUri');
url.searchParams.set('pageSize', '100');
if (pageToken) {
url.searchParams.set('pageToken', pageToken);
}
const response = await fetch(url.toString(), {
headers: { Authorization: `Bearer ${accessToken}` },
});
if (!response.ok) {
throw new Error(`Failed to fetch locations: ${response.status}`);
}
const data = await response.json();
if (data.locations) {
allLocations.push(...data.locations);
}
pageToken = data.nextPageToken;
} while (pageToken);
return allLocations;
}
Fetching Reviews with the Google Business Reviews API
With account and location IDs ready, you can fetch reviews. The API returns them in reverse chronological order by default.
Basic Review Fetching
interface ReviewReply {
comment: string;
updateTime: string;
}
interface Review {
id: string;
name: string;
reviewer: {
displayName: string;
profilePhotoUrl?: string;
isAnonymous: boolean;
};
rating: number;
starRating: 'ONE' | 'TWO' | 'THREE' | 'FOUR' | 'FIVE';
comment?: string;
createTime: string;
updateTime: string;
reviewReply?: ReviewReply;
}
interface ReviewsResponse {
reviews: Review[];
averageRating?: number;
totalReviewCount?: number;
nextPageToken?: string;
}
async function getReviews(
accessToken: string,
accountId: string,
locationId: string,
options?: { pageSize?: number; pageToken?: string }
): Promise<ReviewsResponse> {
// Extract numeric IDs if full resource names are provided
const accId = accountId.includes('/') ? accountId.split('/').pop() : accountId;
const locId = locationId.includes('/') ? locationId.split('/').pop() : locationId;
const pageSize = Math.min(options?.pageSize || 50, 50); // Max 50 per request
const url = new URL(
`https://mybusiness.googleapis.com/v4/accounts/${accId}/locations/${locId}/reviews`
);
url.searchParams.set('pageSize', String(pageSize));
if (options?.pageToken) {
url.searchParams.set('pageToken', options.pageToken);
}
const response = await fetch(url.toString(), {
headers: { Authorization: `Bearer ${accessToken}` },
});
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`Failed to fetch reviews: ${response.status} ${errorBody}`);
}
const data = await response.json();
// Map star rating strings to numeric values
const starRatingMap: Record<string, number> = {
ONE: 1,
TWO: 2,
THREE: 3,
FOUR: 4,
FIVE: 5,
};
const reviews = (data.reviews || []).map((review: any) => ({
id: review.reviewId || review.name?.split('/').pop(),
name: review.name,
reviewer: {
displayName: review.reviewer?.displayName || 'Anonymous',
profilePhotoUrl: review.reviewer?.profilePhotoUrl || null,
isAnonymous: review.reviewer?.isAnonymous || false,
},
rating: starRatingMap[review.starRating] || 0,
starRating: review.starRating,
comment: review.comment || '',
createTime: review.createTime,
updateTime: review.updateTime,
reviewReply: review.reviewReply
? {
comment: review.reviewReply.comment,
updateTime: review.reviewReply.updateTime,
}
: null,
}));
return {
reviews,
averageRating: data.averageRating,
totalReviewCount: data.totalReviewCount,
nextPageToken: data.nextPageToken,
};
}
Fetching All Reviews with Pagination
async function getAllReviews(
accessToken: string,
accountId: string,
locationId: string
): Promise<Review[]> {
const allReviews: Review[] = [];
let pageToken: string | undefined;
do {
const response = await getReviews(accessToken, accountId, locationId, {
pageSize: 50,
pageToken,
});
allReviews.push(...response.reviews);
pageToken = response.nextPageToken;
// Small delay to avoid rate limiting
if (pageToken) {
await new Promise((resolve) => setTimeout(resolve, 100));
}
} while (pageToken);
return allReviews;
}
Replying to Reviews Programmatically
Replying to reviews at scale is where the API really pays off. Timely responses improve customer perception and can help local SEO.
Creating a Review Reply
async function replyToReview(
accessToken: string,
reviewName: string,
replyText: string
): Promise<{ success: boolean }> {
// reviewName format: accounts/{accountId}/locations/{locationId}/reviews/{reviewId}
const response = await fetch(
`https://mybusiness.googleapis.com/v4/${reviewName}/reply`,
{
method: 'PUT',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
comment: replyText,
}),
}
);
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`Failed to reply to review: ${response.status} ${errorBody}`);
}
return { success: true };
}
Updating an Existing Reply
The same endpoint handles both creating and updating:
async function updateReviewReply(
accessToken: string,
reviewName: string,
newReplyText: string
): Promise<{ success: boolean }> {
// PUT request updates the existing reply
return replyToReview(accessToken, reviewName, newReplyText);
}
Deleting a Reply
async function deleteReviewReply(
accessToken: string,
reviewName: string
): Promise<{ success: boolean }> {
const response = await fetch(
`https://mybusiness.googleapis.com/v4/${reviewName}/reply`,
{
method: 'DELETE',
headers: {
Authorization: `Bearer ${accessToken}`,
},
}
);
// 204 No Content indicates successful deletion
if (!response.ok && response.status !== 204) {
const errorBody = await response.text();
throw new Error(`Failed to delete reply: ${response.status} ${errorBody}`);
}
return { success: true };
}
Reply Best Practices
| Scenario | Recommended Action |
|---|---|
| 5-star review with comment | Thank the customer, mention specific details from their review |
| 5-star review without comment | Brief thank you, invite them back |
| 3-4 star review | Thank them, address any concerns mentioned |
| 1-2 star review | Apologize, offer to resolve offline, provide contact info |
| Review with no reply after 24h | Prioritize for immediate response |
Stop building social integrations from scratch.
One API call to publish, schedule, and manage posts across 15+ platforms.
Handling Review Updates and Deletions
Google lets customers edit or delete their reviews. Your app needs to handle this.
Detecting Review Changes
The updateTime field tells you when a review was last modified:
interface ReviewChange {
reviewId: string;
changeType: 'new' | 'updated' | 'deleted';
review?: Review;
}
async function detectReviewChanges(
accessToken: string,
accountId: string,
locationId: string,
previousReviews: Map<string, Review>
): Promise<ReviewChange[]> {
const currentReviews = await getAllReviews(accessToken, accountId, locationId);
const changes: ReviewChange[] = [];
const currentReviewMap = new Map(
currentReviews.map((r) => [r.id, r])
);
// Check for new and updated reviews
for (const review of currentReviews) {
const previous = previousReviews.get(review.id);
if (!previous) {
changes.push({ reviewId: review.id, changeType: 'new', review });
} else if (previous.updateTime !== review.updateTime) {
changes.push({ reviewId: review.id, changeType: 'updated', review });
}
}
// Check for deleted reviews
for (const [reviewId] of previousReviews) {
if (!currentReviewMap.has(reviewId)) {
changes.push({ reviewId, changeType: 'deleted' });
}
}
return changes;
}
Polling Strategy
Google doesn't provide webhooks for review changes, so you'll need to poll:
async function pollForReviewChanges(
accessToken: string,
accountId: string,
locationId: string,
onNewReview: (review: Review) => Promise<void>,
onUpdatedReview: (review: Review) => Promise<void>,
onDeletedReview: (reviewId: string) => Promise<void>
): Promise<void> {
let previousReviews = new Map<string, Review>();
// Initial fetch
const initialReviews = await getAllReviews(accessToken, accountId, locationId);
previousReviews = new Map(initialReviews.map((r) => [r.id, r]));
// Poll every 5 minutes
setInterval(async () => {
try {
const changes = await detectReviewChanges(
accessToken,
accountId,
locationId,
previousReviews
);
for (const change of changes) {
switch (change.changeType) {
case 'new':
await onNewReview(change.review!);
previousReviews.set(change.reviewId, change.review!);
break;
case 'updated':
await onUpdatedReview(change.review!);
previousReviews.set(change.reviewId, change.review!);
break;
case 'deleted':
await onDeletedReview(change.reviewId);
previousReviews.delete(change.reviewId);
break;
}
}
} catch (error) {
console.error('Error polling for review changes:', error);
}
}, 5 * 60 * 1000); // 5 minutes
}
Review Analytics and Insights
The GBP API returns aggregate metrics alongside individual reviews. Useful for dashboards and reporting.
interface ReviewAnalytics {
totalReviews: number;
averageRating: number;
ratingDistribution: Record<number, number>;
repliedCount: number;
unrepliedCount: number;
replyRate: number;
}
function analyzeReviews(reviews: Review[]): ReviewAnalytics {
const ratingDistribution: Record<number, number> = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
let totalRating = 0;
let repliedCount = 0;
for (const review of reviews) {
ratingDistribution[review.rating]++;
totalRating += review.rating;
if (review.reviewReply) {
repliedCount++;
}
}
const totalReviews = reviews.length;
const unrepliedCount = totalReviews - repliedCount;
return {
totalReviews,
averageRating: totalReviews > 0 ? totalRating / totalReviews : 0,
ratingDistribution,
repliedCount,
unrepliedCount,
replyRate: totalReviews > 0 ? (repliedCount / totalReviews) * 100 : 0,
};
}
Rate Limits and Quotas
| Quota Type | Limit |
|---|---|
| Queries per minute | 300 |
| Queries per day | 10,000 (default, can request increase) |
| Batch requests | Not supported for reviews |
Implementing Rate Limiting
class RateLimiter {
private requests: number[] = [];
private readonly maxRequests: number;
private readonly windowMs: number;
constructor(maxRequests: number = 300, windowMs: number = 60000) {
this.maxRequests = maxRequests;
this.windowMs = windowMs;
}
async waitForSlot(): Promise<void> {
const now = Date.now();
this.requests = this.requests.filter((time) => now - time < this.windowMs);
if (this.requests.length >= this.maxRequests) {
const oldestRequest = this.requests[0];
const waitTime = this.windowMs - (now - oldestRequest) + 100;
await new Promise((resolve) => setTimeout(resolve, waitTime));
}
this.requests.push(Date.now());
}
}
const rateLimiter = new RateLimiter(300, 60000);
async function rateLimitedRequest<T>(
requestFn: () => Promise<T>
): Promise<T> {
await rateLimiter.waitForSlot();
return requestFn();
}
Token Management and Refresh
Access tokens expire after one hour. You'll need automatic token refresh to keep things running.
interface TokenStore {
accessToken: string;
refreshToken: string;
expiresAt: number;
}
class GoogleBusinessTokenManager {
private tokenStore: TokenStore;
private clientId: string;
private clientSecret: string;
constructor(
initialTokens: TokenStore,
clientId: string,
clientSecret: string
) {
this.tokenStore = initialTokens;
this.clientId = clientId;
this.clientSecret = clientSecret;
}
async getValidAccessToken(): Promise<string> {
// Refresh if token expires in less than 5 minutes
const bufferMs = 5 * 60 * 1000;
if (Date.now() >= this.tokenStore.expiresAt - bufferMs) {
await this.refreshAccessToken();
}
return this.tokenStore.accessToken;
}
private async refreshAccessToken(): Promise<void> {
const response = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
refresh_token: this.tokenStore.refreshToken,
client_id: this.clientId,
client_secret: this.clientSecret,
grant_type: 'refresh_token',
}),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Token refresh failed: ${response.status} ${errorText}`);
}
const data = await response.json();
this.tokenStore.accessToken = data.access_token;
this.tokenStore.expiresAt = Date.now() + data.expires_in * 1000;
// Google may return a new refresh token; update if provided
if (data.refresh_token) {
this.tokenStore.refreshToken = data.refresh_token;
}
}
}
Error Handling
The GBP API returns specific error codes you can handle programmatically. Here's a pattern that covers the common cases:
type ErrorType = 'refresh-token' | 'retry' | 'user-error' | 'unknown';
interface HandledError {
type: ErrorType;
message: string;
shouldRetry: boolean;
retryAfterMs?: number;
}
function handleGBPError(statusCode: number, errorBody: string): HandledError {
const lowerBody = errorBody.toLowerCase();
// Authentication errors
if (
lowerBody.includes('invalid_grant') ||
lowerBody.includes('token has been expired or revoked')
) {
return {
type: 'refresh-token',
message: 'Access token expired. Please reconnect your account.',
shouldRetry: false,
};
}
if (lowerBody.includes('invalid_token') || lowerBody.includes('unauthorized')) {
return {
type: 'refresh-token',
message: 'Invalid access token. Please reconnect your account.',
shouldRetry: false,
};
}
// Permission errors
if (lowerBody.includes('permission_denied') || lowerBody.includes('forbidden')) {
return {
type: 'user-error',
message: 'You do not have permission to manage this location.',
shouldRetry: false,
};
}
// Resource not found
if (lowerBody.includes('not_found')) {
return {
type: 'user-error',
message: 'The requested resource was not found.',
shouldRetry: false,
};
}
// Rate limiting
if (
statusCode === 429 ||
lowerBody.includes('rate_limit') ||
lowerBody.includes('quota')
) {
return {
type: 'retry',
message: 'Rate limit exceeded. Retrying after delay.',
shouldRetry: true,
retryAfterMs: 60000,
};
}
// Service unavailable
if (statusCode === 503 || lowerBody.includes('service_unavailable')) {
return {
type: 'retry',
message: 'Service temporarily unavailable.',
shouldRetry: true,
retryAfterMs: 30000,
};
}
return {
type: 'unknown',
message: `Unexpected error: ${statusCode} ${errorBody}`,
shouldRetry: false,
};
}
Retry Logic with Exponential Backoff
async function withRetry<T>(
operation: () => Promise<T>,
maxRetries: number = 3
): Promise<T> {
let lastError: Error | undefined;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error: any) {
lastError = error;
const handled = handleGBPError(
error.statusCode || 500,
error.message || ''
);
if (!handled.shouldRetry || attempt === maxRetries) {
throw error;
}
const backoffMs = handled.retryAfterMs || Math.pow(2, attempt) * 1000;
console.log(`Retry attempt ${attempt + 1} after ${backoffMs}ms`);
await new Promise((resolve) => setTimeout(resolve, backoffMs));
}
}
throw lastError;
}
Using Late for Review Management
Building this integration from scratch takes real effort: OAuth flows, token management, rate limiting, error handling, and keeping up with API changes. API integration best practices
Late (https://getlate.dev) handles all of that for you with a unified API.
Why Use Late
Instead of wiring up all the plumbing yourself, Late gives you:
- Unified API: One consistent interface for Google Business Profile and other platforms
- Automatic Token Management: Token refresh and storage handled for you
- Built-in Rate Limiting: No quota headaches
- Standardized Error Responses: Same error format across all platforms
- Webhook Support: Get notified of new reviews without polling
Getting Started with Late
import { Late } from '@getlate/sdk';
const late = new Late({
apiKey: process.env.LATE_API_KEY,
});
// Fetch reviews with a single call
const reviews = await late.reviews.list({
accountId: 'your-connected-account-id',
platform: 'googlebusiness',
});
// Reply to a review
await late.reviews.reply({
reviewId: 'review-id',
message: 'Thank you for your feedback!',
});
And if you ever need to expand to Yelp, Facebook, or TripAdvisor, you won't rewrite anything.
Next Steps
- Sign up for Late at getlate.dev
- Connect your Google Business Profile accounts
- Start managing reviews programmatically with our unified API
Check out our documentation for the complete API reference and more examples.
Managing Google Business Profile reviews programmatically opens up real automation potential for reputation management. Whether you build directly on the GBP API or use a unified platform like Late, the key is solid error handling, respecting rate limits, and keeping access tokens fresh. The code in this guide gives you a working foundation for either path.