Engineering

Publish Google Business Profile posts across multiple locations

Connect Google Business locations with your own OAuth credentials, publish through Outstand, and handle per-location outcomes and retries without duplicating successful posts.

A promotion for three branches is one campaign, but it is not one Google Business Profile post. Google's local-post create endpoint takes a single location as its parent. Each branch gets a separate post resource, and each publication can succeed or fail independently.

Outstand lets you express the shared campaign as one request with several connected account IDs. Each connected Google Business account represents one location. The important work is selecting the right IDs, checking every destination's result, and retrying without duplicating branches that already published.

This tutorial uses a text-and-button update first. You need an Outstand API key and Google Cloud credentials approved for Business Profile API access. See the Google Business integration overview for the product context.

Understand what is native and what Outstand handles

The native Google create method is:

plaintext
1POST https://mybusiness.googleapis.com/v4/accounts/{accountId}/locations/{locationId}/localPosts

Its body uses Google's summary, topicType, and optional callToAction, event, offer, and media fields. Calling this method for another branch means another create operation.

Outstand's create-post endpoint is POST https://api.outstand.so/v1/posts. Its body uses content, an accounts array, and a top-level google_business configuration block. Outstand translates that representation into location-specific Google requests and records destination-level publishing results.

This is not a claim that Google's own interface lacks multi-location publishing. Google's Business Profile help documents selecting multiple profiles in its posting interface. This article covers building that workflow into your application through an API.

1. Complete Google's approval and BYOK setup

Google Business Profile is BYOK-only in Outstand. There is no managed Outstand Google Business OAuth app to use instead. Add your own Google Cloud OAuth client credentials before connecting locations.

Keep three separate requirements in mind:

  1. Project access: Google must approve your Cloud project's Business Profile API access. Google's prerequisites currently require applicants to manage a verified profile active for at least 60 days and have a website representing that business. These are application requirements, not an Outstand approval process.
  2. OAuth configuration: Enable the APIs needed for your workflow and configure the consent screen and web OAuth client. Outstand uses Account Management for account discovery, Business Information for locations, and the legacy My Business v4 API for local posts. Performance API access is relevant if you also need location metrics.
  3. Business authorization: The person completing OAuth must have access to the intended locations and grant https://www.googleapis.com/auth/business.manage.

The current Outstand Google OAuth implementation uses this production provider callback:

plaintext
1https://www.outstand.so/app/api/socials/google_business/callback

Notice the underscore in google_business. Register the exact callback, not your application's eventual return page. Follow the Google Business configuration guide for the rest of the setup.

You can add the credentials from the dashboard or your backend:

bash
1export OUTSTAND_API_KEY='YOUR_OUTSTAND_API_KEY'
2
3curl --fail-with-body https://api.outstand.so/v1/social-networks \
4	-H "Authorization: Bearer $OUTSTAND_API_KEY" \
5	-H 'Content-Type: application/json' \
6	-d '{
7		"network": "google_business",
8		"client_key": "YOUR_GOOGLE_OAUTH_CLIENT_ID",
9		"client_secret": "YOUR_GOOGLE_OAUTH_CLIENT_SECRET"
10	}'

Do not ship this API key or the OAuth secret to browser code. Google's project approval and any applicable OAuth app verification are separate concerns; successfully creating an OAuth client does not establish API access. The key also needs the relevant Outstand permissions: account listing requires accounts:read, and post creation requires both posts:create and posts:publish.

2. Connect and explicitly select locations

In the Outstand dashboard, open Accounts, connect Google Business, and sign in with the Google account that manages your branches. Review the returned location names and addresses, select the intended branches, and confirm the selection.

Selecting three locations creates three Outstand social accounts, not one account with three implicit publishing destinations. Connecting locations also does not mean every subsequent post targets all of them. Publication targets are chosen separately for each request.

Start with the dashboard connection flow for this tutorial. Do not assume the generic REST pending-connection finalizer supports Google Business just because other networks use it; the current Google Business dashboard flow has its own finalization route.

Retrieve the connected locations:

bash
1curl --fail-with-body \
2	'https://api.outstand.so/v1/social-accounts?network=google_business&limit=100&offset=0' \
3	-H "Authorization: Bearer $OUTSTAND_API_KEY"

The response's data array contains accounts with encoded id values. Copy those IDs into your application's branch mapping. Use network_unique_id, where returned, to distinguish Google's location resource from Outstand's account ID. Store your own branch label alongside the mapping rather than relying on potentially identical business names.

The account listing reference describes pagination. Continue through offsets until you have collected total matching accounts. If a branch is missing, resolve discovery or access before publishing; do not infer that the first page is the complete estate.

For a multi-tenant product, use tenant assignments and the tenantId listing filter to build the appropriate customer's selection. Validate the resulting target set against your own authorization rules too.

3. Publish to one location, then several

Test one known branch before sending a campaign everywhere. Replace the placeholder below with an encoded ID from account listing, and replace the example destination URL with your real page.

