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.
| Operation | Request | Permission |
|---|---|---|
listPortalManuals | GET /portal-manuals | view_guides |
listPortalManualsByProperty | GET /properties/{propertyId}/portal-manuals | view_guides |
createPortalManual | POST /portal-manuals | modify_guides |
updatePortalManual | PATCH /portal-manuals/{id} | modify_guides |
listPortalManualCategories | GET /portal-manual-categories | view_guides |
createPortalManualCategory | POST /portal-manual-categories | modify_guides |
updatePortalManualCategory | PATCH /portal-manual-categories/{id} | modify_guides |
reorderPortalManualCategories | POST /portal-manual-categories/reorder | modify_guides |
deletePortalManualCategory | DELETE /portal-manual-categories/{id} | modify_guides |
searchCoverIcons | GET /cover-icons | None |
searchStockPhotos | GET /stock-photos | None |
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.
How manuals fit together
Section titled “How manuals fit together”- Manual: the article.
nicknameis the internal label operators search by and is never shown to guests. The guest-facing title and body are translations, referenced bytitleIdanddescriptionId. - 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’svisibilityRulesfor that reservation.
Listing manuals
Section titled “Listing manuals”GET /portal-manuals returns every manual in the organization, ordered by nickname. It is not paginated.
curl "https://api-us.suiteop.com/api/v1/portal-manuals?search=wifi&tagIds=5e8a1c3d-…&tagIds=7b20e9f4-…" \ -H "Authorization: Bearer sk_live_your_key_here"| Filter | Notes |
|---|---|
search | Case-insensitive substring of nickname, up to 200 characters. Guest-facing titles aren’t searched |
propertyIds | Manuals with a direct scope to at least one of these properties. Manuals reaching them through a group or tag aren’t matched |
propertyGroupIds | Manuals scoped to at least one of these property groups |
tagIds | Manuals 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.
Creating a manual
Section titled “Creating a manual”nickname is required, and so is a guest-facing title unless displayMode is pms_variable or you link existing copy with titleId.
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.idField notes:
isActivedefaults tofalse. Omit it and the manual is created switched off: assigned, but shown to no guest until you set it totrue.propertyIdassigns the manual to one property. Omit it for an org-level manual, then assign it withaddEntityScopes. A property outside your access returns404.titleanddescriptionare 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.titleIdanddescriptionIdlink an existing translation instead; when you send both forms, the text wins.displayModedefaults tomulti_lingual_text.pms_variableshows the value of a custom field instead of the description and requirescustomFieldId;customFieldIdis rejected in any other mode. No operation lists custom fields.videoUrlmust be anhttporhttpsURL.manualCategoryIdcomes fromlistPortalManualCategories. Omit it or sendnullfor an uncategorised manual.internalDescriptionis a staff note and is never shown to guests.- The response echoes
imageId, not theimageUrlyou sent.listPortalManualsreturns theimageUrl.
Visibility rules
Section titled “Visibility rules”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:
ruleCategory | Fields read |
|---|---|
relative_date | startDateRelativeType 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 |
seasonal | seasonalWindows, at least one { "start": "2026-07-01", "end": "2026-08-31" } |
length_of_stay | lengthOfStayFromDays and lengthOfStayToDays; omit either for no bound |
booking_source | sourceRuleType only_selected with a non-empty includedBookingSources, or all_except_selected with excludedBookingSources. Channels such as airbnb, booking_com, vrbo, direct |
verification_status | A 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"] } ]}Updating a manual
Section titled “Updating a manual”PATCH /portal-manuals/{id} takes the fields to change inside a data object. Fields you leave out keep their current value.
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
titleordescriptionfield;titleIdanddescriptionIdonly relink existing translations, andnullunlinks them. visibilityRulesreplaces the whole set. To change one rule, send all the others with it.[]makes the manual always visible.isActive: falsehides the manual from every portal and keeps its assignments.- Changing
displayModetopms_variableneedscustomFieldId; switching back needscustomFieldId: 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.
Cover images and icons
Section titled “Cover images and icons”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:
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:
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.
Assigning manuals
Section titled “Assigning manuals”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.
| Operation | Request | Effect |
|---|---|---|
listEntityScopes | GET /entity-scopes | Current propertyIds, propertyGroupIds and tagIds. Needs view_properties |
addEntityScopes | POST /entity-scopes/add | Adds the IDs you send and keeps the rest |
removeEntityScopes | POST /entity-scopes/remove | Removes the IDs you send and keeps the rest |
updateEntityScopes | POST /entity-scopes | Replaces every assignment with what you send |
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:
portalSettingsIdsis rejected forentityType: "manual". updateEntityScopestreats an omitted array as empty, so sending onlypropertyIdsclears the manual’s groups and tags. For a property-limited caller it replaces only the assignments within their access, and an ID outside it returns404.listEntityScopesreturns four empty arrays for an unknown ID, another organization’s ID or the wrongentityType, rather than an error.
Property-limited callers
Section titled “Property-limited callers”When the credential’s member is limited to some properties:
listPortalManualsreturns manuals assigned to at least one of their properties, plus org-level manuals.GET /properties/{propertyId}/portal-manualsreturns an empty list for a property outside their access.createPortalManualwith apropertyIdoutside their access returns404. WithoutpropertyId, the new org-level manual stays visible to them so they can assign it.updatePortalManualneeds every assignment of the manual to be within their access.
Ordering
Section titled “Ordering”- 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-manualsreturns them in this order. No API operation changes it. - Categories are shown in
sortOrder. New categories go last. UsereorderPortalManualCategoriesto change the order. listPortalManualsis ordered bynickname, not portal order.
Categories
Section titled “Categories”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.
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.
titleis English, 1 to 200 characters after trimming, and must not match another category’s English title, ignoring case and surrounding spaces; a duplicate returns409. 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 (nodatawrapper). 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/reorderwithorderedIds, 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 returns409while any manual is filed under the category. Move those manuals first withupdatePortalManual(listPortalManualsshows eachmanualCategoryId). 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.