Publish posts to Google and Facebook
Publish an announcement, an event, and an offer, schedule one post across many locations, confirm it went live on each site, manage a location's posts, and react to post webhooks.
A post is a dated card on a location's Google Business Profile or Facebook Page: an announcement, an event, or an offer. One call creates it, publishing runs in the background, and a second call tells you whether each site accepted it.
All three post types use POST /posts with the body wrapped in input. A
flat body is rejected with a 400. The SDKs build the wrapper and validate
the payload before sending.
1. Publish an announcement
The simplest post: a message, an optional button, an optional image.
curl -X POST https://listingsapi.com/api/v4/posts \-H "Authorization: API $LISTINGSAPI_KEY" \-H "Content-Type: application/json" \-d '{ "input": { "postName": "Grand Opening Announcement", "locationIds": ["TG9jYXRpb246MTgwMDI4OQ=="], "postType": "ANNOUNCEMENT", "postSites": ["GOOGLE"], "postMessage": [ { "site": "GOOGLE", "message": "We are now open! Visit us this week for our grand opening." } ], "postCta": [ { "site": "GOOGLE", "type": "LEARN_MORE", "url": "https://acmekitchen.example.com/opening" } ], "postMediaUrl": [ { "site": "GOOGLE", "url": "https://cdn.example.com/opening.jpg", "type": "IMAGE" } ] }}'{ "data": { "createSocialPost": { "success": true, "errors": [], "socialPost": { "id": "U29jaWFsUG9zdDo0NDEyMg==", "name": "Grand Opening Announcement", "status": "INPROGRESS", "type": "ANNOUNCEMENT", "sites": ["GOOGLE"], "publishDetails": [ { "site": "GOOGLE", "status": "PUBLISHING", "createdDate": "2026-06-28", "submissionError": null } ], "postLinks": [], "locationIds": [1800289] } } }}Keep socialPost.id. status starts at INPROGRESS and postLinks is
empty until a site accepts the post; step 5 reads both back.
2. Publish an event
An event adds postContextInfo with a title, startDay, endDay, and
optional display times. Google rejects an event without a title, and the
SDKs raise ValidationError before sending if it is missing.
curl -X POST https://listingsapi.com/api/v4/posts \-H "Authorization: API $LISTINGSAPI_KEY" \-H "Content-Type: application/json" \-d '{ "input": { "postName": "Summer Sale Kickoff Event", "locationIds": ["TG9jYXRpb246MTgwMDI4OQ=="], "postType": "EVENT", "postSites": ["GOOGLE"], "postContextInfo": { "title": "Summer Sale Kickoff", "startDay": "2026-07-15", "startTime": "10:00am", "endDay": "2026-07-15", "endTime": "06:00pm" }, "postMessage": [ { "site": "GOOGLE", "message": "Join us for the Summer Sale kickoff: refreshments, giveaways, and up to 40% off in store." } ] }}'The response has the same shape as step 1 with type: "EVENT" and the dates
echoed under socialPost.contextInfo. postContextInfo holds the event's
own dates; postScheduledDates (step 4) controls when the post itself is
published and retired.
3. Publish an offer
An offer's postContextInfo carries a title, couponCode, discount,
redeemUrl, termsConditions, and the validity window startDay and
endDay. Pair it with a GET_OFFER button on Facebook or ORDER on Google;
there is no REDEEM type.
curl -X POST https://listingsapi.com/api/v4/posts \-H "Authorization: API $LISTINGSAPI_KEY" \-H "Content-Type: application/json" \-d '{ "input": { "postName": "Summer 25% Off", "locationIds": ["TG9jYXRpb246MTgwMDI4OQ=="], "postType": "OFFER", "postSites": ["GOOGLE"], "postContextInfo": { "title": "25% off storewide", "couponCode": "SUMMER25", "discount": "25%", "redeemUrl": "https://acmekitchen.example.com/summer", "termsConditions": "Valid in store only through July 6. Cannot be combined with other offers.", "startDay": "2026-06-22", "endDay": "2026-07-06" }, "postMessage": [ { "site": "GOOGLE", "message": "Celebrate with us: 25% off everything in store this week." } ] }}'The response echoes the offer fields under socialPost.contextInfo and sets
scheduledStartDate and scheduledEndDate from the validity window.
4. Send one post to many locations, on a schedule
POST /bulk-posts takes the same input and fans one campaign out to every
id in locationIds. Add postScheduledDates with ISO-8601 startDatetime
and endDatetime to publish later and retire automatically; the post is then
created as SCHEDULED. Give each site its own message when the tone differs.
curl -X POST https://listingsapi.com/api/v4/bulk-posts \-H "Authorization: API $LISTINGSAPI_KEY" \-H "Content-Type: application/json" \-d '{ "input": { "postName": "July Reopening Announcement", "locationIds": ["TG9jYXRpb246MTgwMDI4OQ==", "TG9jYXRpb246MTgwMDI5MA=="], "postType": "ANNOUNCEMENT", "postSites": ["GOOGLE", "FACEBOOK"], "postMessage": [ { "site": "GOOGLE", "message": "We are back! Visit us this week for our summer menu." }, { "site": "FACEBOOK", "message": "We are back! Swing by for the new summer menu." } ], "postScheduledDates": { "startDatetime": "2026-07-10T09:00:00Z", "endDatetime": "2026-07-24T23:59:00Z" } }}'The bulk endpoint shares its mutation with POST /posts, so the response is
keyed createSocialPost too, with status: "SCHEDULED" and both ids in
locationIds. A validation problem returns 200 with
success: false and a populated errors array; the SDKs raise
ValidationError for it. Read a bulk campaign back with
GET /bulk-posts/{postId}, which adds
per-location status.
5. Check publish status and collect post links
GET /posts/{postId} returns the campaign under socialPostView. Poll it
after a create until socialPostInfo.status leaves INPROGRESS, then read
publishDetails[] for each site's outcome and postLinks[] for the live
URLs. Space polls tens of seconds apart; a 429 comes with Retry-After.
curl https://listingsapi.com/api/v4/posts/U29jaWFsUG9zdDo0NDEyMg== \-H "Authorization: API $LISTINGSAPI_KEY"{ "data": { "socialPostView": { "socialPostInfo": { "id": "U29jaWFsUG9zdDo0NDEyMg==", "status": "SUCCESS", "views": 128, "clicks": 14, "publishDetails": [ { "site": "GOOGLE", "status": "PUBLISHED", "publishedDate": "2026-06-28", "submissionError": null } ], "postLinks": [ { "site": "GOOGLE", "link": "https://www.google.com/maps/place/?post=44122" } ] }, "socialPostAnalyticsBySite": [ { "site": "GOOGLE", "views": 128, "clicks": 14, "reactions": 3, "shares": 0, "comments": 1 } ] } }}views, clicks, reactions, shares, and comments accrue on the same
record, broken down per site in socialPostAnalyticsBySite. The SDKs return
socialPostInfo directly, which is why the samples read post.status.
6. List a location's posts and delete one
GET /locations/{locationId}/posts is an offset page (page, perPage up
to 50) of every campaign that includes the location, under postsByLocation
with records[] and pageInfo.totalRecords. Pass the base64 id verbatim;
do not URL-encode it. There is no type filter, so pick out offers or events
from type in your own code. Multi-location campaigns from step 4 are listed
separately by GET /locations/{locationId}/bulk-posts.
DELETE /posts/{postId} removes the campaign here and takes it down from
every site it reached. There is no undo, so confirm the id from the list
first.
curl "https://listingsapi.com/api/v4/locations/TG9jYXRpb246MTgwMDI4OQ==/posts?page=1&perPage=20" \-H "Authorization: API $LISTINGSAPI_KEY" curl -X DELETE https://listingsapi.com/api/v4/posts/U29jaWFsUG9zdDo0NDEyMg== \-H "Authorization: API $LISTINGSAPI_KEY"A delete answers data.deleteSocialPost with success: true and the
socialPostId it removed. Deleting an id that is already gone returns an
errors envelope with data: null instead, so a second delete is easy to
detect.
7. React to post webhooks
With a webhook URL set at /dashboard/webhooks, four events cover the post
lifecycle, all in the standard envelope. They fire per location, not per
campaign: a bulk post to 20 locations produces 20 post_item_id values with
one shared post_id.
function handle(payload: { event: string; location_id: string; data: Record<string, unknown> }) {if (!payload.event.startsWith('local_post.')) return; const key = String(payload.data.post_item_id); // one row per locationswitch (payload.event) { case 'local_post.published': markLive(key, payload.location_id, payload.data.post_external_id); break; case 'local_post.rejected': markRejected(key, payload.location_id, payload.data.rejection_reason); break; case 'local_post.deleted': markDeleted(key); break;}}published_url is usually absent, so keep polling step 5 for postLinks if
you need a clickable link. Answer 200 within 5 seconds; there are no
retries. The receiver and signature check are in
React to events with webhooks.
Next steps
- Connect Google and import locations and Connect Facebook Pages: the prerequisite for each site.
- Fetch profile analytics: profile-level views and actions alongside the per-post counters here.
- React to events with webhooks: the receiver these events land on.
- Reference: Create an announcement post, Create an event post, Create an offer post, Create a bulk post, Get a post, Fetch posts for a location, Delete a post.