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.

input fieldPurpose
postNameInternal campaign name. Not shown to customers.
locationIdsBase64 location ids.
postTypeANNOUNCEMENT, EVENT, or OFFER.
postSitesGOOGLE, FACEBOOK, or both.
postMessage[]One { site, message } per site in postSites.
postCta[]Optional { site, type, url }. type is BOOK, ORDER, SHOP, LEARN_MORE, SIGN_UP, or GET_OFFER.
postMediaUrl[]Optional { site, url, type } with type IMAGE or VIDEO.
postContextInfoEvent dates or offer details. Required for EVENT and OFFER.

1. Publish an announcement

The simplest post: a message, an optional button, an optional image.

Announcement
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" }    ]  }}'
JSON
{  "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.

Event
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.

Offer
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.

Bulk post
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.

Post status
curl https://listingsapi.com/api/v4/posts/U29jaWFsUG9zdDo0NDEyMg== \-H "Authorization: API $LISTINGSAPI_KEY"
JSON
{  "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 }      ]    }  }}
FieldValuesMeaning
socialPostInfo.statusINPROGRESS, SCHEDULED, SUCCESS, EXPIREDThe campaign as a whole. SUCCESS means publishing finished, not that every site accepted it.
publishDetails[].statusPUBLISHING, PUBLISHED, ERROROne entry per site. On ERROR, submissionError says why.
postLinks[]{ site, link }The live URL per site, once published.

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.

List and delete
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.

EventFires whenExtra data field
local_post.createdThe item is queued for Google. Not live yet.
local_post.publishedGoogle accepted it and it is live.post_external_id, Google's resource name.
local_post.rejectedGoogle rejected it or submission failed.rejection_reason.
local_post.deletedThe item was deleted.post_external_id.
Route local_post events
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