Skip to content

Google Business API

Postproxy publishes local updates to a Google Business Profile location — standard posts, events, offers, and standalone gallery photos — and manages the Business Profile itself: opening hours, attributes, services, food menus, place action links, and the photo gallery. Every request targets a specific location via location_id. Customer reviews are read and replied to through the Profile Comments API, and location analytics flow through profile stats.

Every request below uses the base URL https://api.postproxy.dev and an Authorization: Bearer YOUR_API_KEY header. Replace YOUR_API_KEY and the example IDs with your own.

Every post targets one or more profiles. A profile is one connected account — for Google Business, a connected Business Profile account that publishes to its locations (each post picks a location via placements). Reference one in a request by its id (the prof_abc123 in the examples below) or by the platform name "google_business", which selects the group’s Google Business profile (a group holds at most one profile per platform). List what’s connected with GET /api/profiles.

Profiles live in profile groups — containers that organize the accounts for one brand, client, or project. List groups with GET /api/profile_groups, and connect a new Google Business profile with the Initialize Connection endpoint.

Example: get profiles and profile groups

List the profiles you can post to — GET /api/profiles:

Terminal window
curl -X GET "https://api.postproxy.dev/api/profiles" \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": [
{
"id": "prof_abc123",
"name": "My Business",
"platform": "google_business",
"status": "active",
"profile_group_id": "grp_xyz789",
"expires_at": null,
"post_count": 19,
"avatar_url": "https://cdn.postproxy.dev/uploads/avatar_prof_abc123.jpg"
}
]
}

List your profile groups — GET /api/profile_groups:

Terminal window
curl -X GET "https://api.postproxy.dev/api/profile_groups" \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": [
{ "id": "grp_xyz789", "name": "Main Brand", "profiles_count": 5 },
{ "id": "grp_def456", "name": "Client Project", "profiles_count": 3 }
]
}
Platform IDgoogle_business
Formatsstandard (default), event, offer, photo
Character limit1,500 (Photo has no body)
MediaOptional (local posts); required (Photo). No video
PlacementsLocations — location_id (required)
CommentsReviews via Profile Comments
Profile managementHours, attributes, services, food menus, action links, profile media — see Business Profile management
Direct messagesNo
Post chainsNo
FormatDescription
standardPlain local post (default)
eventEvent with a title and date range
offerPromotion with a validity window and optional coupon
photoStandalone photo uploaded to the location’s gallery (no text, no CTA)

Shared parameters (standard, event, offer)

Section titled “Shared parameters (standard, event, offer)”
ParameterTypeRequiredDescription
formatstringNostandard (default), event, offer, or photo
location_idstringYesFull location resource path (e.g. accounts/123/locations/456)
language_codestringNoBCP 47 code (e.g. en, de). Defaults to en. Metadata only
cta_action_typestringNoLEARN_MORE, BOOK, ORDER, SHOP, SIGN_UP, or CALL
cta_urlstringConditionalHTTPS URL the CTA button opens. Required for every CTA except CALL
ParameterTypeRequiredDescription
event_titlestringYesEvent title shown on the card
event_start_datestringYesYYYY-MM-DD
event_end_datestringYesYYYY-MM-DD
event_start_timestringNoHH:MM (24-hour)
event_end_timestringNoHH:MM (24-hour)
ParameterTypeRequiredDescription
event_start_datestringYesYYYY-MM-DD — start of validity
event_end_datestringYesYYYY-MM-DD — end of validity
event_start_time / event_end_timestringNoHH:MM (24-hour)
event_titlestringNoOffer headline (defaults to “Special Offer”)
offer_coupon_codestringNoPromo code shown with the offer
offer_redeem_urlstringNoURL where the offer can be redeemed
offer_termsstringNoTerms and conditions
ParameterTypeRequiredDescription
location_idstringYesFull location resource path

photo uploads a single image to the location’s gallery. The post body is ignored, and CTA / language parameters do not apply.

