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
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
curl "https://listingsapi.com/api/v4/locations?first=50" \-H "Authorization: API $LISTINGSAPI_KEY"{ "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
Both SDKs also accept a fields list to restrict the match to specific
fields, for example name only.
curl "https://listingsapi.com/api/v4/locations/search?query=Jenny&first=10" \-H "Authorization: API $LISTINGSAPI_KEY"{ "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
curl -G https://listingsapi.com/api/v4/locations-by-ids \--data-urlencode 'ids=["TG9jYXRpb246MTgwMDI4OQ==","TG9jYXRpb246MTgwMDI5MA=="]' \-H "Authorization: API $LISTINGSAPI_KEY"{ "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
curl -G https://listingsapi.com/api/v4/locations-by-store-codes \--data-urlencode 'storeCodes=["ACME01","ACME02"]' \-H "Authorization: API $LISTINGSAPI_KEY"{ "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
Plus the optional fields listed on the reference page: Google GCID categories, social URLs, coordinates, owner contact, payment methods, service area, and more.
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 } ] }}'{ "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
Plus the optional fields listed on the reference page.
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": [] } ] }}'{ "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
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=="] }}'{ "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.
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
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=="] } }'{ "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
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" } ] }}'{ "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
curl https://listingsapi.com/api/v4/locations/photos/requests/e6c2d9d9-9a18-4015-88cf-9a4e19a6f49a \-H "Authorization: API $LISTINGSAPI_KEY"{ "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." } ] } }}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
curl https://listingsapi.com/api/v4/locations/TG9jYXRpb246MTgwMDI4OQ==/photos \-H "Authorization: API $LISTINGSAPI_KEY"{ "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
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 }}'{ "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
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="] }}'{ "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
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
- Add a location, Update a location, Upload location photos, and Find and archive locations: the task guides built on these endpoints.
- Look up countries and categories:
the
countryIso,stateIso, andsubCategoryIdvalues a create needs. - Place Action Links: the full model for Google action buttons on create and update.
- Locations: the resource landing page, and the SDK references for Python and Node.
- Photo methods have their own SDK pages: Python photos and Node photos.