Locations

Every Locations endpoint with a cURL, Python and Node sample, in usage order, covering list, search, lookups by id and store code, create, update, archive, and photos.

A location is the record every listing, review, post, and analytics figure attaches to. The Locations resource has thirteen endpoints: four reads that find locations, four writes that create, change, and archive them, and five that manage photos. A Read key covers the reads. Every POST needs a Write key; a Read key on a write returns HTTP 400 with SY90016.

Every id in this guide is the base64 Relay id, for example TG9jYXRpb246MTgwMDI4OQ== (decodes to Location:1800289). Both SDKs also accept the numeric databaseId and encode it for you.

Endpoints at a glance

MethodPathWhat it doesReference
GET/locationsPage through every location, newest first.List all locations
GET/locations/searchFind locations by keyword across name, address, and store id.Search locations
GET/locations-by-idsResolve a batch of base64 ids to full records.Get locations by IDs
GET/locations-by-store-codesResolve your own store codes to full records.Get locations by store codes
POST/locationsCreate a location and queue it for submission.Create a location
POST/locations/updateChange any writable field on one location.Update a location
POST/locations/archiveSchedule locations for archival at the end of the billing cycle.Archive locations
POST/locations/cancel_archivePull scheduled locations back to active.Cancel scheduled archival
POST/locations/photosAttach a logo, cover, or gallery photos by URL.Upload photos
GET/locations/photos/requests/{requestId}Poll a queued bulk upload.Check bulk upload status
GET/locations/{locationId}/photosList every media file on a location.Get photos
POST/locations/photos/starStar or unstar additional photos.Star photos
POST/locations/photos/removeDetach additional photos.Remove photos

List all locations

GET /locations pages through every location in the account, newest first. It is a cursor connection: edges[].node holds each record, and pageInfo.endCursor feeds the after parameter for the next page. Use it to sync the whole account into your own system.

Parameters

NameInRequiredNotes
firstqueryNoPage size going forward. Default 50. Combine with after.
afterqueryNoThe previous page's pageInfo.endCursor.
lastqueryNoPage size going backward. Combine with before.
beforequeryNoThe cursor of the first edge of the page you are moving back from. pageInfo has no startCursor.
filterqueryNoJSON-encoded object to narrow the set server-side, for example by approval status. Neither SDK list method takes it; call REST for a filtered slice.
List
curl "https://listingsapi.com/api/v4/locations?first=50" \-H "Authorization: API $LISTINGSAPI_KEY"
JSON
{  "data": {    "allLocations": {      "edges": [        {          "cursor": "TG9jYXRpb246MTgwMDI4OQ==",          "node": {            "id": "TG9jYXRpb246MTgwMDI4OQ==",            "databaseId": 1800289,            "name": "Jenny Home",            "storeId": null,            "approved": "APPROVED",            "archived": false,            "archivalScheduledAt": null,            "city": "Austin",            "stateIso": "TX",            "countryIso": "US",            "subCategoryId": 1432,            "planName": "Launch",            "placeActionLinks": [],            "discoveredPlaceActionLinks": []          }        }      ],      "pageInfo": {        "hasNextPage": false,        "hasPreviousPage": false,        "endCursor": "TG9jYXRpb246MTgwMDI4OQ==",        "total": 1      }    }  }}

Read pageInfo.hasNextPage and stop when it is false. The page-level count is pageInfo.total. The node's id is the identifier every other endpoint expects.

Search locations

GET /locations/search matches a keyword against the location name, street address, city, and store id. It is a cursor connection with the same edges[].node and pageInfo.endCursor shape as the list. Use it for a type-ahead picker, or to confirm a store exists before you create it.

Parameters

NameInRequiredNotes
queryqueryNoThe search keyword. Omit to page every location unfiltered.
firstqueryNoPage size. Default 50.
afterqueryNoThe previous page's pageInfo.endCursor.
filterqueryNoJSON-encoded filter object, as on the list endpoint.

Both SDKs also accept a fields list to restrict the match to specific fields, for example name only.

Search
curl "https://listingsapi.com/api/v4/locations/search?query=Jenny&first=10" \-H "Authorization: API $LISTINGSAPI_KEY"
JSON
{  "data": {    "searchLocations": {      "edges": [        {          "cursor": "TG9jYXRpb246MTgwMDI4OQ==",          "node": {            "id": "TG9jYXRpb246MTgwMDI4OQ==",            "databaseId": 1800289,            "name": "Jenny Home",            "storeId": null,            "street": "600 Congress Ave, Suite 200",            "city": "Austin",            "approved": "APPROVED",            "archived": false          }        }      ],      "pageInfo": {        "endCursor": "TG9jYXRpb246MTgwMDI4OQ==",        "hasNextPage": false,        "hasPreviousPage": false,        "total": 1      }    }  }}

