Posts
Every Posts endpoint with a cURL, Python and Node sample: announcements, events and offers, scheduling, per-location and bulk campaigns, status polling and deletion.
A post is a short update published to a location's Google Business Profile
or Facebook page: an announcement, a dated event, or an offer with a coupon.
One campaign can target many locations and both sites at once. Publishing is
asynchronous, so every create call returns immediately and you poll for the
result. The three GET endpoints work with a Read key; creating and
deleting needs a Write key.
Endpoints at a glance
Values you will use everywhere
All four enums below are shared by every create endpoint.
Publish an announcement
POST /posts
The simplest post type: a message, optionally with one button and one image
per site. Provide one postMessage entry for every site listed in
postSites. The response returns the new campaign with status set to
INPROGRESS, because publishing runs in the background.
Parameters
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://jennyhome.example/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. It is the base64 post id every other endpoint here
needs. Both SDK helpers take a single message string and expand it into one
entry per site, so you only build the postMessage array by hand in cURL.
Publish an event
POST /posts
An event renders as a dated card. The event's own schedule lives in
postContextInfo. The separate postScheduledDates object controls when the
post itself appears and expires, which is usually the run-up to the event.
Google rejects an event with no title, and both SDKs raise a validation error
before sending if title is missing.
Parameters
Everything from the announcement table also applies.
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 and up to 40 percent off in store." } ] }}'{ "data": { "createSocialPost": { "success": true, "socialPost": { "id": "U29jaWFsUG9zdDo0NDEyMw==", "status": "INPROGRESS", "type": "EVENT", "scheduledStartDate": "2026-07-15", "scheduledEndDate": "2026-07-15", "contextInfo": { "title": "Summer Sale Kickoff", "startDay": "2026-07-15", "startTime": "10:00am", "endDay": "2026-07-15", "endTime": "06:00pm" } } } }}The echoed contextInfo is the event window. scheduledStartDate and
scheduledEndDate describe the post's own life, which matches the event
dates here because no postScheduledDates was sent.
Publish an offer
POST /posts
An offer is a promotion card carrying a coupon code, a discount, a redeem
link and terms. All of that goes in postContextInfo, and startDay with
endDay bound the offer's validity rather than the post's.
Parameters
curl -X POST https://listingsapi.com/api/v4/posts \-H "Authorization: API $LISTINGSAPI_KEY" \-H "Content-Type: application/json" \-d '{ "input": { "postName": "Festive 25 Percent Off", "locationIds": ["TG9jYXRpb246MTgwMDI4OQ=="], "postType": "OFFER", "postSites": ["GOOGLE"], "postContextInfo": { "title": "25% off storewide", "couponCode": "FESTIVE25", "discount": "25%", "redeemUrl": "https://jennyhome.example/festive", "termsConditions": "Valid in-store only through July 6.", "startDay": "2026-06-22", "endDay": "2026-07-06" }, "postMessage": [ { "site": "GOOGLE", "message": "Celebrate with us, 25 percent off everything in store this week." } ], "postCta": [ { "site": "GOOGLE", "type": "GET_OFFER", "url": "https://jennyhome.example/festive" } ] }}'{ "data": { "createSocialPost": { "success": true, "socialPost": { "id": "U29jaWFsUG9zdDo0NDEyNA==", "status": "INPROGRESS", "type": "OFFER", "contextInfo": { "title": "25% off storewide", "couponCode": "FESTIVE25", "discount": "25%", "startDay": "2026-06-22", "endDay": "2026-07-06" } } } }}Google takes ORDER and Facebook takes GET_OFFER for offer buttons. If a
promotion ends early, delete the campaign rather than editing it.
Read one post and its status
GET /posts/{postId}
The endpoint that turns an accepted campaign into a result. It answers under
data.socialPostView with the campaign content, a publishDetails entry per
site, engagement counters, and the live links once publishing succeeds.
Parameters
curl https://listingsapi.com/api/v4/posts/U29jaWFsUG9zdDo0NDEyMg== \-H "Authorization: API $LISTINGSAPI_KEY"{ "data": { "socialPostView": { "socialPostId": "U29jaWFsUG9zdDo0NDEyMg==", "socialPostInfo": { "status": "SUCCESS", "type": "ANNOUNCEMENT", "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" } ] }, "publishedLocationsCount": 1, "socialPostAnalyticsBySite": [ { "site": "GOOGLE", "views": 128, "clicks": 14, "reactions": 3, "shares": 0, "comments": 1 } ] } }}Two status fields matter, and they answer different questions.
A campaign can read SUCCESS while one site inside it holds ERROR, so
always check publishDetails rather than the top-level status alone.
List the posts on a location
GET /locations/{locationId}/posts
An offset-paginated history of every campaign that includes one location,
under data.postsByLocation. Use it to audit what is already live before
scheduling something new.
Parameters
curl "https://listingsapi.com/api/v4/locations/TG9jYXRpb246MTgwMDI4OQ==/posts?page=1&perPage=10" \-H "Authorization: API $LISTINGSAPI_KEY"{ "data": { "postsByLocation": { "records": [ { "id": "U29jaWFsUG9zdDo0NDEyMg==", "name": "Summer Hours Update", "status": "SUCCESS", "type": "ANNOUNCEMENT", "createdAt": "2026-06-28", "views": 128, "clicks": 14, "sites": ["GOOGLE"] } ], "pageInfo": { "totalPages": 1, "hasNextPage": false, "totalRecords": 1 } } }}This route has no server-side filter. It accepts only page and perPage,
and anything else is ignored, so read type and status on each record and
filter in your own code. Sort and filter server-side with the bulk list
endpoint below instead.
Delete a post
DELETE /posts/{postId}
Permanent, and it unpublishes the post from every site it reached. There is no undo and no edit endpoint, so correcting a live post means deleting it and publishing a replacement.
Parameters
curl -X DELETE https://listingsapi.com/api/v4/posts/U29jaWFsUG9zdDo0NDEyMg== \-H "Authorization: API $LISTINGSAPI_KEY" \-H "Content-Type: application/json" \-d '{ "clientMutationId": "delete-44122-req-1" }'{ "data": { "deleteSocialPost": { "success": true, "socialPostId": "U29jaWFsUG9zdDo0NDEyMg==" } }}Deleting an unknown or already deleted post returns an error envelope with
data set to null rather than success: false.
Publish one campaign across many locations
POST /bulk-posts
Same contract as POST /posts, built for fleets: list every location in
locationIds and one campaign covers them all. The response is keyed under
createSocialPost because both routes share the same underlying mutation.
Parameters
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" } }}'{ "data": { "createSocialPost": { "success": true, "errors": null, "socialPost": { "id": "U29jaWFsUG9zdDo5OTIwMQ==", "name": "July Reopening Announcement", "status": "SCHEDULED", "type": "ANNOUNCEMENT", "scheduledStartDate": "2026-07-10", "scheduledEndDate": "2026-07-24", "sites": ["GOOGLE", "FACEBOOK"], "locationIds": [1800289, 1800290] } } }}Both SDK helpers default sites to Google and Facebook, expand a per-site
message map for you, and encode numeric location ids, so a bulk publish is
usually the shortest call in either SDK.
Read one bulk campaign
GET /bulk-posts/{postId}
The bulk counterpart to GET /posts/{postId}. It returns the same shape,
with publishedLocationsCount above one and a per-store analytics breakdown
in socialPostAnalyticsByLocationId.
Parameters
curl https://listingsapi.com/api/v4/bulk-posts/U29jaWFsUG9zdDo1MTA4OA== \-H "Authorization: API $LISTINGSAPI_KEY"{ "data": { "socialPostView": { "socialPostId": "U29jaWFsUG9zdDo1MTA4OA==", "socialPostInfo": { "name": "Festive Offer - All Stores", "status": "SUCCESS", "type": "OFFER", "views": 4210, "clicks": 318, "sites": ["GOOGLE", "FACEBOOK"] }, "socialPostAnalyticsByLocationId": [ { "locationId": 1800289, "siteStats": [ { "site": "GOOGLE", "status": "PUBLISHED", "views": 1040, "clicks": 80 }, { "site": "FACEBOOK", "status": "PUBLISHED", "views": 360, "clicks": 26 } ] } ] } }}socialPostAnalyticsBySite totals the campaign across every store, while
socialPostAnalyticsByLocationId breaks the same numbers down per store. Scan
the per-store status values to find any location where a site failed.
List bulk campaigns on a location
GET /locations/{locationId}/bulk-posts
Offset-paginated under data.rollupSocialPosts. Unlike the per-location list,
this route does accept filters and sorting, which makes it the right endpoint
for a campaign dashboard.
Parameters
Sortable fields are name, type, status, created_at,
schedule_start_date, schedule_end_date, client_name, clicks, views
and shares.
curl -G https://listingsapi.com/api/v4/locations/TG9jYXRpb246MTgwMDI4OQ==/bulk-posts \-H "Authorization: API $LISTINGSAPI_KEY" \--data-urlencode 'page=1' \--data-urlencode 'perPage=10' \--data-urlencode 'sortFields={"field":"created_at","order":"Descending"}'{ "data": { "rollupSocialPosts": { "records": [ { "id": "U29jaWFsUG9zdDo1MTA4OA==", "name": "Festive Offer - All Stores", "status": "SUCCESS", "type": "OFFER", "createdAt": "2026-06-20", "views": 4210, "clicks": 318 } ], "pageInfo": { "totalPages": 1, "hasNextPage": false, "totalRecords": 1 } } }}Both SDKs JSON-encode the filters and sortFields objects for you, so pass
them as native dictionaries or objects.
Errors you will see
Full list in Error codes.
Next steps
- Publish posts: the task version of this page, with a scheduled campaign walked end to end.
- Check listing status and live links: confirm a location is live on Google before you post to it.
- Connect Google and import locations: a location needs a connected account before any site accepts a post.
- React to events with webhooks: the
local_postevents replace polling for publish results. - Posts reference for the full field list on every endpoint above.