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

MethodPathWhat it doesReference
POST/postsPublish an announcement, a plain message with optional button and image.Create an announcement post
POST/postsPublish an event, with a title and a start and end window.Create an event post
POST/postsPublish an offer, with a coupon code, discount and terms.Create an offer post
GET/posts/{postId}One campaign with per-site publish status and engagement counts.Get a post
GET/locations/{locationId}/postsA page of campaigns that target one location.Fetch posts for a location
DELETE/posts/{postId}Permanently delete a campaign and unpublish it everywhere.Delete a post
POST/bulk-postsOne campaign fanned out across many locations.Create a bulk post
GET/bulk-posts/{postId}One bulk campaign with a per-location analytics breakdown.Fetch a bulk post by ID
GET/locations/{locationId}/bulk-postsBulk campaigns that include one location, with filters and sorting.Fetch bulk posts for a location

Values you will use everywhere

All four enums below are shared by every create endpoint.

FieldAccepted values
postTypeANNOUNCEMENT, EVENT, OFFER, COVID19, PRODUCT
postSites and every siteGOOGLE, FACEBOOK
postCta[].typeBOOK, ORDER, SHOP, LEARN_MORE, SIGN_UP, GET_OFFER
postMediaUrl[].typeIMAGE, VIDEO

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

NameInRequiredNotes
input.postNamebodyYesInternal campaign name, never shown to customers.
input.locationIdsbodyYesArray of base64 location ids.
input.postTypebodyYesANNOUNCEMENT here.
input.postSitesbodyYesArray of GOOGLE and FACEBOOK.
input.postMessagebodyNoArray of objects with site and message.
input.postCtabodyNoArray of objects with site, type, url.
input.postMediaUrlbodyNoArray of objects with site, url, type.
input.clientMutationIdbodyNoEchoed back for correlation.
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://jennyhome.example/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. 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

NameInRequiredNotes
input.postTypebodyYesEVENT here.
input.postContextInfo.titlebodyYes in practiceEvent title. Google rejects events without one.
input.postContextInfo.startDay, endDaybodyNoDates, YYYY-MM-DD.
input.postContextInfo.startTime, endTimebodyNoDisplay strings such as 10:00am.
input.postScheduledDatesbodyNoObject with startDatetime, endDatetime, removalSites.

Everything from the announcement table also applies.

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 and up to 40 percent off in store." }    ]  }}'
JSON
{  "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

NameInRequiredNotes
input.postTypebodyYesOFFER here.
input.postContextInfo.titlebodyYes in practiceLabels the offer. Both SDKs require it.
input.postContextInfo.couponCodebodyNoCode the customer redeems.
input.postContextInfo.discountbodyNoFree text such as 25%.
input.postContextInfo.redeemUrlbodyNoWhere the offer is redeemed.
input.postContextInfo.termsConditionsbodyNoFine print.
input.postContextInfo.startDay, endDaybodyNoValidity window, YYYY-MM-DD.
Offer
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" }    ]  }}'
JSON
{  "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

NameInRequiredNotes
postIdpathYesBase64 post id from the create response or a list.
Get a post
curl https://listingsapi.com/api/v4/posts/U29jaWFsUG9zdDo0NDEyMg== \-H "Authorization: API $LISTINGSAPI_KEY"
JSON
{  "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.

FieldValueMeaning
socialPostInfo.statusINPROGRESSAccepted, publishing is still running.
SUCCESSPublishing finished. Read publishDetails for the per-site result.
SCHEDULEDWaiting for the postScheduledDates window to open.
EXPIREDThe scheduled window closed and the post is no longer live.
publishDetails[].statusPUBLISHINGThat site has not answered yet.
PUBLISHEDLive on that site. Look in postLinks for the URL.
ERRORThat site refused it. submissionError says why.

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

NameInRequiredNotes
locationIdpathYesBase64 id, passed verbatim.
pagequeryNoOne-based page number, default 1.
perPagequeryNoDefault 10, must not exceed 50.
Posts on a location
curl "https://listingsapi.com/api/v4/locations/TG9jYXRpb246MTgwMDI4OQ==/posts?page=1&perPage=10" \-H "Authorization: API $LISTINGSAPI_KEY"
JSON
{  "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

NameInRequiredNotes
postIdpathYesBase64 post id.
clientMutationIdbodyNoOptional, echoed back for correlation.
Delete
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" }'
JSON
{  "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

NameInRequiredNotes
input.postNamebodyYesInternal campaign name.
input.locationIdsbodyYesEvery base64 location id to publish to.
input.postTypebodyYesEVENT, OFFER and PRODUCT also need postContextInfo or products.
input.postSitesbodyYesGOOGLE, FACEBOOK, or both.
input.postScheduledDatesbodyNostartDatetime and endDatetime as ISO 8601. Creates the post as SCHEDULED.
input.componentIdentifierbodyNoYour own reference, useful as a filter later.
Bulk publish
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"    }  }}'
JSON
{  "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

NameInRequiredNotes
postIdpathYesBase64 bulk post id, not a raw integer.
Get a bulk post
curl https://listingsapi.com/api/v4/bulk-posts/U29jaWFsUG9zdDo1MTA4OA== \-H "Authorization: API $LISTINGSAPI_KEY"
JSON
{  "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

NameInRequiredNotes
locationIdpathYesBase64 id, passed verbatim.
page, perPagequeryNoOffset pagination.
filtersqueryNoJSON-encoded, by type, status, creationDateRange or siteUrls.
sortFieldsqueryNoJSON-encoded, such as field created_at with order Descending.
locationIdsqueryNoJSON-encoded array of extra location ids to include.
componentIdentifierqueryNoLimits results to campaigns your integration created.

Sortable fields are name, type, status, created_at, schedule_start_date, schedule_end_date, client_name, clicks, views and shares.

Bulk posts on a location
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"}'
JSON
{  "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

Code or statusMeaningFix
400 missing $input variableThe body was not wrapped in input.Nest every field under a single input key.
SY90016 as HTTP 400A Read key was used on a create or delete.Use a Write key.
401Missing or invalid key.Send the literal word API, a space, then the key.
429 with Retry-AfterToo many calls, often from polling a post's status.Wait the advertised seconds. See Rate limits.
200 with success: falseValidation failed inside the mutation.Read errors in data.createSocialPost.
per_page must not exceed 50perPage above the cap on the location list.Ask for 50 or fewer.
tag is not a supported parameter for this querytag sent to the per-location posts route.Send only page and perPage.

Full list in Error codes.

Next steps