Google Business posts — list, create, update and delete
Posts are the updates, events and offers that appear on a business’s Google listing. They are served by
Google’s My Business API (v4), the same one as reviews. A post is public the moment it is created, so
MauKirim validates it against Google’s rules first and an invalid post spends no budget. All endpoints address the
business by its MauKirim id (gml_…) and need a package that includes the connector.
Every call here is one metered Google call; see rate limits.
The post object
Section titled “The post object”{ "id": "p1", "topicType": "STANDARD", "summary": "Diskon 20% hari ini", "state": "LIVE", "callToAction": { "actionType": "ORDER", "url": "https://toko.example/order" }, "mediaUrl": "https://cdn.example/a.jpg", "event": null, "offer": null, "searchUrl": "https://g.page/post/p1", "createdAt": "2026-10-14T03:00:00.000Z", "updatedAt": "2026-10-14T03:00:00.000Z"}| Field | Meaning |
|---|---|
id |
The post id to use in update and delete. Not Google’s full resource name |
topicType |
STANDARD, EVENT or OFFER |
state |
Google’s state, for example LIVE or REJECTED; null if Google did not say |
callToAction |
{ actionType, url } or null (url is null for CALL) |
mediaUrl |
The photo’s Google-hosted URL (or the source URL until Google has processed it), or null |
event |
{ title, startDate, endDate } (YYYY-MM-DD) for an EVENT; otherwise null |
offer |
{ couponCode, redeemOnlineUrl, termsConditions } for an OFFER; otherwise null |
searchUrl |
Where the post appears on Google Search, when Google provides it |
createdAt, updatedAt |
ISO timestamps from Google, or null |
What you can send
Section titled “What you can send”The body of a create or an update is a JSON object with only these fields; any other field is refused
(400 invalid_input), so state cannot be set from here.
| Field | Type | Rule |
|---|---|---|
summary |
string | Required. 1–1,500 characters |
topicType |
string | STANDARD (default), EVENT or OFFER |
languageCode |
string | Up to 16 characters, for example id or en |
callToAction |
object | { "actionType": "BOOK" | "ORDER" | "SHOP" | "LEARN_MORE" | "SIGN_UP" | "CALL", "url": "https://…" }. Every type but CALL needs an https url; CALL must not have one (Google uses the business’s phone) |
mediaUrl |
string | One photo, a public https URL that Google fetches |
event |
object | Required for EVENT, refused otherwise. title 1–58 characters, startDate and endDate as real YYYY-MM-DD dates with startDate not after endDate; optional startTime and endTime as HH:MM |
offer |
object | Only for OFFER. Optional couponCode (up to 58), redeemOnlineUrl (https) and termsConditions (up to 5,000) |
An OFFER post cannot carry event dates through this endpoint. If Google insists on a date range for offers
and refuses the post with 400 invalid_input, create it through the
google door (api: "reviews", path /localPosts) with Google’s own
body. We have not been able to confirm Google’s offer rules for every case, so we say so rather than promise.
GET /gmb/locations/{id}/posts
Section titled “GET /gmb/locations/{id}/posts”| Scope | read or gmb |
| Query | pageSize 1–100 (default 20), pageToken from the previous page |
curl "https://app.maukirim.com/api/v1/gmb/locations/gml_8f3kq2x1ab/posts?pageSize=20" \ -H "Authorization: Bearer mk_live_…"const response = await fetch(`https://app.maukirim.com/api/v1/gmb/locations/${id}/posts`, { headers: { Authorization: `Bearer ${process.env.MAUKIRIM_KEY}` },})const { posts, nextPageToken } = await response.json()$curl = curl_init("https://app.maukirim.com/api/v1/gmb/locations/$id/posts");curl_setopt_array($curl, [ CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('MAUKIRIM_KEY')], CURLOPT_RETURNTRANSFER => true,]);$posts = json_decode(curl_exec($curl), true)['posts'];{ "ok": true, "posts": [ { "id": "p1", "topicType": "STANDARD", "summary": "Diskon 20% hari ini", "…": "…" } ], "nextPageToken": null }POST /gmb/locations/{id}/posts
Section titled “POST /gmb/locations/{id}/posts”Publishes a post. 201 Created.
| Scope | gmb |
| Body | application/json, see above |
| Header | Idempotency-Key (optional, strongly recommended): 8–128 characters of letters, digits and _ . : - |
curl -X POST https://app.maukirim.com/api/v1/gmb/locations/gml_8f3kq2x1ab/posts \ -H "Authorization: Bearer mk_live_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: post-gml_8f3kq2x1ab-promo-oct" \ -d '{ "summary": "Diskon 20% hari ini", "callToAction": { "actionType": "ORDER", "url": "https://toko.example/order" }, "mediaUrl": "https://cdn.example/a.jpg" }'const response = await fetch(`https://app.maukirim.com/api/v1/gmb/locations/${id}/posts`, { method: "POST", headers: { Authorization: `Bearer ${process.env.MAUKIRIM_KEY}`, "Content-Type": "application/json", "Idempotency-Key": `post-${id}-promo-oct`, }, body: JSON.stringify({ topicType: "EVENT", summary: "Grand opening", event: { title: "Grand opening", startDate: "2026-12-01", endDate: "2026-12-02", startTime: "09:00" }, }),})const { post, replayed } = await response.json()$curl = curl_init("https://app.maukirim.com/api/v1/gmb/locations/$id/posts");curl_setopt_array($curl, [ CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('MAUKIRIM_KEY'), 'Content-Type: application/json', "Idempotency-Key: post-$id-promo-oct", ], CURLOPT_POSTFIELDS => json_encode(['summary' => 'Diskon 20% hari ini', 'callToAction' => ['actionType' => 'CALL']]), CURLOPT_RETURNTRANSFER => true,]);$post = json_decode(curl_exec($curl), true)['post'];{ "ok": true, "replayed": false, "post": { "id": "p2", "topicType": "STANDARD", "state": "LIVE", "…": "…" } }The same Idempotency-Key with the same body returns the stored answer with "replayed": true and no second
post; the same key with a different body is 409 gmb_idempotency_conflict. Without a key, a retry after a
timeout can publish the post twice. A key is shared by every write on your account for 24 hours, so use a
different key for each different post.
PATCH /gmb/locations/{id}/posts/{postId}
Section titled “PATCH /gmb/locations/{id}/posts/{postId}”Changes an existing post. The body takes the same fields as a create, but only the ones you send are changed
and nothing is defaulted — in particular topicType — so sending just a new summary to an event or an offer
keeps it an event or an offer. An empty body is refused (invalid_input). Returns { "ok": true, "post": { … } }.
| Scope | gmb |
| Path | id the business; postId from the list or the create answer |
curl -X PATCH https://app.maukirim.com/api/v1/gmb/locations/gml_8f3kq2x1ab/posts/p1 \ -H "Authorization: Bearer mk_live_…" -H "Content-Type: application/json" \ -d '{ "summary": "Diskon 30% sampai Jumat" }'A change is idempotent by nature (the same body gives the same post), so it takes no Idempotency-Key.
DELETE /gmb/locations/{id}/posts/{postId}
Section titled “DELETE /gmb/locations/{id}/posts/{postId}”Removes the post from the public listing. It cannot be undone. Returns { "ok": true }.
| Scope | gmb |
curl -X DELETE https://app.maukirim.com/api/v1/gmb/locations/gml_8f3kq2x1ab/posts/p1 -H "Authorization: Bearer mk_live_…"A postId that is not a plain id (letters, digits and _ . = -) is refused before Google is called.
Errors
Section titled “Errors”| HTTP | Code | Condition |
|---|---|---|
| 400 | invalid_input |
A field outside the list above, a value outside its limits, event without EVENT (or missing for EVENT), a CALL button with a url, a malformed Idempotency-Key, or Google rejected the post |
| 401 | api_key_invalid / api_key_revoked / api_key_expired |
|
| 402 | gmb_plan_required |
The package does not include the connector |
| 403 | api_key_scope_denied |
Writes need gmb; reads need read or gmb |
| 403 | gmb_forbidden |
Google says the connected account may not post for this business |
| 404 | gmb_location_not_found |
Not your business, no such business, it left Google, or (update, delete) Google has no such post |
| 409 | gmb_reconnect_required |
The connection must be reconnected in the dashboard |
| 409 | gmb_location_disabled |
The business or its connection is switched off |
| 409 | gmb_idempotency_conflict |
The key was used with a different body |
| 429 | gmb_rate_limited |
No budget now (Retry-After); also a request whose key is still running |
| 502 | gmb_upstream_error |
Google failed |
| 503 | gmb_api_unavailable |
The Business Profile API is not enabled or approved for the deployment’s Google project |