Skip to content

Google Business photos — list, upload, add by URL and delete

The photos and videos on a connected business’s Google listing, served by Google’s My Business API (v4). Listing and deleting are one metered Google call; an upload is three and adding by URL is one. All need a package that includes the connector.

Two ways to add a photo.

  • Upload the file with POST …/media/upload: a multipart/form-data request with a JPEG or PNG of 10 KB to 5 MB. Use it when the picture only exists as a file (a client’s phone, your own upload form).
  • Add it by URL with POST …/media: Google fetches the picture from a public https address. This is the only way to add a video. If Google cannot fetch the image or rejects its size or format, the call answers 400 invalid_input; Google’s picture requirements are its own.
{
"id": "m1",
"format": "PHOTO",
"category": "EXTERIOR",
"url": "https://lh3.googleusercontent.com/…",
"thumbnailUrl": "https://lh3.googleusercontent.com/…=s200",
"width": 800,
"height": 600,
"createdAt": "2026-10-14T03:10:00.000Z"
}
Field Meaning
id The media id for delete
format PHOTO or VIDEO
category Where Google shows it: one of COVER, PROFILE, LOGO, EXTERIOR, INTERIOR, PRODUCT, AT_WORK, FOOD_AND_DRINK, MENU, COMMON_AREA, ROOMS, TEAMS, ADDITIONAL; or null
url, thumbnailUrl Google-hosted addresses, or null
width, height Pixels, or null
createdAt ISO timestamp, or null
Scope read or gmb
Query pageSize 1–100 (default 25), pageToken
Terminal window
curl "https://app.maukirim.com/api/v1/gmb/locations/gml_8f3kq2x1ab/media" -H "Authorization: Bearer mk_live_…"
{ "ok": true, "media": [ { "id": "m1", "format": "PHOTO", "category": "EXTERIOR", "…": "…" } ], "totalMediaItemCount": 1, "nextPageToken": null }

Adds a photo or video to the public listing. 201 Created.

Scope gmb
Header Idempotency-Key (optional, recommended): 8–128 characters of letters, digits and _ . : -
Field Required Rule
sourceUrl yes An absolute https URL, up to 2,000 characters
mediaFormat no PHOTO (default) or VIDEO
category no One of the categories above; default ADDITIONAL

Any other field is 400 invalid_input.

Terminal window
curl -X POST https://app.maukirim.com/api/v1/gmb/locations/gml_8f3kq2x1ab/media \
-H "Authorization: Bearer mk_live_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: media-gml_8f3kq2x1ab-front-1" \
-d '{ "sourceUrl": "https://cdn.example/front.jpg", "category": "EXTERIOR" }'
const response = await fetch(`https://app.maukirim.com/api/v1/gmb/locations/${id}/media`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MAUKIRIM_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": `media-${id}-front-1`,
},
body: JSON.stringify({ sourceUrl: "https://cdn.example/front.jpg", category: "EXTERIOR" }),
})
const { media } = await response.json()
$curl = curl_init("https://app.maukirim.com/api/v1/gmb/locations/$id/media");
curl_setopt_array($curl, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('MAUKIRIM_KEY'),
'Content-Type: application/json',
"Idempotency-Key: media-$id-front-1",
],
CURLOPT_POSTFIELDS => json_encode(['sourceUrl' => 'https://cdn.example/front.jpg', 'category' => 'EXTERIOR']),
CURLOPT_RETURNTRANSFER => true,
]);
$media = json_decode(curl_exec($curl), true)['media'];
{ "ok": true, "replayed": false, "media": { "id": "m2", "format": "PHOTO", "category": "EXTERIOR", "…": "…" } }

The same key with the same body is "replayed": true and adds no second photo; a different body under the same key is 409 gmb_idempotency_conflict.


Uploads a photo file to the public listing. 201 Created. The request is multipart/form-data, not JSON.

Scope gmb
Content-Type multipart/form-data (your HTTP client sets the boundary)
Header Idempotency-Key (optional, recommended): 8–128 characters of letters, digits and _ . : -
Form field Required Rule
file yes The picture. JPEG or PNG, from 10 KB to 5 MB
category no One of the categories above; default ADDITIONAL

