نشر منشورات على Google وFacebook

انشر إعلانًا وفعالية وعرضًا، وجدول منشورًا واحدًا لمواقع متعددة، وتحقق من نشره على كل منصة، وأدر منشورات موقع، واستجب لإشعارات webhooks للمنشورات.

المنشور هو بطاقة مؤرخة على ملف الموقع في Google Business Profile أو صفحته على Facebook: إعلان أو فعالية أو عرض. ينشئه استدعاء واحد، وتُنفَّذ عملية النشر في الخلفية، ويخبرك استدعاء ثانٍ بما إذا كانت كل منصة قد قبلته.

تستخدم أنواع المنشورات الثلاثة جميعها POST /posts مع تغليف جسم الطلب داخل input. يُرفض جسم الطلب المسطح مع إرجاع 400. تنشئ حزم SDK الغلاف وتتحقق من صحة الحمولة قبل الإرسال.

الحقل inputالغرض
postNameاسم الحملة الداخلي. لا يُعرض للعملاء.
locationIdsمعرّفات المواقع بتنسيق base64.
postTypeANNOUNCEMENT أو EVENT أو OFFER.
postSitesGOOGLE أو FACEBOOK أو كلاهما.
postMessage[]{ site, message } واحد لكل منصة في postSites.
postCta[]{ site, type, url } اختياري. تكون قيمة type هي BOOK أو ORDER أو SHOP أو LEARN_MORE أو SIGN_UP أو GET_OFFER.
postMediaUrl[]{ site, url, type } اختياري، مع type بقيمة IMAGE أو VIDEO.
postContextInfoتواريخ الفعالية أو تفاصيل العرض. مطلوبة لكل من EVENT وOFFER.

1. نشر إعلان

يتضمن الإعلان رسالة وزرًا اختياريًا وصورة اختيارية.

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

احتفظ بـ socialPost.id. يبدأ status بالقيمة INPROGRESS ويظل postLinks فارغًا إلى أن تقبل إحدى المنصات المنشور؛ وتسترجع الخطوة 5 كليهما.

2. نشر فعالية

تضيف الفعالية postContextInfo الذي يتضمن title وstartDay وendDay وأوقات عرض اختيارية. يرفض Google أي فعالية بلا عنوان، وتُطلق حزم SDK الاستثناء ValidationError قبل الإرسال إذا كان العنوان مفقودًا.

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

تأتي الاستجابة بالبنية نفسها كما في الخطوة 1، مع type: "EVENT" والتواريخ المُرجَعة ضمن socialPost.contextInfo. يحتوي postContextInfo على تواريخ الفعالية نفسها؛ ويتحكم postScheduledDates (الخطوة 4) في وقت نشر المنشور نفسه ووقت إنهاء نشره.

3. نشر عرض

يتضمن postContextInfo الخاص بالعرض كلًا من title وcouponCode وdiscount وredeemUrl وtermsConditions وفترة الصلاحية المحددة بـ startDay وendDay. أرفقه بزر GET_OFFER على Facebook أو ORDER على Google؛ فلا يوجد نوع 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." }    ]  }}'

تُرجع الاستجابة حقول العرض ضمن socialPost.contextInfo وتضبط scheduledStartDate وscheduledEndDate استنادًا إلى فترة الصلاحية.

4. إرسال منشور واحد إلى مواقع متعددة وفق جدول زمني

يقبل POST /bulk-posts الحقل input نفسه ويرسل حملة واحدة إلى كل معرّف في locationIds. أضف postScheduledDates مع startDatetime وendDatetime بتنسيق ISO-8601 للنشر لاحقًا وإنهاء النشر تلقائيًا؛ ويُنشأ المنشور حينها بالحالة SCHEDULED. خصّص لكل منصة رسالة خاصة بها عندما تختلف النبرة.

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

تُرجع نقطة النهاية الخاصة بالعمليات المجمّعة البنية نفسها التي يُرجعها POST /posts، لذا تستخدم الاستجابة المفتاح createSocialPost أيضًا، مع status: "SCHEDULED" وكلا المعرّفين في locationIds. عند وجود مشكلة في التحقق من الصحة، تُرجع نقطة النهاية 200 مع success: false ومصفوفة errors تحتوي على عناصر؛ وتُطلق حزم SDK الاستثناء ValidationError لهذه المشكلة. استرجع الحملة المجمّعة باستخدام GET /bulk-posts/{postId}، الذي يضيف حالة لكل موقع.