FormatImage maxFormatsCountMin dimensions
standard / event / offer5 MBjpg, png1 (optional)400×300 (recommended 1200×900, 4:3)
photo5 MBjpg, png1 (required)250×250
  • Local-post formats accept text-only posts; photo requires an image and ignores body text.
  • Video is not supported by Google Business local posts or media.
Terminal window
# standard post with a CTA
curl -X POST "https://api.postproxy.dev/api/posts" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"post": { "body": "We are now open on Sundays, 10am–4pm — come visit!" },
"profiles": ["prof_abc123"],
"platforms": {
"google_business": {
"format": "standard",
"location_id": "accounts/123456789/locations/987654321",
"cta_action_type": "LEARN_MORE",
"cta_url": "https://acme.example.com/hours"
}
}
}'
Terminal window
# event
curl -X POST "https://api.postproxy.dev/api/posts" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"post": { "body": "Live music, free coffee, prizes." },
"profiles": ["prof_abc123"],
"platforms": {
"google_business": {
"format": "event",
"location_id": "accounts/123456789/locations/987654321",
"event_title": "5-Year Anniversary",
"event_start_date": "2026-06-15",
"event_end_date": "2026-06-15"
}
}
}'
Terminal window
# offer with a coupon
curl -X POST "https://api.postproxy.dev/api/posts" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"post": { "body": "20% off all whole-bean coffee through the end of the month." },
"profiles": ["prof_abc123"],
"media": ["https://example.com/offer.jpg"],
"platforms": {
"google_business": {
"format": "offer",
"location_id": "accounts/123456789/locations/987654321",
"event_start_date": "2026-06-01",
"event_end_date": "2026-06-30",
"offer_coupon_code": "BEANS20",
"cta_action_type": "SHOP",
"cta_url": "https://example.com/shop"
}
}
}'
Terminal window
# photo (uploads to the location gallery; body is ignored)
curl -X POST "https://api.postproxy.dev/api/posts" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"post": {},
"profiles": ["prof_abc123"],
"media": ["https://example.com/storefront.jpg"],
"platforms": {
"google_business": {
"format": "photo",
"location_id": "accounts/123456789/locations/987654321"
}
}
}'

A Google Business profile may manage multiple Google accounts and any number of locations. List them with the placements endpoint — each placement’s id is the full resource path (e.g. accounts/123456789/locations/987654321) you pass as location_id. It is always required.

Terminal window
# List locations (placements)
curl "https://api.postproxy.dev/api/profiles/prof_abc123/placements" \
-H "Authorization: Bearer YOUR_API_KEY"

Each placement carries a metadata.place_id — the location’s Google Maps Place ID — so you can cross-reference it against Google Maps / Places or build a reviews permalink without a separate lookup. It’s omitted when Google returns no Place ID for the location.

{
"data": [
{
"id": "accounts/123456789/locations/987654321",
"name": "Acme Coffee — Downtown",
"metadata": {
"place_id": "ChIJN1t_tDeuEmsRUsoyG83frY4"
}
}
]
}

Google Business reviews live on a location, not on a post, so they surface through the Profile Comments API — not the post-level Comments API. Reviews sync twice daily, at 06:00 and 18:00 UTC.

Terminal window
# List reviews (and your replies) for a profile
curl "https://api.postproxy.dev/api/profiles/prof_abc123/comments" \
-H "Authorization: Bearer YOUR_API_KEY"
Terminal window
# Reply to a review — parent_id is the review's external ID
curl -X POST "https://api.postproxy.dev/api/profiles/prof_abc123/comments" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"body": "Thank you for the kind words — see you next time!",
"parent_id": "accounts/123456789/locations/987654321/reviews/AbFvOq"
}'

A profile_comment.created webhook fires for each new review (across all locations of the account).

Beyond publishing, Postproxy reads and updates the Business Profile data behind each location — opening hours, attributes, services, food menus, action links, and profile photos. This is separate from publishing local posts to that location.

