Google Business locations — list, read, update, create and delete
Businesses are the Google Business Profile locations reachable through the Google accounts you
connected in the dashboard. Each has a MauKirim id (gml_…) used in every other
call. These endpoints need a package that includes the connector; without it every one answers
402 gmb_plan_required. What else you can do with a business is on its own pages:
posts, photos, insights,
attributes and categories,
verification, managers, reviews and the
Google door.
A business can also be created — POST /gmb/connections/{id}/locations,
documented with the connections it is made under — and deleted from Google (below).
Businesses found on connect or sync arrive switched off. When a Google account is connected or synced, every
business MauKirim finds for the first time is imported with status: "disabled" and disabledBy: "user". A
switched-off business costs no allowance: only active businesses count against your package’s limit. You choose
which ones to turn on, now or later, with POST /gmb/locations/activation. Nothing is
lost by leaving one off: it stays in your list, a later sync keeps its details fresh without turning it on, and turning
it on later takes one call.
GET /gmb/locations
Section titled “GET /gmb/locations”Lists your businesses from MauKirim’s own table, ordered by title. It makes no Google call and spends no budget, so it is safe to call often.
| Scope | read or gmb |
| Auth | Authorization: Bearer mk_live_… |
| Query param | Type | Default | Validation |
|---|---|---|---|
connectionId |
string | — | Only the businesses of one connection (gmc_…) |
externalId |
string | — | Only the businesses connected for one of your own clients through the hosted connect flow — your id for them. An empty value (?externalId=) keeps only the businesses you connected yourself |
status |
string | — | active, removed_upstream or disabled; anything else is 400 invalid_input |
q |
string | — | Case-insensitive match in the title and address |
limit |
integer | 50 | 1–200 |
offset |
integer | 0 | 0 or more |
curl "https://app.maukirim.com/api/v1/gmb/locations?status=active&q=kopi&limit=2" \ -H "Authorization: Bearer mk_live_…"const response = await fetch("https://app.maukirim.com/api/v1/gmb/locations?status=active&q=kopi&limit=2", { headers: { Authorization: `Bearer ${process.env.MAUKIRIM_KEY}` },})const { total, locations } = await response.json()$curl = curl_init('https://app.maukirim.com/api/v1/gmb/locations?status=active&q=kopi&limit=2');curl_setopt_array($curl, [ CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('MAUKIRIM_KEY')], CURLOPT_RETURNTRANSFER => true,]);$data = json_decode(curl_exec($curl), true);{ "ok": true, "total": 3, "locations": [ { "id": "gml_8f3kq2x1ab", "connectionId": "gmc_2m9dq7zk0p", "connectionLabel": "Agency account", "connectionEmail": "agency@example.com", "connectionStatus": "active", "externalId": "", "title": "Kopi Nusantara 1-1", "addressSummary": "Jl. Melati 1, Jakarta, DKI Jakarta 10110", "primaryCategory": "Coffee shop", "phone": "+62 21 5550 100", "websiteUri": "https://kopi1.example.com", "placeId": "place_1_0", "verificationState": "verified", "status": "active", "disabledBy": null, "lastSyncedAt": "2026-10-14T01:05:00.000Z" } ]}total is the number of businesses matching the filters, not the page size: page with offset until
offset + locations.length >= total.
| Field | Meaning |
|---|---|
id |
MauKirim business id. Use it in every other call |
connectionId, connectionLabel, connectionEmail, connectionStatus |
The Google account the business is reached through (the list includes these; the single read does not) |
externalId |
Your id for the end user whose Google account this business came through (see Connect your clients), or "" for a business you connected yourself. Present in the list; the single read does not carry it |
title, addressSummary, primaryCategory, phone, websiteUri, placeId |
Public profile data as of the last read. Any can be null |
verificationState |
Whether Google lets this business owner manage it publicly: verified or unverified, or null if Google did not say |
status |
active, removed_upstream or disabled — see statuses |
disabledBy |
Who switched a disabled business off; null for any other status. user: it is waiting for your choice (every newly found business starts here) or you switched it off — turn it on with POST /gmb/locations/activation. operator: MauKirim support switched it off and it cannot be turned on from the API. connection: its Google connection was lost, so reconnect it |
lastSyncedAt |
When MauKirim last read it from Google |
To find the businesses waiting for a choice, list with ?status=disabled (and externalId for one of your own
clients’ businesses). The single read, GET /gmb/locations/{id}, carries disabledBy too.
Errors
Section titled “Errors”| HTTP | Code | Condition |
|---|---|---|
| 400 | invalid_input |
A bad status, limit or offset |
| 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 |
The key has neither read nor gmb; forbidden for an inactive account |
| 500 | server_error |
Unexpected |
POST /gmb/locations/activation
Section titled “POST /gmb/locations/activation”Chooses which businesses are switched on. Businesses found when a Google account is connected or synced arrive switched off and cost no allowance; this is how you turn the ones you want on, and turn others off again.
| Scope | gmb |
| Auth | Authorization: Bearer mk_live_… |
| Body | JSON object with activate and/or deactivate: lists of business ids (gml_…), at most 200 each. At least one id in total; no other field |
| Idempotency | Not needed. Setting a state a business already has changes nothing, so a retry is safe |
curl -X POST https://app.maukirim.com/api/v1/gmb/locations/activation \ -H "Authorization: Bearer mk_live_…" \ -H "Content-Type: application/json" \ -d '{"activate": ["gml_8f3kq2x1ab", "gml_4d7wn0c9ze"], "deactivate": ["gml_1a2b3c4d5e"]}'const response = await fetch("https://app.maukirim.com/api/v1/gmb/locations/activation", { method: "POST", headers: { Authorization: `Bearer ${process.env.MAUKIRIM_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ activate: ["gml_8f3kq2x1ab", "gml_4d7wn0c9ze"], deactivate: ["gml_1a2b3c4d5e"] }),})const { activated, deactivated, active, maxLocations } = await response.json()$curl = curl_init('https://app.maukirim.com/api/v1/gmb/locations/activation');curl_setopt_array($curl, [ CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('MAUKIRIM_KEY'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'activate' => ['gml_8f3kq2x1ab', 'gml_4d7wn0c9ze'], 'deactivate' => ['gml_1a2b3c4d5e'], ]), CURLOPT_RETURNTRANSFER => true,]);$result = json_decode(curl_exec($curl), true);{ "ok": true, "activated": ["gml_8f3kq2x1ab", "gml_4d7wn0c9ze"], "deactivated": ["gml_1a2b3c4d5e"], "active": 9, "maxLocations": 10}| Field | Meaning |
|---|---|
activated |
Businesses this call switched on. One that was already on is left out |
deactivated |
Businesses this call switched off. One that was already off is left out |
active |
How many businesses are on after the change |
maxLocations |
Your package’s allowance of active businesses; null when it has none |
It is atomic against your allowance. Switching off frees allowance and is applied in the same call, so a call that
turns three on and two off needs room for one more. If the result would exceed the allowance the call answers
409 gmb_location_limit and changes nothing: no business is half switched. Switch some others off, or upgrade, and
send it again.
What cannot be switched on from here. A business MauKirim support switched off (disabledBy: "operator") is
409 gmb_location_disabled; one whose Google connection was lost (disabledBy: "connection") is
409 gmb_reconnect_required, because only the account owner can reconnect it; one
that has left Google (removed_upstream) is 404 gmb_location_not_found.
Switching off is local and reversible: the public Google listing is not touched, nothing is deleted, and the business
can be switched on again later (while the allowance has room). A business that is off is not read for reviews, posts,
photos or any other call: those answer 409 gmb_location_disabled until you turn it on.
The call makes no Google request and spends no budget.
Errors
Section titled “Errors”| HTTP | Code | Condition |
|---|---|---|
| 400 | invalid_input |
The body is not a JSON object, has a field other than activate / deactivate, names no business, has a malformed or more than 200 ids, or lists the same id in both |
| 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 |
The key lacks gmb |
| 404 | gmb_location_not_found |
An id is not your business (the same answer as for one that does not exist), or it has left Google. Nothing was changed |
| 409 | gmb_location_limit |
Switching on would exceed the allowance; nothing was changed |
| 409 | gmb_location_disabled / gmb_reconnect_required |
A business was switched off by MauKirim support, or its connection was lost; nothing was changed |
| 500 | server_error |
Unexpected |
GET /gmb/locations/{id}
Section titled “GET /gmb/locations/{id}”Reads one business.
| Scope | read or gmb |
| Path | id — the business id (gml_…) |
| Query | live — 1 or true to read it from Google now |
Stored copy (default). Free. It answers even for a business that is disabled or removed_upstream:
curl https://app.maukirim.com/api/v1/gmb/locations/gml_8f3kq2x1ab \ -H "Authorization: Bearer mk_live_…"{ "ok": true, "live": false, "location": { "id": "gml_8f3kq2x1ab", "connectionId": "gmc_2m9dq7zk0p", "title": "Kopi Nusantara 1-1", "addressSummary": "Jl. Melati 1, Jakarta, DKI Jakarta 10110", "primaryCategory": "Coffee shop", "phone": "+62 21 5550 100", "websiteUri": "https://kopi1.example.com", "placeId": "place_1_0", "verificationState": "verified", "status": "active", "lastSyncedAt": "2026-10-14T01:05:00.000Z" }}Live (?live=1). MauKirim re-reads the business from Google (one metered call), updates the stored
copy, and also returns the two fields the list does not carry: the description and the opening hours.
curl "https://app.maukirim.com/api/v1/gmb/locations/gml_8f3kq2x1ab?live=1" \ -H "Authorization: Bearer mk_live_…"const response = await fetch(`https://app.maukirim.com/api/v1/gmb/locations/${id}?live=1`, { headers: { Authorization: `Bearer ${process.env.MAUKIRIM_KEY}` },})const { location, description, regularHours } = await response.json()$curl = curl_init("https://app.maukirim.com/api/v1/gmb/locations/$id?live=1");curl_setopt_array($curl, [ CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('MAUKIRIM_KEY')], CURLOPT_RETURNTRANSFER => true,]);$data = json_decode(curl_exec($curl), true);{ "ok": true, "live": true, "location": { "id": "gml_8f3kq2x1ab", "title": "Kopi Nusantara 1-1", "status": "active", "…": "…" }, "description": "Fresh coffee, daily.", "regularHours": { "periods": [ { "openDay": "MONDAY", "openTime": { "hours": 8 }, "closeDay": "MONDAY", "closeTime": { "hours": 17 } } ] }}description and regularHours are null when the business has none. A live read of a disabled business
is 409 gmb_location_disabled; of a removed_upstream one, 404 gmb_location_not_found.
Errors
Section titled “Errors”| HTTP | Code | Condition |
|---|---|---|
| 402 | gmb_plan_required |
The package does not include the connector |
| 403 | api_key_scope_denied |
Neither read nor gmb |
| 403 | gmb_forbidden |
(live) Google says the connected account has no rights over the business |
| 404 | gmb_location_not_found |
Not your business, no such business, or (live) it left Google. Identical for all of these — the API never confirms that an id is real |
| 409 | gmb_reconnect_required |
(live) The connection must be reconnected in the dashboard |
| 409 | gmb_location_disabled |
(live) The business or its connection is switched off |
| 429 | gmb_rate_limited |
(live) No budget now; Retry-After and retryAfterSeconds say how long |
| 502 | gmb_upstream_error |
(live) Google failed |
PATCH /gmb/locations/{id}
Section titled “PATCH /gmb/locations/{id}”Changes the public Google listing of one business. Only an allow-listed set of fields can be changed;
anything else is refused rather than ignored. To change a category, find its resource name first with
GET …/categories.
| Scope | gmb |
| Body | application/json, at least one field |
| Header | Idempotency-Key (optional, recommended): 8–128 characters of letters, digits and _ . : - |
| Field | Type | Limits |
|---|---|---|
title |
string | 1–100 characters |
description |
string | 0–750 characters (empty clears it) |
phone |
string | Up to 32 characters, digits and + ( ) . - space, starting with + or a digit |
websiteUri |
string | An absolute http or https URL, up to 2000 characters |
regularHours |
object | Google’s regularHours shape ({ "periods": [ … ] }) |
specialHours |
object | Google’s specialHours shape ({ "specialHourPeriods": [ … ] }): holidays and other days that differ from the regular hours |
openStatus |
string | OPEN, CLOSED or CLOSED_TEMPORARILY |
categories |
array | 1–10 category resource names such as categories/gcid:coffee_shop (letters, digits and _ : . - after categories/). The first is the primary category |
labels |
array | Up to 10 texts of 1–255 characters, your own tags for the business (an empty list clears them) |
Anything else — address, status, placeId… — answers 400 invalid_input. A category that Google does not
know is rejected by Google as 400 invalid_input too. A body with no field is
400 invalid_input too. MauKirim checks the limits before it calls Google, so an invalid edit spends no budget.
curl -X PATCH https://app.maukirim.com/api/v1/gmb/locations/gml_8f3kq2x1ab \ -H "Authorization: Bearer mk_live_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: edit-gml_8f3kq2x1ab-2026-10-14" \ -d '{ "title": "Kopi Nusantara — Melati", "phone": "+62 21 5550 200", "description": "Fresh coffee, daily, until 17:00." }'
# Close a business temporarily and set its categories (primary first):curl -X PATCH https://app.maukirim.com/api/v1/gmb/locations/gml_8f3kq2x1ab \ -H "Authorization: Bearer mk_live_…" -H "Content-Type: application/json" \ -H "Idempotency-Key: edit-gml_8f3kq2x1ab-closed-1" \ -d '{ "openStatus": "CLOSED_TEMPORARILY", "categories": ["categories/gcid:coffee_shop", "categories/gcid:cafe"] }'const response = await fetch(`https://app.maukirim.com/api/v1/gmb/locations/${id}`, { method: "PATCH", headers: { Authorization: `Bearer ${process.env.MAUKIRIM_KEY}`, "Content-Type": "application/json", "Idempotency-Key": `edit-${id}-2026-10-14`, }, body: JSON.stringify({ title: "Kopi Nusantara — Melati", phone: "+62 21 5550 200" }),})$curl = curl_init("https://app.maukirim.com/api/v1/gmb/locations/$id");curl_setopt_array($curl, [ CURLOPT_CUSTOMREQUEST => 'PATCH', CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('MAUKIRIM_KEY'), 'Content-Type: application/json', "Idempotency-Key: edit-$id-2026-10-14", ], CURLOPT_POSTFIELDS => json_encode(['title' => 'Kopi Nusantara — Melati', 'phone' => '+62 21 5550 200']), CURLOPT_RETURNTRANSFER => true,]);$data = json_decode(curl_exec($curl), true);{ "ok": true, "replayed": false, "location": { "id": "gml_8f3kq2x1ab", "title": "Kopi Nusantara — Melati", "phone": "+62 21 5550 200", "status": "active", "…": "…" }, "description": "Fresh coffee, daily, until 17:00.", "regularHours": null}The response is the business as Google now has it, so you see what was actually applied. Repeating the
same request with the same Idempotency-Key returns the stored answer with "replayed": true and makes
no second write at Google; the same key with a different body is 409 gmb_idempotency_conflict. The
change costs one Google call (a replay costs none), so it can be answered 429 gmb_rate_limited — see
rate limits.
Errors
Section titled “Errors”| HTTP | Code | Condition |
|---|---|---|
| 400 | invalid_input |
Not a JSON object; a field that cannot be changed; a value outside its limits; no field; a malformed Idempotency-Key. Also when Google rejects the value |
| 402 | gmb_plan_required |
The package does not include the connector |
| 403 | api_key_scope_denied |
The key lacks gmb (a read key cannot write) |
| 403 | gmb_forbidden |
Google says the connected account has no right to edit this business |
| 404 | gmb_location_not_found |
Not your business, no such business, or it left Google |
| 409 | gmb_reconnect_required |
The connection needs to be reconnected |
| 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; also an identical request with the same key still running |
| 502 | gmb_upstream_error |
Google failed |
DELETE /gmb/locations/{id}
Section titled “DELETE /gmb/locations/{id}”Deletes the business from Google. Its listing, and the reviews and photos on it, go with it, and it cannot be undone from MauKirim. Opening a business again means creating it again as a new, unverified one.
| Scope | gmb |
| Path | id — the business id (gml_…) |
| Body | JSON { "confirmTitle": "<the business's exact title>" } |
The body is required. confirmTitle must equal the business’s exact title as MauKirim shows it
(title in GET /gmb/locations/{id}): same spelling, same capitals. This is the way a
repository host asks you to type the name before deleting it; a missing or different title is
400 invalid_input and nothing is sent to Google. It has no Idempotency-Key, and repeating it after
success answers 404 gmb_location_not_found, because the business is gone.
curl -X DELETE https://app.maukirim.com/api/v1/gmb/locations/gml_8f3kq2x1ab \ -H "Authorization: Bearer mk_live_…" \ -H "Content-Type: application/json" \ -d '{ "confirmTitle": "Kopi Nusantara 1-1" }'const response = await fetch(`https://app.maukirim.com/api/v1/gmb/locations/${id}`, { method: "DELETE", headers: { Authorization: `Bearer ${process.env.MAUKIRIM_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ confirmTitle: "Kopi Nusantara 1-1" }),})$curl = curl_init("https://app.maukirim.com/api/v1/gmb/locations/$id");curl_setopt_array($curl, [ CURLOPT_CUSTOMREQUEST => 'DELETE', CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('MAUKIRIM_KEY'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode(['confirmTitle' => 'Kopi Nusantara 1-1']), CURLOPT_RETURNTRANSFER => true,]);$result = json_decode(curl_exec($curl), true);{ "ok": true }MauKirim keeps its own row for the business and sets its status to removed_upstream, so usage and history
still point at something. From then on the business is not found by anything that would call Google, and it no
longer counts against your allowance. If you only want to stop managing a business from MauKirim without removing
it from Google, disconnect its connection instead.
The call costs one Google call (see rate limits).
Errors
Section titled “Errors”| HTTP | Code | Condition |
|---|---|---|
| 400 | invalid_input |
confirmTitle is missing or is not the business’s exact title; nothing was sent to Google |
| 402 | gmb_plan_required |
The package does not include the connector |
| 403 | api_key_scope_denied |
The key lacks gmb |
| 403 | gmb_forbidden |
Google says the connected account may not delete this business (only an owner can) |
| 404 | gmb_location_not_found |
Not your business, no such business, it left Google, or it was already deleted |
| 409 | gmb_reconnect_required / gmb_location_disabled |
The connection needs reconnecting; the business or its connection is switched off |
| 429 | gmb_rate_limited |
No budget now (Retry-After) |
| 502 | gmb_upstream_error |
Google failed |