How to Manage Google Business Profile Photos via API
Upload cover, logo, interior, and gallery photos to a Google Business Profile location programmatically, list what's there, delete outdated images, and keep photo sets consistent across every location.
Two ways a photo reaches the profile
| Profile media | photo post format | |
|---|---|---|
| Endpoint | POST .../google_business/create_media | POST /api/posts with format: "photo" |
| Category | Any: COVER, LOGO, INTERIOR, MENU, … | Always ADDITIONAL (the general gallery) |
| Description | Up to 2,000 characters | None |
| Scheduling, drafts, approvals, queues | No, immediate | Yes, same as any post |
| Listed by | GET .../google_business/media | GET /api/posts |
Use profile media when the category matters: setting the cover photo, replacing the logo, tagging interior and exterior shots so Google files them correctly. Use the photo post format when the photo is part of a content calendar and should go through the same scheduling and approval flow as posts.
Every request below takes location_id, the placement id from List placements.
Google’s photo requirements
| Requirement | |
|---|---|
| Format | JPG or PNG |
| File size | 10 KB to 5 MB |
| Minimum resolution | 250 × 250 px |
| Recommended | 720 × 720 px; cover photos 16:9, 1024 × 576 px |
| Content | Well-lit, in focus, no heavy filters or overlaid text |
Google reviews every photo before it appears publicly. An image that violates the content policy is rejected silently from the public listing; the API call itself succeeds.
Upload a photo with a category
Google fetches the file from the URL you give it, so media_url has to be a public HTTPS address that stays valid for a while. A short-lived signed URL or an address on a private network fails.
curl -X POST "https://api.postproxy.dev/api/profiles/prof_abc123/google_business/create_media" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "location_id": "accounts/113344/locations/558899", "media_url": "https://cdn.acme.example/storefront-2026.jpg", "category": "EXTERIOR", "description": "Storefront on Market Street after the September refit" }'Available categories:
| Category | Used for |
|---|---|
COVER | The large photo at the top of the listing |
PROFILE | The small square identity photo |
LOGO | Brand logo |
EXTERIOR / INTERIOR | Building and premises |
PRODUCT | Products on sale |
AT_WORK | Staff, services being performed |
FOOD_AND_DRINK / MENU | Dishes and printed menus |
COMMON_AREA / ROOMS | Lodging and shared spaces |
TEAMS | Team photos |
ADDITIONAL | Everything else (default) |
Set the cover photo or logo
COVER, PROFILE, and LOGO are single-slot categories: uploading a new one replaces the current one on the listing. Google decides how the cover is cropped, so upload it at 16:9 with the subject centred.
curl -X POST "https://api.postproxy.dev/api/profiles/prof_abc123/google_business/create_media" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "location_id": "accounts/113344/locations/558899", "media_url": "https://cdn.acme.example/logo-2026.png", "category": "LOGO" }'List the gallery
curl "https://api.postproxy.dev/api/profiles/prof_abc123/google_business/media?location_id=accounts/113344/locations/558899&page_size=100" \ -H "Authorization: Bearer YOUR_API_KEY"{ "mediaItems": [ { "name": "accounts/113344/locations/558899/media/AF1QipN…", "mediaFormat": "PHOTO", "locationAssociation": { "category": "EXTERIOR" }, "googleUrl": "https://lh3.googleusercontent.com/…", "createTime": "2026-09-16T09:12:00Z", "dimensions": { "widthPixels": 1600, "heightPixels": 1200 }, "insights": { "viewCount": "1284" } } ], "nextPageToken": "…"}name is the handle for deletion. Page size goes up to 250; pass page_token from the response to continue. Customer-uploaded photos appear in this list too, with no locationAssociation, so filter on it to see only the business’s own uploads.
Delete a photo
curl -X DELETE "https://api.postproxy.dev/api/profiles/prof_abc123/google_business/delete_media" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "location_id": "accounts/113344/locations/558899", "media_name": "accounts/113344/locations/558899/media/AF1QipN…" }'Only media the business uploaded can be deleted. Customer photos have to be flagged to Google through the listing, which the API does not expose.
Push one photo set to every location
A rebrand or a seasonal campaign usually means the same cover and logo on every location. Loop the placements:
const BASE = "https://api.postproxy.dev";const headers = { Authorization: `Bearer ${process.env.POSTPROXY_API_KEY}`, "Content-Type": "application/json",};
const { data: locations } = await fetch(`${BASE}/api/profiles/${PROFILE}/placements`, { headers }) .then((r) => r.json());
const assets = [ { media_url: "https://cdn.acme.example/cover-autumn.jpg", category: "COVER" }, { media_url: "https://cdn.acme.example/logo-2026.png", category: "LOGO" },];
for (const loc of locations) { for (const asset of assets) { await fetch(`${BASE}/api/profiles/${PROFILE}/google_business/create_media`, { method: "POST", headers, body: JSON.stringify({ location_id: loc.id, ...asset }), }); }}Location-specific photos (the actual storefront) stay per location; only the brand assets are shared.
Errors
| Status | Cause |
|---|---|
400 | location_id or media_url missing, an unknown category, or a description over 2,000 characters |
404 | The profile cannot see that location_id, or media_name does not exist |
422 | Google could not fetch the URL, the file fails the format or size rules, or the category is not allowed for the location’s type |
Full parameter tables are on the Google Business API reference.