An empty edges array means nothing matched. The node carries the full address, category, and approved status, so a result row needs no second lookup.

Get locations by IDs

GET /locations-by-ids returns the full record for a batch of base64 ids in one call, far cheaper than one request per location. Both active and archived locations resolve. It returns a plain array, not a connection. Use it to hydrate ids from a webhook payload or your own store.

Parameters

NameInRequiredNotes
idsqueryYesJSON-encoded array of base64 ids. URL-encode the whole value; curl -G --data-urlencode does this for you.
By ids
curl -G https://listingsapi.com/api/v4/locations-by-ids \--data-urlencode 'ids=["TG9jYXRpb246MTgwMDI4OQ==","TG9jYXRpb246MTgwMDI5MA=="]' \-H "Authorization: API $LISTINGSAPI_KEY"
JSON
{  "data": {    "getLocationsByIds": [      {        "id": "TG9jYXRpb246MTgwMDI4OQ==",        "databaseId": 1800289,        "storeId": null,        "name": "Jenny Home",        "archived": false,        "archivedAt": null,        "city": "Austin",        "stateIso": "TX",        "countryIso": "US",        "subCategoryId": 1432,        "businessHours": [],        "locationPhotos": [],        "placeActionLinks": [],        "discoveredPlaceActionLinks": []      }    ]  }}

Ids that do not exist in your account are dropped silently rather than raising an error. Compare the returned count with what you sent to detect stale ids.

Get locations by store codes

GET /locations-by-store-codes is the inverse lookup: it resolves your own store codes to full records. It returns a plain array. Use it when your POS or franchise roster keys locations by store code and you never stored our ids.

Parameters

NameInRequiredNotes
storeCodesqueryYesJSON-encoded array of store code strings. URL-encode the whole value.
By store codes
curl -G https://listingsapi.com/api/v4/locations-by-store-codes \--data-urlencode 'storeCodes=["ACME01","ACME02"]' \-H "Authorization: API $LISTINGSAPI_KEY"
JSON
{  "data": {    "getLocationsByStoreCodes": [      {        "id": "TG9jYXRpb246MTgwMDI4OQ==",        "databaseId": 1800289,        "storeId": "ACME01",        "name": "Jenny Home",        "archived": false,        "city": "Austin",        "countryIso": "US",        "planName": "Launch"      }    ]  }}

Match each object's storeId back to your roster. Codes with no match are simply absent. A location only has a store code if storeId was set on create or update.

Create a location

POST /locations creates a location and queues it for submission to every publisher in your plan, or to enabledSiteIds only when you pass that field. Every field goes under input. Five fields are mandatory; the rest are optional and any you leave out publishers show as blank.

Parameters

NameInRequiredNotes
input.namebodyYesBusiness name, 2 to 150 characters.
input.descriptionbodyYesMinimum 200 characters (SY10005). Publishers use it as the listing copy.
input.countryIsobodyYesA supported country code (SY10036). From countries.
input.citybodyYesMandatory for countries with city-level addressing (SY10126). Only a service-area business with hideAddress: true may omit it.
input.subCategoryIdbodyYesNumeric databaseId from subcategories.
input.street, input.stateIso, input.postalCode, input.phonebodyNoValidated against the country. Send them for any storefront; a location needs an address and phone before it can publish.
input.bizUrlbodyNoPublished as the website.
input.storeIdbodyNoYour own store code, unique across the account.
input.businessHoursbodyNoSeven entries of { day, type, slots }. type is OPEN or CLOSED; a closed day has empty slots.
input.placeActionLinksbodyNoGoogle action links: placeActionType, uri, optional isPreferred. No action needed on create. See Place Action Links.
input.enabledSiteIdsbodyNoPublish to a subset of your plan's sites. Ids from plan sites.
input.clientMutationIdbodyNoEchoed back for correlation. Not an idempotency key.

Plus the optional fields listed on the reference page: Google GCID categories, social URLs, coordinates, owner contact, payment methods, service area, and more.