5. التحقق من حالة النشر وجمع روابط المنشورات

يُرجع GET /posts/{postId} الحملة ضمن socialPostView. استعلم منه دوريًا بعد الإنشاء حتى تتغير قيمة socialPostInfo.status عن INPROGRESS، ثم اقرأ publishDetails[] لمعرفة نتيجة كل منصة وpostLinks[] للحصول على عناوين URL للمنشورات المنشورة فعليًا. اترك فاصلًا بعشرات الثواني بين الاستعلامات؛ وتأتي استجابة 429 مصحوبة بـ 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 }      ]    }  }}
الحقلالقيمالمعنى
socialPostInfo.statusINPROGRESS, SCHEDULED, SUCCESS, EXPIREDالحملة ككل. تعني SUCCESS أن عملية النشر انتهت، وليس أن كل منصة قبلت المنشور.
publishDetails[].statusPUBLISHING, PUBLISHED, ERRORعنصر واحد لكل منصة. عند ERROR، يوضح submissionError السبب.
postLinks[]{ site, link }عنوان URL الفعلي للمنشور على كل منصة، بمجرد نشره.

تتراكم views وclicks وreactions وshares وcomments في السجل نفسه، مع تفصيلها حسب المنصة في socialPostAnalyticsBySite. تُرجع حزم SDK القيمة socialPostInfo مباشرةً، ولهذا تقرأ الأمثلة post.status.

6. عرض منشورات موقع وحذف أحدها

GET /locations/{locationId}/posts صفحة تستخدم الإزاحة (page، perPage بحد أقصى 50) وتعرض جميع الحملات التي تشمل الموقع، ضمن postsByLocation مع records[] وpageInfo.totalRecords. مرّر معرّف base64 دون تغيير؛ ولا تُخضعه لترميز URL. لا يوجد عامل تصفية حسب النوع، لذا استخرج العروض أو الفعاليات من type في الشيفرة الخاصة بك. تُعرض الحملات متعددة المواقع من الخطوة 4 بشكل منفصل بواسطة GET /locations/{locationId}/bulk-posts.

يزيل DELETE /posts/{postId} الحملة هنا ويحذفها من كل منصة وصلت إليها. لا يمكن التراجع عن الحذف، لذا تحقّق من المعرّف في القائمة أولًا.

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"

تُعيد عملية الحذف data.deleteSocialPost مع success: true وsocialPostId الذي حذفته. أما حذف معرّف سبق حذفه فيُعيد استجابة errors مع data: null بدلًا من ذلك، لذا يسهل اكتشاف محاولة الحذف الثانية.

7. الاستجابة لإشعارات webhooks الخاصة بالمنشورات

عند تعيين عنوان URL لـ webhook في /dashboard/webhooks، تغطي أربعة أحداث دورة حياة المنشور، وتَرِد جميعها في الحمولة القياسية. تُطلق هذه الأحداث لكل موقع، وليس لكل حملة: فنشر منشور جماعي في 20 موقعًا ينتج عنه 20 قيمة لـ post_item_id تشترك جميعها في قيمة واحدة لـ post_id.

الحدثيُطلق عندماحقل data الإضافي
local_post.createdأُدرج العنصر في قائمة الانتظار لإرساله إلى Google. لم يُنشر بعد.
local_post.publishedقبلته Google وأصبح منشورًا.post_external_id، اسم المورد لدى Google.
local_post.rejectedرفضته Google أو فشلت عملية الإرسال.rejection_reason.
local_post.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 غير موجود، لذا واصل الاستعلام الدوري في الخطوة 5 للحصول على postLinks إذا كنت بحاجة إلى رابط قابل للنقر. أرسل استجابة 200 خلال 5 ثوانٍ؛ إذ لا تُعاد محاولة الإرسال. يرد شرح المستقبِل والتحقّق من التوقيع في الاستجابة للأحداث باستخدام webhooks.

الخطوات التالية