bash
1export CAMPAIGN_CREATE_KEY="$(uuidgen)"
2
3curl --fail-with-body https://api.outstand.so/v1/posts \
4	-H "Authorization: Bearer $OUTSTAND_API_KEY" \
5	-H 'Content-Type: application/json' \
6	-H "Idempotency-Key: $CAMPAIGN_CREATE_KEY" \
7	-d '{
8		"content": "Explore our winter menu and plan your next visit.",
9		"accounts": ["YOUR_OUTSTAND_LOCATION_A_ID"],
10		"google_business": {
11			"topicType": "STANDARD",
12			"callToAction": {
13				"actionType": "LEARN_MORE",
14				"url": "https://example.com/winter-menu"
15			}
16		}
17	}'

Omitting scheduledAt requests immediate publishing. To schedule instead, include a future ISO 8601 timestamp such as "scheduledAt": "2026-12-01T09:00:00Z". That schedules Outstand's publishing work, not a native Google local-post scheduling field.

For the real multi-location campaign, use the same body with several IDs:

json
1"accounts": [
2	"YOUR_OUTSTAND_LOCATION_A_ID",
3	"YOUR_OUTSTAND_LOCATION_B_ID",
4	"YOUR_OUTSTAND_LOCATION_C_ID"
5]

Generate a new idempotency key for this new operation. Do not include the test branch again unless you intentionally want another post there.

Always use account IDs, not "google_business", branch nicknames, or Google location resource names. Unresolved identifiers can be dropped when other supplied identifiers resolve, so compare the returned post.socialAccounts IDs with your intended target set. Treat a mismatch as an integration error.

All Google Business destinations in one post share its Google configuration and content. For branch-specific wording, URLs, offer terms, or schedules, create separate posts for the relevant locations or groups. A network-level override is not a per-location override.

4. Respect the local-post content model

Keep the copy within 1,500 characters yourself. Outstand's current provider truncates Google's summary to that length; silent truncation is not a substitute for reviewing the final message.

For an image, use one container with content and media: [{ "url": "YOUR_PUBLIC_IMAGE_URL", "filename": "winter-menu.jpg" }] instead of the top-level content. Use the media upload workflow when uploading through Outstand. Google fetches local-post media from a URL, so avoid private or short-lived links. Start with a public photo rather than assuming Google's general profile-video capabilities apply identically to local posts.

Outstand supports STANDARD, EVENT, and OFFER. For either EVENT or OFFER, provide google_business.event.title, startDate, startTime, endDate, and endTime. Outstand accepts those date/time fields directly inside event and constructs Google's nested event.schedule internally.

Google's resource reference says callToAction is ignored for offers. Use offer.redeemOnlineUrl for an offer redemption link. For a CALL action, omit callToAction.url; other supported action types require it in Outstand. Product posts are not supported by Google's local-post API.

5. Inspect every outcome

A successful create response means you have an Outstand post, not proof that every branch is live on Google. Save post.id, then check post details:

ts
1// Run server-side with Node.js 22+ using your TypeScript runner.
2const apiKey = process.env.OUTSTAND_API_KEY;
3if (!apiKey) throw new Error('Set OUTSTAND_API_KEY');
4const postId = 'YOUR_OUTSTAND_POST_ID';
5
6const response = await fetch(
7	`https://api.outstand.so/v1/posts/${encodeURIComponent(postId)}`,
8	{ headers: { Authorization: `Bearer ${apiKey}` } },
9);
10if (!response.ok) throw new Error(await response.text());
11const result = await response.json();
12if (!result.success) throw new Error(result.error ?? 'Post lookup failed');
13for (const account of result.post.socialAccounts) {
14	console.log({
15		accountId: account.id,
16		status: account.status,
17		platformPostId: account.platformPostId,
18		error: account.error,
19	});
20}

Evaluate pending, published, or failed for each account. The post-level publishedAt can be set when only one destination succeeds. A publishing webhook can replace repeated polling, but still inspect destination results.

There is another boundary: Google's post states include PROCESSING, LIVE, and REJECTED. Outstand's provider records successful creation and Google's resource name; it does not wait for Google's LIVE state. Verify customer-visible publication separately when that distinction matters.

6. Retry the right operation

Separate request retries from publishing retries:

  • Create request timed out: Repeat the identical body with the same Idempotency-Key. Outstand retains keys for 24 hours. A 409 in-progress response calls for waiting according to Retry-After, not generating a fresh key. Read the idempotency guide before automating this.
  • Some locations failed: Investigate each error. After fixing the cause, create a new post targeting only failed locations, with a new key. Reusing the original key replays creation; it does not republish failed destinations. Partially published posts cannot be edited or rescheduled.
  • Outcome is ambiguous: Reconcile Outstand status and Google's posts before issuing another create. An idempotency key on Outstand creation is not an exactly-once guarantee across an uncertain downstream Google request.

For quota errors, pace work and use bounded exponential backoff with jitter. Google's quota guidance explains 429 responses and notes that zero quota means API access has not been granted. Do not interpret its listed quotas for other GBP APIs as a guaranteed local-post throughput allowance. Credential, permission, and content-policy failures need correction, not an endless retry loop.

The production invariant is simple: record the intended locations, the Outstand post ID, and each destination's outcome. A campaign is complete when every intended branch has a known result, not when one HTTP request returns successfully.