Google Business Reviews API 2026: Integration Guide with Code

Learn how to integrate the Google Business Reviews API to fetch, reply to, and manage reviews programmatically with TypeScript examples.

Miki Palet

by

·14 min read·

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

OperationHTTP MethodEndpoint
List reviewsGET/accounts/{id}/locations/{id}/reviews
Get single reviewGET/accounts/{id}/locations/{id}/reviews/{reviewId}
Reply to reviewPUT/accounts/{id}/locations/{id}/reviews/{reviewId}/reply
Update replyPUT/accounts/{id}/locations/{id}/reviews/{reviewId}/reply
Delete replyDELETE/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

  1. Navigate to Google Cloud Console
  2. Create a new project or select an existing one
  3. 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

  1. Go to APIs & Services > OAuth consent screen
  2. Select External user type (unless you have a Google Workspace organization)
  3. Fill in the required fields: app name, user support email, developer contact email
  4. Add the required scope: https://www.googleapis.com/auth/business.manage
  5. Add test users (required while in testing mode)

Step 4: Create OAuth Credentials

  1. Go to APIs & Services > Credentials
  2. Click Create Credentials > OAuth client ID
  3. Select Web application
  4. Add your authorized redirect URIs
  5. 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

ScenarioRecommended Action
5-star review with commentThank the customer, mention specific details from their review
5-star review without commentBrief thank you, invite them back
3-4 star reviewThank them, address any concerns mentioned
1-2 star reviewApologize, offer to resolve offline, provide contact info
Review with no reply after 24hPrioritize 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 TypeLimit
Queries per minute300
Queries per day10,000 (default, can request increase)
Batch requestsNot 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

  1. Sign up for Late at getlate.dev
  2. Connect your Google Business Profile accounts
  3. 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.

Learn more about this topic with AI