Guest Portals
A guest portal is the page a guest opens for their stay: pre-check-in, chat, issue reporting, the review surveys, quick-access links and an optional special-offer card. Each portal is one configuration, and properties point at it. A portal branding is the set of colours, logos and social links a portal wears. This guide covers the portal and branding operations. Every field is listed on the operation’s page in the API Reference.
| Operation | Request | Permission |
|---|---|---|
listPortals | GET /portals | view_properties |
getPortal | GET /portals/{id} | view_properties |
createPortal | POST /portals | modify_properties |
updatePortal | PATCH /portals/{id} | modify_properties |
listPortalBrandings | GET /portal-brandings | view_properties |
createPortalBranding | POST /portal-brandings | modify_properties |
updatePortalBranding | PATCH /portal-brandings/{id} | modify_properties |
assignPortalToProperty | POST /properties/{propertyId}/assign-portal | modify_properties |
Paths are relative to your region’s base URL, for example https://api-us.suiteop.com/api/v1. See Authentication for permissions.
Writing guest copy on a portal (specialOfferTitle, specialOfferType, specialOfferCtaText and precheckinPromptMessage) needs manage_organization as well as modify_properties. Every other portal field needs only modify_properties. See Special offer and pre-check-in copy.
Portals and brandings can’t be deleted or duplicated through the API. The special-offer image and the branding background image are uploaded in SuiteOp, not here.
How portals fit together
Section titled “How portals fit together”- Portal: the configuration: name, support contacts, feature flags, quick-access tiles, special offer, pre-check-in prompt and
brandingId. - Default portal: at most one portal per organization has
isDefault: true. Properties that no other portal claims use it. - Which portal a guest sees: a portal assigned directly to the property wins. Otherwise a portal scoped to the property’s group, then one scoped to one of its tags, then the organization’s default portal.
- Branding: a portal with
brandingId: nullwears the organization’s default branding, the one withisDefault: trueinlistPortalBrandings. At most one branding is the default.
Listing and reading portals
Section titled “Listing and reading portals”GET /portals returns every portal in the organization. It is not paginated and not sorted. Each row carries only id, name, isDefault, supportEmail and brandingId. name can be null on portals created before names were required.
curl https://api-us.suiteop.com/api/v1/portals \ -H "Authorization: Bearer sk_live_your_key_here"writableOnly=true narrows the list to portals whose every property the caller holds. It changes nothing for an API key, which always has access to every property.
GET /portals/{id} returns the whole configuration: the support block, every feature flag, the special-offer settings, precheckinPromptMessageId, excludedQuickAccessTypes and brandingId. Read it before an update so you know what you’re changing. A portal in another organization returns 404.
The detail doesn’t include the guest copy itself, only its translation IDs (specialOfferTitleId, specialOfferTypeId, specialOfferCtaTextId, precheckinPromptMessageId). Pass one to GET /translations/{translationId}/entries to read the text; see Translations.
Creating a portal
Section titled “Creating a portal”name is the only required field. Every feature flag defaults to on except isSpecialOfferActive, which defaults to off.
curl -X POST https://api-us.suiteop.com/api/v1/portals \ -H "Authorization: Bearer sk_live_your_key_here" \ -H "Idempotency-Key: 0f7a3c52-9d1e-4b8a-a6c4-2e5f8b9d1c37" \ -H "Content-Type: application/json" \ -d '{ "name": "Downtown Lofts", "supportEmail": "[email protected]", "supportWhatsapp": "+33612345678", "communicationMethod": "whatsapp", "supportNotes": "We answer between 8am and 10pm.", "brandingId": "b3e1f9a4-…", "excludedQuickAccessTypes": ["uber", "rent_baby_gear"], "isSpecialOfferActive": true, "specialOfferType": "Upgrade", "specialOfferTitle": "Late check-out", "specialOfferCtaText": "Book it", "specialOfferUrl": "https://example.com/late-checkout" }'The response is 201 with the new portal:
{ "data": { "id": "c41e7d20-…", "name": "Downtown Lofts", "isDefault": false, "supportPhone": null, "supportWhatsapp": "+33612345678", "supportTextPhone": null, "supportNotes": "We answer between 8am and 10pm.", "communicationMethod": "whatsapp", "isPrecheckinEnabled": true, "isPrecheckOrderAutomatic": true, "isManualReviewEnabled": true, "isTicketingEnabled": true, "isReviewRequestCheckoutEnabled": true, "isChatEnabled": true, "isReviewRequestCheckinEnabled": true, "isConfettiEnabled": true, "isCartAbandonmentEnabled": true, "isWalletPassEnabled": true, "isSpecialOfferActive": true, "specialOfferUrl": "https://example.com/late-checkout", "specialOfferTitleId": "5a8d2e61-…", "specialOfferTypeId": "9c0f4b17-…", "specialOfferCtaTextId": "2e7b6a90-…", "precheckinPromptMessageId": null, "excludedQuickAccessTypes": ["uber", "rent_baby_gear"], "brandingId": "b3e1f9a4-…", "createdAt": "2026-09-27T10:02:11.482Z", "updatedAt": "2026-09-27T10:02:11.482Z" }, "meta": { "requestId": "…" }}A new portal serves no property until you assign one; see Assigning portals. getPortal, createPortal and updatePortal all return this shape.
Field notes:
nameis 1 to 60 characters and must be unique in the organization, ignoring case. A clash returns409.isDefault: truemakes this the organization’s default portal and takes the flag off the portal that had it.- Support numbers (
supportPhone,supportWhatsapp,supportTextPhone) accept common formats and are stored in international form, for example+33612345678. A number that can’t be parsed is rejected with400.supportNotesis up to 2000 characters. communicationMethod(whatsapp,textorphone) chooses what the portal’s contact bubble opens. The matching number (supportWhatsapp,supportTextPhoneorsupportPhone) must be set in the same request, or the create returns400. Omit it to leave the bubble off.brandingIdmust be a branding in your organization; another organization’s ID returns404. Omit it for the default branding.excludedQuickAccessTypeslists the tiles to hide, fromuber,report_an_issue,find_parking,find_parking_justparkandrent_baby_gear. If you omit it, the portal storesnull, and anulllist hides every tile exceptreport_an_issue. Send[]to show all five.isWalletPassEnabled: falsestops offering guests an Apple Wallet or Google Wallet pass for their stay. The pass updates itself as the stay changes and shows the door codes once the portal does. Turning it off removes any door code from passes already issued, and those passes stop updating.isManualReviewEnabled: falseauto-approves guest pre-check-in submissions. It has nothing to do with house manuals.isChatEnabled: falseremoves the chat launcher, and the server refuses guest messages too.isPrecheckOrderAutomatic(defaulttrue) shows guests the pre-check-in steps in the order that saves them the most typing. While it’s on, a step’ssortOrderonly orders steps within the same group, and some groups hold two step types (other_guestswithauthority_reporting, anddamage_depositwithscreen_and_protect); set it tofalseto show the steps in your ownsortOrder. See Pre-check-in Steps.specialOfferUrlmust be anhttporhttpsURL.
Updating a portal
Section titled “Updating a portal”PATCH /portals/{id} takes the fields to change inside a data object. Fields you leave out keep their current value; null clears a nullable field.
curl -X PATCH https://api-us.suiteop.com/api/v1/portals/c41e7d20-… \ -H "Authorization: Bearer sk_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{ "data": { "isChatEnabled": false, "communicationMethod": "phone", "supportPhone": "+33198765432" }, "expectedUpdatedAt": "2026-09-27T10:02:11.482Z" }'The response is 200 with the updated portal.
expectedUpdatedAtis optional and goes besidedata, not inside it. If the portal has changed since that time, the update returns409and changes nothing. Without it, the last write wins. MCP callers can’t send it.communicationMethodis checked against the portal as it will be after the update, so the number can come from this request or already be on the portal. Clearing the number the current method uses without changing the method returns400.nameis checked for uniqueness only when it changes, so resending the current name is safe.isDefault: truemoves the default to this portal.isDefault: falseon the only default portal returns409: make another portal the default instead.excludedQuickAccessTypesreplaces the whole list.brandingId: nullswitches the portal back to the default branding.isSpecialOfferActive: trueon a portal that never had special-offer copy shows an empty card. SendspecialOfferTitleandspecialOfferCtaTextin the same request.- The translation IDs and the special-offer image can’t be set here. Send text instead.
Special offer and pre-check-in copy
Section titled “Special offer and pre-check-in copy”specialOfferTitle, specialOfferType and specialOfferCtaText (up to 200 characters each) and precheckinPromptMessage (up to 6000) are plain text. Surrounding spaces are trimmed.
- English. Text sent through the API is stored as the English version.
- Other languages follow in the background. After the request commits, the organization’s other languages are machine-translated from the English, so they appear a little later.
- A change overwrites hand-written translations. When you change a field’s text on update, every other language of that field is re-translated, replacing any translation someone wrote by hand.
- Resending the stored text changes nothing, and neither does an empty string. You can echo a portal you just read without re-triggering translation.
- Permission. Creating copy, or changing it on update, needs
manage_organization. Without it the request returns403and nothing is saved. Resending unchanged text doesn’t need it. - Omitting
precheckinPromptMessagekeeps the built-in pre-check-in prompt.
Brandings
Section titled “Brandings”GET /portal-brandings returns every branding in the organization, the default first and then oldest first. It is not paginated. Each branding carries id, name, nickname, textColor, primaryColor, secondaryColor, backgroundColor, footerColor, logoWhiteUrl, facebookUrl, linkedinUrl, instagramUrl, isDefault, showPoweredBy, customUrlId, calloutMessageId and updatedAt. Background images aren’t returned. showPoweredBy is deprecated and always false: the guest portal never shows a SuiteOp line. calloutMessageId is null when the branding has no callout; otherwise read the callout’s text in every language with getTranslationEntries on that ID.
curl -X POST https://api-us.suiteop.com/api/v1/portal-brandings \ -H "Authorization: Bearer sk_live_your_key_here" \ -H "Idempotency-Key: 6b2d9e14-3a7f-4c85-b0e1-8f4a2c6d7e93" \ -H "Content-Type: application/json" \ -d '{ "name": "Riva Tower", "nickname": "Riva, dark theme", "primaryColor": "#290C62", "backgroundColor": "#0E1116", "logoWhiteUrl": "https://cdn.example.com/riva-white.png", "instagramUrl": "https://instagram.com/rivatower" }'The response is 201 with the branding in the list shape. Use its id as brandingId on createPortal or updatePortal.
name(1 to 60 characters) is the only required field; the rest take the platform defaults. It is shown in portal headers. Names don’t have to be unique.nicknameis an internal label, up to 60 characters, never shown to guests. When sent it must not be blank, andnullis rejected.- Colours are hex with a leading
#. Three-digit shorthand such as#fffis expanded to six digits.backgroundColor: nullmeans the default white page. - URLs (
logoWhiteUrl,facebookUrl,linkedinUrl,instagramUrl) must be absolutehttporhttpsURLs. An empty string clears one. isDefault: truemakes this the default branding and takes the flag off the previous one.customUrlIdis the domain the branding is served on. Omit it, or sendnull, for the organization’s default domain. An ID from another organization returns404, and a custom URL that isn’t a guest-portal URL returns400. No API operation lists custom URLs.showPoweredByis deprecated and ignored. It is still accepted, on create and update, only so older clients don’t fail; leave it out.calloutMessage(create only, up to 2000 characters) seeds the portal’s callout text in English and is translated in the background. The response’scalloutMessageIdis how you read it back; no operation edits it, so later changes are made in the app.
PATCH /portal-brandings/{id} takes the same fields, except calloutMessage, inside a data object; fields you leave out keep their value. expectedUpdatedAt is optional and goes beside data: send the updatedAt you last read, and the update returns 409 and changes nothing if the branding has changed since. Without it, the last write wins. isDefault: false on the default branding returns 409: make another branding the default instead. An unknown ID returns 404.
Assigning portals
Section titled “Assigning portals”POST /properties/{propertyId}/assign-portal with {"portalSettingsId": "<portal id>"} points one property at a portal, and null clears it. The response is the property’s id and its portalSettingsId. Details are under Properties.
To assign a portal to property groups or tags, use the entity-scope operations with entityType: "portal_settings" and the portal’s id as entityId (see House Manuals for how they work). For portals, propertyIds sets the same direct assignment as assign-portal: updateEntityScopes moves the listed properties onto this portal and unassigns properties that are left out. portalSettingsIds is rejected.
Property-limited OAuth tokens
Section titled “Property-limited OAuth tokens”API keys can reach every property. An OAuth token acts with its user’s access, so when that user is limited to some properties:
listPortalsstill returns every portal.getPortalreturns404for a portal that serves none of their properties, unless it serves no property at all.updatePortalreturns404unless the user holds every property the portal serves, or it serves none.- Making a portal the default, on create or update, returns
403. Only a user with access to every property can move the default. assignPortalToPropertyreturns404for a property they can’t access, and403for a portal that is none of these: one they can read, the default portal, or one serving no property.