Using the Nowistay public REST API

All help ressources
>
Using the Nowistay public REST API

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.

What you can do with the API

  • Properties and calendars: search and read properties, update operating settings, and manage daily availability, rates and stay restrictions.
  • Bookings and payments: create direct bookings, update eligible reservations, request payment during creation, instruct cancellation refunds and read safe payment-request status.
  • Guests: read your customer base, create guest records and retrieve stay history.
  • Missions: create, update and assign missions, filter workloads, and read mission details and completion reports.
  • Conversations: find guest conversations, read their messages and send a host reply through the selected delivery channel.
  • Private knowledge: read, add and update property questions and answers, or merge them into other properties.
  • Welcome guides: edit localized introductions and structured pages, change page positions and upload images.
  • Upsells and reporting: read the upsell catalog and booking purchases, and export booking or mission lists in table format.
  • Existing tools: custom booking parameters, team-member reads, property activity feeds and booking webhooks remain available.

Before you start

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.

  • You need an active AI Channel Manager subscription on the properties you want to expose. The API only returns properties where this entitlement is active.
  • You sign in as a host or property manager. The API never exposes properties you do not own.
  • The base URL is https://api.nowistay.com
  • All endpoints under /public/v1/* require an OAuth2 access token, sent as Authorization: Bearer ....

The fastest way: try it from the docs playground

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.

  1. Hard-refresh the page (Cmd+Shift+R on Mac, Ctrl+F5 on Windows).
  2. Wait for the small banner at the top to say OAuth playground ready.
  3. Click the green Authorize button (top right).
  4. In the dialog, in the OAuth2 section, click Authorize.
  5. You are redirected to the Nowistay login and consent page.
  6. Log in as a host or property manager with the AI Channel Manager subscription.
  7. Review the scopes the playground is asking for and approve them.
  8. You land back on /public/docs, already authenticated.
  9. Open GET /public/v1/properties, click Try it out, then Execute.
  10. You should see your properties in the response.

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).

Your own scripts: create a personal API credential

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.

  1. In the app, open the menu and go to My account > Developer, or open app.nowistay.com/hub/user/developer directly.
  2. Click New credential.
  3. Give it a clear name (for example Reporting script) and tick only the permissions it needs.
  4. Click Create credential. The secret key is shown once: copy it now and store it somewhere safe. You will not be able to see it again.

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.

Managing your credentials

  • Last used shows when each credential last called the API. It is the easiest way to spot an old script that still runs, or a credential you can safely remove.
  • Rotate secret generates a new secret key if the current one may have leaked, or when you rotate keys periodically. The new key is shown once; the previous key keeps working for 7 days so your script can migrate, then it stops.
  • Delete removes a credential you no longer use. Anything still using it stops working immediately.

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.

How to add scopes to an existing API token

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.

Personal credentials: replace, test, then switch

  1. Open My account > Developer, or use https://app.nowistay.com/hub/user/developer. Note the permissions used by the existing integration.
  2. Click New credential. Give the replacement a recognizable name and select the existing permissions you still need plus the new scopes. For example, calendar price updates need calendar.write; sending messages needs messages.write; requesting a booking payment needs both bookings.write and payments.write.
  3. Click Create credential and securely copy the Client ID and Client secret shown once.
  4. Request a new access token from 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.
  5. Update your integration’s credentials and cached access token. Verify that its existing operations and the newly enabled operation work.
  6. Once all consumers have switched, use Delete on the previous credential. Deleting it immediately stops integrations still using it.

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.

Partner OAuth apps: new registration and fresh consent

  1. Submit 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.
  2. Start a new authorization-code flow with PKCE for that registration. Ask the host to approve the desired scopes on the consent page, then exchange the new code for tokens.
  3. Switch the integration to those tokens after testing. Existing refresh tokens and authorization codes keep their original scopes and cannot be used to add permissions.

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.

OAuth, in plain terms

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:

  • Register your app (one time): /public/oauth/register (POST)
  • Send the user to give consent: /public/oauth/authorize (GET, browser redirect)
  • Exchange the code for tokens: /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.

New apps start as unverified

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:

  • The consent screen shows hosts a clear warning that the app has not been verified by Nowistay.
  • An unverified app can be connected to a maximum of 3 host accounts.
  • Registered clients that never complete a single connection are cleaned up automatically after 30 days.

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.

Available scopes

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.

Operations that require two scopes

  • Payment requests and explicit full/partial cancellation refunds: bookings.write + payments.write. Cancellation without a refund uses bookings.write alone.
  • Guest booking history: customers.read + bookings.read.
  • Booking upsell purchases: upsells.read + bookings.read. Catalog reads need only upsells.read.
  • Knowledge copying: knowledge.read + knowledge.write.
  • A booking PATCH that mixes core fields and custom parameter values: 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.

Step by step: build your own integration

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.

Step 1. Register your OAuth client

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.

Step 2. Send the user to the authorize URL

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=S256

The 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_STATE

Step 3. Exchange the code for tokens

curl -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.

Step 4. Call the API

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"

Step 5. Refresh the token when it expires

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.

Endpoint examples

Read availability, rates and stay restrictions

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.

Update a property calendar

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.

List bookings for a property in a date range

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"

Get a booking detail with guest contact info

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.

Update a direct booking

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.

Add a custom field to your bookings

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.

Create a cleaning mission and assign a team member

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
}'

Read the activity feed of a property

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).

Additional endpoints and examples

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.

Property details and operating settings

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
}'

Create a booking and request payment

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.

Refunds and payment-related booking restrictions

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.

  • Once a payment order exists, changed dates, total, nightly rate, cleaning amount, tax or currency are rejected. This also applies to historical failed or expired orders. Accepted no-op pricing inputs remain permitted.
  • A paid confirmed booking cannot be moved back to pending. A refunded cancelled booking cannot be reactivated.
  • Contact details, postal addresses, notes and supported check-in/out edits remain permitted.
  • Concurrent edits and payment-request creation use a consistent booking snapshot: the payment request uses an earlier successful edit, or the conflicting edit is rejected after the order exists.

Guest records and stay history

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.

Mission details, reports and filters

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"

Read conversations and send a host reply

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.

Private property questions and answers

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
  ]
}'

Localized welcome guides, pages and images

  • 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.

Read upsells and purchases

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"

Export booking and mission tables

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.

Compatibility with existing integrations

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.

How to structure welcome guide page blocks

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"
}

Headings (header)

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
  }
}

Paragraphs and inline formatting (paragraph)

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>"
  }
}

Bulleted and nested lists (list)

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": []
          }
        ]
      }
    ]
  }
}

Numbered lists (list)

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": []
      }
    ]
  }
}

Checklists (list)

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": []
      }
    ]
  }
}

Quotes and callouts (quote)

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"
  }
}

Separators (delimiter)

A visual separator has an empty data object. Its position in blocks determines where the separation appears.

{
  "id": "guide00007",
  "type": "delimiter",
  "data": {}
}

Tables (table)

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"
      ]
    ]
  }
}

Images (image)

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
  }
}

YouTube videos and Shorts (video)

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"
  }
}

Existing embed blocks (embed)

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": ""
  }
}

Upsell catalog (upsellCatalog)

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": {}
}

One upsell product (upsellProduct)

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.

Editor controls and formats to preserve

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.

Edit a page without losing content

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.

Webhooks (real-time events)

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.

Rate limits

  • Read endpoints: 120 requests per minute per client and per route.
  • Write endpoints (POST, PATCH): 30 requests per minute per client and per route.
  • OAuth endpoints: 20 requests per minute per IP or client.

When you hit the limit, the API returns 429 Rate limit exceeded. Back off and retry after a few seconds.

Error format

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).

  • 401: Missing or invalid bearer token.
  • 403: Token is valid but missing a required scope, or you do not have the AI Channel Manager entitlement.
  • 404: Resource not found, or you do not have access to it.
  • 409: Conflict (for example, parameter key already exists).
  • 422: Validation failed.
  • 429: Rate limit hit.

Good practices

  • Ask for the smallest scope set. Add new scopes later as your integration grows.
  • Store tokens server side. Never ship a long-lived token to a public client or commit it to a repo.
  • Handle 401 by refreshing. When refresh also fails, send the user back through the authorize flow.
  • Use the silent flag on writes. Set to true when you are syncing data in bulk and do not want to trigger guest notifications.
  • Keep the requestId in your logs. It makes support requests much faster to resolve.

Where to go next

  • Open the live docs and play with every endpoint: api.nowistay.com/public/docs.
  • Read the full schema and download an OpenAPI file at /public/openapi.json.
  • Need a scope or an endpoint that is not listed yet? Write to support and tell us about your use case.

‍

‍

‍

Ready to Put Your Rental on Autopilot?

Join 300+ property managers who save hours every week with AI-powered guest communication.