GuideGuides/Publish GBP posts
How do I publish Google Business Profile posts through an API?

How to Publish Google Business Profile Posts Programmatically

Publish announcements, offers and events to Google Business Profile via API, schedule them, handle the missing-connection draft, and measure post performance.

At a glance

What this guide accomplishes
Create, schedule, update and measure Google Business Profile posts across a location's connected platforms.
APIs used
Reference
Posts
Prerequisites
  • An API key with write access
  • A location connected to at least one platform
  • The clientId and locationId you stored at onboarding
Typical use cases
  • Pushing offers and announcements from your own scheduler
  • Publishing events to Google Business Profile at scale
  • Reporting post performance back to a business owner

Posting is one call to publish and one to measure, with a lifecycle in between that trips people up in exactly two places: a post can go out now or be scheduled, and a post to a platform that is not connected does not fail, it comes back as a draft. Get those two right and the rest is create, list, update, delete.

How a post moves

  1. ComposeAnnouncement, offer or event, per platform message and a CTA
  2. CreatePOST /api/v1/postsPublishes now, or schedules, or saves a draft
  3. PublishLive on each connected platform; a missing connection stays a draft
  4. MeasureGET /api/v1/posts/analyticsViews, reactions, shares, comments over time
From create to live to measured

Post types, and which are Google-only

There are three postType values, and they are not equal across platforms:

  • announcement: a plain update. Works on the platforms that accept posts.
  • offer: a promotion with a title, a window and a coupon. Google-only.
  • event: a dated event with a start and end. Google-only.

Send an offer or event with platforms that includes only Google, or the extra fields have nowhere to go on the others.

Publish or schedule

POST /api/v1/posts publishes immediately unless you set scheduledFor, or draft: true to hold it. Each platform gets its own message field, so messageGoogle is Google's copy. The call below schedules a September offer to Google.

curl -X POST https://ai.synup.com/api/v1/posts \
  -H "Authorization: Bearer $SYNUP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "locationId": "LOCATION_ID",
    "name": "September whitening offer",
    "postType": "offer",
    "platforms": ["google"],
    "messageGoogle": "20% off teeth whitening this September. Book online.",
    "ctaType": "book",
    "ctaUrl": "https://grovestreetdental.example/book",
    "eventTitle": "September whitening offer",
    "eventStartAt": "2026-09-01T09:00:00Z",
    "eventEndAt": "2026-09-30T17:00:00Z",
    "scheduledFor": "2026-09-01T09:00:00Z"
  }'
const API = "https://ai.synup.com/api/v1";
const headers = {
  Authorization: `Bearer ${process.env.SYNUP_API_KEY}`,
  "Content-Type": "application/json",
};

async function createPost(locationId, post) {
  const res = await fetch(`${API}/posts`, {
    method: "POST",
    headers,
    body: JSON.stringify({ locationId, ...post }),
  });
  if (!res.ok) throw new Error(`${res.status}: ${(await res.json()).error}`);
  const { data } = await res.json();
  // data.status: draft | scheduled | active | error
  // data.missingPlatforms: platforms with no connection; data.missingFields: what a draft still needs
  return data;
}
import os
import requests

API = "https://ai.synup.com/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['SYNUP_API_KEY']}", "Content-Type": "application/json"}

def create_post(location_id, post):
    r = requests.post(f"{API}/posts", headers=HEADERS, timeout=30,
                      json={"locationId": location_id, **post})
    r.raise_for_status()
    return r.json()["data"]  # status, missingPlatforms, missingFields

Publish a draft later

When a draft was held because a platform was not connected, publish it once the connection exists with POST /api/v1/posts/{id}/publish. It pushes the existing draft live without you rebuilding the body.

curl -X POST https://ai.synup.com/api/v1/posts/POST_ID/publish \
  -H "Authorization: Bearer $SYNUP_API_KEY"

List, inspect, update, remove

GET /api/v1/posts returns a location's posts with a stats block of totals, and each row's own status. GET /api/v1/posts/{id} returns one. PATCH /api/v1/posts/{id} edits a post before or after it publishes, and DELETE /api/v1/posts/{id} removes one.

# List a location's posts
curl "https://ai.synup.com/api/v1/posts?locationId=LOCATION_ID&clientId=CLIENT_ID&limit=20" \
  -H "Authorization: Bearer $SYNUP_API_KEY"

# Edit the copy of an existing post
curl -X PATCH https://ai.synup.com/api/v1/posts/POST_ID \
  -H "Authorization: Bearer $SYNUP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "messageGoogle": "Now 25% off through September. Book online." }'

# Remove a post
curl -X DELETE https://ai.synup.com/api/v1/posts/POST_ID \
  -H "Authorization: Bearer $SYNUP_API_KEY"
async function listPosts(locationId, clientId) {
  const params = new URLSearchParams({ locationId, clientId, limit: "20" });
  const res = await fetch(`${API}/posts?${params}`, { headers });
  if (!res.ok) throw new Error(`${res.status}: ${(await res.json()).error}`);
  const { data } = await res.json();
  return data; // { rows, total, stats: { totalPosts, totalViews, totalClicks, totalEngagement } }
}
def list_posts(location_id, client_id):
    r = requests.get(f"{API}/posts", headers=HEADERS, timeout=30,
                     params={"locationId": location_id, "clientId": client_id, "limit": 20})
    r.raise_for_status()
    return r.json()["data"]  # rows, total, stats

Measure post performance

GET /api/v1/posts/analytics aggregates a location's posts into a summary (totalPosts, totalViews, totalReactions, totalShares, totalComments, periodDays) with a prevSummary for the period comparison, a byPlatform breakdown, and trend, engagementTrend and postsTrend series for charts.

curl "https://ai.synup.com/api/v1/posts/analytics?locationId=LOCATION_ID&clientId=CLIENT_ID" \
  -H "Authorization: Bearer $SYNUP_API_KEY"

A location with no posts yet returns the same shape with zeros, not an error, so you can render the panel before the first post goes out.

Running out of things to post

GET /api/v1/post-ideas returns AI-generated post ideas for a location, drawn from the business and the season, which is a useful prompt library when a customer stares at an empty composer. You still choose and publish; the ideas are suggestions, not scheduled posts.

Next

Posts are one channel. The numbers they move show up in the profile. Retrieve Google Business Profile analytics covers views, calls and direction requests. If you want a single fast recipe rather than the whole surface, the publish a post recipe is the one call. Posts also sit inside the six-feature local marketing guide.