Create
curl -X POST https://listingsapi.com/api/v4/locations \-H "Authorization: API $LISTINGSAPI_KEY" \-H "Content-Type: application/json" \-d '{  "input": {    "name": "Acme Kitchen",    "description": "Acme Kitchen is a family-owned neighborhood restaurant in downtown New York serving wood-fired pizza, hand-rolled pasta, and a short seasonal menu built around produce from nearby growers. Our dining room seats forty, takeout and delivery run until close, and the bar pours regional wine and beer on tap.",    "storeId": "ACME01",    "street": "123 Jump Street",    "city": "New York",    "stateIso": "NY",    "postalCode": "10013",    "countryIso": "US",    "phone": "6443859313",    "bizUrl": "https://acmekitchen.example.com",    "subCategoryId": 1432,    "businessHours": [      { "day": "MONDAY", "type": "OPEN", "slots": [{ "start": "09:00am", "end": "05:00pm" }] },      { "day": "TUESDAY", "type": "OPEN", "slots": [{ "start": "09:00am", "end": "05:00pm" }] },      { "day": "WEDNESDAY", "type": "OPEN", "slots": [{ "start": "09:00am", "end": "05:00pm" }] },      { "day": "THURSDAY", "type": "OPEN", "slots": [{ "start": "09:00am", "end": "05:00pm" }] },      { "day": "FRIDAY", "type": "OPEN", "slots": [{ "start": "09:00am", "end": "05:00pm" }] },      { "day": "SATURDAY", "type": "OPEN", "slots": [{ "start": "10:00am", "end": "02:00pm" }] },      { "day": "SUNDAY", "type": "CLOSED", "slots": [] }    ],    "placeActionLinks": [      { "placeActionType": "FOOD_ORDERING", "uri": "https://order.acmekitchen.example.com", "isPreferred": true }    ]  }}'
JSON
{  "data": {    "createLocation": {      "clientMutationId": null,      "success": true,      "errors": null,      "location": {        "id": "TG9jYXRpb246MTgwMDI5MA==",        "databaseId": 1800290,        "storeId": "ACME01",        "name": "Acme Kitchen",        "city": "New York",        "countryIso": "US",        "status": "PENDING",        "placeActionLinks": [          { "placeActionType": "FOOD_ORDERING", "uri": "https://order.acmekitchen.example.com", "isPreferred": true, "name": null, "submissionStatus": null }        ],        "discoveredPlaceActionLinks": []      }    }  }}

Keep both ids: the base64 id is what every later write needs, and databaseId is the number you see in dashboards and webhooks. status starts as PENDING and moves to approved on its own. A place action link's name and submissionStatus stay null until it syncs to Google. Python's locations.add() validates the mandatory fields locally and raises ValidationError before any network call; locations.create(input) takes a raw camelCase dict instead.

Update a location

POST /locations/update is a partial update: input.id is the only required field and only the fields you send change. The same call fixes a phone number or rewrites the weekly schedule. Python's locations.update() and Node's updateLocation() both take one camelCase object.

Parameters

NameInRequiredNotes
input.idbodyYesThe location's base64 id. The field is id, not locationId.
input.phone, input.bizUrl, input.name, address fieldsbodyNoAny writable field from the create call.
input.businessHoursbodyNoSend all seven days; a partial week is rejected. Add day: "SPECIAL" entries with a specialDate for holiday overrides, alongside the week, never instead of it.
input.moreHoursbodyNoSecondary schedules such as delivery, each { hoursTypeId, moreHour }. No specialDate here.
input.temporarilyClosedbodyNotrue stops publishing hours while the location is closed.
input.primaryGbpSiteCategoryId, input.additionalGbpSiteCategoryIdsbodyNoGoogle categories as GCIDs, for example gcid:restaurant. Send countryIso with them.
input.placeActionLinksbodyNoAppend or upsert: each item takes action add (default) or delete. Links you do not mention are left unchanged.
input.submissionDisabledSiteIdsbodyNoSite ids to exclude from submission.

Plus the optional fields listed on the reference page.

