Skip to content
Dashboard

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.

OperationRequestPermission
listPortalsGET /portalsview_properties
getPortalGET /portals/{id}view_properties
createPortalPOST /portalsmodify_properties
updatePortalPATCH /portals/{id}modify_properties
listPortalBrandingsGET /portal-brandingsview_properties
createPortalBrandingPOST /portal-brandingsmodify_properties
updatePortalBrandingPATCH /portal-brandings/{id}modify_properties
assignPortalToPropertyPOST /properties/{propertyId}/assign-portalmodify_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.

  • 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: null wears the organization’s default branding, the one with isDefault: true in listPortalBrandings. At most one branding is the default.

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.

Terminal window
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.

name is the only required field. Every feature flag defaults to on except isSpecialOfferActive, which defaults to off.

Terminal window
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,
"supportEmail": "[email protected]",
"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:

  • name is 1 to 60 characters and must be unique in the organization, ignoring case. A clash returns 409.
  • isDefault: true makes 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 with 400. supportNotes is up to 2000 characters.
  • communicationMethod (whatsapp, text or phone) chooses what the portal’s contact bubble opens. The matching number (supportWhatsapp, supportTextPhone or supportPhone) must be set in the same request, or the create returns 400. Omit it to leave the bubble off.
  • brandingId must be a branding in your organization; another organization’s ID returns 404. Omit it for the default branding.
  • excludedQuickAccessTypes lists the tiles to hide, from uber, report_an_issue, find_parking, find_parking_justpark and rent_baby_gear. If you omit it, the portal stores null, and a null list hides every tile except report_an_issue. Send [] to show all five.
  • isWalletPassEnabled: false stops 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: false auto-approves guest pre-check-in submissions. It has nothing to do with house manuals.
  • isChatEnabled: false removes the chat launcher, and the server refuses guest messages too.
  • isPrecheckOrderAutomatic (default true) shows guests the pre-check-in steps in the order that saves them the most typing. While it’s on, a step’s sortOrder only orders steps within the same group, and some groups hold two step types (other_guests with authority_reporting, and damage_deposit with screen_and_protect); set it to false to show the steps in your own sortOrder. See Pre-check-in Steps.
  • specialOfferUrl must be an http or https URL.

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.

Terminal window
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.

  • expectedUpdatedAt is optional and goes beside data, not inside it. If the portal has changed since that time, the update returns 409 and changes nothing. Without it, the last write wins. MCP callers can’t send it.
  • communicationMethod is 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 returns 400.
  • name is checked for uniqueness only when it changes, so resending the current name is safe.
  • isDefault: true moves the default to this portal. isDefault: false on the only default portal returns 409: make another portal the default instead.
  • excludedQuickAccessTypes replaces the whole list.
  • brandingId: null switches the portal back to the default branding.
  • isSpecialOfferActive: true on a portal that never had special-offer copy shows an empty card. Send specialOfferTitle and specialOfferCtaText in the same request.
  • The translation IDs and the special-offer image can’t be set here. Send text instead.

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 returns 403 and nothing is saved. Resending unchanged text doesn’t need it.
  • Omitting precheckinPromptMessage keeps the built-in pre-check-in prompt.

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.

Terminal window
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.
  • nickname is an internal label, up to 60 characters, never shown to guests. When sent it must not be blank, and null is rejected.
  • Colours are hex with a leading #. Three-digit shorthand such as #fff is expanded to six digits. backgroundColor: null means the default white page.
  • URLs (logoWhiteUrl, facebookUrl, linkedinUrl, instagramUrl) must be absolute http or https URLs. An empty string clears one.
  • isDefault: true makes this the default branding and takes the flag off the previous one.
  • customUrlId is the domain the branding is served on. Omit it, or send null, for the organization’s default domain. An ID from another organization returns 404, and a custom URL that isn’t a guest-portal URL returns 400. No API operation lists custom URLs.
  • showPoweredBy is 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’s calloutMessageId is 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.

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.

API keys can reach every property. An OAuth token acts with its user’s access, so when that user is limited to some properties:

  • listPortals still returns every portal. getPortal returns 404 for a portal that serves none of their properties, unless it serves no property at all.
  • updatePortal returns 404 unless 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.
  • assignPortalToProperty returns 404 for a property they can’t access, and 403 for a portal that is none of these: one they can read, the default portal, or one serving no property.