Skip to content
Dashboard

House Manuals

A house manual is one article in a property’s guest portal: a welcome book, a Wi-Fi card, an appliance guide. Each manual can be filed under a category, which becomes a heading in the portal, and is assigned to the properties whose guests should see it. This guide covers the manual and category operations and the cover-image helpers. Every field is listed on the operation’s page in the API Reference.

OperationRequestPermission
listPortalManualsGET /portal-manualsview_guides
listPortalManualsByPropertyGET /properties/{propertyId}/portal-manualsview_guides
createPortalManualPOST /portal-manualsmodify_guides
updatePortalManualPATCH /portal-manuals/{id}modify_guides
listPortalManualCategoriesGET /portal-manual-categoriesview_guides
createPortalManualCategoryPOST /portal-manual-categoriesmodify_guides
updatePortalManualCategoryPATCH /portal-manual-categories/{id}modify_guides
reorderPortalManualCategoriesPOST /portal-manual-categories/reordermodify_guides
deletePortalManualCategoryDELETE /portal-manual-categories/{id}modify_guides
searchCoverIconsGET /cover-iconsNone
searchStockPhotosGET /stock-photosNone

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 creates and edits manuals usually needs both, because the IDs and current values you edit come from the list reads. Assigning manuals to properties needs modify_properties as well as modify_guides (see Assigning manuals).

Manuals can’t be deleted through the API. Set isActive to false to take one off every portal.

  • Manual: the article. nickname is the internal label operators search by and is never shown to guests. The guest-facing title and body are translations, referenced by titleId and descriptionId.
  • Category: a heading guests see manuals grouped under. A manual has at most one, manualCategoryId, or none.
  • Scopes: the properties, property groups and tags a manual is assigned to. A manual with no scopes is an org-level manual: it exists and can be edited, but no guest sees it.
  • What a guest sees: active manuals (isActive: true) assigned to the guest’s property directly, through its property group or through one of its tags, filtered by the manual’s visibilityRules for that reservation.

GET /portal-manuals returns every manual in the organization, ordered by nickname. It is not paginated.

Terminal window
curl "https://api-us.suiteop.com/api/v1/portal-manuals?search=wifi&tagIds=5e8a1c3d-…&tagIds=7b20e9f4-…" \
-H "Authorization: Bearer sk_live_your_key_here"
FilterNotes
searchCase-insensitive substring of nickname, up to 200 characters. Guest-facing titles aren’t searched
propertyIdsManuals with a direct scope to at least one of these properties. Manuals reaching them through a group or tag aren’t matched
propertyGroupIdsManuals scoped to at least one of these property groups
tagIdsManuals scoped to at least one of these tags

Repeat a key for several values. Within one filter any value matches; different filters combine with AND. Once any scope filter is set, org-level manuals are excluded.

Each row carries id, nickname, isActive, manualCategoryId, titleId, descriptionId, displayMode, customFieldId, videoUrl, internalDescription, imageId, imageUrl, iconName, visibilityRules, createdAt and updatedAt. The guest-facing title and body aren’t included: pass titleId or descriptionId to GET /translations/{translationId}/entries (see Translations).

To see what one property carries, use GET /properties/{propertyId}/portal-manuals, described under Properties. It resolves direct, group and tag assignments, orders manuals as the portal does, and adds each row’s sortOrder for that property.

nickname is required, and so is a guest-facing title unless displayMode is pms_variable or you link existing copy with titleId.

Terminal window
curl -X POST https://api-us.suiteop.com/api/v1/portal-manuals \
-H "Authorization: Bearer sk_live_your_key_here" \
-H "Idempotency-Key: 8c1f3e2a-4b5d-4e6f-9a7b-1c2d3e4f5a6b" \
-H "Content-Type: application/json" \
-d '{
"nickname": "Harbour Cottage wifi",
"title": "Getting online",
"description": "The network is HarbourGuest. The password is on the card by the kettle.",
"manualCategoryId": "3f2b8a61-…",
"iconName": "wifi",
"propertyId": "9a3d7e40-…",
"isActive": true
}'

The response is 201 with the new manual:

{
"data": {
"id": "c7e4a9d2-…",
"nickname": "Harbour Cottage wifi",
"isActive": true,
"displayMode": "multi_lingual_text",
"videoUrl": null,
"internalDescription": null,
"manualCategoryId": "3f2b8a61-…",
"customFieldId": null,
"titleId": "1a9e5c70-…",
"descriptionId": "6d2f8b13-…",
"imageId": null,
"iconName": "wifi",
"visibilityRules": [],
"createdAt": "2026-09-26T09:14:02.118Z",
"updatedAt": "2026-09-26T09:14:02.118Z"
},
"meta": { "requestId": "…" }
}
const res = await fetch('https://api-us.suiteop.com/api/v1/portal-manuals', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SUITEOP_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({
nickname: 'Harbour Cottage wifi',
title: 'Getting online',
description: 'The network is HarbourGuest.',
isActive: true,
}),
})
const { data } = await res.json()
const manualId = data.id

