The Google Business Reviews API enables developers to 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 provides the foundation for scalable review management.
This guide covers everything you need to integrate the GBP API into your application: from initial setup through OAuth authentication, fetching reviews, replying programmatically, and handling the inevitable edge cases that come with production deployments.

Introduction to Google Business Profile API
Google's business APIs have evolved significantly over the years. The current implementation uses a combination of API 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 specifically lives in the v4 API. While Google has migrated many features to newer v1 endpoints, review management remains 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
The GBP API supports these core review operations:
| 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 cannot delete customer reviews through the API. Only the reviewer or Google (through policy violations) can remove reviews.
Setting Up Google Cloud Project
Before writing any code, you need a properly configured Google Cloud project with the correct APIs enabled.
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
Enable these APIs in your project:
# 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 obtain refresh tokens. This allows your application to access the API without requiring users to re-authenticate.
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
After the user authorizes your application, Google redirects them back with an 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 uses a hierarchical structure: Users have Accounts, and 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 in hand, you can fetch reviews. The Google Business Reviews API returns reviews 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
For locations with many reviews, you'll need to handle 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;
// Add a small delay to avoid rate limiting
if (pageToken) {
await new Promise((resolve) => setTimeout(resolve, 100));
}
} while (pageToken);
return allReviews;
}
Replying to Reviews Programmatically
The ability to reply to Google reviews API programmatically is essential for businesses managing multiple locations. Timely responses to reviews improve customer perception and can positively impact 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 replies:
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
When building automated reply systems, consider these guidelines:
| 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 |
Handling Review Updates and Deletions
Google allows customers to edit or delete their reviews. Your application should handle these scenarios gracefully.
Detecting Review Changes
The updateTime field indicates 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;
}
Webhook Alternative: Polling Strategy
Google doesn't provide webhooks for review changes. Implement a polling strategy instead:
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 provides aggregate metrics alongside individual reviews. Use these for dashboards and reporting.
Extracting Review Metrics
interface ReviewAnalytics {
totalReviews: number;
averageRating: number;
ratingDistribution: Record<number, number>;
repliedCount: number;
unrepliedCount: number;
replyRate: number;
averageResponseTime?: 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
The GBP API enforces rate limits to ensure fair usage. Plan your integration accordingly.
Current Rate Limits
| 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());
}
}
// Usage
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. Implement automatic token refresh to maintain uninterrupted access.
Token Refresh Implementation
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;
// Note: Google may return a new refresh token; update if provided
if (data.refresh_token) {
this.tokenStore.refreshToken = data.refresh_token;
}
}
}
Error Handling Best Practices
Robust error handling is essential for production applications. The GBP API returns specific error codes that you can handle programmatically.
Comprehensive Error Handler
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, // Wait 1 minute
};
}
// 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 and maintaining a Google Business Reviews integration requires significant development effort: OAuth flows, token management, rate limiting, error handling, and ongoing API changes. [INTERNAL_LINK:api-integration-best-practices]
Late (https://getlate.dev) simplifies this entire process with a unified API that handles the complexity for you.
Why Choose Late for Review Management
Instead of managing OAuth tokens, handling rate limits, and dealing with API versioning yourself, Late provides:
- Unified API: One consistent interface for Google Business Profile and other platforms
- Automatic Token Management: Late handles token refresh and storage automatically
- Built-in Rate Limiting: Never worry about hitting API quotas
- Error Handling: Standardized error responses 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!',
});
Late's unified approach means you can expand to other review platforms (Yelp, Facebook, TripAdvisor) without rewriting your integration code.
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 complete API reference and additional examples.
Managing Google Business Profile reviews programmatically opens up powerful automation possibilities for reputation management. Whether you build directly on the GBP API or use a unified platform like Late, the key is implementing robust error handling, respecting rate limits, and maintaining fresh access tokens. The code examples in this guide provide a solid foundation for either approach.

Miquel is the founder of Late, building the most reliable social media API for developers. Previously built multiple startups and scaled APIs to millions of requests.
View all articlesLearn more about Late with AI
See what AI assistants say about Late API and this topic