# Create a listing

Create a Google Business Profile listing through the API for a business that has none, then verify it so it shows on Google Maps and Search.

By the end of this page, you have:

- connected a Google account that has no Google Business Profile (`googlebusiness`) listing yet
- checked that the business is not already on Google
- created the listing with `POST /v1/accounts/{accountId}/gmb-locations`
- started its verification, which is what makes it appear on Google Maps and Search

<Callout type="warn">
Google's Business Profile API policies require the business's prior, specific and express consent before a listing is created on its behalf. You collect that consent before Step 4.
</Callout>

You need an API key and a profile (`profileId`). Placeholders in the examples (`$ZERNIO_API_KEY`, `$ACCOUNT_ID`) stand for your own values.

## Step 1: connect the Google account

Start the connect with `allowNoLocation=true` ([Get OAuth connect URL](/connect/get-connect-url)). Without it, a Google account with no listing ends with `error=no_google_locations`.

```bash
curl "https://zernio.com/api/v1/connect/googlebusiness?profileId=$PROFILE_ID&headless=true&allowNoLocation=true&redirect_url=https://myapp.com/callback" \
  -H "Authorization: Bearer $ZERNIO_API_KEY"
```

Send the user's browser to the returned `authUrl`. In the headless flow your `redirect_url` receives `step=select_location` and a `pendingDataToken`, and the list of locations is empty. Pass `redirect_url` back unchanged: the flag travels inside it.

Confirm with no location ([Select location](/connect/select-google-business-location)):

```bash
curl -X POST https://zernio.com/api/v1/connect/googlebusiness/select-location \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "profileId": "$PROFILE_ID",
    "pendingDataToken": "$PENDING_DATA_TOKEN"
  }'
```

Response (`200`), abridged:

```json
{
  "account": {
    "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
    "platform": "googlebusiness",
    "selectedLocationId": null,
    "needsLocation": true
  }
}
```

`needsLocation: true` means the account exists but has no location yet. Use its `accountId` as `$ACCOUNT_ID` below.

## Step 2: check whether the business already exists

Recommended. Call `POST /v1/accounts/{accountId}/gmb-locations/search` with a `query` or a partial `location` ([Search existing Google listings](/connect/search-google-business-locations)). Google returns at most 10 matches.

```bash
curl -X POST https://zernio.com/api/v1/accounts/$ACCOUNT_ID/gmb-locations/search \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "query": "Cafe Central Madrid" }'
```

Response (`200`):

```json
{
  "success": true,
  "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
  "googleLocations": [
    {
      "name": "googleLocations/ChIJ_example",
      "location": { "title": "Cafe Central", "storefrontAddress": { "regionCode": "ES", "locality": "Madrid" } },
      "requestAdminRightsUri": "https://business.google.com/..."
    }
  ]
}
```

A match that already has an owner carries `requestAdminRightsUri`. Send the business owner there to request admin rights instead of creating a duplicate.

## Step 3: pick a category

Call `GET /v1/accounts/{accountId}/gmb-categories` ([List categories](/connect/list-google-business-categories)). `regionCode` and `languageCode` are required. `query` matches the start of the name in that language: `Restaurante` finds `Restaurante afgano`, `afgano` finds nothing.

```bash
curl "https://zernio.com/api/v1/accounts/$ACCOUNT_ID/gmb-categories?regionCode=ES&languageCode=es&query=Restaurante" \
  -H "Authorization: Bearer $ZERNIO_API_KEY"
```

Response (`200`):

```json
{
  "success": true,
  "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
  "categories": [
    { "name": "categories/gcid:afghani_restaurant", "displayName": "Restaurante afgano" }
  ],
  "nextPageToken": "CgQIARAB"
}
```

Keep the `name` as `primaryCategory`. Pass `nextPageToken` as `pageToken` for the next 100.

## Step 4: create the listing

Call `POST /v1/accounts/{accountId}/gmb-locations` ([Create location](/connect/create-google-business-location)).