What the file is is decided by its bytes, not by its name or the Content-Type your client declares for the part. A JPEG sent as application/octet-stream is accepted; a script named logo.png is refused with 415 gmb_unsupported_media. GIF, WebP, HEIC and every other format are refused the same way, so convert them first. A file under 10 KB is 400 invalid_input (Google rejects it upstream, so MauKirim says so before spending budget). A file over 5 MB is 413 gmb_file_too_large; when your client announces the size in Content-Length the request is refused from that header (allowing a little for the form’s own framing) before the body is read, so an oversized upload is never held in memory and costs no Google budget.

Terminal window
curl -X POST https://app.maukirim.com/api/v1/gmb/locations/gml_8f3kq2x1ab/media/upload \
-H "Authorization: Bearer mk_live_…" \
-H "Idempotency-Key: upload-gml_8f3kq2x1ab-front-1" \
-F "file=@front.jpg" \
-F "category=EXTERIOR"
import { openAsBlob } from "node:fs"
const form = new FormData()
form.set("file", await openAsBlob("front.jpg"), "front.jpg")
form.set("category", "EXTERIOR")
const response = await fetch(`https://app.maukirim.com/api/v1/gmb/locations/${id}/media/upload`, {
method: "POST",
// Do not set Content-Type yourself: fetch adds the multipart boundary.
headers: {
Authorization: `Bearer ${process.env.MAUKIRIM_KEY}`,
"Idempotency-Key": `upload-${id}-front-1`,
},
body: form,
})
const { media } = await response.json()
$curl = curl_init("https://app.maukirim.com/api/v1/gmb/locations/$id/media/upload");
curl_setopt_array($curl, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('MAUKIRIM_KEY'),
"Idempotency-Key: upload-$id-front-1",
],
// An array with a CURLFile makes cURL send multipart/form-data.
CURLOPT_POSTFIELDS => [
'file' => new CURLFile('front.jpg', 'image/jpeg', 'front.jpg'),
'category' => 'EXTERIOR',
],
CURLOPT_RETURNTRANSFER => true,
]);
$media = json_decode(curl_exec($curl), true)['media'];
{ "ok": true, "replayed": false, "media": { "id": "m3", "format": "PHOTO", "category": "EXTERIOR", "…": "…" } }

It costs three Google calls, each metered against the shared budget (see rate limits): Google first reserves a place for the file, then MauKirim sends the bytes, then it creates the media item that points at them. Any of the three can be answered 429 gmb_rate_limited with Retry-After; wait and send the upload again with the same Idempotency-Key. If the last step fails the bytes sit unused on Google’s side, which is harmless.

The same key with the same file and category is "replayed": true and uploads nothing twice; the same key with a different file or category is 409 gmb_idempotency_conflict.


DELETE /gmb/locations/{id}/media/{mediaId}

Section titled “DELETE /gmb/locations/{id}/media/{mediaId}”

Removes one photo or video from the public listing. It cannot be undone. Returns { "ok": true }.

Scope gmb
Terminal window
curl -X DELETE https://app.maukirim.com/api/v1/gmb/locations/gml_8f3kq2x1ab/media/m1 -H "Authorization: Bearer mk_live_…"
HTTP Code Condition
400 invalid_input A field outside the list, sourceUrl not an https URL, an unknown category or mediaFormat, a malformed key or id, or Google rejected the file. For an upload also: no file field (not multipart), an empty file, or a file under 10 KB
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 change this business’s photos
404 gmb_location_not_found Not your business, no such business, it left Google, or (delete) no such photo
413 gmb_file_too_large (Upload) The file is over 5 MB. Refused from Content-Length before the body is read when it is announced
415 gmb_unsupported_media (Upload) The bytes are not a JPEG or PNG, whatever the file name or declared type says
409 gmb_reconnect_required / gmb_location_disabled / gmb_idempotency_conflict As on posts
429 gmb_rate_limited No budget now (Retry-After)
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