Every endpoint is scoped to one connected profile and one location:

/api/profiles/:profile_id/google_business/...

No extra connection or scope is needed: any Google Business profile connected to Postproxy can use these endpoints. Every request requires location_id — the location’s full Google resource path (accounts/{accountId}/locations/{locationId}), exactly the placement id from Placements. A location_id your profile can’t reach returns 404; a missing one returns 400.

MethodEndpointDescription
GET/api/profiles/:profile_id/google_business/locationGet the location resource
PATCH/api/profiles/:profile_id/google_business/update_locationUpdate location fields
GET/api/profiles/:profile_id/google_business/available_location_categoriesList valid location categories
PATCH/api/profiles/:profile_id/google_business/update_hoursUpdate regular, special, or more hours
GET/api/profiles/:profile_id/google_business/attributesGet the location’s attributes
GET/api/profiles/:profile_id/google_business/available_attributesList attributes the location can set
PATCH/api/profiles/:profile_id/google_business/update_attributesUpdate attributes
GET/api/profiles/:profile_id/google_business/service_listGet the service list
PATCH/api/profiles/:profile_id/google_business/update_service_listReplace the service list
GET/api/profiles/:profile_id/google_business/food_menusGet the food menus
PATCH/api/profiles/:profile_id/google_business/update_food_menusReplace the food menus
GET/api/profiles/:profile_id/google_business/place_action_linksList place action links
POST/api/profiles/:profile_id/google_business/create_place_action_linkCreate a place action link
PATCH/api/profiles/:profile_id/google_business/update_place_action_linkUpdate a place action link
DELETE/api/profiles/:profile_id/google_business/delete_place_action_linkDelete a place action link
GET/api/profiles/:profile_id/google_business/mediaList profile media
POST/api/profiles/:profile_id/google_business/create_mediaAdd a photo or video to the profile
DELETE/api/profiles/:profile_id/google_business/delete_mediaDelete a profile media item

Google’s update calls are field-masked: every PATCH here takes a fields array naming exactly which parts of the resource you’re replacing, and anything named in fields but absent from the body is cleared. Nested objects are replaced wholesale, not merged.

So the reliable pattern for every write is:

  1. GET the resource.
  2. Edit the object you got back.
  3. PATCH it with fields naming only what you changed.

Payloads use Google’s own shapes and camelCased keys, unchanged in both directions, so a response can be sent straight back as a request body.

Google does not offer every capability to every profile. What a location supports depends on its primary category, its country, and location-level eligibility:

SurfaceGate
AttributesValid attribute IDs depend on primary category and region — always list available attributes first
Service listRequires metadata.canModifyServiceList on the location
Food menusRequires metadata.canHaveFoodMenus — mostly restaurant-like categories
Place action linksWhich placeActionType values are accepted varies by profile setup
Profile mediaCategory relevance and moderation vary by profile type

Requests blocked by one of these gates return 422 with a message naming the flag.

GET /api/profiles/:profile_id/google_business/location

ParameterTypeRequiredDescription
location_idstringYesPlacement ID (see above)
read_maskstringNoComma-separated Google field paths. Defaults to the full profile-oriented set (name, title, website, phones, categories, address, hours, service area, service items, labels, latlng, openInfo, profile, metadata)
Terminal window
curl -X GET "https://api.postproxy.dev/api/profiles/prof_abc123/google_business/location?location_id=accounts/113344/locations/558899" \
-H "Authorization: Bearer YOUR_API_KEY"
{
"name": "locations/558899",
"title": "Acme Coffee",
"websiteUri": "https://acme.example",
"phoneNumbers": { "primaryPhone": "+1 555-0100" },
"categories": { "primaryCategory": { "name": "categories/gcid:coffee_shop", "displayName": "Coffee shop" } },
"storefrontAddress": { "regionCode": "US", "locality": "San Francisco", "addressLines": ["1 Market Street"] },
"regularHours": { "periods": [ { "openDay": "MONDAY", "openTime": { "hours": 9 }, "closeDay": "MONDAY", "closeTime": { "hours": 17 } } ] },
"profile": { "description": "Neighbourhood coffee since 2009." },
"metadata": { "canModifyServiceList": true, "canHaveFoodMenus": false, "placeId": "ChIJ..." }
}

