Connect Google Business Profile and import your locations

Mint a Google connect link, match the owner's Google listings to your locations, confirm the matches, and import listings that have no location yet.

Google Business Profile is the publisher most of the API depends on. Until a Google account is connected, the Google Maps row in a location's premium listings stays REQUIRING_ACTION, Google reviews are not pulled in, posts cannot publish to GOOGLE, Google analytics come back null, and the Apple Business Connect submission cannot start. Connect it first and the rest follows.

The bulk flow (steps 1 to 6) connects a whole Google account once and matches every listing it owns to your locations. The single-location flow (step 7) connects one location from a button on its own page.

The link is scoped to the account that owns the API key, so the body carries only your two redirect URLs. Send the owner to the returned url; Google returns them to successUrl or errorUrl when they finish.

Connect link
curl -X POST https://listingsapi.com/api/v4/connected-accounts/connect-google \-H "Authorization: API $LISTINGSAPI_KEY" \-H "Content-Type: application/json" \-d '{  "input": {    "successUrl": "https://app.example.com/connect/google/success",    "errorUrl": "https://app.example.com/connect/google/error"  }}'
JSON
{  "data": {    "bulkConnectLinkForGoogle": {      "success": true,      "url": "https://claim.verifymybiz.com/locations/redirect_oauth_bulk/google_oauth2?onboarding=false&platform=local&token=9009dc92-...",      "errors": null    }  }}

The link is valid for 24 hours. Check that url is non-null rather than trusting success alone. If the owner's credentials later expire, mint a new link the same way: the existing connection is refreshed, not duplicated.

2. List connected accounts after OAuth returns

When the owner lands on your successUrl, list the Google accounts. This is an offset page: records[] plus pageInfo.totalRecords, paged with page and perPage.

List accounts
curl "https://listingsapi.com/api/v4/connected-accounts?publisher=GoogleAccount&page=1&perPage=20" \-H "Authorization: API $LISTINGSAPI_KEY"
JSON
{  "data": {    "connectedAccountsInfo": {      "pageInfo": { "hasNextPage": false, "totalRecords": 1 },      "records": [        {          "connectedAccountId": "4f712c17-4f95-42dd-90f4-97171a2e67b5",          "email": "owner@brightsmile-dental.com",          "status": "CONNECTED",          "requestMatchesStatus": "MATCH_COMPLETED",          "connectedLocationsCount": 0        }      ]    }  }}

Pick the record whose email is the owner who just consented and keep its connectedAccountId. Every later call takes this UUID.

statusMeaning
CONNECTEDCredentials are valid. Safe to trigger matching.
MATCH_IN_PROGRESSA match run is queued or running.
CONNECTIVITY_ISSUEThe grant expired. connectivityIssue says why; re-issue the connect link.

The optional status query filter uses different words (Connected, NotConnected, ConnectionIssue); feeding a record's own status back in returns a 400.

3. Trigger matching

Matching pulls every profile the account can reach and pairs each with one of your locations. It only produces suggestions; nothing is linked yet.

Trigger matches
curl -X POST https://listingsapi.com/api/v4/connected-accounts/trigger-matches \-H "Authorization: API $LISTINGSAPI_KEY" \-H "Content-Type: application/json" \-d '{ "input": { "connectedAccountIds": ["4f712c17-4f95-42dd-90f4-97171a2e67b5"] } }'

The response is data.connectedAccountsTriggerMatches with success and failedIds. success: true means the job was queued; any account that could not be queued is listed in failedIds.

4. Poll until MATCH_COMPLETED

Poll the account's details every 15 seconds with a bounded loop. The interval also keeps a Launch plan inside its 10 requests a minute.

Poll details
curl https://listingsapi.com/api/v4/connected-accounts/4f712c17-4f95-42dd-90f4-97171a2e67b5/details \-H "Authorization: API $LISTINGSAPI_KEY"

The record sits at data.connectedAccountDetails.details and has the same fields as a list record. requestMatchesStatus is MATCH_IN_PROGRESS while the job runs and MATCH_COMPLETED when suggestions are ready. Completed means the matcher finished, not that anything is linked: connectedLocationsCount stays 0 until you confirm.

5. Review and confirm suggestions

Each suggestion pairs one of your locations (locationInfo, with locationId as its databaseId) with one Google listing (suggestedLocationInfo). Show both sides to an operator, or auto-accept when name and city agree.

Suggestions
curl "https://listingsapi.com/api/v4/connected-accounts/4f712c17-4f95-42dd-90f4-97171a2e67b5/connection-suggestions?page=1&perPage=20" \-H "Authorization: API $LISTINGSAPI_KEY"
JSON
{  "data": {    "connectionSuggestionsForAccount": {      "pageInfo": { "hasNextPage": false, "totalRecords": 2 },      "records": [        {          "accountType": "GMB",          "matchedDataDatabaseId": "0d5e306f-0da0-46e3-929f-fd3c67ccd8cc",          "locationId": "73415",          "locationInfo": { "name": "My Business", "city": "Lincoln", "stateIso": "NE" },          "suggestedLocationInfo": { "locationInfo": { "name": "My Business", "city": "Lincoln" } }        }      ]    }  }}

A suggestion has no id. To confirm it you build a match-record id: the base64 of <MatchRecordType>:<matchedDataDatabaseId>, with the type chosen from accountType. For Google (GMB) the type is GmbLocationMatchedData, so the record above encodes as R21iTG9jYXRpb25NYXRjaGVkRGF0YTowZDVlMzA2Zi0wZGEwLTQ2ZTMtOTI5Zi1mZDNjNjdjY2Q4Y2M=. Facebook (FB) uses FbLocationMatchedData and Apple (APPLE) uses AppleLocationMatchedData.

