GoogleとFacebookに投稿を公開する

お知らせ、イベント、特典を公開し、複数の拠点への1件の投稿を予約し、各サイトで公開されたことを確認し、拠点の投稿を管理し、投稿のWebhookに対応します。

投稿は、拠点のGoogle Business ProfileまたはFacebookページに表示される日付付きのカードで、お知らせ、イベント、特典のいずれかです。1回の呼び出しで投稿を作成すると、公開処理がバックグラウンドで実行され、2回目の呼び出しで各サイトが投稿を受け付けたかどうかを確認できます。

3種類の投稿はすべてPOST /postsを使用し、リクエストボディをinputでラップします。ラップされていないリクエストボディは400で拒否されます。SDKはラッパーを作成し、送信前にペイロードを検証します。

inputフィールド用途
postName内部用のキャンペーン名です。顧客には表示されません。
locationIdsbase64の拠点IDです。
postTypeANNOUNCEMENT、EVENT、またはOFFERです。
postSitesGOOGLE、FACEBOOK、またはその両方です。
postMessage[]postSites内のサイトごとに1つの{ site, message }です。
postCta[]任意の{ site, type, url }です。typeはBOOK、ORDER、SHOP、LEARN_MORE、SIGN_UP、またはGET_OFFERです。
postMediaUrl[]typeがIMAGEまたはVIDEOの、任意の{ site, url, type }です。
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. イベントを公開する

イベントでは、title、startDay、endDay、および任意の表示用時刻を含むpostContextInfoを追加します。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を含めます。FacebookではGET_OFFERボタン、GoogleではORDERボタンと組み合わせます。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. 複数の拠点への1件の投稿を予約する

POST /bulk-postsは同じinputを受け取り、locationIds内のすべてのIDに1つのキャンペーンを送信します。後で公開し、自動的に公開を終了するには、ISO-8601形式のstartDatetimeとendDatetimeを含むpostScheduledDatesを追加します。この場合、投稿は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内の両方のIDを含みます。検証に問題がある場合は、success: falseとエラー内容の入ったerrors配列を含む200が返され、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サイトごとに1つのエントリです。ERRORの場合、submissionErrorが理由を示します。
postLinks[]{ site, link }公開後の、サイトごとの公開URLです。

views、clicks、reactions、shares、commentsは同じレコードに蓄積され、socialPostAnalyticsBySite内にサイトごとの内訳が格納されます。SDKはsocialPostInfoを直接返すため、サンプルではpost.statusを読み取ります。

6. 拠点の投稿を一覧表示し、1件を削除する

GET /locations/{locationId}/postsは、その拠点を含むすべてのキャンペーンをオフセット方式のページ(page、perPageは最大50)で返します。結果はpostsByLocationの下のrecords[]とpageInfo.totalRecordsに格納されます。base64のIDは変更せずに渡し、URLエンコードしないでください。種類によるフィルターはないため、クライアント側のコードでtypeから特典やイベントを抽出してください。手順4の複数拠点向けキャンペーンは、GET /locations/{locationId}/bulk-postsで別途一覧表示されます。

DELETE /posts/{postId}は、ここにあるキャンペーンを削除し、配信先のすべてのサイトからも削除します。元に戻すことはできないため、先に一覧でIDを確認してください。

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"

削除すると、success: trueと削除したsocialPostIdを含むdata.deleteSocialPostが返されます。すでに削除されたIDを削除しようとすると、代わりにdata: nullを含むerrorsレスポンスが返されるため、2回目の削除を容易に検出できます。

7. 投稿のWebhookに反応する

/dashboard/webhooksにWebhook URLを設定すると、投稿のライフサイクルを網羅する4つのイベントが、いずれも標準のペイロードで送信されます。イベントはキャンペーン単位ではなく拠点単位で発生するため、20拠点への一括投稿では、1つの共通のpost_idを持つ20個のpost_item_id値が生成されます。

イベント発生するタイミング追加のdataフィールド
local_post.created項目がGoogleへの送信キューに入ります。まだ公開されていません。
local_post.publishedGoogleが受け入れ、公開されています。post_external_id(Googleのリソース名)です。
local_post.rejectedGoogleが拒否したか、送信に失敗しました。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を取得してください。5秒以内に200を返してください。再試行は行われません。受信処理と署名の検証については、Webhookでイベントに対応するを参照してください。

次の手順