| Field | Rule |
|---|---|
| `title`, `languageCode`, `primaryCategory` | Required. `languageCode` cannot change after creation. |
| `storefrontAddress` or `serviceArea` | One of them is required. |
| `phone` or `websiteUri` | One is required when there is no `storefrontAddress`. |
| `validateOnly` | `true` checks the request with Google and creates nothing. |

Send `validateOnly: true` first, and an `Idempotency-Key` header so a retry never creates two listings.

```bash
curl -X POST https://zernio.com/api/v1/accounts/$ACCOUNT_ID/gmb-locations \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Fontaneria Rapida Barcelona",
    "languageCode": "es",
    "primaryCategory": "categories/gcid:plumber",
    "serviceArea": {
      "businessType": "CUSTOMER_LOCATION_ONLY",
      "regionCode": "ES",
      "places": { "placeInfos": [{ "placeName": "Barcelona", "placeId": "ChIJ5TCOcRaYpBIRCmZHTz37sEQ" }] }
    },
    "websiteUri": "https://fontaneria-rapida.example",
    "validateOnly": true
  }'
```

Run it again without `validateOnly` to create. Response (`200`):

```json
{
  "success": true,
  "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
  "location": {
    "id": "1234567890123456789",
    "name": "locations/1234567890123456789",
    "title": "Fontaneria Rapida Barcelona"
  },
  "voiceOfMerchantState": { "hasVoiceOfMerchant": false }
}
```

The location is attached to the account right away. `hasVoiceOfMerchant` stays `false` until the listing is verified, and `voiceOfMerchantState` is `null` if Google could not return it (the listing still exists).

## Step 5: verify the listing

The listing does not appear on Google Maps or Search until it is verified.

Google sends the PIN or postcard to the business owner, and only the owner can start verification. The [flow](/platforms/google-business/business-profile#verification) is three calls: options, start, complete.

For a service-area-only business, the options request must include `context.address`, the business's postal address. Without it Google returns `400` "Service business context is required".

```bash
curl -X POST https://zernio.com/api/v1/accounts/$ACCOUNT_ID/gmb-verifications/options \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "languageCode": "es",
    "context": {
      "address": {
        "regionCode": "ES",
        "postalCode": "28001",
        "locality": "Madrid",
        "addressLines": ["Calle Ejemplo 1"]
      }
    }
  }'
```

Response (`200`), abridged:

```json
{
  "success": true,
  "options": [
    { "verificationMethod": "ADDRESS" },
    { "verificationMethod": "PHONE_CALL", "phoneNumber": "+34..." }
  ]
}
```

Start the method with `POST /gmb-verifications` and submit the code with `POST /gmb-verifications/{verificationId}/complete`. Both are described in [Business Profile management](/platforms/google-business/business-profile#verification).

Once `hasVoiceOfMerchant` is `true`, everything else works on the listing: [details, hours, attributes and photos](/platforms/google-business/business-profile), [services and menus](/platforms/google-business/services-menus), [posts](/platforms/google-business/posts) and [reviews](/platforms/google-business/inbox).

## If it fails

| Response | Cause | Fix |
|---|---|---|
| `400` naming a field on create | A required field or an address rule is not met. | Add the field the message names. Without `storefrontAddress`, send `serviceArea` plus `phone` or `websiteUri`. |
| `400` "Service business context is required" | Verification options for a service-area business had no `context.address`. | Add the postal address. |
| `409` with `profile_platform_conflict` | The account already has a location. | Nothing was created. Use the existing location. |
| `409` with the same `Idempotency-Key` | The first request is still processing. | Retry after a short backoff. |
| `422` | The `Idempotency-Key` was reused with a different body. | Use a new key. |
| An error naming `locations/<id>` | Google created the listing but it could not be attached. | Attach it with `PUT /v1/accounts/{accountId}/gmb-locations`. |

[Error handling](/guides/error-handling) covers the envelope.

## Related

- [Business Profile management](/platforms/google-business/business-profile): verification, hours, photos and attributes.
- [Multi-location posting](/platforms/google-business/multi-location): locations an account manages.
- [Connecting accounts](/guides/connecting-accounts): hosted and headless modes.