Google과 Facebook에 게시물 게시하기

공지, 이벤트, 혜택을 게시하고, 여러 사업장에 하나의 게시물을 예약 게시하고, 각 사이트에 게시되었는지 확인하고, 사업장의 게시물을 관리하고, 게시물 웹훅에 대응합니다.

게시물은 사업장의 Google Business Profile 또는 Facebook 페이지에 표시되는 날짜가 있는 카드로, 공지, 이벤트 또는 혜택입니다. 한 번의 호출로 게시물을 생성하면 게시 작업이 백그라운드에서 실행되며, 두 번째 호출로 각 사이트에서 게시물을 수락했는지 확인할 수 있습니다.

3가지 게시물 유형 모두 POST /posts를 사용하며, 본문을 input으로 감싸야 합니다. 감싸지 않은 본문은 400과 함께 거부됩니다. SDK는 래퍼를 구성하고 전송 전에 페이로드를 검증합니다.

input 필드용도
postName내부 캠페인 이름입니다. 고객에게는 표시되지 않습니다.
locationIds사업장의 Base64 id입니다.
postTypeANNOUNCEMENT, EVENT 또는 OFFER입니다.
postSitesGOOGLE, FACEBOOK 또는 둘 다입니다.
postMessage[]postSites의 사이트마다 { 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. 여러 사업장에 하나의 게시물 예약 게시하기

POST /bulk-posts는 동일한 input을 받아 locationIds의 모든 id에 하나의 캠페인을 전송합니다. 나중에 게시하고 자동으로 게시를 종료하려면 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사이트당 항목이 하나씩 있습니다. ERROR인 경우 submissionError에 이유가 표시됩니다.
postLinks[]{ site, link }게시된 후 확인할 수 있는 사이트별 실제 게시 URL입니다.

views, clicks, reactions, shares, comments는 동일한 레코드에 누적되며, socialPostAnalyticsBySite에서 사이트별로 구분됩니다. SDK는 socialPostInfo를 직접 반환하므로 예제에서는 post.status를 읽습니다.

6. 사업장의 게시물 목록 조회 및 게시물 1개 삭제

GET /locations/{locationId}/posts는 해당 사업장이 포함된 모든 캠페인을 postsByLocation 아래에 records[] 및 pageInfo.totalRecords와 함께 제공하는 오프셋 페이지(page, perPage는 최대 50)입니다. 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 응답을 반환하므로 두 번째 삭제 요청을 쉽게 감지할 수 있습니다.

7. 게시물 웹훅에 대응

/dashboard/webhooks에 웹훅 URL을 설정하면 4가지 이벤트가 게시물의 수명 주기를 포괄하며, 모두 표준 페이로드로 전달됩니다. 이 이벤트는 캠페인별이 아닌 사업장별로 발생합니다. 사업장 20개에 일괄 게시하면 하나의 post_id를 공유하는 post_item_id 값 20개가 생성됩니다.

이벤트발생 시점추가 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으로 응답해야 하며, 재시도는 없습니다. 수신기와 서명 검증은 웹훅으로 이벤트에 대응에 설명되어 있습니다.

다음 단계