PATCH /api/profiles/:profile_id/google_business/update_location

ParameterTypeRequiredDescription
location_idstringYesPlacement ID
fieldsarrayYesWhich fields to replace
(field values)YesOne key per entry in fields

Patchable fields: title, storeCode, websiteUri, phoneNumbers, categories, storefrontAddress, serviceArea, labels, latlng, openInfo, profile.

Hours and the service list live on the same Google resource but have their own endpoints below.

Terminal window
curl -X PATCH "https://api.postproxy.dev/api/profiles/prof_abc123/google_business/update_location" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"location_id": "accounts/113344/locations/558899",
"fields": ["websiteUri", "profile"],
"websiteUri": "https://acme.example/coffee",
"profile": { "description": "Neighbourhood coffee since 2009. Now with a roastery." }
}'

Naming a field in fields without sending a value returns 400 — that would otherwise silently clear it.

Category values in a categories patch are Google resource names, not display names, and they differ per region. Resolve them here.

GET /api/profiles/:profile_id/google_business/available_location_categories

ParameterTypeRequiredDescription
location_idstringYesPlacement ID
region_codestringYesCLDR region, e.g. US
language_codestringNoBCP-47 code for display names (default en)
filterstringNoGoogle category filter expression
viewstringNoBASIC (default) or FULL
page_sizeintegerNo1–100 (default 100)
page_tokenstringNoFrom the previous response

PATCH /api/profiles/:profile_id/google_business/update_hours

ParameterTypeRequiredDescription
location_idstringYesPlacement ID
fieldsarrayYesAny of regularHours, specialHours, moreHours
regularHoursobjectConditionalWeekly schedule
specialHoursobjectConditionalDate-specific overrides (holidays)
moreHoursarrayConditionalNamed hour sets, e.g. kitchen or pickup hours

At least one block matching fields must be present, or the request is a 400.

Times accept either "09:00" / "09:00:00" or Google’s { "hours": 9, "minutes": 0 } object; both are normalised before the call. Days are MONDAYSUNDAY; dates are { "year", "month", "day" }.

Read current hours through Get location — they are part of the location resource.

Terminal window
curl -X PATCH "https://api.postproxy.dev/api/profiles/prof_abc123/google_business/update_hours" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"location_id": "accounts/113344/locations/558899",
"fields": ["regularHours"],
"regularHours": {
"periods": [
{ "openDay": "MONDAY", "openTime": "09:00", "closeDay": "MONDAY", "closeTime": "17:30" },
{ "openDay": "TUESDAY", "openTime": "09:00", "closeDay": "TUESDAY", "closeTime": "17:30" }
]
}
}'

Structured facts Google shows on the profile — payments accepted, accessibility, amenities, links.

GET /api/profiles/:profile_id/google_business/attributes

ParameterTypeRequiredDescription
location_idstringYesPlacement ID
{
"name": "locations/558899/attributes",
"attributes": [
{ "name": "attributes/has_wheelchair_accessible_entrance", "values": [true] },
{ "name": "attributes/url_menu", "uriValues": [{ "uri": "https://acme.example/menu" }] }
]
}

GET /api/profiles/:profile_id/google_business/available_attributes

ParameterTypeRequiredDescription
location_idstringYesPlacement ID
category_namestringNoLook up by category (categories/gcid:...) instead of by location
region_codestringNoCLDR region, required alongside category_name
language_codestringNoBCP-47 code for display names
show_allbooleanNoReturn metadata for every attribute Google knows
page_sizeintegerNo1–200 (default 200)
page_tokenstringNoFrom the previous response

