Guest Portal Guides
A guide is a list of local recommendations in the guest portal: “Coffee in Vail”, “Things to do downtown”. Each guide list holds places (a café, a restaurant, an activity), each with a name, a short recommendation, an address and optional photo and links. These are the lists managed under Settings → Guest content → Guides in the app. Every field is listed on the operation’s page in the API Reference.
| Operation | Request | Permission |
|---|---|---|
listGuideLists | GET /guide-lists | view_guides |
getGuideList | GET /guide-lists/{id} | view_guides |
createGuideList | POST /guide-lists | modify_guides |
updateGuideList | PATCH /guide-lists/{id} | modify_guides |
addGuidePlace | POST /guide-places | modify_guides |
importGuidePlace | POST /guide-places/import | modify_guides |
updateGuidePlace | PATCH /guide-places/{id} | modify_guides |
removeGuidePlace | DELETE /guide-places/{id} | modify_guides |
deleteGuideList | DELETE /guide-lists/{id} | delete_guide |
reorderGuideLists | POST /guide-lists/reorder | modify_guides |
reorderGuidePlaces | POST /guide-lists/{guideListId}/places/reorder | modify_guides |
Paths are relative to your region’s base URL, for example https://api-us.suiteop.com/api/v1. See Authentication for permissions.
modify_guides does not include view_guides. A key that edits guides usually needs both, because the list and place IDs come from the reads. Attaching a list to portals is an entity-scope write and needs modify_properties as well as modify_guides.
deleteGuideList deletes a list for good, and there is no archive. To take a list out of view without deleting it, remember that a list with no scopes appears in no guest portal, but with showOnBookingSites: true it is still published on every booking site of the organization. So remove its scopes and set showOnBookingSites to false.
How guides fit together
Section titled “How guides fit together”- Guide list:
nicknameis the label shown in the Guides tab. The guest-facing heading is a translation referenced bynameId; a list with no heading shows guests itsnicknameinstead.categoryis a coarse filter value:food_and_drink,things_to_do,shopping,nightlife,outdoors,transit,servicesoressentials. - Place: one recommendation in a list.
titleTextandshortDescriptionTextare plain English and are auto-translated into the portal’s other languages. - Scopes: the portals, properties, property groups and tags a list is attached to, written with the entity-scope operations and
entityType: "guide_list".isActiveon a list istrueonce it has at least one scope of any kind. - Booking sites:
showOnBookingSites: truealso publishes the list on the organization’s booking sites: those listing a property the list reaches, or all of them when the list has no scopes.
Listing guide lists
Section titled “Listing guide lists”GET /guide-lists returns the same lists, in the same order, as the Guides tab. It is not paginated.
curl "https://api-us.suiteop.com/api/v1/guide-lists?portalSettingsId=2b7c4e10-…" \ -H "Authorization: Bearer sk_live_your_key_here"portalSettingsId (optional, from listPortals) keeps only lists scoped directly to that portal. A list that reaches the portal only through a property, group or tag scope is not matched.
Each row carries id, nickname, category, showOnBookingSites, sortOrder, itemCount, portalSettingsIds (the portals it is scoped to directly), scopeCount, isActive, createdAt and updatedAt. An OAuth token whose user is limited to some properties sees scopeCount and portalSettingsIds narrowed to the scopes that reach its own properties.
Reading one list and its places
Section titled “Reading one list and its places”GET /guide-lists/{id} returns the list with every place in guest order. Place names and recommendations come back as English text. The list’s guest-facing heading does not: pass nameId to GET /translations/{translationId}/entries (see Translations). A list in another organization or outside your property scope returns 404.
{ "data": { "id": "5d1e8a40-…", "nickname": "Vail — Coffee", "nameId": "a93c0f12-…", "category": "food_and_drink", "showOnBookingSites": false, "coverImageUrl": null, "itemCount": 1, "createdAt": "2026-10-09T09:12:44.512Z", "updatedAt": "2026-10-09T09:12:44.512Z", "places": [ { "id": "c40b7e95-…", "guideListId": "5d1e8a40-…", "titleText": "Loaded Joe's", "shortDescriptionText": "Our favourite breakfast burrito, five minutes' walk from the village.", "imageUrl": null, "priceCents": 2, "websiteUrl": "https://example.com", "rating": 4.6, "ratingsCount": 812, "googlePlaceId": null, "googleMapsUrl": null, "categories": ["cafe"], "address": { "formattedAddress": "141 E Meadow Dr, Vail, CO 81657, USA", "streetAddress": "141 E Meadow Dr", "city": "Vail", "state": "CO", "postalCode": "81657", "countryCode": "US", "latitude": 39.6413, "longitude": -106.3734 }, "sortOrder": 0, "createdAt": "2026-10-09T09:13:02.020Z", "updatedAt": "2026-10-09T09:13:02.020Z" } ] }, "meta": { "requestId": "…" }}Places imported from Google, in the app or with importGuidePlace, carry a googlePlaceId; places added with addGuidePlace never do. Older places can have a null address or categories, and any address field that wasn’t stored is null.
Creating a list and attaching it to a portal
Section titled “Creating a list and attaching it to a portal”A new list starts with no scopes, so isActive is false and it appears in no guest portal until you attach it. Created with showOnBookingSites: true, it is published on every booking site straight away. Create it first, then attach it in a separate call: the create returns the ID even if the scope write is later refused.
curl -X POST https://api-us.suiteop.com/api/v1/guide-lists \ -H "Authorization: Bearer sk_live_your_key_here" \ -H "Idempotency-Key: 4f0c2a9e-1b3d-4c5e-8f7a-9b0c1d2e3f4a" \ -H "Content-Type: application/json" \ -d '{ "nickname": "Vail — Coffee", "name": "Coffee in Vail", "category": "food_and_drink" }'| Field | Notes |
|---|---|
nickname | Required, 1–200 characters. Label in the Guides tab, and the heading guests see when there is no name |
name | Optional guest-facing heading in plain English, up to 200 characters. Stored as a translation |
category | Optional, one of the eight values above, or null for uncategorized |
coverImageUrl | Optional http(s) URL. An https image is copied into SuiteOp storage after the save (Unsplash and Google photos stay hotlinked); an http URL is stored as-is |
showOnBookingSites | Optional, defaults to false |
The response is 201 with id, nickname, nameId, category, showOnBookingSites, sortOrder, createdAt and updatedAt. The cover photo is not echoed back; read it with getGuideList.
Then attach the list, for example to one portal:
curl -X POST https://api-us.suiteop.com/api/v1/entity-scopes/add \ -H "Authorization: Bearer sk_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{ "entityType": "guide_list", "entityId": "5d1e8a40-…", "portalSettingsIds": ["2b7c4e10-…"] }'See Entity Scopes for the exact request shape and the other scope kinds.
Updating a list
Section titled “Updating a list”PATCH /guide-lists/{id} takes { "data": { … } } with any of nickname, category, coverImageUrl and showOnBookingSites. Fields you omit are left unchanged; null clears category or coverImageUrl.
It cannot change the guest-facing heading (edit the translation behind nameId) or where the list shows (use the entity-scope operations).
Adding places
Section titled “Adding places”POST /guide-places adds one place. Call it once per place.
const res = await fetch('https://api-us.suiteop.com/api/v1/guide-places', { method: 'POST', headers: { Authorization: `Bearer ${process.env.SUITEOP_API_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': crypto.randomUUID(), }, body: JSON.stringify({ guideListId: '5d1e8a40-…', titleText: "Loaded Joe's", shortDescriptionText: "Our favourite breakfast burrito, five minutes' walk from the village.", priceCents: 2, categories: ['cafe'], address: { formattedAddress: '141 E Meadow Dr, Vail, CO 81657, USA', latitude: null, longitude: null, }, }),})const { data: place } = await res.json()| Field | Notes |
|---|---|
guideListId | Required |
address | Required. formattedAddress is required, and latitude and longitude must be present but may be null. A place without coordinates still lists for guests but is left off the map. The other address fields are optional |
titleText | Plain English, up to 200 characters |
shortDescriptionText | Plain English, up to 2,000 characters |
priceCents | A price level, not money. 1–4 shows as $–$$$$; other values hide the indicator |
rating | 0–5. ratingsCount is how many ratings it is based on |
categories | Google Place types, for example ["cafe"] or ["restaurant", "bar"]. Values outside the published list are rejected |
imageUrl, websiteUrl, googleMapsUrl | http(s) URLs. An empty string is stored as null. An https imageUrl is copied into SuiteOp storage like coverImageUrl |
addGuidePlace looks nothing up: it doesn’t search Google or fetch a photo. Send the details and links you already have, or use importGuidePlace to have SuiteOp fill them in.
A new place gets sortOrder 0, so it can list above places you added earlier. Call reorderGuidePlaces after a batch of adds to set the order.
The response is 201 with the place’s stored fields. Its name, recommendation and photo are not echoed back; read them with getGuideList.
Importing a place from Google
Section titled “Importing a place from Google”POST /guide-places/import adds one place from its Google place ID and fills in what you leave out. It takes the same fields as addGuidePlace, except that titleText is required here (1 to 200 characters) and categories holds at most 20, plus googlePlaceId (required) and an optional googlePhotoReference:
- Recommendation. Without
shortDescriptionText, SuiteOp uses Google’s editorial summary of the place, and when Google has none, asks an AI model to write one from the name, address andcategories(at most 20 here). - Photo. Without
imageUrl, SuiteOp resolvesgooglePhotoReference(aplaces/<placeId>/photos/<photoId>name or a legacy photo token, up to 2,048 characters) to its Google photo, else picks an Unsplash photo by the place’s name. AnimageUrlofnullor""counts as not sent, so an import always tries to add a photo. - What you send wins. A non-blank
shortDescriptionTextorimageUrlyou send is stored as-is and skips that lookup.
curl -X POST https://api-us.suiteop.com/api/v1/guide-places/import \ -H "Authorization: Bearer sk_live_your_key_here" \ -H "Idempotency-Key: 9b1f3c2e-5d4a-4e8b-a7c6-2f0e1d3b5a49" \ -H "Content-Type: application/json" \ -d '{ "guideListId": "5d1e8a40-…", "googlePlaceId": "ChIJN1t_tDeuEmsRUsoyG83frY4", "titleText": "Loaded Joe'"'"'s", "categories": ["cafe"], "address": { "formattedAddress": "141 E Meadow Dr, Vail, CO 81657, USA", "latitude": 39.6406, "longitude": -106.3742 } }'The response is 201 with the place’s stored fields, like addGuidePlace; read the recommendation and photo it got with getGuideList. The lookups are best effort: if Google, Unsplash or the model fails or finds nothing, the place is still created, without that recommendation or photo.
Each import is paid and counts against the organization’s model-backed budget, shared by every key and token in the organization. Over it, the call returns 429 before anything is looked up. Use addGuidePlace when you already have the text and photo, and send an Idempotency-Key so a retry doesn’t import the place twice. A list deleted while the lookups run returns 404.
Updating and removing places
Section titled “Updating and removing places”PATCH /guide-places/{id} takes { "data": { … } } with any place field. A changed titleText or shortDescriptionText overwrites the English text and re-translates the other languages. An empty shortDescriptionText clears it; an empty titleText is ignored. address, when sent, needs formattedAddress, latitude and longitude as on create; streetAddress, city, state, postalCode and countryCode you leave out keep their stored values. A place can’t be moved to another list: remove it and add it there.
DELETE /guide-places/{id} removes a place permanently, and its photo with it. There is no undo, so read the place first if you may need to add it back. The other places keep their order.
Ordering places
Section titled “Ordering places”POST /guide-lists/{guideListId}/places/reorder sets the order guests see. Send every place ID of the list in the order you want; index 0 is shown first. It is not a move-one-place call.
curl -X POST https://api-us.suiteop.com/api/v1/guide-lists/5d1e8a40-…/places/reorder \ -H "Authorization: Bearer sk_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{ "orderedIds": ["c40b7e95-…", "71aa02d3-…", "e98f6c14-…"] }'A duplicated ID, an ID from another list, or a set that leaves out any place returns 400 validation_error and nothing is reordered. Read the list with getGuideList immediately before you reorder, so a place added in the meantime isn’t missing from your set.
Ordering lists
Section titled “Ordering lists”POST /guide-lists/reorder sets the order guests see the lists in. It takes { "orderedIds": [...] } with every list ID that listGuideLists returns to you, index 0 first, and answers { "success": true }. A duplicated, missing or unknown ID returns 400 validation_error and nothing moves. Lists hidden from an OAuth token limited to some properties keep their position.
Deleting a list
Section titled “Deleting a list”DELETE /guide-lists/{id} deletes the list and every place in it, with their photos. Guests stop seeing it at once and there is no undo. It needs delete_guide, which modify_guides doesn’t include. The response is { "id": "…" }. A list in another organization, or outside the caller’s property access, returns 404. To hide a list instead, remove its scopes with the entity-scope operations; to drop one place, use removeGuidePlace.
Concurrent edits
Section titled “Concurrent edits”updateGuideList and updateGuidePlace accept an optional expectedUpdatedAt. Send the updatedAt from your last read or write exactly as returned, with milliseconds. If the list or place changed since, the update returns 409 (CONFLICT) and writes nothing. Omit it to overwrite regardless.
Things to watch for
Section titled “Things to watch for”- Lists on booking sites with no portal. A list with
showOnBookingSites: trueand no scopes is served on every booking site of the organization. Only a caller with access to all properties can change such a list or its places, or change itsshowOnBookingSites; an OAuth token whose user is limited to some properties gets403 authorization_error. API keys reach every property, so this never applies to them. Once the list has at least one scope, this restriction no longer applies. - Lists outside your property scope return
404, the same as a list that doesn’t exist. - Inactive is not an error. Places you add to an unscoped list are saved but reach no guest portal until the list is attached. If the list has
showOnBookingSites: true, they appear on every booking site immediately.
Related
Section titled “Related”- Entity Scopes: attach guide lists to portals, properties, groups and tags
- Guest Portals: find
portalSettingsIdvalues - Translations: read and edit the list’s guest-facing heading
- House Manuals: the other guest-portal content you can manage through the API