Field notes:

  • isActive defaults to false. Omit it and the manual is created switched off: assigned, but shown to no guest until you set it to true.
  • propertyId assigns the manual to one property. Omit it for an org-level manual, then assign it with addEntityScopes. A property outside your access returns 404.
  • title and description are plain English, up to 200 and 5000 characters. They are stored as translations and translated into the organization’s other languages in the background, so other languages appear a little later. titleId and descriptionId link an existing translation instead; when you send both forms, the text wins.
  • displayMode defaults to multi_lingual_text. pms_variable shows the value of a custom field instead of the description and requires customFieldId; customFieldId is rejected in any other mode. No operation lists custom fields.
  • videoUrl must be an http or https URL.
  • manualCategoryId comes from listPortalManualCategories. Omit it or send null for an uncategorised manual.
  • internalDescription is a staff note and is never shown to guests.
  • The response echoes imageId, not the imageUrl you sent. listPortalManuals returns the imageUrl.

visibilityRules limits which reservations see a manual. An empty array, the default, means always visible. Send at most one rule per ruleCategory, five in all:

ruleCategoryFields read
relative_datestartDateRelativeType with daysBefore, and endDateRelativeType with daysAfter. Anchors: before_check_in, after_check_in, before_check_out, after_check_out, check_in_time, check_out_time. daysBeforeTimeUnit and daysAfterTimeUnit are day (default) or hour
seasonalseasonalWindows, at least one { "start": "2026-07-01", "end": "2026-08-31" }
length_of_staylengthOfStayFromDays and lengthOfStayToDays; omit either for no bound
booking_sourcesourceRuleType only_selected with a non-empty includedBookingSources, or all_except_selected with excludedBookingSources. Channels such as airbnb, booking_com, vrbo, direct
verification_statusA non-empty verificationStatuses, for example ["verified"]

Fields that don’t belong to a rule’s category are ignored.

{
"visibilityRules": [
{
"ruleCategory": "relative_date",
"startDateRelativeType": "before_check_in",
"daysBefore": 2,
"endDateRelativeType": "after_check_out",
"daysAfter": 0
},
{ "ruleCategory": "verification_status", "verificationStatuses": ["verified"] }
]
}

PATCH /portal-manuals/{id} takes the fields to change inside a data object. Fields you leave out keep their current value.

Terminal window
curl -X PATCH https://api-us.suiteop.com/api/v1/portal-manuals/c7e4a9d2-… \
-H "Authorization: Bearer sk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{"data": {"manualCategoryId": "4b7c2e90-…", "isActive": false}}'

The response is 200 with the updated manual, in the same shape as the create response.

  • Title and body text can’t be edited here. There is no title or description field; titleId and descriptionId only relink existing translations, and null unlinks them.
  • visibilityRules replaces the whole set. To change one rule, send all the others with it. [] makes the manual always visible.
  • isActive: false hides the manual from every portal and keeps its assignments.
  • Changing displayMode to pms_variable needs customFieldId; switching back needs customFieldId: null.
  • Property-limited callers can only edit a manual whose every assignment is within their access. A manual that is also assigned to a property, group or tag outside it returns 404, as does one they can’t see at all.

A manual’s cover is a photo or an icon, never both. Sending imageUrl and a non-empty iconName together is rejected with 400. On update, setting one clears the other.

Icons. iconName takes a kebab-case name such as wifi or bed-double, painted in the property’s brand colour. The name isn’t validated: an unknown name is saved and shows as a blank placeholder to guests. Look names up with searchCoverIcons:

Terminal window
curl "https://api-us.suiteop.com/api/v1/cover-icons?query=wash&limit=10" \
-H "Authorization: Bearer sk_live_your_key_here"
{
"data": { "items": [{ "name": "washing-machine" }], "total": 1 },
"meta": { "requestId": "…" }
}

query is a case-insensitive substring match, limit is 1 to 100 (default 25), and total counts matches before the limit. The catalogue is the same for every organization.

Photos. imageUrl takes an http or https URL that is already hosted. It is stored and served as-is, never copied, so a link that later breaks shows a broken cover. On update, null or "" removes the cover and the previous image is released. imageId, update only, binds an image record that already exists; prefer imageUrl.

searchStockPhotos searches Unsplash for generic cover photos:

Terminal window
curl "https://api-us.suiteop.com/api/v1/stock-photos?query=coastal%20balcony&perPage=5" \
-H "Authorization: Bearer sk_live_your_key_here"