Update
curl -X POST https://listingsapi.com/api/v4/locations/update \-H "Authorization: API $LISTINGSAPI_KEY" \-H "Content-Type: application/json" \-d '{  "input": {    "id": "TG9jYXRpb246MTgwMDI4OQ==",    "phone": "5125550199",    "bizUrl": "https://acmekitchen.example.com",    "primaryGbpSiteCategoryId": "gcid:restaurant",    "businessHours": [      { "day": "MONDAY", "type": "OPEN", "slots": [{ "start": "09:00am", "end": "05:00pm" }] },      { "day": "TUESDAY", "type": "OPEN", "slots": [{ "start": "09:00am", "end": "05:00pm" }] },      { "day": "WEDNESDAY", "type": "OPEN", "slots": [{ "start": "09:00am", "end": "05:00pm" }] },      { "day": "THURSDAY", "type": "OPEN", "slots": [{ "start": "09:00am", "end": "05:00pm" }] },      { "day": "FRIDAY", "type": "OPEN", "slots": [{ "start": "09:00am", "end": "05:00pm" }] },      { "day": "SATURDAY", "type": "OPEN", "slots": [{ "start": "10:00am", "end": "02:00pm" }] },      { "day": "SUNDAY", "type": "CLOSED", "slots": [] }    ]  }}'
JSON
{  "data": {    "updateLocation": {      "clientMutationId": null,      "success": true,      "errors": null,      "location": {        "id": "TG9jYXRpb246MTgwMDI4OQ==",        "databaseId": 1800289,        "name": "Jenny Home",        "phone": "5125550199",        "status": "APPROVED",        "placeActionLinks": [          { "placeActionType": "FOOD_ORDERING", "uri": "https://order.example.com", "isPreferred": true, "name": null, "submissionStatus": null }        ],        "discoveredPlaceActionLinks": []      }    }  }}

The response echoes the updated location so you can confirm the new values landed. Every update returns the location's full current placeActionLinks set. To change a link's URL, send a delete for the old link and an add for the new one in the same request.

Archive locations

POST /locations/archive schedules one or more locations for archival. Archiving is not immediate: each location enters ARCHIVE_SCHEDULED, stays live and billed until the current billing cycle ends, and is archived when the next invoice is generated. Use it to offboard closed branches in a batch.

Parameters

NameInRequiredNotes
input.locationIdsbodyYesArray of base64 ids to archive.
input.clientMutationIdbodyNoEchoed back. Not an idempotency key.
Archive
curl -X POST https://listingsapi.com/api/v4/locations/archive \-H "Authorization: API $LISTINGSAPI_KEY" \-H "Content-Type: application/json" \-d '{  "input": {    "locationIds": ["TG9jYXRpb246MTgwMDI4OQ==", "TG9jYXRpb246MTgwMDI5MA=="]  }}'
JSON
{  "data": {    "archiveLocations": {      "clientMutationId": null,      "bulkEditTaskId": null,      "errors": null,      "success": null,      "result": [        { "locationId": "TG9jYXRpb246MTgwMDI4OQ==", "status": "ARCHIVE_SCHEDULED", "success": true, "errors": null },        { "locationId": "TG9jYXRpb246MTgwMDI5MA==", "status": "ARCHIVE_SCHEDULED", "success": true, "errors": null }      ]    }  }}

The top-level success is null; read each result[] entry's own success instead, so one bad id does not block the rest of the batch.

result[].statusMeaning
ARCHIVE_SCHEDULEDQueued. The location stays live and billed until the billing cycle ends.
ARCHIVEDAlready archived by an earlier call or cycle.

While scheduled, the location's archivalScheduledAt is set on list and search results. An archived location cannot be updated until it is reactivated.

Cancel scheduled archival

POST /locations/cancel_archive pulls locations in ARCHIVE_SCHEDULED back to ACTIVE, and billing continues. It only works while the billing cycle is still open; once a location is fully archived this call returns 404 for it.

Parameters

NameInRequiredNotes
input.locationIdsbodyYesArray of base64 ids currently in ARCHIVE_SCHEDULED.
input.clientMutationIdbodyNoEchoed back unchanged.
Cancel archive
curl -X POST https://listingsapi.com/api/v4/locations/cancel_archive \-H "Authorization: API $LISTINGSAPI_KEY" \-H "Content-Type: application/json" \-d '{ "input": { "locationIds": ["TG9jYXRpb246MTgwMDI4OQ=="] } }'
JSON
{  "data": {    "cancelLocationsArchive": {      "clientMutationId": null,      "bulkEditTaskId": null,      "errors": null,      "success": null,      "result": [        { "locationId": "TG9jYXRpb246MTgwMDI4OQ==", "status": "ACTIVE", "success": true, "errors": null }      ]    }  }}

Each result[] entry confirms the location is back to ACTIVE. Both SDKs require two extra positional arguments, a selection type and a changedBy identifier, which they send alongside locationIds; the REST body needs only the ids.

Upload photos to a location

