Pubblica post su Google e Facebook

Pubblica annunci, eventi e offerte, programma un post per più sedi, verificane l'uscita su ogni sito, gestisci i post per sede e reagisci ai webhook.

Un post è una scheda con una data sul Google Business Profile o sulla Pagina Facebook di una sede: un annuncio, un evento o un'offerta. Una chiamata lo crea, la pubblicazione avviene in background e una seconda chiamata ti indica se ciascun sito lo ha accettato.

Tutti e tre i tipi di post usano POST /posts con il corpo racchiuso in input. Un corpo privo di questa struttura viene rifiutato con 400. Gli SDK costruiscono la struttura contenitore e convalidano il payload prima dell'invio.

Campo inputScopo
postNameNome interno della campagna. Non viene mostrato ai clienti.
locationIdsId delle sedi in base64.
postTypeANNOUNCEMENT, EVENT o OFFER.
postSitesGOOGLE, FACEBOOK o entrambi.
postMessage[]Un { site, message } per sito in postSites.
postCta[]{ site, type, url } facoltativo. type è BOOK, ORDER, SHOP, LEARN_MORE, SIGN_UP o GET_OFFER.
postMediaUrl[]{ site, url, type } facoltativo con type pari a IMAGE o VIDEO.
postContextInfoDate dell'evento o dettagli dell'offerta. Obbligatorio per EVENT e OFFER.

1. Pubblica un annuncio

Un annuncio prevede un messaggio, un pulsante facoltativo e un'immagine facoltativa.

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]      }    }  }}

Conserva socialPost.id. status ha inizialmente il valore INPROGRESS e postLinks è vuoto finché un sito non accetta il post; al passaggio 5 vengono riletti entrambi.

2. Pubblica un evento

Un evento aggiunge postContextInfo con title, startDay, endDay e orari di visualizzazione facoltativi. Google rifiuta un evento senza titolo e, se il titolo manca, gli SDK generano ValidationError prima dell'invio.

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

La risposta ha la stessa struttura del passaggio 1, con type: "EVENT" e le date restituite sotto socialPost.contextInfo. postContextInfo contiene le date dell'evento stesso; postScheduledDates (passaggio 4) controlla quando il post viene pubblicato e ritirato.

3. Pubblica un'offerta

Il campo postContextInfo di un'offerta contiene title, couponCode, discount, redeemUrl, termsConditions e il periodo di validità definito da startDay e endDay. Abbina l'offerta a un pulsante GET_OFFER su Facebook o ORDER su Google; non esiste un tipo REDEEM.

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

La risposta restituisce i campi dell'offerta sotto socialPost.contextInfo e imposta scheduledStartDate e scheduledEndDate in base al periodo di validità.

4. Invia un post a più sedi, con una pubblicazione programmata

POST /bulk-posts accetta lo stesso input e invia una campagna a ogni id in locationIds. Aggiungi postScheduledDates con startDatetime e endDatetime in formato ISO-8601 per pubblicare in un secondo momento e ritirare automaticamente il post; il post viene quindi creato come SCHEDULED. Assegna a ciascun sito un messaggio specifico quando il tono cambia.

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"    }  }}'

L'endpoint per le operazioni in blocco restituisce la stessa struttura di POST /posts, quindi anche la risposta usa la chiave createSocialPost, con status: "SCHEDULED" ed entrambi gli id in locationIds. Un problema di convalida restituisce 200 con success: false e un array errors popolato; in questo caso gli SDK generano ValidationError. Rileggi una campagna in blocco con GET /bulk-posts/{postId}, che aggiunge lo stato per ciascuna sede.

5. Controlla lo stato di pubblicazione e raccogli i link dei post

GET /posts/{postId} restituisce la campagna sotto socialPostView. Interrogalo periodicamente dopo la creazione finché socialPostInfo.status non passa da INPROGRESS a un altro valore, quindi leggi publishDetails[] per l'esito su ciascun sito e postLinks[] per gli URL dei post pubblicati. Distanzia le interrogazioni di decine di secondi; 429 è accompagnato da 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 }      ]    }  }}
CampoValoriSignificato
socialPostInfo.statusINPROGRESS, SCHEDULED, SUCCESS, EXPIREDLa campagna nel suo complesso. SUCCESS significa che la pubblicazione è terminata, non che ogni sito abbia accettato il post.
publishDetails[].statusPUBLISHING, PUBLISHED, ERRORUna voce per sito. In caso di ERROR, submissionError ne indica il motivo.
postLinks[]{ site, link }L'URL del post pubblicato per ciascun sito, una volta completata la pubblicazione.

views, clicks, reactions, shares e comments si accumulano nello stesso record, suddivisi per sito in socialPostAnalyticsBySite. Gli SDK restituiscono direttamente socialPostInfo, motivo per cui gli esempi leggono post.status.

6. Elenca i post di una sede ed eliminane uno

GET /locations/{locationId}/posts è una pagina basata su offset (page, perPage fino a 50) di tutte le campagne che includono la sede, sotto postsByLocation con records[] e pageInfo.totalRecords. Passa l'id base64 senza modificarlo; non applicare la codifica URL. Non è disponibile alcun filtro per tipo, quindi seleziona le offerte o gli eventi da type nel tuo codice. Le campagne per più sedi del passaggio 4 vengono elencate separatamente da GET /locations/{locationId}/bulk-posts.

DELETE /posts/{postId} rimuove la campagna qui e da tutti i siti su cui è stata pubblicata. L'operazione non può essere annullata, quindi verifica prima l'id nell'elenco.

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"

Una richiesta di eliminazione restituisce data.deleteSocialPost con success: true e il socialPostId rimosso. L'eliminazione di un id già rimosso restituisce invece una risposta errors con data: null, quindi è facile rilevare una seconda richiesta di eliminazione.

7. Reagisci ai webhook dei post

Con un URL webhook impostato in /dashboard/webhooks, quattro eventi coprono il ciclo di vita del post, tutti nel payload standard. Si attivano per sede, non per campagna: un post inviato in blocco a 20 sedi produce 20 valori di post_item_id con un unico post_id condiviso.

EventoSi attiva quandoCampo data aggiuntivo
local_post.createdL'elemento viene messo in coda per Google. Non è ancora pubblicato.
local_post.publishedGoogle lo ha accettato ed è pubblicato.post_external_id, il nome della risorsa di Google.
local_post.rejectedGoogle lo ha rifiutato oppure l'invio non è riuscito.rejection_reason.
local_post.deletedL'elemento è stato eliminato.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 è solitamente assente, quindi continua a eseguire il polling del passaggio 5 per ottenere postLinks se ti serve un link cliccabile. Rispondi con 200 entro 5 secondi; non sono previsti nuovi tentativi. Il ricevitore e la verifica della firma sono descritti in Reagisci agli eventi con i webhook.

Passaggi successivi