Each entry carries a valueType telling you which value shape to send back.

PATCH /api/profiles/:profile_id/google_business/update_attributes

ParameterTypeRequiredDescription
location_idstringYesPlacement ID
attributesarrayYesObjects with a name and a type-specific value field
attribute_maskarrayNoWhich attribute names to replace. Defaults to exactly the names in attributes

Value shapes by type:

TypeShape
Boolean"values": [true]
Enum / multi-select"repeatedEnumValue": { "setValues": ["TOKEN"] }
URL"uriValues": [{ "uri": "https://..." }]

attribute_mask defaults to the names you sent, so a partial update never clears attributes you left out. Widen it deliberately to remove one.

Terminal window
curl -X PATCH "https://api.postproxy.dev/api/profiles/prof_abc123/google_business/update_attributes" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"location_id": "accounts/113344/locations/558899",
"attributes": [
{ "name": "attributes/pay_credit_card", "values": [true] },
{ "name": "attributes/url_menu", "uriValues": [{ "uri": "https://acme.example/menu" }] }
]
}'

GET /api/profiles/:profile_id/google_business/service_list

ParameterTypeRequiredDescription
location_idstringYesPlacement ID
{
"name": "locations/558899",
"serviceItems": [
{ "freeFormServiceItem": {
"category": "categories/gcid:coffee_shop",
"label": { "displayName": "Espresso bar", "languageCode": "en" }
} }
]
}

PATCH /api/profiles/:profile_id/google_business/update_service_list

ParameterTypeRequiredDescription
location_idstringYesPlacement ID
serviceItemsarrayYesThe complete list — this replaces what’s there

Requires metadata.canModifyServiceList on the location; otherwise 422. Item shapes are Google’s, and which fields are accepted varies by location, so read the current list and edit those objects rather than building new ones.

GET /api/profiles/:profile_id/google_business/food_menus

ParameterTypeRequiredDescription
location_idstringYesPlacement ID
fieldsarrayNoname, menus

PATCH /api/profiles/:profile_id/google_business/update_food_menus

ParameterTypeRequiredDescription
location_idstringYesPlacement ID
menusarrayYesThe complete menu list — this replaces what’s there

Requires metadata.canHaveFoodMenus; otherwise 422. Structure is menu → sections → items, all labels carry a languageCode, and prices need currencyCode and units.

Terminal window
curl -X PATCH "https://api.postproxy.dev/api/profiles/prof_abc123/google_business/update_food_menus" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"location_id": "accounts/113344/locations/558899",
"menus": [{
"labels": [{ "displayName": "Main menu", "languageCode": "en" }],
"sections": [{
"labels": [{ "displayName": "Drinks", "languageCode": "en" }],
"items": [{
"labels": [{ "displayName": "Filter coffee", "languageCode": "en" }],
"attributes": { "price": { "currencyCode": "USD", "units": "3" } }
}]
}]
}]
}'

The action buttons on the profile — “Order online”, “Book”, “Reserve a table”.

GET /api/profiles/:profile_id/google_business/place_action_links

ParameterTypeRequiredDescription
location_idstringYesPlacement ID
page_sizeintegerNo1–100 (default 100)
page_tokenstringNoFrom the previous response
{
"placeActionLinks": [{
"name": "locations/558899/placeActionLinks/12345",
"uri": "https://acme.example/order",
"placeActionType": "FOOD_ORDERING",
"isPreferred": true,
"isEditable": true,
"providerType": "MERCHANT"
}]
}

POST /api/profiles/:profile_id/google_business/create_place_action_link

ParameterTypeRequiredDescription
location_idstringYesPlacement ID
uristringYesWhere the button goes
place_action_typestringYesSee below
is_preferredbooleanNoMake this the preferred link for its type (one per type per location)