POST /locations/photos attaches images by public URL: you host the image, the API fetches and stores it. A location keeps one LOGO, one COVER, and any number of ADDITIONAL photos. Re-uploading a logo or cover replaces the existing one.

Parameters

NameInRequiredNotes
input.locationIdbodyYesThe location's base64 id.
input.photosbodyYesArray of image objects; must be non-empty.
input.photos[].photobodyYesA public JPEG or PNG URL. The field is photo, not url.
input.photos[].typebodyNoLOGO, COVER, or ADDITIONAL. Defaults to an additional photo.
input.photos[].starredbodyNoStar the photo as it lands. Additional photos only, at most 4 per location.
input.photos[].sourcebodyNoFree-text label recording where the image came from.
Upload
curl -X POST https://listingsapi.com/api/v4/locations/photos \-H "Authorization: API $LISTINGSAPI_KEY" \-H "Content-Type: application/json" \-d '{  "input": {    "locationId": "TG9jYXRpb246MTgwMDI4OQ==",    "photos": [      { "photo": "https://cdn.example.com/jenny-home/logo.png", "type": "LOGO" },      { "photo": "https://cdn.example.com/jenny-home/storefront.jpg", "type": "ADDITIONAL" }    ]  }}'
JSON
{  "data": {    "addLocationPhotos": {      "clientMutationId": null,      "success": true,      "request": {},      "photos": [        { "id": "TWVkaWFGaWxlOjU4Nzg5MzE=", "databaseId": 5878931, "type": "LOGO", "starred": false,          "url": "https://sy-media-store.s3-us-west-2.amazonaws.com/f868bb3f/59c670a3/f47d62a6.png" },        { "id": "TWVkaWFGaWxlOjU4Nzg5MzI=", "databaseId": 5878932, "type": "ADDITIONAL", "starred": false,          "url": "https://sy-media-store.s3-us-west-2.amazonaws.com/f868bb3f/59c670a3/ef4ffb12.jpg" }      ],      "errors": null    }  }}

Keep each photo's id; the star and remove endpoints take it. This is the inline shape for a small batch. A large batch is queued instead: photos comes back empty and request.requestId is set, and you poll the next endpoint with that id. The threshold is server-side configuration, so branch on whether request.requestId is present.

Check bulk photo upload status

GET /locations/photos/requests/{requestId} polls a queued upload until it settles, then reports the outcome per image. Use it to drive a progress bar and to surface exactly which files were rejected and why.

Parameters

NameInRequiredNotes
requestIdpathYesThe UUID from request.requestId on a queued upload response. Only poll ids handed back by a recent upload.
Upload status
curl https://listingsapi.com/api/v4/locations/photos/requests/e6c2d9d9-9a18-4015-88cf-9a4e19a6f49a \-H "Authorization: API $LISTINGSAPI_KEY"
JSON
{  "data": {    "getLocationPhotosUploadStatus": {      "requestId": "e6c2d9d9-9a18-4015-88cf-9a4e19a6f49a",      "status": "SUCCESS",      "photos": [        { "mediaId": "TWVkaWFGaWxlOjkxODI3", "locationId": "TG9jYXRpb246MTgwMDI4OQ==", "status": "SUCCESS", "errorMessage": null },        { "mediaId": null, "locationId": "TG9jYXRpb246MTgwMDI4OQ==", "status": "ERROR", "errorMessage": "Image dimensions below the minimum 250x250." }      ]    }  }}
Job statusMeaning
PROCESSINGStill working. Poll again after a few seconds.
SUCCESSProcessing finished. Individual photos can still have failed, so read photos[].status and photos[].errorMessage.
ERRORThe job itself failed. Inspect photos[].errorMessage.

The job status only says processing finished, not that every image landed. Back off a few seconds between polls; a tight loop here is a fast way to hit 429.

Get photos for a location

GET /locations/{locationId}/photos returns every media file on a location as a plain array: logo, cover, and additional images, each with its public url, thumbnailUrl, category, starred flag, and lifetime viewsCount. Call it before an upload to see which slot is missing, and to collect ids for the star and remove calls.

Parameters