Confirm matches
# base64 of GmbLocationMatchedData:0d5e306f-0da0-46e3-929f-fd3c67ccd8cccurl -X POST https://listingsapi.com/api/v4/connected-accounts/confirm-matches \-H "Authorization: API $LISTINGSAPI_KEY" \-H "Content-Type: application/json" \-d '{ "input": { "matchRecordIds": ["R21iTG9jYXRpb25NYXRjaGVkRGF0YTowZDVlMzA2Zi0wZGEwLTQ2ZTMtOTI5Zi1mZDNjNjdjY2Q4Y2M="] } }'

The response is data.confirmConnectMatches with success and failedIds. failedIds holds the decoded matchedDataDatabaseId values that could not be confirmed, such as a record already consumed or a location archived since matching. An id built with the wrong type does not return a 400: the call answers 200 with success: false and no failedIds, so check success every time. Unsent suggestions stay untouched.

6. Import listings that have no location yet

Matching only pairs listings with locations you already have. To bring in the rest, list everything the account can see, then create a location from each listing you want.

Account listings
curl -X POST https://listingsapi.com/api/v4/connected-accounts/connected-account-listings \-H "Authorization: API $LISTINGSAPI_KEY" \-H "Content-Type: application/json" \-d '{ "connectedAccountId": "4f712c17-4f95-42dd-90f4-97171a2e67b5", "perPage": 50 }'
JSON
{  "data": {    "connectedAccountListings": {      "pageInfo": { "hasNextPage": false, "totalRecords": 60 },      "records": [        {          "id": "R21iQnVsa0RhdGFMYWtlOjA2MDQ4NmExLTA0NzQtNDc4NS1iMjNhLTBkOGM4MzhmNGQ5OA==",          "locationName": "Trial",          "address": "123 William St, NY 10038, Manhattan, US",          "liveLink": null        }      ]    }  }}

Pass locationInfo with a search term to narrow a large account by name, address, phone, postal code, or store code. The id on each record is the connectedAccountListingId the next two calls take. Neither SDK wraps create-location-from-listing yet, so the Python and Node samples call REST directly with the same header.

Create from listing
curl -X POST https://listingsapi.com/api/v4/connected-accounts/create-location-from-listing \-H "Authorization: API $LISTINGSAPI_KEY" \-H "Content-Type: application/json" \-d '{  "input": {    "connectedAccountId": "4f712c17-4f95-42dd-90f4-97171a2e67b5",    "connectedAccountListingId": "R21iQnVsa0RhdGFMYWtlOjA2MDQ4NmExLTA0NzQtNDc4NS1iMjNhLTBkOGM4MzhmNGQ5OA=="  }}'
JSON
{  "data": {    "createLocationFromConnectedAccountListing": { "success": true, "locationId": 88277, "errors": null }  }}

The location is created from the Google data (name, address, hours, categories, phone, photos, attributes) and linked back to the listing at once, so it starts in sync. locationId is the plain databaseId. On failure success is false and errors carries a code such as SY81067 (the listing id did not resolve). The field is connectedAccountListingId; listingId is rejected by the schema.

If the listing belongs to a location you already have, link it instead of creating a duplicate.

Link a listing
curl -X POST https://listingsapi.com/api/v4/connected-accounts/connect-listing \-H "Authorization: API $LISTINGSAPI_KEY" \-H "Content-Type: application/json" \-d '{  "input": {    "locationId": "TG9jYXRpb246MTgwMDI4OQ==",    "connectedAccountId": "4f712c17-4f95-42dd-90f4-97171a2e67b5",    "connectedAccountListingId": "R21iQnVsa0RhdGFMYWtlOjA2MDQ4NmExLTA0NzQtNDc4NS1iMjNhLTBkOGM4MzhmNGQ5OA=="  }}'

A failed link returns success: false with a message, for example Listing record not found, or a note that the listing is already connected to another location.

7. Connect a single location instead

For a "Connect Google" button on one location's page, skip matching and mint a link tied to that location. The owner lands back on the page you name and the credentials attach to that location only.

Single-location link
curl -X POST https://listingsapi.com/api/v4/locations/oauth_connect_url \-H "Authorization: API $LISTINGSAPI_KEY" \-H "Content-Type: application/json" \-d '{  "input": {    "locationId": "TG9jYXRpb246MTgwMDI4OQ==",    "site": "GOOGLE",    "successUrl": "https://app.example.com/locations/1800289/connected",    "errorUrl": "https://app.example.com/locations/1800289/connect-failed"  }}'

The response is data.createConnectUrl with success, url, and errors. The link is single-use, so mint a fresh one per attempt. A relative or unparseable redirect URL comes back as success: true with a null url, so check url. A key that may not touch this location gets a 403 with SY90003.

8. What happens next

Once a listing is linked, the Google Maps row in the location's premium listings leaves REQUIRING_ACTION, moves through IN_PROGRESS, and settles at SYNCED with a listingUrl. Google is added as a review source on its own, so reviews start arriving in the interactions list, and updates you make to the location flow to the profile.

With a webhook URL set in the dashboard you do not need to poll. Each payload carries location_id and data.connected_account_id.

EventFires whenWhat to do
connection.location_connectedA listing is linked to a location; data.platform is google.Mark the location connected in your UI.
connection.reauth_requiredThe account's credentials were invalidated. Nothing syncs until re-authorized.Mint a new link (step 1) and ask the owner to sign in again.
connection.listing_inaccessibleCredentials are fine but access to that one listing was removed.Ask the owner to restore access on Google, or link a different listing.

Next steps