Guest portal
Besides the registration, the guest link is the guide to their stay: how to get there, how to get in, how the house works, what is around and who to call. The access codes, only once registration is complete.
Until now the link of each booking did one thing: let the guest register and sign. With the portal on, that same link is also the guide to the stay, with your branding and in the guest’s language. There is no second link and no app: whatever your PMS already hands out, or whatever our emails send, leads to the portal.
It comes switched off, on every account. With the portal off the link works exactly as it did: the content is not even read. You can fill it in and preview it (“View as guest”, on each property) before switching it on, and it is included in the price: what you pay per filed booking does not change.
What the guest sees
The portal is split into four sections, with an icon bar at the bottom on mobile and a side menu on desktop. Only the sections with content show up, and the section travels in the link (#casa), so a message can point straight at one of them.
| Field | Type | Description |
|---|---|---|
Stay | always | The usual: dates and times, the registration, the signature and the guests in the party. |
The house | always · access behind a lock | In the order the guest walks through it: how to get there (with a “House address” button that opens their maps app), where to park (with its map), how to get in — instructions, codes and wifi, behind the lock —, how the house works by topic, the rules they signed in the contract and what to do when leaving. |
The area | always | Your recommendations by category — restaurants, cafés, supermarket, pharmacy, beach, sights, transport, leisure —, each with its description per language and a button to the map. |
Help | always | Your phone (the property’s or the account’s), WhatsApp, 112 and the useful numbers you add: taxi, health centre, duty pharmacy, local police, repairs. |
Every text goes per language: the guest reads theirs, failing that Spanish, failing that whatever exists. Categories, manual topics and phone types are translated on their own; proper names (a restaurant, a taxi company) are not.
When the codes open
The access instructions, the codes and the wifi sit behind a lock that opens when two conditions hold at once:
| Field | Type | Description |
|---|---|---|
Registration complete | condition | Every guest registered and the contract signed. It is exactly the condition that fires the checkin.completed webhook, so the portal and your PMS never disagree. |
Inside the window | condition | From N hours before the check-in time (48 by default, 1 to 168, set per account) until the end of the check-out day. Less than an hour leaves no room for an early guest; more than a week leaves the codes exposed in a forwarded link for too long. |
The codes do not travel with the page: the guest taps “Show the codes” and they are fetched at that moment. Each time is logged — when, from which network (truncated: last octet removed) and why it was allowed — for 90 days, and you can read it over the API. What that log never stores is the codes themselves.
Unlock now. The guest is at the door and has not signed: a button on the booking page — and an API call — opens the access without waiting for the registration or the window. Who opened it and when is recorded, it still closes at the end of the check-out day, and it can be undone.
| Field | Type | Description |
|---|---|---|
pending | closed | Someone still has to register or sign. |
tooEarly | closed | Registration complete, but it is too early: it opens at opensAt. |
open | open | The codes are visible, until closesAt. openedBy says whether it was the registration or a manual unlock. |
ended | closed | The check-out day is over. |
If you turn it on, when the access opens the guest gets an “all set for your arrival” email with the link to the portal. Never with the codes: an email gets forwarded; the portal, with its log of who looked, does not.
Switching it on and tuning it
/settings/settingsIn the panel, Guest portal → Settings. Over the API, the guestPortal object of /settings. Like every other setting, only what you send changes.
| Field | Type | Description |
|---|---|---|
guestPortal.enabled | boolean | Turn the portal on or off for the account. Off, the guest link works as it always did. |
guestPortal.hoursBefore | integer 1-168 | From how many hours before check-in the codes and the wifi can be seen. Default 48. |
guestPortal.phone | string | null | Contact phone of the account, the one the guest sees under Help. Each property can have its own. null removes it. |
guestPortal.readyEmail | boolean | “All set for your arrival” email with the link (never the codes) when the access becomes visible. |
curl -X PATCH https://api.kinapi.es/api/v1/settings \
-H "ocp-apim-subscription-key: YOUR_API_KEY" \
-H "content-type: application/json" \
-d '{"guestPortal":{"enabled":true,"hoursBefore":24,"phone":"+34600000000"}}'Common content, blocks and own content
Fifty properties are not filled in one by one. Seven sections of the portal can be shared — arrival, parking, the house manual, the house rules, the area, departure and the useful numbers — and they are written in three places:
| Field | Type | Description |
|---|---|---|
Common content | account | Written once, it applies to every property that does not say otherwise. House rules, departure or useful numbers are usually the same across the whole portfolio. |
A block | named variant | A version of one section with a name of its own — “Alameda car park”, “City centre” — assigned to the properties that use it. A building with six flats has one car park: write it once, assign it six times. |
Own content | property | What belongs to that flat. And what can only be its own, because it is not shared: the address (the house map), the access, the codes, the wifi and the phone. |
For each section of each property you decide where it comes from, and the rule is always the same:
| Field | Type | Description |
|---|---|---|
hidden | 1st | The property hides the section: nothing is shown, even if the common content has it. |
block | 2nd | The property uses a block (blockId): the block is shown, if it has content. |
own | 3rd | The property has its own content for the section: its own is shown. |
common | 4th | None of the above: the common content of the account is shown. |
empty | 5th | Nothing anywhere: the section does not appear. |
The house manual is the exception: it is composed topic by topic. The property writes heating and rubbish, the common content has kitchen and washing machine, and the guest sees all four; a topic present in both comes out with the property’s text.
In the panel, the Guest portal entry of the menu has three tabs: Properties (a table with the state of every section on every property — ✓ own, ≡ common, ⧉ block — and bulk assignment of a variant to several properties), Common and Settings. On each property, every shared section has its selector — common, a variant, own or hidden —, and the page has “Copy from another property” (everything but codes and wifi) and “Previous / Next” to walk the portfolio without going back to the list.
The common content of the account
/portal/common/portal/commonThe PUT replaces the whole common content: a section you leave out is removed. Read, change, write. The read returns only the sections with content.
| Field | Type | Description |
|---|---|---|
arrival | text per language | How to get there: entrance, floor, door, what to look for. An object with one text per language (es, en, de, fr, nl, it, pt); line breaks are kept and https links become links. |
parking | text per language | Where to leave the car. |
parkingMapUrl | string | https link to any map service (Google Maps, Apple Maps, Waze…). The guest gets a button that opens it in their app. |
manual | object | How the house works, by fixed topic: climatizacion, cocina, aguaCaliente, lavadora, basura, tv, exteriores, otros. Each topic, one text per language. Topic titles are translated on their own; a topic you leave out is not shown. |
showHouseRules | boolean | Show in the portal the house rules the guest signs in the contract (the booking’s, else the property’s, else the account default). They are not written here: they already exist. |
area | object | { enabled, places }. enabled: false hides the section without deleting the places. Each place: category (restaurante, cafe, supermercado, farmacia, playa, visita, transporte, ocio, otro), name, mapUrl and description per language. |
departure | text per language | What to do before leaving and where to leave the keys. |
usefulNumbers | array | Useful numbers, below yours and 112: type (taxi, centroSalud, farmacia, policia, mantenimiento, otro), optional name (“Radio Taxi Tarifa”) and number. |
curl -X PUT https://api.kinapi.es/api/v1/portal/common \
-H "ocp-apim-subscription-key: YOUR_API_KEY" \
-H "content-type: application/json" \
-d '{
"departure": { "es": "Deja las llaves en la caja y cierra la puerta al salir.", "en": "Leave the keys in the box and shut the door behind you." },
"showHouseRules": true,
"usefulNumbers": [
{ "type": "taxi", "name": "Radio Taxi", "number": "+34 956 000 000" },
{ "type": "centroSalud", "number": "+34 956 111 111" }
]
}'Blocks
/portal/blocks/portal/blocks/portal/blocks/{id}/portal/blocks/{id}/portal/blocks/{id}| Field | Type | Description |
|---|---|---|
sectionrequired | string | Which section it is a variant of: arrival, parking, manual, houseRules, area, departure or usefulNumbers. It cannot change afterwards. POST only. |
namerequired | string | Your name for the block (“Alameda car park”). The guest never sees it. Up to 60 characters. |
content | object | The same fields as the common content; only those of the block’s section are kept. On PATCH it replaces the content. |
propertyIds | string[] | The properties that use this block instead of the common or their own content. On PATCH it is the full list: a property left out stops using it and goes back to its own or the common content. |
curl -X POST https://api.kinapi.es/api/v1/portal/blocks \
-H "ocp-apim-subscription-key: YOUR_API_KEY" \
-H "content-type: application/json" \
-d '{
"section": "parking",
"name": "Alameda car park",
"content": {
"parking": { "es": "Plaza 14 del parking de la Alameda, a 200 m. El mando abre la barrera.", "en": "Bay 14 at the Alameda car park, 200 m away. The remote opens the barrier." },
"parkingMapUrl": "https://maps.app.goo.gl/…"
},
"propertyIds": ["b8f3a2c1-…", "5c7d9e01-…"]
}'{
"id": "3f9c…",
"section": "parking",
"name": "Alameda car park",
"content": { "parking": { "es": "…", "en": "…" }, "parkingMapUrl": "https://maps.app.goo.gl/…" },
"propertyIds": ["b8f3a2c1-…", "5c7d9e01-…"],
"updatedAt": "2026-10-23T10:00:00.000Z"
}The id is what goes into a property’s origins as block:3f9c…. Deleting a block sends its properties back to their own or the common content; nothing of theirs is deleted.
The portal of a property
/properties/{propertyId}/portal/properties/{propertyId}/portalWhat belongs to each accommodation: the shared sections above, plus what can only be its own and where each section comes from. The PUT replaces the whole portal, codes and wifi included: what you leave out is removed.
| Field | Type | Description |
|---|---|---|
houseMapUrl | string | Map link to the house. The guest sees a “House address” button. |
access | text per language | How to get in: where the key box is, what each code opens. Shown only once registration is complete and inside the window. |
codes | array | The codes, encrypted at rest: type (portal, puerta, cajaLlaves, garaje, otro) and value as typed on the lock. Behind the “Show the codes” button, and every reveal is logged. |
wifi | object | { network, password }. Without password, open network. Shown with the codes. |
phone | string | Contact phone of this property. Without it, the account’s (guestPortal.phone). |
origins | object | Per shared section, where it comes from on this property: common, own, hidden or block:<id>. Left out: own if the property brings content for that section, common otherwise. With common or a block, the own content you send for that section is discarded. |
curl -X PUT https://api.kinapi.es/api/v1/properties/b8f3a2c1-…/portal \
-H "ocp-apim-subscription-key: YOUR_API_KEY" \
-H "content-type: application/json" \
-d '{
"houseMapUrl": "https://maps.app.goo.gl/…",
"arrival": { "es": "Portal verde junto a la farmacia. 2.º izquierda.", "en": "Green street door next to the pharmacy. 2nd floor, left." },
"access": { "en": "The key box is to the right of the street door; the door code opens the garage too." },
"codes": [ { "type": "portal", "value": "4821" }, { "type": "cajaLlaves", "value": "0719" } ],
"wifi": { "network": "Sol3B", "password": "tarifa-2026" },
"manual": { "climatizacion": { "en": "The thermostat is in the hallway; 21 ºC is plenty." } },
"origins": { "parking": "block:3f9c…", "area": "hidden" }
}'{
"propertyId": "b8f3a2c1-…",
"houseMapUrl": "https://maps.app.goo.gl/…",
"arrival": { "es": "…", "en": "…" },
"access": { "en": "…" },
"codes": [ { "type": "portal", "value": "4821" }, { "type": "cajaLlaves", "value": "0719" } ],
"wifi": { "network": "Sol3B", "password": "tarifa-2026" },
"manual": { "climatizacion": { "en": "…" } },
"origins": {
"arrival": { "source": "own" },
"parking": { "source": "block", "blockId": "3f9c…" },
"manual": { "source": "own" },
"houseRules": { "source": "common" },
"area": { "source": "hidden" },
"departure": { "source": "common" },
"usefulNumbers": { "source": "common" }
},
"updatedAt": "2026-10-23T10:05:00.000Z"
}The codes come in clear here, and only here: they open the house. They never appear in lists, the response carries cache-control: no-store, and your integration should treat them like a password. The fields above are the own content; origins says what the guest sees after resolving blocks and the common content, and manual is composed topic by topic with the common one.
The access of a booking
/checkins/{reservationId}/portal/checkins/{reservationId}/portal/unlock/checkins/{reservationId}/portal/unlockWhether the guest of a booking can already see the codes, since or until when, whether someone unlocked it by hand and when they looked. The POST is “Unlock now” — signed api: plus the label of your key — and the DELETE undoes it. All three return the same object; never the codes, which belong to the property.
{
"enabled": true,
"state": "open",
"closesAt": "2026-08-18T21:59:59.999Z",
"openedBy": "unlock",
"unlocked": { "at": "2026-08-14T13:02:11.000Z", "by": "api:PMS" },
"reveals": [
{ "at": "2026-08-14T15:40:02.000Z", "network": "83.45.12.0", "reason": "unlock" }
]
}A cancelled booking cannot be unlocked: 409 cancelled. There are no new webhook events: checkin.completed is still the moment the access opens, if the booking is already inside the window.
Check-in and check-out times
The code window and what the guest reads under Stay depend on the check-in and check-out times of the booking, and those resolve in a cascade: the booking → the property → the account → 16:00 / 11:00. A booking that brings its time keeps it; one that does not inherits the property’s (checkInTime and checkOutTime on /properties), and if the property has none, the account’s (stay on /settings).
Changing a property’s or the account’s time moves the future bookings that inherited it and have not been filed yet, the same as from the panel. Those that brought their own are not touched.
From an agent
The MCP server has one tool per operation above, to configure the portal by talking:
| Field | Type | Description |
|---|---|---|
kinapi_get_property_portal · kinapi_set_property_portal | property | The portal of a property, with where each section comes from; and replacing it. |
kinapi_get_portal_common · kinapi_set_portal_common | account | The common content, and replacing it. |
kinapi_list_portal_blocks · kinapi_create_portal_block · kinapi_update_portal_block · kinapi_delete_portal_block | blocks | The named variants and which properties they apply to. |
kinapi_get_checkin_access · kinapi_unlock_checkin_access · kinapi_lock_checkin_access | booking | Whether the guest can already see the codes, “Unlock now” and undoing it. |
kinapi_update_settings | account | Switch the portal on, the window, the phone, the “all set” email and the default times. |
"Put in the common content that the keys go in the box, and show the house rules" "Create a parking block called 'Alameda car park' with this text and assign it to the six flats on Alameda street" "Has the guest of booking 4821 seen the codes yet?" "The guest of 4821 is at the door and has not signed: open the access for them"