Skip to Content
ProvidersGoogle My Business (Google Business Profile)

Google My Business (Google Business Profile)

Category: Local SEO / Publishing
Integration type: Tenant OAuth (stored in integrations table)
External API: Google Business Profile API v1 (formerly My Business API)


Purpose

Google My Business (now officially called Google Business Profile, GBP) integration serves two distinct functions:

  1. GBP Post publishing — The GBP Post Writer agent generates posts (offers, updates, events); the GBP Publisher worker pushes them to the tenant’s business listing on Google Search and Google Maps
  2. Review management — The Review Response Writer agent generates responses to customer reviews; the DM Portal shows pending reviews for HITL approval before responses are posted
  3. Business insights — Profile views, search queries, direction requests, phone call clicks — pulled for the Report Writer

GBP is particularly high-value for local-service businesses (plumbers, dentists, restaurants) where Google Maps visibility directly drives revenue.


Config Structure

OAuth flow

scope: https://www.googleapis.com/auth/business.manage Stored in integrations: provider: 'google_my_business' api_key: encrypt(refresh_token) metadata: { accountId: 'accounts/123456789', // GBP Account ID locationId: 'accounts/123456789/locations/987654321', // Location resource name locationName: 'Acme Plumbing - Sydney', accessToken: string, accessTokenExpiresAt: string, }

Platform OAuth credentials (env vars)

The Google Business Profile API uses the same Google OAuth app as Search Console and Analytics. Only the scope differs.

GOOGLE_CLIENT_ID=xxxxxxxx.apps.googleusercontent.com GOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxxxxxxxxx

Integration Pattern

Publishing a GBP post

class GoogleMyBusinessTool { constructor( private locationId: string, // Full resource name: 'accounts/.../locations/...' private accessToken: string, private baseUrl: string = 'https://mybusiness.googleapis.com/v4', ) {} private headers() { return { Authorization: `Bearer ${this.accessToken}` }; } async createPost(options: { summary: string; // Post body text (max 1,500 chars) callToAction?: { actionType: 'BOOK' | 'ORDER' | 'SHOP' | 'LEARN_MORE' | 'SIGN_UP' | 'CALL'; url?: string; // Required for all except CALL }; mediaUrl?: string; // Public image URL topicType?: 'STANDARD' | 'EVENT' | 'OFFER'; // Default: STANDARD event?: { title: string; startDate: string; // YYYY-MM-DD endDate: string; }; offer?: { couponCode?: string; redeemUrl?: string; termsConditions?: string; }; }): Promise<{ postName: string; postUrl?: string }> { const body: any = { languageCode: 'en', summary: options.summary, topicType: options.topicType ?? 'STANDARD', }; if (options.callToAction) { body.callToAction = { actionType: options.callToAction.actionType, url: options.callToAction.url, }; } if (options.mediaUrl) { body.media = [{ mediaFormat: 'PHOTO', sourceUrl: options.mediaUrl, }]; } if (options.topicType === 'EVENT' && options.event) { body.event = { title: options.event.title, schedule: { startDate: this.parseDate(options.event.startDate), endDate: this.parseDate(options.event.endDate), }, }; } if (options.topicType === 'OFFER' && options.offer) { body.offer = options.offer; } const response = await axios.post( `${this.baseUrl}/${this.locationId}/localPosts`, body, { headers: this.headers() }, ); return { postName: response.data.name, postUrl: response.data.searchUrl, }; } async listReviews(options?: { pageSize?: number; orderBy?: 'update_time desc' | 'rating desc'; }): Promise<GBPReview[]> { const response = await axios.get( `${this.baseUrl}/${this.locationId}/reviews`, { headers: this.headers(), params: { pageSize: options?.pageSize ?? 20, orderBy: options?.orderBy ?? 'update_time desc', }, }, ); return (response.data.reviews ?? []).map((r: any) => ({ reviewId: r.reviewId, reviewer: r.reviewer.displayName, starRating: r.starRating, // 'ONE' | 'TWO' | 'THREE' | 'FOUR' | 'FIVE' comment: r.comment, createTime: r.createTime, updateTime: r.updateTime, reply: r.reviewReply?.comment ?? null, })); } async replyToReview(reviewId: string, reply: string): Promise<void> { await axios.put( `${this.baseUrl}/${this.locationId}/reviews/${reviewId}/reply`, { comment: reply }, { headers: this.headers() }, ); } async getInsights(options: { startDate: string; // YYYY-MM-DD endDate: string; metrics: string[]; }): Promise<LocationInsight[]> { const response = await axios.post( `${this.baseUrl}/${this.locationId}:reportInsights`, { locationNames: [this.locationId], basicRequest: { metricRequests: options.metrics.map(m => ({ metric: m })), timeRange: { startTime: new Date(options.startDate).toISOString(), endTime: new Date(options.endDate).toISOString(), }, }, }, { headers: this.headers() }, ); return response.data.locationMetrics ?? []; } async verify(): Promise<void> { const response = await axios.get( `${this.baseUrl}/${this.locationId}`, { headers: this.headers() }, ); if (!response.data.name) throw new Error('Google Business Profile location not found'); } private parseDate(dateStr: string) { const [year, month, day] = dateStr.split('-').map(Number); return { year, month, day }; } }

Review Response Workflow

Review posted on Google Maps ▼ (webhook or polling — GBP has no webhook, polling every 4hrs) Review saved to reviews table in PostgreSQL Review Response Writer agent generates draft reply ▼ (HITL — shown in DM Portal reviews queue) DM staff approves or edits reply GBP Publisher posts reply via replyToReview()

GBP Post Types

TypeUseKey fields
STANDARDGeneral updates, news, announcementssummary, optional image + CTA
EVENTWorkshop, open day, seasonal eventsummary, event.title, startDate, endDate
OFFERDiscount, promotion, limited-time dealsummary, offer.couponCode, offer.redeemUrl, expiry

GBP posts expire after 7 days (STANDARD) or at the event end date (EVENT/OFFER). The Social Calendar Planner accounts for this when scheduling GBP posts.


Test Cases

Unit tests (packages/tools/src/google-my-business.test.ts)

TestApproach
createPost() POSTs to /localPosts with correct bodyMock axios.post; assert summary, topicType
createPost() includes event block when topicType: 'EVENT'Assert event dates in request
createPost() omits CTA when not providedAssert no callToAction in body
listReviews() maps star rating stringsMock response; assert starRating: 'FIVE'
replyToReview() PUTs to correct URLAssert URL contains reviews/{reviewId}/reply
getInsights() passes correct metric requestsAssert metricRequests array
verify() throws when location not foundMock 404; assert throws

© 2026 Leadmetrics — Internal use only