Business Profile management
Read and update a Google Business Profile listing through the API, with verification, hours, photos, attributes and action links.
By the end of this page, you can read and update a Google Business Profile (googlebusiness) listing: verification, hours and description, photos, attributes such as delivery and Wi-Fi, and booking links.
- Verification: Voice of Merchant state, and how to run a verification.
- Location details: hours, description, website, phone numbers, categories.
- Photos: list, add and delete listing media.
- Attributes: the per-category flags such as delivery and Wi-Fi.
- Action links: booking, ordering and appointment URLs.
The service list and food menus have their own page, Services & Food Menus.
Before you start
You need a connected Google Business Profile account (accountId).
Every endpoint lives under /v1/accounts/{accountId}/gmb-* and targets the account's selected location. Pass locationId as a query parameter to target another one the account manages.
Verification comes first, because the rest depends on it. Reviews, edits and other listing data surface only once the location has Voice of Merchant.
Verification
GET /v1/accounts/{accountId}/gmb-verifications returns the location's Voice of Merchant state and its verification history (Get verification state).
voiceOfMerchantState.hasVoiceOfMerchant is true when the listing is verified and live on Google. When it is false, verify.hasPendingVerification tells a verification in progress from one never started.
To verify a location:
- Fetch the methods Google offers with
POST /gmb-verifications/options. - Start one with
POST /gmb-verifications. - Submit the code with
POST /gmb-verifications/{verificationId}/complete.
verificationId is the last segment of a verification's name.
POST /gmb-verifications is a real-world action: Google mails a postcard, places a call or sends an SMS or email to the business.
A service-area business must include context with its service address on the options call, otherwise Google returns 400.
import Zernio from '@zernio/node';
const zernio = new Zernio();
const { data: state } = await zernio.gmbverifications.getGoogleBusinessVerifications({
path: { accountId: '66b2e19d8c3f5a7e9d0b1c2d' }
});
const { data: options } = await zernio.gmbverifications.fetchGoogleBusinessVerificationOptions({
path: { accountId: '66b2e19d8c3f5a7e9d0b1c2d' },
body: { languageCode: 'en-US' }
});
const { data: started } = await zernio.gmbverifications.startGoogleBusinessVerification({
path: { accountId: '66b2e19d8c3f5a7e9d0b1c2d' },
body: { method: 'SMS', languageCode: 'en-US', phoneNumber: '+14155550123' }
});
const verificationId = started.verification.name.split('/').pop();
await zernio.gmbverifications.completeGoogleBusinessVerification({
path: { accountId: '66b2e19d8c3f5a7e9d0b1c2d', verificationId },
body: { pin: '123456' }
});Response (200) of the state call, for a location that failed an SMS verification and has none pending:
{
"success": true,
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"locationId": "12345678901234567890",
"voiceOfMerchantState": {
"hasVoiceOfMerchant": false,
"hasBusinessAuthority": true,
"verify": { "hasPendingVerification": false }
},
"verifications": [
{
"name": "locations/12345678901234567890/verifications/4T1775504407480",
"method": "SMS",
"state": "FAILED",
"createTime": "2026-04-06T19:40:07.480Z"
}
]
}Response (200) of the options call. verificationMethod is ADDRESS, EMAIL, PHONE_CALL, SMS, AUTO or VETTED_PARTNER. Calls and SMS carry the phoneNumber Google will use:
{
"success": true,
"options": [
{ "verificationMethod": "SMS", "phoneNumber": "+14155550123" },
{ "verificationMethod": "ADDRESS" }
]
}Response (200) of the start call. The last segment of verification.name is the verificationId the complete call needs:
{
"success": true,
"verification": {
"name": "locations/12345678901234567890/verifications/4T1775504407481",
"method": "SMS",
"state": "PENDING",
"createTime": "2026-04-07T10:12:00Z"
}
}Response (200) of the complete call: the same verification, with state COMPLETED when Google accepts the pin and FAILED when it does not.
Reviews, edits and other listing data only surface once a location has Voice of Merchant. It must also be matched to a published Maps place (it has a placeId, see Location details).
When GET /gmb-reviews returns an empty array for a listing that shows reviews on Google, check hasVoiceOfMerchant here first.
Location details
GET /v1/accounts/{accountId}/gmb-location-details returns the listing (Get location details).
readMask picks the fields: name, title, regularHours, specialHours, profile, websiteUri, phoneNumbers, categories, serviceArea, serviceItems, storefrontAddress, openInfo, metadata, moreHours.
The location block is always present. It carries:
placeId- the public
reviewUrl(the "write a review" link, handy behind a QR code) mapsUriisVerified
PUT on the same path updates any field named in updateMask (Update location details).
const { data: details } = await zernio.gmblocationdetails.getGoogleBusinessLocationDetails({
path: { accountId: '66b2e19d8c3f5a7e9d0b1c2d' },
query: { readMask: 'regularHours,specialHours,profile,websiteUri' }
});
console.log(details.location.reviewUrl, details.regularHours);
await zernio.gmblocationdetails.updateGoogleBusinessLocationDetails({
path: { accountId: '66b2e19d8c3f5a7e9d0b1c2d' },
body: {
updateMask: 'regularHours',
regularHours: {
periods: [
{ openDay: 'MONDAY', openTime: '09:00', closeDay: 'MONDAY', closeTime: '17:00' },
{ openDay: 'TUESDAY', openTime: '09:00', closeDay: 'TUESDAY', closeTime: '17:00' }
]
}
}
});Response (200) of the read:
{
"success": true,
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"locationId": "12345678901234567890",
"location": {
"name": "Joe's Pizza",
"placeId": "ChIJExampleJoesPizzaPlaceId",
"reviewUrl": "https://search.google.com/local/writereview?placeid=ChIJExampleJoesPizzaPlaceId",
"mapsUri": "https://maps.google.com/maps?cid=1234567890123456789",
"isVerified": true
},
"title": "Joe's Pizza",
"regularHours": {
"periods": [
{ "openDay": "MONDAY", "openTime": "11:00", "closeDay": "MONDAY", "closeTime": "22:00" }
]
},
"specialHours": {
"specialHourPeriods": [{ "startDate": { "year": 2026, "month": 12, "day": 25 }, "closed": true }]
},
"profile": { "description": "Authentic New York style pizza since 1985" },
"websiteUri": "https://joespizza.com"
}Response (200) of the update:
{
"success": true,
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"locationId": "12345678901234567890"
}Unverified or new location. Google omits placeId, reviewUrl and mapsUri, so they come back null and isVerified is false.
Photos
GET /v1/accounts/{accountId}/gmb-media lists the listing's photos. POST adds one from a public URL (Media). DELETE with mediaId removes one.
category decides where the photo appears: COVER, PROFILE, LOGO, EXTERIOR, INTERIOR, PRODUCT, FOOD_AND_DRINK, MENU, COMMON_AREA, ROOMS, TEAMS, AT_WORK or ADDITIONAL.
const { data: media } = await zernio.gmbmedia.listGoogleBusinessMedia({
path: { accountId: '66b2e19d8c3f5a7e9d0b1c2d' }
});
const { data: photo } = await zernio.gmbmedia.createGoogleBusinessMedia({
path: { accountId: '66b2e19d8c3f5a7e9d0b1c2d' },
body: {
sourceUrl: 'https://cdn.example.com/photos/interior.jpg',
description: 'Dining area with outdoor seating',
category: 'INTERIOR'
}
});
console.log(media.totalMediaItemsCount, photo.googleUrl);Response (200) of the list:
{
"success": true,
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"locationId": "12345678901234567890",
"mediaItems": [
{
"name": "accounts/123456789/locations/12345678901234567890/media/AF1QipN...",
"mediaFormat": "PHOTO",
"googleUrl": "https://lh3.googleusercontent.com/...",
"description": "Storefront at dusk",
"createTime": "2026-11-02T09:14:00Z",
"locationAssociation": { "category": "EXTERIOR" }
}
],
"totalMediaItemsCount": 37
}Response (200) of the upload:
{
"success": true,
"name": "accounts/123456789/locations/12345678901234567890/media/AF1QipM...",
"mediaFormat": "PHOTO",
"googleUrl": "https://lh3.googleusercontent.com/..."
}Attributes
GET /v1/accounts/{accountId}/gmb-attributes returns the amenities, services and payment types set on the listing. PUT updates the ones named in attributeMask (Attributes).
Which attributes exist depends on the business category. Get attribute metadata lists the valid names, value types and enum values for a location or a category.
Common ones are has_dine_in, has_takeout, has_delivery, has_wifi, has_outdoor_seating and pay_credit_card_types_accepted.
const { data: attributes } = await zernio.gmbattributes.getGoogleBusinessAttributes({
path: { accountId: '66b2e19d8c3f5a7e9d0b1c2d' }
});
await zernio.gmbattributes.updateGoogleBusinessAttributes({
path: { accountId: '66b2e19d8c3f5a7e9d0b1c2d' },
body: {
attributes: [
{ name: 'has_delivery', values: [true] },
{ name: 'has_outdoor_seating', values: [true] }
],
attributeMask: 'has_delivery,has_outdoor_seating'
}
});
console.log(attributes.attributes);Response (200) of the read:
{
"success": true,
"attributes": [
{ "name": "has_delivery", "valueType": "BOOL", "values": [true] },
{ "name": "has_takeout", "valueType": "BOOL", "values": [true] },
{ "name": "has_outdoor_seating", "valueType": "BOOL", "values": [true] },
{
"name": "pay_credit_card_types_accepted",
"valueType": "REPEATED_ENUM",
"repeatedEnumValue": { "setValues": ["visa", "mastercard", "amex"] }
}
]
}Response (200) of the update: success, accountId, locationId and the attributes array as Google stored it.
Action links
Action links are the booking, ordering and reservation buttons on the listing (Action links).
GET /v1/accounts/{accountId}/gmb-place-actionslists them.POSTcreates one with auriand aplaceActionType.PATCHchanges theurior type of an existing link by itsname.DELETEwithnameremoves it.
placeActionType is APPOINTMENT, ONLINE_APPOINTMENT, DINING_RESERVATION, FOOD_ORDERING, FOOD_DELIVERY, FOOD_TAKEOUT or SHOP_ONLINE.
const { data: link } = await zernio.gmbplaceactions.createGoogleBusinessPlaceAction({
path: { accountId: '66b2e19d8c3f5a7e9d0b1c2d' },
body: { uri: 'https://order.ubereats.com/joespizza', placeActionType: 'FOOD_ORDERING' }
});
await zernio.gmbplaceactions.updateGoogleBusinessPlaceAction({
path: { accountId: '66b2e19d8c3f5a7e9d0b1c2d' },
body: { name: link.name, uri: 'https://order.doordash.com/joespizza' }
});Response (200) of the create:
{
"success": true,
"name": "locations/12345678901234567890/placeActionLinks/456",
"uri": "https://order.ubereats.com/joespizza",
"placeActionType": "FOOD_ORDERING"
}Response (200) of the update: the same 4 fields, with the uri you sent.
If it fails
Every endpoint here proxies Google, so a 400 carries Google's own complaint.
On GET /gmb-location-details it means readMask names a field Google does not know, most often location, which is a response-only block:
{
"error": "Request contains an invalid argument.",
"code": "gbp_bad_request"
}Remove the name from readMask and retry. The PUT on the same path answers 400 when updateMask is missing.
The other surfaces fail their own way:
- Verification:
POST /gmb-verifications/optionsanswers400withoutlanguageCode, and for a service-area business withoutcontextcarrying its address. On the complete call a400means the PIN is wrong or the verification is no longerPENDING. - Photos:
POST /gmb-mediaanswers400when the media format is not one Google accepts, or the URL is not one it can fetch. - Attributes: the
PUTanswers400when an attribute name or value is not one the location's category offers. - Action links:
PATCH /gmb-place-actionsanswers400with "At least one of uri or placeActionType is required" when the body carries neither.
On any endpoint here:
401with codetoken_invalid: Google revoked the token. Reconnect the account.403: the Google login has no permission on this location.
Error handling covers the envelope.
Related
- Location details, Verifications, Media, Attributes and Action links: the full schemas.
- Services & Food Menus: the service list and the menus of a food business.
- Multi-Location Posting: the
locationIdvalues these endpoints accept. - Inbox: reviews, which need Voice of Merchant.
- Analytics: performance metrics for the listing.