Each item has imageUrl, thumbnailUrl, description, photographerName and photographerUrl; total and totalPages come with them. query is required; perPage is 1 to 30 (default 20) and page 1 to 100. Pass the chosen imageUrl to createPortalManual or updatePortalManual; Unsplash usage is reported for you. Stock photos never show the actual property. Where stock search isn’t configured, the call returns 503; use an icon instead.

Neither search needs a permission, so a key holding only modify_guides can use them. Both also serve upsell covers.

Manual assignments are managed with the entity-scope operations, with entityType set to manual. Writing assignments needs both modify_properties and modify_guides; without modify_guides the write returns 403.

OperationRequestEffect
listEntityScopesGET /entity-scopesCurrent propertyIds, propertyGroupIds and tagIds. Needs view_properties
addEntityScopesPOST /entity-scopes/addAdds the IDs you send and keeps the rest
removeEntityScopesPOST /entity-scopes/removeRemoves the IDs you send and keeps the rest
updateEntityScopesPOST /entity-scopesReplaces every assignment with what you send
Terminal window
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 '{
"entityId": "c7e4a9d2-…",
"entityType": "manual",
"propertyGroupIds": ["2e6d0f5b-…"],
"tagIds": ["5e8a1c3d-…"]
}'
{
"data": {
"propertyIds": ["9a3d7e40-…"],
"propertyGroupIds": ["2e6d0f5b-…"],
"tagIds": ["5e8a1c3d-…"],
"portalSettingsIds": []
},
"meta": { "requestId": "…" }
}
  • Adding an ID the manual already has, or removing one it doesn’t have, succeeds without change, so both are safe to retry.
  • Manuals can’t be assigned to guest portals: portalSettingsIds is rejected for entityType: "manual".
  • updateEntityScopes treats an omitted array as empty, so sending only propertyIds clears the manual’s groups and tags. For a property-limited caller it replaces only the assignments within their access, and an ID outside it returns 404.
  • listEntityScopes returns four empty arrays for an unknown ID, another organization’s ID or the wrong entityType, rather than an error.

When the credential’s member is limited to some properties:

  • listPortalManuals returns manuals assigned to at least one of their properties, plus org-level manuals.
  • GET /properties/{propertyId}/portal-manuals returns an empty list for a property outside their access.
  • createPortalManual with a propertyId outside their access returns 404. Without propertyId, the new org-level manual stays visible to them so they can assign it.
  • updatePortalManual needs every assignment of the manual to be within their access.
  • Manuals within a property are ordered by the assignment’s sortOrder, then by creation time; manuals without a position come last. GET /properties/{propertyId}/portal-manuals returns them in this order. No API operation changes it.
  • Categories are shown in sortOrder. New categories go last. Use reorderPortalManualCategories to change the order.
  • listPortalManuals is ordered by nickname, not portal order.

GET /portal-manual-categories lists categories in portal order, with title resolved into language (for example fr or pt-BR; default English). A language with no translation falls back to English. title is null for a category that was set up without one; its id still works. Not paginated.

Terminal window
curl -X POST https://api-us.suiteop.com/api/v1/portal-manual-categories \
-H "Authorization: Bearer sk_live_your_key_here" \
-H "Idempotency-Key: 2d4f6a8c-1b3e-4c5d-8e7f-9a0b1c2d3e4f" \
-H "Content-Type: application/json" \
-d '{"title": "Kitchen appliances"}'
{
"data": {
"id": "4b7c2e90-…",
"title": "Kitchen appliances",
"titleId": "8e3a1d55-…",
"sortOrder": 6,
"createdAt": "2026-09-26T09:20:44.506Z",
"updatedAt": "2026-09-26T09:20:44.506Z"
},
"meta": { "requestId": "…" }
}
  • Titles are unique. title is English, 1 to 200 characters after trimming, and must not match another category’s English title, ignoring case and surrounding spaces; a duplicate returns 409. List first and reuse an existing ID. Other languages are translated in the background.
  • Renaming: PATCH /portal-manual-categories/{id} with {"title": "…"} at the top level (no data wrapper). Manuals stay filed under it. The other languages are re-translated only if all of them are machine translations; if anyone wrote or reviewed one by hand, only English changes. See Translations to push the new wording anyway.
  • Reordering: POST /portal-manual-categories/reorder with orderedIds, the complete list of category IDs in display order (1 to 200). The IDs you send take positions 0, 1, 2 and so on; any ID you leave out keeps its old number, so a partial list can leave two categories sharing a position. If any ID is unknown or not writable by you, nothing changes. The response is {"data": {"reordered": <count>}, "meta": {"requestId": "…"}}.
  • Deleting: DELETE /portal-manual-categories/{id} is permanent and also deletes the title’s translations. It returns 409 while any manual is filed under the category. Move those manuals first with updatePortalManual (listPortalManuals shows each manualCategoryId). Don’t delete and recreate to rename: the manuals lose their category.
  • Property-limited callers can’t rename or reorder a category used by manuals on properties outside their access; the call returns 403.