place_action_type: APPOINTMENT, ONLINE_APPOINTMENT, DINING_RESERVATION, FOOD_ORDERING, FOOD_DELIVERY, FOOD_TAKEOUT, SHOP_ONLINE.

PATCH /api/profiles/:profile_id/google_business/update_place_action_link

ParameterTypeRequiredDescription
location_idstringYesPlacement ID
namestringYesFull resource name from the list response
fieldsarrayYesAny of uri, placeActionType, isPreferred
uri / place_action_type / is_preferredConditionalOne per entry in fields

DELETE /api/profiles/:profile_id/google_business/delete_place_action_link

ParameterTypeRequiredDescription
location_idstringYesPlacement ID
namestringYesFull resource name
{ "success": true }

A name belonging to a different location returns 400.

Photos and videos on the Business Profile itself — distinct from media attached to a local post.

GET /api/profiles/:profile_id/google_business/media

ParameterTypeRequiredDescription
location_idstringYesPlacement ID
page_sizeintegerNo1–250 (default 100)
page_tokenstringNoFrom the previous response

POST /api/profiles/:profile_id/google_business/create_media

ParameterTypeRequiredDescription
location_idstringYesPlacement ID
media_urlstringYesPublic https URL — Google fetches the file itself
categorystringNoDefault ADDITIONAL
media_formatstringNoPHOTO (default) or VIDEO
descriptionstringNoMax 2000 characters

category: CATEGORY_UNSPECIFIED, COVER, PROFILE, LOGO, EXTERIOR, INTERIOR, PRODUCT, AT_WORK, FOOD_AND_DRINK, MENU, COMMON_AREA, ROOMS, TEAMS, ADDITIONAL.

Google downloads the file from media_url, so it has to be publicly reachable — a signed URL that expires quickly, or a localhost address, will fail.

Terminal window
curl -X POST "https://api.postproxy.dev/api/profiles/prof_abc123/google_business/create_media" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"location_id": "accounts/113344/locations/558899",
"media_url": "https://cdn.example.com/storefront.jpg",
"category": "COVER",
"description": "Our storefront on Market Street"
}'

DELETE /api/profiles/:profile_id/google_business/delete_media

ParameterTypeRequiredDescription
location_idstringYesPlacement ID
media_namestringYesFull resource name from the list response
StatusMeaning
400Missing location_id, an unsupported fields entry, a fields entry with no value, a bad enum, or a name on another location
403Google refused the operation for this account
404The location_id isn’t reachable from this profile
422Not a google_business profile, the connection needs reconnecting, Google rejected the payload, or the location isn’t eligible (canModifyServiceList / canHaveFoodMenus)
429Google is rate-limiting; retry after the Retry-After header

Not supported.

Location performance comes through the standard profile stats endpoint, one series per location — pass the location_id as placement_id. Snapshots are trailing 30-day totals: Search and Maps impressions (summed into views), website clicks, call clicks, direction requests, conversations, bookings, and food orders. See Stats fields by network.

Google Business has no follower count, and Google exposes no per-post stats for local posts — customer engagement surfaces as reviews instead (Reviews).

Subscribe with the Webhooks API:

Terminal window
curl -X POST "https://api.postproxy.dev/api/webhooks" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/postproxy",
"events": ["platform_post.published", "profile_comment.created"]
}'

Events relevant to Google Business:

EventWhen
post.processedA post is ready to publish
platform_post.publishedA post was published to the platform
platform_post.failedA post failed to publish (retries exhausted)
platform_post.failed_waiting_for_retryA publish attempt failed; will retry
profile_comment.createdA new review was synced, or a reply was published. Filter on data.object.placement_id per location
profile.connected / .disconnectedConnection state changed
profile.statsNew profile stats snapshot
media.failedA media attachment failed to process
  • location_id is required on every post; it is the full Business Profile resource path.
  • Video is not supported — local posts and the gallery accept images only.
  • Some verticals (e.g. lodging) have local posts disabled by Google and will return a validation error.