The Nowistay public REST API connects your scripts and partner integrations to your account. Read and update property settings, calendars, bookings and missions; manage guest records, conversations, private knowledge and welcome guides; request booking payments and cancellation refunds; and read upsells, purchases and portfolio reports. Explore the exact request and response schemas at /public/docs and /public/openapi.json.
This guide walks you through three paths: the fastest way to try the API (the interactive docs playground), the simplest way to run your own scripts (a personal API credential created in the app) and the standard OAuth flow you will use when you build an app for other hosts.
Access is checked against ownership and the property’s assistant entitlement. An active PMS connection is required for operations that depend on it, including calendar management and direct-booking creation. Other resources do not require a PMS connection solely to be read or edited.
https://api.nowistay.com/public/v1/* require an OAuth2 access token, sent as Authorization: Bearer ....Open https://api.nowistay.com/public/docs in your browser. The page is a full Swagger UI of the API, with a built-in OAuth playground that auto-registers a client for you. No coding needed to test calls.
Cmd+Shift+R on Mac, Ctrl+F5 on Windows)./public/docs, already authenticated.GET /public/v1/properties, click Try it out, then Execute.From there you can try every endpoint with one click, see the exact JSON Nowistay returns and copy the cURL command for any call. This is the recommended way to explore the API before writing code.
Important. The playground client lives only in your browser. It is fine for testing, but for a real integration you should create a personal API credential or register your own OAuth client (both explained below).
If you are automating your own account (a reporting script, a sync with your accounting tool, a small dashboard), you do not need the full OAuth flow at all. Create a personal API credential in the app and exchange it for a token in a single call. No consent screen, no refresh tokens to manage.
Reporting script) and tick only the permissions it needs.Your script then exchanges the credential for an access token (valid one hour) and calls the API:
curl -X POST https://api.nowistay.com/public/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=client_credentials" \
--data-urlencode "client_id=YOUR_CLIENT_ID" \
--data-urlencode "client_secret=YOUR_CLIENT_SECRET"When the token expires, request a new one the same way. There is no refresh token on this flow, and that is the point: two lines of code, nothing to babysit.
You can create up to 10 credentials, each with its own name and permissions. A credential only reads and writes the data your own account can access, and it needs the same active AI Channel Manager subscription as the rest of the API.
You cannot edit the scopes of an existing token or personal credential. Create a replacement credential with the complete set of permissions your integration needs. Existing clients, access tokens, authorization codes and refresh tokens keep their original grants; no permissions are added automatically.
https://app.nowistay.com/hub/user/developer. Note the permissions used by the existing integration.calendar.write; sending messages needs messages.write; requesting a booking payment needs both bookings.write and payments.write.POST /public/oauth/token with grant_type=client_credentials and the replacement credentials. Check the returned scope value and test an endpoint allowed by those scopes. GET /public/v1/properties specifically requires properties.read.Rotate secret does not add scopes. It only replaces the secret for the same credential; the previous secret remains valid for 7 days. Refreshing or requesting another access token cannot expand the original grant. Write scopes do not automatically include read scopes.
POST /public/oauth/register with the complete desired scope set and your normal application details. An equivalent registration with exactly the same scope set reuses its matching client; a changed scope set creates a new registration. Save the returned client credentials.In the docs playground, refresh the page and authorize again with the required scopes. New permissions are listed with readable labels and their identifiers in English, French, Spanish, German, Dutch and Italian.
The API uses standard OAuth 2.1 with the authorization code flow and PKCE. In one sentence: the user opens a Nowistay page, says yes, you receive a short code, you exchange it for an access token, and you call the API with that token. Tokens expire and can be refreshed without bothering the user again.
The three OAuth endpoints you will use:
/public/oauth/register (POST)/public/oauth/authorize (GET, browser redirect)/public/oauth/token (POST)You can also fetch the OAuth metadata at /.well-known/oauth-authorization-server/public-api, which lists every endpoint and supported feature.
Anyone can register an OAuth client, so hosts need a way to tell a brand-new app from an established integration. Every newly registered app therefore starts as unverified:
Building something for more than 3 accounts? Write to support with your app name, website and use case. Once your app is verified, the warning disappears and the account limit is lifted.
The REST API supports 25 scopes, including ten additional permissions for property edits, messages, guides, knowledge, guest creation, upsells and payments. Select only the permissions your integration needs.
properties.read: Read property details and operating settings.properties.write: Update property details and operating settings.calendar.read: Read availability, nightly rates and stay restrictions.calendar.write: Update availability, nightly rates and stay restrictions.bookings.read: Read bookings, financial details and safe payment-request status.bookings.sensitive.read: Include guest contact details, postal addresses and access codes in booking responses; also requires bookings.read for reads.bookings.write: Create and update direct bookings. Payment requests and refunds also require payments.write.booking_parameters.read: Read custom booking parameter definitions and values.booking_parameters.write: Create and update parameter definitions and values.missions.read: Read missions and completion reports.missions.write: Create, update and assign missions.activity.read: Read property activity logs.team_members.read: Read team members, contact details and property assignments.webhooks.read: Read webhook subscriptions.webhooks.write: Create, update, pause and delete webhook subscriptions.customers.read: Read guest records, contact details and stay history.customers.write: Create guest records.upsells.read: Read the upsell catalog and booking purchases.payments.write: Request payment during booking creation and instruct full or partial cancellation refunds; also requires bookings.write.messages.read: Read guest conversations and messages.messages.write: Send messages to guests.guides.read: Read welcome guides and page content.guides.write: Edit guides, create and update pages, change page positions and upload images.knowledge.read: Read private property questions and answers.knowledge.write: Create and update private knowledge. Copying also requires knowledge.read.The eight default REST scopes remain properties.read, calendar.read, bookings.read, booking_parameters.read, missions.read, activity.read, team_members.read and webhooks.read. Defaults do not upgrade existing tokens.
bookings.write + payments.write. Cancellation without a refund uses bookings.write alone.customers.read + bookings.read.upsells.read + bookings.read. Catalog reads need only upsells.read.knowledge.read + knowledge.write.bookings.write + booking_parameters.write. A parameter-only PATCH keeps booking_parameters.write and also works on OTA bookings.New write endpoints return minimal action results unless the token also has the relevant read scope. Existing booking redaction remains in place. Mission and conversation endpoints include their related booking context under their own scopes.
These examples use placeholder IDs and credentials. Replace them with your own values. This flow is for partner apps; personal credentials use client_credentials as shown above.
Call POST /public/oauth/register with the permissions you need. Save the returned client_id and client_secret securely. The example requests property, booking and mission reads.
curl -X POST https://api.nowistay.com/public/oauth/register \
-H "Content-Type: application/json" \
-d '{"clientName":"My integration","redirectUris":["https://my-app.example.com/oauth/callback"],"grantTypes":["authorization_code","refresh_token"],"responseTypes":["code"],"tokenEndpointAuthMethod":"client_secret_post","scope":"properties.read bookings.read missions.read"}'For a public client without a backend, use "tokenEndpointAuthMethod": "none" and PKCE, without a client secret.
Generate a PKCE verifier, its S256 challenge and a random state. Redirect the user to the authorization endpoint. The following URL is one line; URL-encode query values.
https://api.nowistay.com/public/oauth/authorize?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=https%3A%2F%2Fmy-app.example.com%2Foauth%2Fcallback&scope=properties.read%20bookings.read%20missions.read&state=RANDOM_STATE&code_challenge=BASE64URL_S256_HASH&code_challenge_method=S256The host signs in, reviews the permissions and approves or denies access. On approval, the callback contains a one-time code and your state. Reject a callback whose state does not match.
https://my-app.example.com/oauth/callback?code=AUTHORIZATION_CODE&state=RANDOM_STATEcurl -X POST https://api.nowistay.com/public/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=authorization_code" \
--data-urlencode "code=AUTHORIZATION_CODE" \
--data-urlencode "redirect_uri=https://my-app.example.com/oauth/callback" \
--data-urlencode "client_id=YOUR_CLIENT_ID" \
--data-urlencode "client_secret=YOUR_CLIENT_SECRET" \
--data-urlencode "code_verifier=ORIGINAL_PKCE_VERIFIER"The response includes access_token, token_type, expires_in, refresh_token and scope. Store tokens securely. Access tokens normally last one hour; use the returned expiry.
This example requires properties.read. Read the data and pagination fields in the response.
curl "https://api.nowistay.com/public/v1/properties" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"curl -X POST https://api.nowistay.com/public/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=refresh_token" \
--data-urlencode "refresh_token=YOUR_REFRESH_TOKEN" \
--data-urlencode "client_id=YOUR_CLIENT_ID" \
--data-urlencode "client_secret=YOUR_CLIENT_SECRET"Save the replacement refresh token returned by the refresh response. If refreshing fails, restart authorization. Refreshing keeps the previously granted scopes. Personal credentials do not have a refresh token: request a new access token with client_credentials.
GET /public/v1/properties/{property_id}/calendar requires calendar.read. Pass an inclusive from and to date; one request can cover up to 730 days.
GET /public/v1/properties/101/calendar?from=2026-12-18&to=2026-12-20
Each day can return availability, rate, minStay, maxStay, stopSell, closedToArrival, closedToDeparture, restricted and restrictionReasons. availability is the remaining sellable inventory after confirmed bookings. The restriction fields explain why a date or a check-in/check-out action is restricted.
PATCH /public/v1/properties/{property_id}/calendar requires calendar.write. The request is partial: send only the controls that should change, and omitted fields remain unchanged.
{
"from": "2026-12-18",
"to": "2026-12-31",
"availability": 1,
"rate": 145.0,
"minStay": 2,
"maxStay": 14,
"stopSell": false,
"closedToArrival": false,
"closedToDeparture": false,
"daysOfWeek": [0, 1, 2, 3, 4]
}
daysOfWeek is optional: 0 is Monday and 6 is Sunday. Omit it to update every date in the range. The response reports daysUpdated and the normalized update that was applied. Changes are synchronized through Nowistay PMS.
Calendar operations require ownership, the assistant entitlement and active Nowistay calendar management. To enable calendar scopes on an existing integration, follow How to add scopes to an existing API token above; refreshing a token or rotating its secret is not enough.
Supported filters include propertyId, repeated propertyIds, status, channel, fromDate and toDate. Standard JSON pagination uses limit (maximum 100) and offset. Booking date filters keep their overlap semantics: departure >= fromDate and arrival <= toDate.
curl "https://api.nowistay.com/public/v1/bookings?propertyId=101&fromDate=2026-12-01&toDate=2026-12-31&status=confirmed" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"curl "https://api.nowistay.com/public/v1/bookings/9001" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"Requires the extra bookings.sensitive.read scope. Without it, email, phone and the smart-lock code are omitted.
Core booking edits require bookings.write and an eligible direct booking. OTA reservations remain read-only for core fields. A PATCH containing only customParameters also supports OTA bookings and uses booking_parameters.write.
curl -X PATCH "https://api.nowistay.com/public/v1/bookings/9001" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"notes": "Guest requested a baby cot.",
"reservationNumber": "DIRECT-9001",
"silent": true
}'Existing booking PATCH requests default to silent=true. Set silent=false to trigger the usual update notifications. Booking creation defaults to silent=false. Omitted fields remain unchanged, and existing supported null values still clear fields. Availability synchronization remains part of the shared operation.
Custom booking parameters are useful when you want to mirror data from your own system on each Nowistay reservation.
curl -X POST "https://api.nowistay.com/public/v1/booking-parameters" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"key": "external_status",
"name": "External status"
}'
curl -X PATCH "https://api.nowistay.com/public/v1/bookings/9001" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"customParameters": {
"external_status": "ready"
}
}'Both examples use booking_parameters.write. Mixed core/parameter updates need both booking write scopes; validation failure leaves both unchanged.
Send null as the value to clear a custom field on a booking.
Requires missions.write. Assignment and notification workflows also run for API-created missions.
curl -X POST "https://api.nowistay.com/public/v1/missions" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"propertyId": 101,
"bookingId": 9001,
"missionType": "cleaning",
"title": "Checkout cleaning",
"scheduledAt": "2026-12-18T11:30:00Z",
"timezone": "Europe/Paris",
"assignedTeamMemberId": 301
}'Requires activity.read.
curl "https://api.nowistay.com/public/v1/properties/101/activity?limit=20" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"Events are grouped into three categories: booking, mission and automation (guest messages, smart-lock events).
All paths in this section are relative to /public/v1. Replace example IDs with resources owned by the connected account. The OpenAPI schema lists every supported field, enum and response shape.
Use GET /properties/{id} with properties.read and PATCH /properties/{id} with properties.write. Property lists accept search. Supported edits include name, address, capacity, property type, rental links, cover, check-in/out times, booking cutoffs and default pricing/stay settings.
curl -X PATCH "https://api.nowistay.com/public/v1/properties/101" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"capacity": 4,
"defaultMinStay": 2,
"cutOffDays": 1
}'POST /bookings requires bookings.write, the assistant entitlement and an active PMS connection. Include dates, guest email and guest details, with supported pricing and operational fields. requestPayment=true additionally requires payments.write and creates the booking pending payment.
curl -X POST "https://api.nowistay.com/public/v1/bookings" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"propertyId": 101,
"arrival": "2026-12-18",
"departure": "2026-12-20",
"guestFirstName": "Maya",
"guestLastName": "Martin",
"guestEmail": "maya@example.com",
"adults": 2,
"amount": "290.00",
"currency": "EUR",
"requestPayment": true
}'Creation can succeed even if payment-link generation fails. The response still contains the persisted booking or its ID, with paymentRequest.sent=false and an explicit error. Check this result before retrying so you do not create a duplicate reservation. A checkout URL is returned only by an authorized payment-request action.
Read GET /bookings/{id}/payment-request with bookings.read for safe booking/payment-request status. It does not return checkout URLs. This release does not add a standalone payment administration API.
Existing booking PATCH supports reservationNumber, cancellationRefundMode (none, full or partial) and cancellationRefundAmount. An explicit full or partial cancellation refund requires both bookings.write and payments.write. Cancellation with no refund requires only bookings.write.
curl -X PATCH "https://api.nowistay.com/public/v1/bookings/9001" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"status": "cancelled",
"cancellationRefundMode": "partial",
"cancellationRefundAmount": "50.00"
}'A cancelled booking does not prove that a refund completed. Check the payment status before telling the guest that their refund succeeded. payments.write authorizes payment requests and cancellation refunds, not repricing an existing checkout.
POST /guests uses customers.write; duplicate guest creation returns 409. Existing owner-level customer reads remain available. Guest lists accept propertyId, repeated propertyIds and supported orderBy values: created_at, updated_at, full_name, bookings_count, total_amount, last_stay. Prefix with - for descending order; the default is -updated_at.
curl -X POST "https://api.nowistay.com/public/v1/guests" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"firstName": "Maya",
"lastName": "Martin",
"email": "maya@example.com",
"language": "en"
}'GET /guests/{id}/bookings requires both customers.read and bookings.read and returns eligible stay history.
GET /missions/{id} and GET /missions/{id}/report use missions.read. A valid mission without a completion report returns 200 with report: null. Mission lists accept inclusive scheduled UTC date filters fromDate/toDate, repeated propertyIds and teamMemberIds, alongside existing filters.
curl "https://api.nowistay.com/public/v1/missions?propertyIds=101&propertyIds=102&teamMemberIds=301&fromDate=2026-12-01&toDate=2026-12-31" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"GET /conversations supports propertyId and bookingId filters. GET /conversations/{id}/messages reads message history. Both require messages.read. Send a reply with POST /messages and messages.write, providing exactly one of conversationId or bookingId. Optional channelKind or threadChannelId selects a channel; do not provide both selectors.
curl -X POST "https://api.nowistay.com/public/v1/messages" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"bookingId": 9001,
"message": "Hello Maya, your arrival instructions are ready."
}'Messages follow the normal host-reply delivery path, including multi-room booking routing. A persisted message with failed delivery returns its ID and delivery status: inspect the result before retrying. Guest-message attachments are not supported by these endpoints.
Use GET /properties/{id}/knowledge-questions and GET /properties/{id}/knowledge-questions/{questionId} with knowledge.read. Use POST on the collection and PATCH on an individual question with knowledge.write.
curl -X POST "https://api.nowistay.com/public/v1/properties/101/knowledge-questions" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"question": "Where is the recycling area?",
"answer": "In the courtyard, next to the bicycle shelter."
}'POST /properties/{id}/knowledge-questions/copy treats {id} as the source and requires knowledge.read plus knowledge.write. Send targetPropertyIds to merge into other properties. Copying never deletes or overwrites target answers; every target is validated before writes begin.
curl -X POST "https://api.nowistay.com/public/v1/properties/101/knowledge-questions/copy" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"targetPropertyIds": [
102,
103
]
}'GET/PATCH /properties/{id}/welcome-guide: read or edit the guide, including localized introductions.GET/POST /properties/{id}/welcome-guide/pages: list or create pages.GET/PATCH /properties/{id}/welcome-guide/pages/{pageId}: read or edit an individual page and its position.POST /properties/{id}/welcome-guide/images: upload an image from a supported public URL or base64 input.Reads require guides.read; writes require guides.write. Keep EditorJS content/contents blocks and their IDs, nested formatting and media when editing structured pages. Omitted locales remain unchanged. Plain-text replacement of a structured page is rejected. Guide tokens and guest access links are not returned.
curl -X PATCH "https://api.nowistay.com/public/v1/properties/101/welcome-guide" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"intros": {
"en": "Welcome! Your practical information is below.",
"fr": "Bienvenue ! Retrouvez les informations pratiques ci-dessous."
}
}'Image downloads must use publicly reachable addresses. Redirects are checked and size limits are enforced. Consult the image request schema for the supported URL/base64 fields and limits.
GET /upsells requires upsells.read. GET /bookings/{id}/upsell-purchases requires both upsells.read and bookings.read. These endpoints are read-only.
curl "https://api.nowistay.com/public/v1/bookings/9001/upsell-purchases" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"Add responseFormat=table to booking or mission lists for columns, rows and pagination, with limit up to 500. Standard JSON remains the default and keeps its maximum of 100. Use offset to retrieve subsequent pages. Multiple propertyIds are intersected with propertyId if both filters are supplied.
curl "https://api.nowistay.com/public/v1/bookings?propertyIds=101&propertyIds=102&fromDate=2026-12-01&toDate=2026-12-31&responseFormat=table&limit=500&offset=0" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"Booking tables use bookings.read; mission tables use missions.read. Booking report cleaning assignments additionally require missions.read, and conversation presence requires messages.read. Booking date filters retain overlap semantics; mission date filters use scheduled UTC dates.
Existing REST routes, standard JSON defaults, pagination, booking redaction, supported null clearing and notification defaults remain available. The payment-related booking restrictions described above are newly enforced. REST and MCP share the underlying operations and their side effects, while MCP tool contracts and preview/confirmation behavior remain unchanged.
A Nowistay page contains one EditorJS document per language. The blocks array defines display order. Each object in that array is one block; put the examples below inside content.blocks or contents.<locale>.blocks. Example values are fictional and should be adapted to your property.
Common structure: id is a stable string, unique within the document; retain existing IDs and assign a new one to each new block. type is the exact, case-sensitive block name. data is an object whose fields depend on the type. Also preserve any existing tunes and document metadata returned by a read instead of needlessly rebuilding them.
Create request: POST /public/v1/properties/{id}/welcome-guide/pages with guides.write. Use locale + title + content for one language, or titles + contents for several languages. icon defaults to info. position accepts start, end or after_page; the latter requires afterPageId. Here is a complete English/French request body:
{
"titles": {
"en": "Arrival",
"fr": "Arrivée"
},
"contents": {
"en": {
"blocks": [
{
"id": "arrival001",
"type": "header",
"data": {
"text": "Getting here",
"level": 2
}
},
{
"id": "arrival002",
"type": "paragraph",
"data": {
"text": "Use the <b>main entrance</b>."
}
}
]
},
"fr": {
"blocks": [
{
"id": "arrival001",
"type": "header",
"data": {
"text": "Venir au logement",
"level": 2
}
},
{
"id": "arrival002",
"type": "paragraph",
"data": {
"text": "Utilisez l’<b>entrée principale</b>."
}
}
]
}
},
"position": "end"
}data.text is the heading text. Use data.level: 2, matching the guide editor. The page title itself belongs in title or titles, outside the blocks.
{
"id": "guide00001",
"type": "header",
"data": {
"text": "Arrival / Arrivée",
"level": 2
}
}data.text is a string. Use inline <b>, <i> and <a href="https://…"> markup for bold, italic and links. Escape double quotes inside JSON strings. Do not send a whole HTML page or Markdown as content.
{
"id": "guide00002",
"type": "paragraph",
"data": {
"text": "<b>Wi-Fi</b>: GuestNetwork. <i>Welcome / Bienvenue !</i> <a href=\"https://example.com/\">Information</a>"
}
}Use style: unordered. items contains objects, not strings. Every item has content (formatted text), meta (an object) and items (an array of child items, empty for a leaf). Repeat the same structure for nesting.
{
"id": "guide00003",
"type": "list",
"data": {
"style": "unordered",
"meta": {},
"items": [
{
"content": "Kitchen / Cuisine",
"meta": {},
"items": [
{
"content": "Coffee machine / Machine à café",
"meta": {},
"items": []
}
]
}
]
}
}Use style: ordered with root meta.counterType: numeric. Keep the same item structure and nested arrays as a bulleted list.
{
"id": "guide00004",
"type": "list",
"data": {
"style": "ordered",
"meta": {
"counterType": "numeric"
},
"items": [
{
"content": "Close the windows / Fermez les fenêtres",
"meta": {},
"items": []
},
{
"content": "Return the keys / Rendez les clés",
"meta": {},
"items": []
}
]
}
}Use style: checklist. Put the initial boolean checkbox state in each item’s meta.checked. This is guide content, not a mission completion report.
{
"id": "guide00005",
"type": "list",
"data": {
"style": "checklist",
"meta": {},
"items": [
{
"content": "Windows closed / Fenêtres fermées",
"meta": {
"checked": false
},
"items": []
}
]
}
}data.text is the quote, caption is its attribution (or an empty string), and alignment is the editor alignment setting. Use left; the guest view applies its own quote layout.
{
"id": "guide00006",
"type": "quote",
"data": {
"text": "Please respect the neighbours / Merci de respecter le voisinage.",
"caption": "Your host / Votre hôte",
"alignment": "left"
}
}A visual separator has an empty data object. Its position in blocks determines where the separation appears.
{
"id": "guide00007",
"type": "delimiter",
"data": {}
}data.content is an array of rows; each row is an array of cell strings. Keep the same number of cells per row. With withHeadings: true, the first row supplies column headings. Use false when every row contains ordinary data.
{
"id": "guide00008",
"type": "table",
"data": {
"withHeadings": true,
"content": [
[
"Service",
"Time / Horaire"
],
[
"Check-in / Arrivée",
"16:00"
],
[
"Check-out / Départ",
"10:00"
]
]
}
}data.file.url is the uploaded image URL, not a local filename or base64 string. caption is a string; withBorder, withBackground and stretched are booleans. POST the image to /public/v1/properties/{id}/welcome-guide/images first, then insert the returned editorJsImageBlock into content.blocks. Preserve its returned fields and use a unique block ID. The URL below is illustrative: replace it with the actual returned URL.
{
"id": "guide00009",
"type": "image",
"data": {
"file": {
"url": "https://images.example.com/replace-with-uploaded-image.jpg"
},
"caption": "Entrance / Entrée",
"withBorder": false,
"withBackground": false,
"stretched": false
}
}Use service: youtube for a normal YouTube video, or youtubeShort for a Short. source is its original URL; embed is the matching YouTube embed URL. width and height are numbers and caption is a string. The editor uses 580 × 320 for a normal video and 315 × 560 for a Short. Replace VIDEO_ID_11 in both URLs with the same real 11-character video ID; for Shorts, the source path is /shorts/VIDEO_ID_11. Arbitrary iframe HTML is not the block format.
{
"id": "guide00010",
"type": "video",
"data": {
"service": "youtube",
"source": "https://www.youtube.com/watch?v=VIDEO_ID_11",
"embed": "https://www.youtube.com/embed/VIDEO_ID_11",
"width": 580,
"height": 320,
"caption": "Arrival instructions / Instructions d’arrivée"
}
}embed uses the same data fields as video. Preserve the existing type when editing a page; both render YouTube and YouTube Shorts. Do not assume that every embed provider supported by EditorJS is supported by Nowistay.
{
"id": "guide00011",
"type": "embed",
"data": {
"service": "youtubeShort",
"source": "https://www.youtube.com/shorts/VIDEO_ID_11",
"embed": "https://www.youtube.com/embed/VIDEO_ID_11",
"width": 315,
"height": 560,
"caption": ""
}
}Use an empty data object to place the eligible upsell catalog here. Products, prices and availability come from the configured catalog and guest context; they are not embedded in this block.
{
"id": "guide00012",
"type": "upsellCatalog",
"data": {}
}data.itemId is the numeric ID of an existing upsell. Find the correct ID with GET /public/v1/upsells using upsells.read; replace the fictional 401 below. Keep the original ID unless you intentionally select another product. Writing the page uses guides.write; this block does not create a product, set a price or purchase it.
{
"id": "guide00013",
"type": "upsellProduct",
"data": {
"itemId": 401
}
}REST validation: page creation and updates reject unsupported block types and malformed data with HTTP 422. Every supplied language is checked before any write, including nested list items, table cells and media fields. The error identifies the document and block involved. IDs remain optional for existing documents; when supplied, they must be non-empty unique strings. Metadata and formatting are preserved. This structural validation does not check whether an image is reachable or an upsell exists.
imageLibrary is the editor’s image picker control, not an API gallery block. Use image blocks. linkPreview is a block setting (tunes), not a separate content type; preserve existing data. Do not invent html, code, button blocks or undocumented video providers: acceptance of a JSON object does not guarantee it will display in the guide.
First read GET /public/v1/properties/{id}/welcome-guide/pages/{pageId} with guides.read to obtain content and contents. Work on a copy of the complete document for the language being edited, change only the intended blocks, then PATCH that document to the same URL with guides.write. Sending just one block replaces the document with that one block; blocks are not merged by id. Omitted languages remain unchanged. To move an existing page, use order, not position.
The text and texts fields are not a replacement for the structured document. Follow the read response’s simpleTextUpdateAllowed, recommendedUpdateMode and recommendedUpdateField guidance; a structured page rejects plain-text replacement. The individual examples in this section describe blocks, not complete PATCH requests for an existing page. Read the page again after editing and check languages, IDs, links and media. With only guides.write, the write response is minimal; add guides.read to receive details.
If you do not want to poll the API to detect changes, you can subscribe to webhooks. Nowistay will then call a URL you provide each time a booking is created or updated on the property, and send the same booking data that GET /public/v1/bookings/{id} returns.
Two events are supported today:
booking.created: a new reservation has been recorded on the property.booking.updated: an existing reservation has changed (dates, status, guest info, smart-lock code, etc.).Each webhook is tied to a single property and you can register up to three per property. Deliveries are signed (HMAC-SHA256) so you can verify they really come from Nowistay, retried with exponential backoff on failure, and the endpoint is auto-disabled if it keeps failing for more than 72 hours. Webhook payloads do not contain sensitive guest fields (email, phone, address, smart-lock code). When you need those, call the API with the bookings.sensitive.read scope.
Full setup and signature verification details are in the dedicated article: Public API: receive real-time booking webhooks.
When you hit the limit, the API returns 429 Rate limit exceeded. Back off and retry after a few seconds.
Every error follows the same shape, with a stable code, a human message and a requestId you can include in support requests.
{
"error": {
"code": "validation_error",
"message": "The request contains an invalid value.",
"requestId": "example-request-id"
}
}400: Bad request (malformed body, missing field).
silent flag on writes. Set to true when you are syncing data in bulk and do not want to trigger guest notifications.requestId in your logs. It makes support requests much faster to resolve./public/openapi.json.