NameInRequiredNotes
locationIdpathYesThe base64 id. The raw numeric databaseId is also accepted; the gateway encodes it. A value that is neither returns HTTP 200 with SY90002: Invalid Id in errors.
List photos
curl https://listingsapi.com/api/v4/locations/TG9jYXRpb246MTgwMDI4OQ==/photos \-H "Authorization: API $LISTINGSAPI_KEY"
JSON
{  "data": {    "mediaFilesOfLocation": [      { "id": "TWVkaWFGaWxlOjU4Nzg5MzE=", "categoryName": "Logo", "categoryId": "4", "fileType": "image", "starred": false, "viewsCount": 0,        "url": "https://sy-media-store.s3-us-west-2.amazonaws.com/f868bb3f/59c670a3/f47d62a6.jpg",        "thumbnailUrl": "https://sy-media-store.s3-us-west-2.amazonaws.com/f868bb3f/59c670a3/thumbnail_f47d62a6.jpg" },      { "id": "TWVkaWFGaWxlOjU4Nzg5MzI=", "categoryName": "Additional", "categoryId": "12", "fileType": "image", "starred": true, "viewsCount": 143,        "url": "https://sy-media-store.s3-us-west-2.amazonaws.com/f868bb3f/59c670a3/ef4ffb12.jpg",        "thumbnailUrl": "https://sy-media-store.s3-us-west-2.amazonaws.com/f868bb3f/59c670a3/thumbnail_ef4ffb12.jpg" }    ]  }}

categoryName is Logo, Cover, or Additional. Use thumbnailUrl for a grid and url for the full-size view. A location with no media returns an empty array, not an error.

Star location photos

POST /locations/photos/star marks additional photos for priority placement on publishers that support featured imagery. Only ADDITIONAL photos can be starred, at most 4 per location. The same mutation handles unstarring: the starred flag in the body decides what happens.

Parameters

NameInRequiredNotes
input.locationIdbodyYesThe location's base64 id.
input.photoIdsbodyYesMedia ids from the get-photos or upload response.
input.starredbodyYestrue to star, false to unstar. This flag, not the path, decides; /star and /unstar run the same mutation.
Star
curl -X POST https://listingsapi.com/api/v4/locations/photos/star \-H "Authorization: API $LISTINGSAPI_KEY" \-H "Content-Type: application/json" \-d '{  "input": {    "locationId": "TG9jYXRpb246MTgwMDI4OQ==",    "photoIds": ["TWVkaWFGaWxlOjU4Nzg5MzI="],    "starred": true  }}'
JSON
{  "data": {    "starUnstarLocationPhotos": {      "success": true,      "photos": [        { "id": "TWVkaWFGaWxlOjU4Nzg5MzI=", "databaseId": 5878932, "type": "ADDITIONAL", "starred": true }      ],      "error": null    }  }}

The response key is starUnstarLocationPhotos on either path, and it carries a singular error, not an errors array. A fifth starred photo fails the whole call with SY15012; the check counts photos already starred plus the ones in this request.

Remove photos from a location

POST /locations/photos/remove detaches additional photos by media id. Use it to retire stale storefront images or undo a bad bulk import. Only ADDITIONAL photos can be removed; replace a logo or cover by uploading a new image with that type.

Parameters

NameInRequiredNotes
input.locationIdbodyYesThe location's base64 id.
input.photoIdsbodyYesMedia ids to detach, from the get-photos or upload response.
Remove
curl -X POST https://listingsapi.com/api/v4/locations/photos/remove \-H "Authorization: API $LISTINGSAPI_KEY" \-H "Content-Type: application/json" \-d '{  "input": {    "locationId": "TG9jYXRpb246MTgwMDI4OQ==",    "photoIds": ["TWVkaWFGaWxlOjU4Nzg5MzI="]  }}'
JSON
{  "data": {    "removeLocationPhotos": {      "clientMutationId": null,      "removedCount": 1,      "errors": null    }  }}

removedCount reports how many files were actually detached, so 0 with errors: null means none of those ids were attached to this location. Removal detaches the file; it does not necessarily purge it from your media library.

Errors you will see

CodeMeaningFix
SY10005description missing or under 200 characters on create.Write a fuller description.
SY10036countryIso is not a supported country.Use a code from countries.
SY10126city missing for a country that requires it.Send city, or set hideAddress: true for a service-area business.
SY15012More than 4 starred photos on a location.Unstar one first, or drop starred from the upload.
SY90002Invalid id in the photos path.Pass the base64 id or the plain number, not another form.
SY90016A Read key was used on a write.Use a Write key.
HTTP 404Cancel archive on a location that is no longer ARCHIVE_SCHEDULED.The location is fully archived; use the reactivate flow instead.
HTTP 429Rate limit exceeded.Wait for Retry-After. See rate limits.

Mutations can also return HTTP 200 with success: false and the detail in data.<operation>.errors. The full list is in error codes.

Next steps