Publier des publications sur Google et Facebook

Publiez annonces, événements et offres, programmez-les pour plusieurs établissements, vérifiez leur mise en ligne, gérez-les et traitez les webhooks.

Une publication est une fiche datée sur le profil Google Business Profile ou la page Facebook d'un établissement : une annonce, un événement ou une offre. Un premier appel la crée, la publication s'effectue en arrière-plan et un second appel vous indique si chaque site l'a acceptée.

Les trois types de publication utilisent POST /posts avec le corps enveloppé dans input. Un corps à plat est rejeté avec un 400. Les SDK construisent l'enveloppe et valident les données avant l'envoi.

Champ inputRôle
postNameNom interne de la campagne. Non affiché aux clients.
locationIdsIdentifiants des établissements en base64.
postTypeANNOUNCEMENT, EVENT ou OFFER.
postSitesGOOGLE, FACEBOOK ou les deux.
postMessage[]Un { site, message } par site dans postSites.
postCta[]{ site, type, url } facultatif. type vaut BOOK, ORDER, SHOP, LEARN_MORE, SIGN_UP ou GET_OFFER.
postMediaUrl[]{ site, url, type } facultatif avec type IMAGE ou VIDEO.
postContextInfoDates de l'événement ou détails de l'offre. Obligatoires pour EVENT et OFFER.

1. Publier une annonce

Une annonce comprend un message, un bouton facultatif et une image facultative.

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

Conservez socialPost.id. status a pour valeur initiale INPROGRESS et postLinks reste vide jusqu'à ce qu'un site accepte la publication ; l'étape 5 permet de relire ces deux champs.

2. Publier un événement

Un événement ajoute postContextInfo avec un title, un startDay, un endDay et des horaires d'affichage facultatifs. Google rejette un événement sans titre, et les SDK lèvent ValidationError avant l'envoi si le titre est absent.

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 réponse a la même structure qu'à l'étape 1, avec type: "EVENT" et les dates renvoyées sous socialPost.contextInfo. postContextInfo contient les dates propres à l'événement ; postScheduledDates (étape 4) détermine quand la publication elle-même est mise en ligne et retirée.

3. Publier une offre

Le champ postContextInfo d'une offre contient un title, un couponCode, un discount, un redeemUrl, un termsConditions et la période de validité définie par startDay et endDay. Associez l'offre à un bouton GET_OFFER sur Facebook ou ORDER sur Google ; il n'existe pas de type 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 réponse renvoie les champs de l'offre sous socialPost.contextInfo et définit scheduledStartDate et scheduledEndDate à partir de la période de validité.

4. Envoyer une publication à plusieurs établissements selon un calendrier

POST /bulk-posts accepte le même input et envoie une campagne à chaque id dans locationIds. Ajoutez postScheduledDates avec startDatetime et endDatetime au format ISO-8601 pour publier ultérieurement et retirer automatiquement la publication ; celle-ci est alors créée avec le statut SCHEDULED. Attribuez à chaque site son propre message lorsque le ton diffère.

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

Le point de terminaison d'envoi groupé renvoie la même structure que POST /posts ; la réponse utilise donc elle aussi la clé createSocialPost, avec status: "SCHEDULED" et les deux identifiants dans locationIds. Un problème de validation entraîne le renvoi de 200 avec success: false et un tableau errors renseigné ; les SDK lèvent alors ValidationError. Relisez une campagne d'envoi groupé avec GET /bulk-posts/{postId}, qui ajoute un statut par établissement.

5. Vérifier le statut de publication et récupérer les liens des publications

GET /posts/{postId} renvoie la campagne sous socialPostView. Interrogez ce point de terminaison après une création jusqu'à ce que socialPostInfo.status quitte l'état INPROGRESS, puis lisez publishDetails[] pour connaître le résultat sur chaque site et postLinks[] pour obtenir les URL des publications en ligne. Espacez les interrogations de plusieurs dizaines de secondes ; un 429 est accompagné de 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 }      ]    }  }}
ChampValeursSignification
socialPostInfo.statusINPROGRESS, SCHEDULED, SUCCESS, EXPIREDLa campagne dans son ensemble. SUCCESS signifie que le processus de publication est terminé, et non que chaque site a accepté la publication.
publishDetails[].statusPUBLISHING, PUBLISHED, ERRORUne entrée par site. En cas de ERROR, submissionError en indique la raison.
postLinks[]{ site, link }L'URL de la publication en ligne pour chaque site, une fois celle-ci publiée.

views, clicks, reactions, shares et comments s'accumulent dans le même enregistrement, avec une ventilation par site dans socialPostAnalyticsBySite. Les SDK renvoient directement socialPostInfo, ce qui explique pourquoi les exemples lisent post.status.

6. Répertorier les publications d'un établissement et en supprimer une

GET /locations/{locationId}/posts est une page paginée par décalage (page, perPage jusqu'à 50) contenant toutes les campagnes qui incluent l'établissement, sous postsByLocation avec records[] et pageInfo.totalRecords. Transmettez l'identifiant base64 sans le modifier ; ne l'encodez pas pour une URL. Il n'y a pas de filtre par type : sélectionnez donc les offres ou les événements à partir de type dans votre propre code. Les campagnes couvrant plusieurs établissements de l'étape 4 sont répertoriées séparément par GET /locations/{locationId}/bulk-posts.

DELETE /posts/{postId} supprime la campagne ici et la retire de tous les sites sur lesquels elle a été diffusée. Cette action est irréversible : vérifiez donc d'abord l'identifiant dans la liste.

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"

Une suppression renvoie data.deleteSocialPost avec success: true et le socialPostId supprimé. La suppression d'un identifiant déjà supprimé renvoie à la place une réponse errors avec data: null, ce qui permet de détecter facilement une seconde suppression.

7. Réagir aux webhooks des publications

Lorsqu'une URL de webhook est définie dans /dashboard/webhooks, quatre événements couvrent le cycle de vie d'une publication, tous dans la charge utile standard. Ils sont déclenchés par établissement, et non par campagne : une publication en masse destinée à 20 établissements produit 20 valeurs post_item_id avec un seul post_id commun.

ÉvénementDéclenché lorsqueChamp data supplémentaire
local_post.createdL'élément est en file d'attente pour Google. Il n'est pas encore en ligne.
local_post.publishedGoogle l'a accepté et il est en ligne.post_external_id, le nom de ressource attribué par Google.
local_post.rejectedGoogle l'a rejeté ou l'envoi a échoué.rejection_reason.
local_post.deletedL'élément a été supprimé.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 est généralement absent : continuez donc à interroger l'API comme à l'étape 5 pour obtenir postLinks si vous avez besoin d'un lien cliquable. Renvoyez 200 dans les 5 secondes ; aucune nouvelle tentative n'est effectuée. Le récepteur et la vérification de la signature sont décrits dans Réagir aux événements avec des webhooks.

Étapes suivantes