Connected accounts
Every Connected accounts endpoint with a cURL, Python and Node sample: connect Google and Facebook, list accounts, match and link listings, folders, GMB creation, disconnects.
A connected account is an OAuth grant from a Google Business Profile or
Facebook Pages owner that lets the API read and manage the listings that
account can reach. The resource covers three jobs: minting the OAuth links,
matching or linking the account's listings to your locations, and tearing the
connection down again. The two GET list and detail reads work with a
Read key; every POST, including the read-only listings fetch, needs a
Write key, and a Read key on a write returns HTTP 400 with SY90016.
Every sample below assumes a client built once, the way the first sample
shows, and reads LISTINGSAPI_KEY from the environment. Ids are the
connectedAccountId UUID from the list endpoint and the base64 location id
(TG9jYXRpb246MTgwMDI4OQ==); never URL-encode either.
Endpoints at a glance
Mint a Google connect link
POST /connected-accounts/connect-google
Returns a short-lived OAuth URL. Redirect the account owner to it; Google
returns them to your successUrl or errorUrl. The link is scoped to the
account that owns the API key, so the body carries only your two redirects.
Re-issuing a link for an account whose grant expired refreshes the existing
connection rather than duplicating it.
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" }}'{ "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. errors is populated only when generation
fails, so check that url is non-null. A flat body, or one carrying an
accountId, is rejected with HTTP 400.
Mint a Facebook connect link
POST /connected-accounts/connect-facebook
The Facebook counterpart, with the same body. During the flow the owner accepts Facebook's terms and grants the page-management permissions the API needs; if they skip a permission the connection cannot sync, so re-issue the link and have them consent again.
curl -X POST https://listingsapi.com/api/v4/connected-accounts/connect-facebook \-H "Authorization: API $LISTINGSAPI_KEY" \-H "Content-Type: application/json" \-d '{ "input": { "successUrl": "https://app.example.com/connect/facebook/success", "errorUrl": "https://app.example.com/connect/facebook/error" }}'{ "data": { "bulkConnectLinkForFacebook": { "success": true, "url": "https://claim.verifymybiz.com/locations/redirect_oauth_bulk/facebook?onboarding=false&platform=local&token=bfac8e06-...", "errors": null } }}The response key is bulkConnectLinkForFacebook; otherwise read it exactly
like the Google link. Once the owner finishes, the account appears in the
list endpoint as a FacebookAccount.
Mint a connect link for one location
POST /locations/oauth_connect_url
Use this for a "Connect Google" button on a single location's page. The link is tied to the location you name, so no matching step follows: when the owner consents, the credentials attach to that location directly. Both bulk links above connect a whole account instead.
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" }}'{ "data": { "createConnectUrl": { "success": true, "url": "https://accounts.google.com/o/oauth2/auth?client_id=...&state=8f3c1d92-4a7e-4d0b-9c31-6b2f7ae51c04", "errors": null } }}The link is single-use, so mint a fresh one per attempt rather than caching
it. A relative or unparseable redirect URL comes back as success: true with
a null url, so check url and not just success. A key that may not
touch this location gets a 403 with SY90003. The SDKs uppercase site for
you.
List connected accounts
GET /connected-accounts
Returns every account authorized against your API account, with connection
health and how many locations each one links. This is an offset page:
records[] plus pageInfo.totalRecords, paged with flat page and
perPage query parameters. Ordering is fixed to account email ascending.
curl "https://listingsapi.com/api/v4/connected-accounts?publisher=GoogleAccount&status=Connected&page=1&perPage=20" \-H "Authorization: API $LISTINGSAPI_KEY"{ "data": { "connectedAccountsInfo": { "pageInfo": { "hasNextPage": false, "hasPreviousPage": false, "totalPages": 1, "totalRecords": 1 }, "records": [ { "connectedAccountId": "4f712c17-4f95-42dd-90f4-97171a2e67b5", "connectedAccountType": "GoogleAccount", "email": "owner@brightsmile-dental.com", "status": "CONNECTED", "connectivityIssue": null, "connectedLocationsCount": 0, "requestMatchesStatus": "MATCH_COMPLETED", "lastFetchMatchesRequestedAt": "2026-07-06 21:43:07 UTC" } ] } }}Pick the record whose email is the owner who consented and keep its
connectedAccountId; every later call takes it. Anything other than the
four query parameters is ignored rather than rejected, so a JSON pageInfo
silently returns page 1.
The status filter and the record's status field do not share a
vocabulary. Feeding CONNECTED back in as the filter is an invalid enum and
returns a 400.
Get one account's details
GET /connected-accounts/{connectedAccountId}/details
Returns the same fields as one list record, scoped to one account. Use it to
poll requestMatchesStatus after triggering matches, or to check status
and connectivityIssue before pushing an update through the account.
curl https://listingsapi.com/api/v4/connected-accounts/4f712c17-4f95-42dd-90f4-97171a2e67b5/details \-H "Authorization: API $LISTINGSAPI_KEY"{ "data": { "connectedAccountDetails": { "details": { "connectedAccountId": "4f712c17-4f95-42dd-90f4-97171a2e67b5", "connectedAccountType": "GoogleAccount", "status": "CONNECTED", "connectivityIssue": null, "connectedLocationsCount": 0, "requestMatchesStatus": "MATCH_COMPLETED" } } }}The record sits one level down at data.connectedAccountDetails.details.
requestMatchesStatus moves from MATCH_IN_PROGRESS to MATCH_COMPLETED
when suggestions are ready. Completed means the matcher finished, not that
anything is linked: connectedLocationsCount stays 0 until you confirm or
connect a listing.
Fetch the listings an account can reach
POST /connected-accounts/connected-account-listings
Lists every Google Business Profile or Facebook listing the account has access
to, whether or not it is linked to one of your locations. Each record's id
is the connectedAccountListingId that connect-listing and
create-location-from-listing take. This is a POST that only reads, and it
returns an offset page: records[] plus pageInfo, paged with flat page
and perPage keys in the body.
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, "locationInfo": "William St" }'{ "data": { "connectedAccountListings": { "pageInfo": { "hasNextPage": false, "totalPages": 1, "totalRecords": 60 }, "records": [ { "id": "R21iQnVsa0RhdGFMYWtlOjA2MDQ4NmExLTA0NzQtNDc4NS1iMjNhLTBkOGM4MzhmNGQ5OA==", "accountTypeName": "Google", "locationName": "Trial", "address": "123 William St, NY 10038, Manhattan, US", "phone": "(718) 667-7787", "liveLink": null } ] } }}Walk the set with pageInfo.hasNextPage and pageInfo.totalPages. The SDKs
put connectedAccountId in the body for you, so the trap above only bites
hand-written REST calls.
Trigger matching
POST /connected-accounts/trigger-matches
Queues the job that pulls every profile the named accounts can reach and pairs each with one of your locations. Matching considers every active, unarchived location that is not already connected. It writes suggestions only; nothing is linked until you confirm.
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"] } }'{ "data": { "connectedAccountsTriggerMatches": { "success": true, "failedIds": null } }}success: true means the job was queued, not that matching finished. Any
account that could not be queued is listed in failedIds and success is
false. Poll the details endpoint until requestMatchesStatus is
MATCH_COMPLETED before reading suggestions; every 15 seconds with a bounded
loop keeps a Launch plan inside its 10 requests a minute.
Read connection suggestions
GET /connected-accounts/{connectedAccountId}/connection-suggestions
Returns the pairings the matcher proposed: your location on one side
(locationInfo, with locationId as its databaseId) and the publisher
listing on the other (suggestedLocationInfo). This is an offset page paged
with flat page and perPage query parameters; ordering is fixed to
location info ascending.
curl "https://listingsapi.com/api/v4/connected-accounts/4f712c17-4f95-42dd-90f4-97171a2e67b5/connection-suggestions?page=1&perPage=20" \-H "Authorization: API $LISTINGSAPI_KEY"{ "data": { "connectionSuggestionsForAccount": { "pageInfo": { "hasNextPage": false, "totalPages": 1, "totalRecords": 2 }, "records": [ { "accountType": "GMB", "matchedDataDatabaseId": "0d5e306f-0da0-46e3-929f-fd3c67ccd8cc", "connectedAccountId": "4f712c17-4f95-42dd-90f4-97171a2e67b5", "locationId": "73415", "locationInfo": { "name": "My Business", "city": "Lincoln", "stateIso": "NE" }, "suggestedLocationInfo": { "locationInfo": { "name": "My Business", "city": "Lincoln" } } } ] } }}A suggestion has no id. Keep matchedDataDatabaseId and accountType
together; confirm-matches needs both to build its id. Read too early, before
MATCH_COMPLETED, the endpoint returns records: [] with totalRecords: 0,
which looks exactly like "nothing matched".
Confirm matches
POST /connected-accounts/confirm-matches
Accepts the suggestions you approve and creates the live links. Each id is
the base64 of <MatchRecordType>:<matchedDataDatabaseId>, with the type
chosen from the table above. The account is inferred from the records, so
there is no connectedAccountId field; an unknown field inside input is
rejected with a 400.
# 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="] } }'{ "data": { "confirmConnectMatches": { "success": true, "failedIds": null } }}failedIds holds the decoded matchedDataDatabaseId values that could
not be confirmed, such as a record already consumed or a location archived
since matching, so match them back against the suggestion records. Unsent
suggestions are left untouched. Confirming links the location to the
listing; it does not push data to the publisher on its own.
Connect one listing to a location
POST /connected-accounts/connect-listing
Links a single listing you have already chosen to a single location, with no suggestion or confirm step. Use it when matching is ambiguous, for example two nearby storefronts with similar names, and an operator picks the right one from the account's listings.
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==" }}'{ "data": { "connectListing": { "success": true, "message": null } }}A failed link returns success: false with a non-null message, for example
Listing record not found when the listing id does not resolve, or a note
that the listing is already connected to another location. In both SDKs this
method lives on the listings resource, not on connected accounts.
Disconnect one listing from a location
POST /connected-accounts/disconnect-listing
Severs the link between one location and one publisher. The location and its data stay; only outbound sync to that publisher stops. This is not a bulk endpoint, so loop over locations. To re-point a store to a different Google account, disconnect here first, then match or connect on the new account.
curl -X POST https://listingsapi.com/api/v4/connected-accounts/disconnect-listing \-H "Authorization: API $LISTINGSAPI_KEY" \-H "Content-Type: application/json" \-d '{ "input": { "locationId": "TG9jYXRpb246MTgwMDI4OQ==", "site": "GOOGLE" } }'{ "data": { "disconnectConnectedAccountsLocations": { "clientMutationId": null, "success": true, "errors": null } }}Sending neither site nor providerId returns SY10056. A location with
no active connection for that site returns SY10057 rather than a silent
success, so success: true really does mean a link was removed. The SDKs
take site only and uppercase it.
Create a location from a listing
POST /connected-accounts/create-location-from-listing
Matching only pairs listings with locations you already have. For a listing with no location, this creates one from the publisher's data and links it back to the listing at once. Google imports name, address, coordinates, phone, website, description, regular and special hours, categories, opening date, store code, and timezone, plus payment methods and social links derived from the listing's Google attributes. Photos and videos are not imported, and media on the Google profile is never modified or removed. Facebook imports name, location, hours, categories, description, and contact info.
For a Google import the data is not resubmitted to Google, so its listing
stays at IN_PROGRESS until your first location update publishes to it.
Every other publisher is submitted normally, and this endpoint takes no
site scope, so the location is created against your account's full
publisher set. Narrow it with enabledSiteIds straight after creation, and
send that update immediately: order items for the other publishers are
generated shortly after creation, and any already generated are not
withdrawn by the update.
Neither SDK wraps this endpoint yet, so the Python and Node samples call REST with the same header.
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==" }}'{ "data": { "createLocationFromConnectedAccountListing": { "success": true, "locationId": 88277, "errors": null } }}locationId is the new location's plain databaseId. On failure success
is false, locationId is null, and errors carries a coded reason such
as SY81067 (the listing id did not resolve); other causes are a listing
already linked to another location, or an account whose credentials expired.
Once created, manage the location like any other.
List a Google account's folders
GET /connected-accounts/{connectedAccountId}/folders
Returns the Google Business Profile account groups (folders) the connected
Google account can access. Use it before creating a GMB listing so the new
profile lands in the group the owner expects. The response is a plain array
under getFoldersUnderGoogleAccount, not a page. The path is only valid for
GoogleAccount connections.
curl https://listingsapi.com/api/v4/connected-accounts/4f712c17-4f95-42dd-90f4-97171a2e67b5/folders \-H "Authorization: API $LISTINGSAPI_KEY"{ "data": { "getFoldersUnderGoogleAccount": [ { "folderId": "accounts/104890024806530350198", "folderName": "Bright Smile Dental", "locationCount": null } ] }}Use folderId verbatim; it is the accounts/{id} string, not a number.
locationCount may be null when Google does not report it, so treat it as
optional.
Create a Google Business Profile for a location
POST /locations/create/gmb-listing
Creates a brand-new Google listing for a location that has none, under a connected Google account, and optionally inside a folder. Contrast with connect-listing, which links a listing that already exists. The location must already carry a name, address, and phone, and only one Google listing can exist per location.
curl -X POST https://listingsapi.com/api/v4/locations/create/gmb-listing \-H "Authorization: API $LISTINGSAPI_KEY" \-H "Content-Type: application/json" \-d '{ "input": { "locationId": "TG9jYXRpb246MTgwMDI4OQ==", "connectedAccountId": "4f712c17-4f95-42dd-90f4-97171a2e67b5", "folderId": "accounts/104890024806530350198" }}'{ "data": { "createGmbListingForLocation": { "clientMutationId": null, "success": true, "errors": null } }}Creation is asynchronous: success: true means the request was accepted,
and Google reviews new profiles before they publish. When success is
false, errors says why: missing required fields, an existing Google
listing, an archived location, or an inactive account.
Disconnect a whole Google account
POST /connected-accounts/disconnect-google
Tears down the connection for the entire account: every location linked
through it stops syncing. There is no locationIds field; to detach one
location and keep the account, use disconnect-listing instead. Use this when
a client leaves, or when a broken grant will not clear and you want to start
over with a fresh connect link.
curl -X POST https://listingsapi.com/api/v4/connected-accounts/disconnect-google \-H "Authorization: API $LISTINGSAPI_KEY" \-H "Content-Type: application/json" \-d '{ "input": { "connectedAccountId": "d2397854-52c2-41d7-95f1-b90cba16498d" } }'{ "data": { "gmbBulkDisconnect": { "success": true } }}success: true is returned even for an unknown connectedAccountId, so it
means "the request was processed", not "a live connection was removed". List
connected accounts again if you need certainty.
Disconnect a whole Facebook account
POST /connected-accounts/disconnect-facebook
The Facebook counterpart with the same body. Pages linked through the account stop syncing and Facebook interactions stop appearing.
curl -X POST https://listingsapi.com/api/v4/connected-accounts/disconnect-facebook \-H "Authorization: API $LISTINGSAPI_KEY" \-H "Content-Type: application/json" \-d '{ "input": { "connectedAccountId": "d2397854-52c2-41d7-95f1-b90cba16498d" } }'{ "data": { "fbBulkDisconnect": { "success": true } }}The response key is fbBulkDisconnect. As with Google, success: true for
an unknown id means processed, not removed.
Disconnect one location from a publisher
POST /locations/oauth-disconnect
The disconnect half of the per-location OAuth flow. It is the same
operation as disconnect-listing behind a second path: identical input
body, same upstream mutation, same disconnectConnectedAccountsLocations
response key. Pick whichever path reads better in your integration.
curl -X POST https://listingsapi.com/api/v4/locations/oauth-disconnect \-H "Authorization: API $LISTINGSAPI_KEY" \-H "Content-Type: application/json" \-d '{ "input": { "locationId": "TG9jYXRpb246MTgwMDI4OQ==", "site": "GOOGLE" } }'{ "data": { "disconnectConnectedAccountsLocations": { "clientMutationId": null, "success": true, "errors": null } }}The same SY10056 and SY10057 rules apply, and a key that may not touch
this location gets a 403 with SY90003. Disconnecting leaves the location
and its data intact; only outbound sync to that publisher stops. Send the
owner back through the connect URL, or run matching and connect-listing, to
attach the correct listing afterwards.
Errors you will see
Malformed requests and bad keys return a 4xx with a top-level errors[]
whose message starts with the code. Mutations can also answer 200 with
success: false and the reason inside data.<operation>, so check the body
of every write. The full list is at Error codes.
Next steps
- Connect Google and import locations: the bulk Google flow end to end, with polling and webhook events.
- Connect Facebook Pages: the same flow for Facebook, including the account disconnect.
- Create a Google Business Profile: folders and GMB creation for a location with no Google listing.
- Check listing status and live links:
watch the Google row move to
SYNCEDafter a link lands. - Reference index: Connected accounts; SDK pages: Python connected accounts, Node connected accounts, Python listings, Node listings.