# Services and food menus

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`](https://pozy.ai/docs/api/getGoogleBusinessServices) reads the service list. [`PUT /v1/accounts/{accountId}/gmb-services`](https://pozy.ai/docs/api/updateGoogleBusinessServices) replaces it.

> **PUT replaces the whole list:**
> 
> 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).

```bash cURL
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 }
      }
    ]
  }'
```
```ts Node
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);
```
```python Python
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](https://pozy.ai/docs/google-business/locations).

## Food menus

Restaurants, cafes and other places Google gives menu support to can publish their menus. [`GET /v1/accounts/{accountId}/gmb-food-menus`](https://pozy.ai/docs/api/getGoogleBusinessFoodMenus) reads them and [`PUT`](https://pozy.ai/docs/api/updateGoogleBusinessFoodMenus) 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 |

```bash cURL
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`](https://pozy.ai/docs/api/getGoogleBusinessFoodMenus) | Get food menus |
| [`PUT /v1/accounts/{accountId}/gmb-food-menus`](https://pozy.ai/docs/api/updateGoogleBusinessFoodMenus) | Update food menus |
| [`GET /v1/accounts/{accountId}/gmb-services`](https://pozy.ai/docs/api/getGoogleBusinessServices) | Get services |
| [`PUT /v1/accounts/{accountId}/gmb-services`](https://pozy.ai/docs/api/updateGoogleBusinessServices) | Replace services |
