Google Business
Keep the services a Google Business Profile location offers, and the menus of a restaurant or cafe, in sync with your own system.
Google shows a list of services on many listings (a plumber's pipe repair, a salon's colour treatment) and full menus on restaurants and cafes. If your product already knows what a business sells, push it to Google with these endpoints instead of asking the owner to type it twice. Both work on verified locations only.
#Services
GET /v1/accounts/{accountId}/gmb-services reads the service list. PUT /v1/accounts/{accountId}/gmb-services replaces it.
Google has no way to update one service. Every PUT must contain all the services you want to keep. Read the list, change it, send it back.
Each service is one of two kinds, with an optional price:
| Kind | Fields | When to use it |
|---|---|---|
structuredServiceItem |
serviceTypeId from Google's catalog for the category, optional description |
When Google already knows the service |
freeFormServiceItem |
category (the location's category, such as categories/gcid:hair_salon) and a label with displayName, optional description and languageCode |
Anything Google's catalog does not list |
price uses Google's money format: currencyCode, whole units as a string, and optional nanos for the fraction (500000000 is 0.50).
curl -X PUT https://api.pozy.ai/v1/accounts/6719c8e1a7f2b05d3e9c4f60/gmb-services \
-H "Authorization: Bearer $POZY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"serviceItems": [
{
"freeFormServiceItem": {
"category": "categories/gcid:hair_salon",
"label": { "displayName": "Cut and blow-dry", "description": "45 minutes, includes wash" }
},
"price": { "currencyCode": "EUR", "units": "35" }
},
{
"freeFormServiceItem": {
"category": "categories/gcid:hair_salon",
"label": { "displayName": "Full colour", "description": "Up to 2 hours" }
},
"price": { "currencyCode": "EUR", "units": "79", "nanos": 500000000 }
}
]
}'const accountId = "6719c8e1a7f2b05d3e9c4f60";
// Read, modify, write back the full list
const currentRes = await fetch(`https://api.pozy.ai/v1/accounts/${accountId}/gmb-services`, {
headers: { Authorization: `Bearer ${process.env.POZY_API_KEY}` },
});
const current = await currentRes.json();
if (!currentRes.ok) throw new Error(current.error);
const serviceItems = [
...current.services,
{
freeFormServiceItem: {
category: "categories/gcid:hair_salon",
label: { displayName: "Fringe trim", description: "10 minutes, walk-ins welcome" },
},
price: { currencyCode: "EUR", units: "8" },
},
];
const res = await fetch(`https://api.pozy.ai/v1/accounts/${accountId}/gmb-services`, {
method: "PUT",
headers: {
Authorization: `Bearer ${process.env.POZY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ serviceItems }),
});
if (!res.ok) throw new Error((await res.json()).error);import os
import requests
H = {"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"}
url = "https://api.pozy.ai/v1/accounts/6719c8e1a7f2b05d3e9c4f60/gmb-services"
services = requests.get(url, headers=H, timeout=30).json()["services"]
services.append({
"freeFormServiceItem": {
"category": "categories/gcid:hair_salon",
"label": {"displayName": "Fringe trim", "description": "10 minutes, walk-ins welcome"},
},
"price": {"currencyCode": "EUR", "units": "8"},
})
requests.put(url, headers=H, json={"serviceItems": services}, timeout=30).raise_for_status()The response returns the list as Google stored it. Keep that copy; it is the one to send back next time. The location's categories, which a free-form service must use, come from categories in location details.
#Food menus
Restaurants, cafes and other places Google gives menu support to can publish their menus. GET /v1/accounts/{accountId}/gmb-food-menus reads them and PUT updates them. Send the full menus array with updateMask: "menus".
A menu is a tree: menus contain sections, sections contain items, and items can have options.
| Level | Fields |
|---|---|
| Menu | labels (name per language), sections, optional cuisines and sourceUrl |
| Section | labels, items |
| Item | labels (name and description), attributes, optional options |
| Option | Its own labels and attributes, for variants such as sizes |
Item attributes:
| Attribute | Values |
|---|---|
price |
currencyCode, units, optional nanos |
dietaryRestriction |
For example VEGETARIAN, VEGAN, GLUTEN_FREE |
allergen |
For example DAIRY, GLUTEN, SHELLFISH |
spiciness |
MILD, MEDIUM or HOT |
servesNumPeople |
A whole number |
preparationMethods |
Google's preparation method values |
mediaKeys |
Photos of the item already on the listing |
curl -X PUT https://api.pozy.ai/v1/accounts/6713b0e5d1f4c69a2b8e3f75/gmb-food-menus \
-H "Authorization: Bearer $POZY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"updateMask": "menus",
"menus": [{
"labels": [{ "displayName": "Breakfast", "languageCode": "en" }],
"sections": [{
"labels": [{ "displayName": "Pastries" }],
"items": [{
"labels": [{ "displayName": "Pastel de nata", "description": "Custard tart, baked hourly" }],
"attributes": { "price": { "currencyCode": "EUR", "units": "1", "nanos": 400000000 }, "dietaryRestriction": ["VEGETARIAN"] }
}, {
"labels": [{ "displayName": "Croissant" }],
"attributes": { "price": { "currencyCode": "EUR", "units": "2" } },
"options": [
{ "labels": [{ "displayName": "Plain" }], "attributes": { "price": { "currencyCode": "EUR", "units": "2" } } },
{ "labels": [{ "displayName": "Ham and cheese" }], "attributes": { "price": { "currencyCode": "EUR", "units": "3", "nanos": 500000000 } } }
]
}]
}]
}]
}'Menus only exist on locations Google gives menu support to. On other locations the call returns 400.
#Common errors
| Error | Cause | Fix |
|---|---|---|
400 gbp_bad_request on services |
Google rejected an item, often a category the location does not have or an unknown serviceTypeId |
Use the location's own categories, or a free-form item |
400 on menus |
The location has no menu support, or the account has no location selected | Check the location's category and the selected location |
403 |
The Google login cannot manage this location | Reconnect with an owner or manager |
401 token_invalid |
Google revoked the sign-in | Reconnect the account |
#Endpoints
| Endpoint | What it does |
|---|---|
GET /v1/accounts/{accountId}/gmb-food-menus |
Get food menus |
PUT /v1/accounts/{accountId}/gmb-food-menus |
Update food menus |
GET /v1/accounts/{accountId}/gmb-services |
Get services |
PUT /v1/accounts/{accountId}/gmb-services |
Replace services |