DailyStay has two HTTP APIs. Both speak JSON and share one base URL.
Base URL
https://new.yourdailystay.com/api
API
Use it for
Auth
Call it from
Public Booking API, /public/booking and /public/payments
A hotel website: rooms, availability, new reservations, deposits
None
Browser or server
Partner API, /partner
Your own systems: keeping a website or channel manager in sync (rooms, classes, prices, photos, calendar, bookings), reporting, accounting
API key in X-Api-Key, scopes per capability
Server only
Both APIs come with the Max plan. On a lower plan the public booking endpoints answer as if the page did not exist, and API keys are rejected with 401.
Conventions
Request and response bodies are JSON. Send Content-Type: application/json.
Field names are camelCase. Webhook data is the one exception (see Webhooks).
Stay dates are YYYY-MM-DD. Timestamps are ISO 8601 in UTC.
Money is a decimal in the property's currency, given as currency (for example EUR).
Every response uses the same envelope: check success, then read data or error.
{
"success": false,
"data": null,
"error": {
"code": "NOT_FOUND",
"message": "No such booking.",
"details": []
},
"pagination": null
}
Hotel website quickstart
Building a website for a hotel that runs on DailyStay? This is the whole flow. Reservations made on your site are saved straight into DailyStay, as pending requests the property confirms. No API key is needed for any of it.
The owner switches on the booking page: Property Settings, Features tab, Your own booking page. Until then every public endpoint answers 404.
Find the property id. It is the number at the end of the booking page link on that card (/book/1042). GET /partner/whoami also returns it for any API key.
Load the property and its rooms with GET /public/booking/{propertyId}.
Search a stay with GET /public/booking/{propertyId}/availability.
Book it with POST /public/booking/{propertyId}. The reservation appears in DailyStay as pending and the property is notified.
Optional: take the deposit online with the payment endpoints.
Optional: from your server, keep your own records in step with the Partner API and webhooks.
Browser example
const BASE = 'https://new.yourdailystay.com/api'
const PROPERTY_ID = '1042'
async function call(path, options = {}) {
const response = await fetch(BASE + path, {
...options,
headers: { 'Content-Type': 'application/json', ...options.headers },
})
const body = await response.json()
if (!body.success) throw new Error(body.error?.message ?? 'HTTP ' + response.status)
return body.data
}
// 1. What can be booked for these dates, priced
const search = await call(
'/public/booking/' + PROPERTY_ID + '/availability?checkIn=2026-11-14&checkOut=2026-11-17&guests=2',
)
if (search.rooms.length === 0) console.log(search.notes, search.suggestions)
// 2. Book the first free room
const booking = await call('/public/booking/' + PROPERTY_ID, {
method: 'POST',
body: JSON.stringify({
roomId: search.rooms[0].roomId,
checkIn: '2026-11-14',
checkOut: '2026-11-17',
guests: 2,
guestName: 'Elira Hoxha',
guestEmail: 'guest@example.com',
consentGiven: true,
}),
})
console.log(booking.reference, booking.status) // "482913" "pending"
Never put an API key in browser code. The public booking endpoints need no key, and the Partner API is for your server only.
Generate an API key
Sign in to DailyStay as the property owner. Managers and staff do not see this panel.
Open Property Settings, Features tab, API and webhooks.
Under API keys choose New key. Name it after what will use it, for example "Website sync".
Tick the scopes it needs (see Scopes). With none ticked it gets bookings:read.
Choose Create key. The full key, ds_ followed by 43 characters, is shown once. Copy it into your server's secret store straight away.
DailyStay keeps only a SHA-256 hash of the key, so nobody can show it to you again. If it is lost, create a new key and revoke the old one. The list shows each key's first 12 characters, scopes, creation date and last use.
A key belongs to one property. The property comes from the key itself, never from the URL, so a key cannot reach another property's data.
Authenticate requests
Send the key in the X-Api-Key header on every Partner API request. Not in Authorization, and never in the query string.
Header missing, key unknown or revoked, or the property is no longer on the Max plan. Empty body.
403
Key valid but missing the scope this endpoint needs. Empty body.
Server helper (Node 18+)
const BASE = 'https://new.yourdailystay.com/api'
export async function dailyStay(path) {
const response = await fetch(BASE + path, {
headers: { 'X-Api-Key': process.env.DAILYSTAY_API_KEY },
})
if (response.status === 401) throw new Error('DailyStay rejected the key. Is it revoked?')
if (response.status === 403) throw new Error('The key is missing a scope for ' + path)
const body = await response.json()
if (!body.success) throw new Error(body.error.message)
return body.data
}
Key lifetime and token refresh
An API key is not an access token. There is no login call, no expiry and nothing to refresh: a key works until the owner revokes it, or until the property leaves the Max plan.
Rotate a key without downtime
Create a second key with the same scopes.
Deploy it to your server.
Watch the old key's last use stop moving in the keys list.
Revoke the old key. It stops working at once and stays listed, marked revoked, for the audit trail.
Which credential refreshes
Credential
Expires
Refresh
API key, ds_...
Never, until revoked
Not needed. Rotate by creating a new key.
AI assistant connector (MCP, OAuth)
Access token expires
Your AI client refreshes it on its own with grant_type=refresh_token at /mcp/oauth/token.
DailyStay app sign-in
Yes
Internal to DailyStay's own apps. Not supported for integrations: use an API key.
Scopes
Scope
Grants
Used by
bookings:read
Read the property's bookings, guest contact details included.
GET /partner/bookings, GET /partner/bookings/{bookingId}
bookings:write
Create bookings, set their price and change their status.
POST /partner/bookings, PUT /partner/bookings/{bookingId}/price, PATCH /partner/bookings/{bookingId}/status
rooms:read
Read rooms, room classes, amenities and photos.
GET /partner/rooms, GET /partner/rooms/{roomId}, GET /partner/room-classes
rooms:write
Change a room's name, description, capacity, amenities and photos. Not its price.
PUT /partner/rooms/{roomId}, PUT /partner/rooms/{roomId}/images
pricing:read
Read base prices, the weekend setting and rate rules.
GET /partner/pricing
pricing:write
Set a room's base price.
PUT /partner/rooms/{roomId}/price
calendar:read
Read closed dates, restrictions, occupied nights (no guest details) and the calendar feeds.
GET /partner/calendar, GET /partner/ical
calendar:write
Close dates and open them again.
POST /partner/calendar/blocks, DELETE /partner/calendar/blocks/{ruleId}
guests:read
Reserved. Grants nothing today.
None yet. Guest details come with each booking.
GET /partner/whoami needs no scope. Give each key the least it needs: a reporting tool only ever needs bookings:read. A website that mirrors the property needs every scope above except guests:read: choose **Full website sync** when you create the key. Call GET /partner/whoami to see which scopes a key carries and switch features off, rather than fail, when one is missing.
Public Booking API
For the hotel's own website. No key, open to any origin (CORS), limited to 60 requests a minute per IP address. Nothing here reveals another guest or another booking.
GET/public/booking/{propertyId}
Property and rooms
Everything a booking page shows before dates are picked: contact details, check-in times, deposit rule, cancellation terms and every room that is not in maintenance. Prices here are the rooms' base rates; real prices come from availability.
- 404 NOT_FOUND The booking page is switched off, the id is unknown, or the property is not on the Max plan.
GET/public/booking/by-slug/{slug}
Property by booking address
The same response, found by the booking address name the owner chose in Property Settings (the villa-drita in villa-drita.yourdailystay.com). Read propertyId from the answer and use it for every other call.
Rooms free for the whole stay that fit the party, priced with the property's rates, minimum stays and closed dates applied. When nothing fits, rooms is empty, notes says why in plain words, and suggestions offers up to three nearby windows that do have rooms.
- 404 NOT_FOUND No published booking page for this property.
POST/public/booking/{propertyId}
Create a reservation
Saves the reservation in DailyStay as pending with reservation source Direct, and notifies the property, who confirms it in the app. Availability, price, minimum stay and capacity are all checked again on the server, so a stale search cannot double-book a room.
- 400 VALIDATION_ERROR A rule was broken, for example consent missing, dates in the past, or more guests than the room sleeps. message says which.
- 400 Field validation failed (problem details body, see Errors).
- 409 UNAVAILABLE The room was taken meanwhile, a closed date or minimum stay applies, or the page is not live.
Calling these from your server instead of the browser? Every guest then shares your server's IP address and its 60 requests a minute. Cache the property response and keep availability calls to real searches.
Online payments
Optional. Lets a guest pay the deposit, or the whole stay, right after booking. Needs Stripe or PayPal connected in Property Settings, Payments tab.
Read the providers. Empty means no online payment.
Start a payment for the bookingId you got back from the reservation.
Finish it in the browser with Stripe.js (clientToken is the client secret) or the PayPal buttons (clientToken is the order id).
Confirm the payment so DailyStay records it against the booking.
GET/public/payments/{propertyId}/providers
Payment providers
Which ways this property can be paid online. An empty list means no online payment: show the booking as pending and let the property collect.
Opens a payment for a booking and returns what the browser needs to finish it. The amount is worked out on the server: the deposit percentage when the property takes deposits, otherwise the whole stay. For Stripe, clientToken is the PaymentIntent client secret and publishableKey the Stripe publishable key. For PayPal, clientToken is the order id and publishableKey the PayPal client id.
- 400 PAYMENT_ERROR Online payments are off, the booking is cancelled, or there is nothing to pay.
POST/public/payments/confirm/{paymentId}
Confirm a payment
Call when the guest comes back from paying. DailyStay asks the provider directly whether the money arrived, so this call cannot mark a booking paid by itself. Stripe payments are also confirmed by Stripe's own webhook if the guest closes the tab.
curl -X POST "https://new.yourdailystay.com/api/public/payments/confirm/c41d7e02-93b5-4f6a-8e1c-5a9b2f0d7e64"
Errors
- 404 NOT_FOUND No such payment.
- 400 PAYMENT_ERROR The provider refused or could not be reached.
Partner API
Everything your server needs to run against one property: read and change its rooms, prices, calendar and bookings. The key fixes the property, so no id appears in the URL, and each endpoint asks for the one scope it needs.
GET/partner/whoami
Check a key
Returns the property this key belongs to and the scopes it carries. Call it once at start-up to fail fast on a wrong or revoked key.
Adds a booking to the calendar, for a reservation your system took itself (a direct sale, a phone call). Send totalAmount to book at an agreed price (a quote you gave the guest); leave it out and DailyStay prices the stay from the room's rates and rate rules. The deposit follows the property's deposit setting. The same checks apply as in the dashboard: dates already taken, closed ranges and minimum stays are refused. Guests are not emailed unless you set sendEmails. For a reservation made by a visitor on a hotel website, use the Public Booking API instead: it starts as pending and is accepted by the property.
- 400 VALIDATION_ERROR Unknown status or an invalid field.
- 403 Key lacks bookings:write.
- 409 BOOKING_ERROR Dates taken, a closed range, a stay below the minimum, or an unknown room. message says which.
PATCH/partner/bookings/{bookingId}/status
Change a booking's status
Moves a booking to a new status: confirm a pending one, cancel or reject one (the nights become free again), or mark it checked in or out. Returns the booking.
Sets the agreed total of a booking, for example after the guest accepted a price you quoted. A deposit already paid is kept; otherwise the deposit follows the property's deposit setting. Combine it with Change a booking's status to confirm the stay at that price.
Every room of the key's property with what its room class adds (guest limits, class title), its base price, amenities and photos. name is shared by rooms of the same kind, so show name with number ("Deluxe Apartment 101") to tell them apart. type names the room class; it matches type on GET /partner/room-classes. Read it to build or refresh your own room list, then match rooms by id, which never changes.
The classes (types) rooms belong to, with their guest limits, layout (sizeSqm in m², beds as guests read it, bathrooms, sofaBeds; each null until the property fills it in), selling copy, amenities and photos (images, the first is the cover; imageUrl repeats the cover). Use it to group rooms the way the property does, and show the class photos where a room has none of its own.
Changes a room's name, description, capacity, amenities or photos. Send only what changes; anything left out is kept. Prices are separate (see Set a room's price) so a key that may edit text cannot reprice the property. Returns the room as it is now.
path, stringThe room id, for example 8f2c1a4e-6b0d-4c1e-9a57-2d4b9e1f7c30.
name
body, stringUp to 100 characters. Two rooms cannot share a name.
description
body, stringUp to 1000 characters.
capacity
body, integer1-20.
amenities
body, string[]Replaces the amenity list.
images
body, string[]Replaces the photos. Public https addresses only, at most 30. See Replace photos.
Example
curl -X PUT "https://new.yourdailystay.com/api/partner/rooms/8f2c1a4e-6b0d-4c1e-9a57-2d4b9e1f7c30" \
-H "X-Api-Key: $DAILYSTAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"description":"Corner apartment with a balcony over the harbour."}'
Errors
- 400 VALIDATION_ERROR A photo address is not a public https URL, or there are more than 30.
- 400 UPDATE_ROOM_ERROR The change was refused, for example a duplicate room name.
- 403 Key lacks rooms:write.
- 404 NOT_FOUND No such room.
PUT/partner/rooms/{roomId}/images
Replace photos
Replaces the room's photos with these addresses, in this order; the first one leads on the booking page. DailyStay stores the addresses and shows each photo from where it is hosted, so host the files yourself on a public https address and keep them there. Send an empty list to remove every photo. Duplicates are dropped.
- 400 VALIDATION_ERROR An address is not public https (data:, http: and javascript: are refused), or there are more than 30.
- 403 Key lacks rooms:write.
- 404 NOT_FOUND No such room.
Prices
GET/partner/pricing
Read prices
Base price per room, the weekend setting, and the rate rules. A rule applies to one room class between two dates and multiplies the base price: weekdayMultiplier on weekdays and weekendMultiplier on the days listed in configuration.weekendDays (0 is Sunday). Where rules overlap the highest priority wins. To get a total for exact dates, ask GET /public/booking/{propertyId}/availability: it applies every rule for you.
Sets the room's base price per night. Seasonal and weekend rates are rules applied on top of this base (see Read prices), so changing the base moves every rate that is a multiple of it.
What is closed and what is taken in a window. rules are closed dates (kind: "block") and stay restrictions (kind: "restriction": minimum nights, closed to arrival or departure). bookings are the nights held by stays that are not cancelled or rejected, with no guest details; use bookings:read for those. A rule with roomId: null applies to every room. endDate of a rule is its last night, inclusive; a booking's checkOut is the morning the guest leaves.
Closes a range of nights for one room, or for every room when roomId is empty. The change goes to the connected booking channels like a closure made in the dashboard. Nights that are already sold cannot be closed.
- 409 DATES_BOOKED Some of those nights already have a stay. message says which.
DELETE/partner/calendar/blocks/{ruleId}
Open dates again
Removes a closed range, using the id from the calendar or from Close dates. A closure that comes from a synced calendar feed cannot be removed here: the next import would put it back.
- 409 SYNCED_BLOCK The closure belongs to a synced calendar feed.
GET/partner/ical
List calendar feeds
The iCal feeds the property has set up per room. direction: "export" feeds carry url, the address to give another system (Booking.com, Airbnb, your own site) so it can read this room's availability. direction: "import" feeds are the channel calendars DailyStay reads; their address is never returned, because it usually holds a private token. Use lastImportedAt and lastError to show whether a channel is healthy.
Request received, not yet accepted by the property. Every public booking starts here.
confirmed
Accepted by the property. The room is held.
checked_in
The guest has arrived.
checked_out
The stay is over.
cancelled
Cancelled. The nights are free again.
rejected
Declined by the property. The nights are free again.
Read every booking, 200 at a time
const all = []
for (let offset = 0; ; offset += 200) {
const page = await dailyStay('/partner/bookings?limit=200&offset=' + offset)
all.push(...page)
if (page.length < 200) break
}
Keep a website in sync
The same steps work for any property and any system: a hotel website, a channel manager, a booking app. DailyStay stays the source of truth for rooms, prices, the calendar and bookings; your system keeps its own presentation (display names, copy, layout) and sends back what only it has (photos, reservations it takes itself).
Create a key with **Full website sync** (or only the scopes you need) and call GET /partner/whoami. Turn off whatever a missing scope would need instead of failing later.
Read the catalogue: GET /partner/rooms and GET /partner/room-classes. Store each room's id; it never changes, and it is how you match a room to your own record.
Match rooms. Pair rooms you already have with DailyStay rooms by id (suggest pairs whose names match), let the owner confirm, and offer to create a record for any room that is new. Run this step again whenever a refresh brings a room you have not seen.
Keep your own display name per room if you want one. Treat name as DailyStay's name and never write it back unless the owner asks.
Read prices (GET /partner/pricing) for a "from" price, and ask GET /public/booking/{propertyId}/availability for the real total of a stay: it applies every rule.
Mirror the calendar: GET /partner/calendar for closed dates and occupied nights, or the iCal feeds from GET /partner/ical for systems that only speak iCal. Refresh on a timer and after each webhook.
Push what you own: photos with PUT /partner/rooms/{roomId}/images (public https addresses you host), closures with POST /partner/calendar/blocks, and reservations your system took with POST /partner/bookings.
Import history once with GET /partner/bookings?limit=200&offset=... (past stays included), then follow booking.created, booking.updated and booking.cancelled webhooks. Treat a webhook as a nudge and read the booking again.
Start-up check and catalogue refresh
const { scopes } = await dailyStay('/partner/whoami')
const can = (scope) => scopes.includes(scope)
const rooms = can('rooms:read') ? await dailyStay('/partner/rooms') : []
const unseen = rooms.filter((room) => !myRooms.has(room.id))
if (unseen.length) askOwnerToMatch(unseen) // show your "match rooms" step
// Push photos you host. Public https addresses only.
if (can('rooms:write')) {
await dailyStay('/partner/rooms/' + roomId + '/images', {
method: 'PUT',
body: { images: photoUrls },
})
}
Photos are stored as addresses, not files. Keep the files on your own public https address for as long as they should show on the booking page, and send the full list again whenever it changes.
Errors
HTTP
error.code
When
400
VALIDATION_ERROR
The request broke a booking rule. message says which, in words you can show a guest.
400
none (problem details)
A body field failed validation, for example a malformed email. See below.
400
PAYMENT_ERROR
A payment could not be started or confirmed.
401
none (empty body)
API key missing, unknown or revoked.
403
none (empty body)
API key lacks the scope.
404
NOT_FOUND
Unknown booking or payment, or the booking page is off.
409
UNAVAILABLE
The room is no longer free, or a closed date or minimum stay applies.
409
BOOKING_ERROR, DATES_BOOKED, SYNCED_BLOCK
A Partner API write was refused: the dates are taken, nights are already sold, or the closure belongs to a synced calendar feed. message says which.
429
RATE_LIMITED
Too many requests. Wait for the Retry-After seconds.
5xx
varies
Something failed on our side. Retry with backoff.
400 problem details
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"GuestEmail": [
"The GuestEmail field is not a valid e-mail address."
]
}
}
Rate limits
Endpoints
Limit
/public/booking/...
60 requests a minute per IP address
/public/payments/... and /partner/...
300 requests a minute per IP address
Over the limit you get 429 with error.codeRATE_LIMITED and a Retry-After header in seconds. Back off when you see it: an address that keeps going after 429s is blocked for a while.
Webhooks
Instead of polling, let DailyStay call your server when a booking changes. Add a subscription in Property Settings, Features tab, API and webhooks: an https URL and the events you want (none ticked means all). Copy the signing secret shown with it.
Event
Sent when
booking.created
The property creates a booking in the DailyStay app.
booking.updated
A booking's status changes, for example a website request is confirmed, or a guest checks in.
booking.cancelled
A booking is cancelled or rejected.
Reservations made through the Public Booking API do not send booking.created. You hear about them as booking.updated when the property confirms (or booking.cancelled when it rejects). Edits to dates or guest details do not send an event.
Delivery
POST to your URL with a JSON body and the header Daily-Stay-Signature: t=<unix seconds>,v1=<hex HMAC>.
Any 2xx within 8 seconds counts as delivered. Answer fast and do the work afterwards.
A failed delivery is not retried. After 20 failures in a row the subscription stops being called; it stays listed with its last error. Fix the endpoint, then delete the subscription and add it again.
Treat an event as a nudge: read the booking again with GET /partner/bookings/{bookingId}, and run a periodic sync to catch anything missed.
Inside data the field names are PascalCase (GuestName, CheckIn), unlike the API's camelCase. ReservationSource is always null in webhooks; read the booking from the API if you need it.
Verify the signature
Compute HMAC-SHA256, hex encoded, of <t>.<raw request body> with the subscription secret and compare it with v1. Use the raw bytes as received: parsing and re-serialising the JSON changes them. Reject timestamps older than five minutes to stop replays.
# DailyStay API documentation
Base URL: `https://new.yourdailystay.com/api`
## Contents
- Overview
- Hotel website quickstart
- Generate an API key
- Authenticate requests
- Key lifetime and token refresh
- Scopes
- Public Booking API
- Online payments
- Partner API
- Keep a website in sync
- Errors
- Rate limits
- Webhooks
- AI usage
## Overview
DailyStay has two HTTP APIs. Both speak JSON and share one base URL.
Base URL:
```text
https://new.yourdailystay.com/api
```
| API | Use it for | Auth | Call it from |
| --- | --- | --- | --- |
| Public Booking API, `/public/booking` and `/public/payments` | A hotel website: rooms, availability, new reservations, deposits | None | Browser or server |
| Partner API, `/partner` | Your own systems: keeping a website or channel manager in sync (rooms, classes, prices, photos, calendar, bookings), reporting, accounting | API key in `X-Api-Key`, scopes per capability | Server only |
> **Note:** Both APIs come with the Max plan. On a lower plan the public booking endpoints answer as if the page did not exist, and API keys are rejected with `401`.
### Conventions
- Request and response bodies are JSON. Send `Content-Type: application/json`.
- Field names are camelCase. Webhook `data` is the one exception (see Webhooks).
- Stay dates are `YYYY-MM-DD`. Timestamps are ISO 8601 in UTC.
- Money is a decimal in the property's currency, given as `currency` (for example `EUR`).
- Every response uses the same envelope: check `success`, then read `data` or `error`.
Success:
```json
{
"success": true,
"data": {
"...": "..."
},
"error": null,
"pagination": null
}
```
Error:
```json
{
"success": false,
"data": null,
"error": {
"code": "NOT_FOUND",
"message": "No such booking.",
"details": []
},
"pagination": null
}
```
## Hotel website quickstart
Building a website for a hotel that runs on DailyStay? This is the whole flow. Reservations made on your site are saved straight into DailyStay, as pending requests the property confirms. No API key is needed for any of it.
1. The owner switches on the booking page: Property Settings, Features tab, Your own booking page. Until then every public endpoint answers `404`.
2. Find the property id. It is the number at the end of the booking page link on that card (`/book/1042`). `GET /partner/whoami` also returns it for any API key.
3. Load the property and its rooms with `GET /public/booking/{propertyId}`.
4. Search a stay with `GET /public/booking/{propertyId}/availability`.
5. Book it with `POST /public/booking/{propertyId}`. The reservation appears in DailyStay as `pending` and the property is notified.
6. Optional: take the deposit online with the payment endpoints.
7. Optional: from your server, keep your own records in step with the Partner API and webhooks.
Browser example:
```js
const BASE = 'https://new.yourdailystay.com/api'
const PROPERTY_ID = '1042'
async function call(path, options = {}) {
const response = await fetch(BASE + path, {
...options,
headers: { 'Content-Type': 'application/json', ...options.headers },
})
const body = await response.json()
if (!body.success) throw new Error(body.error?.message ?? 'HTTP ' + response.status)
return body.data
}
// 1. What can be booked for these dates, priced
const search = await call(
'/public/booking/' + PROPERTY_ID + '/availability?checkIn=2026-11-14&checkOut=2026-11-17&guests=2',
)
if (search.rooms.length === 0) console.log(search.notes, search.suggestions)
// 2. Book the first free room
const booking = await call('/public/booking/' + PROPERTY_ID, {
method: 'POST',
body: JSON.stringify({
roomId: search.rooms[0].roomId,
checkIn: '2026-11-14',
checkOut: '2026-11-17',
guests: 2,
guestName: 'Elira Hoxha',
guestEmail: 'guest@example.com',
consentGiven: true,
}),
})
console.log(booking.reference, booking.status) // "482913" "pending"
```
> **Important:** Never put an API key in browser code. The public booking endpoints need no key, and the Partner API is for your server only.
## Generate an API key
1. Sign in to DailyStay as the property owner. Managers and staff do not see this panel.
2. Open Property Settings, Features tab, API and webhooks.
3. Under API keys choose New key. Name it after what will use it, for example "Website sync".
4. Tick the scopes it needs (see Scopes). With none ticked it gets `bookings:read`.
5. Choose Create key. The full key, `ds_` followed by 43 characters, is shown once. Copy it into your server's secret store straight away.
DailyStay keeps only a SHA-256 hash of the key, so nobody can show it to you again. If it is lost, create a new key and revoke the old one. The list shows each key's first 12 characters, scopes, creation date and last use.
> **Note:** A key belongs to one property. The property comes from the key itself, never from the URL, so a key cannot reach another property's data.
## Authenticate requests
Send the key in the `X-Api-Key` header on every Partner API request. Not in `Authorization`, and never in the query string.
Request:
```bash
curl "https://new.yourdailystay.com/api/partner/whoami" \
-H "X-Api-Key: $DAILYSTAY_API_KEY"
```
Response:
```json
{
"success": true,
"data": {
"propertyId": "1042",
"scopes": [
"bookings:read"
]
},
"error": null,
"pagination": null
}
```
| Status | Meaning |
| --- | --- |
| `200` | Key valid and allowed. |
| `401` | Header missing, key unknown or revoked, or the property is no longer on the Max plan. Empty body. |
| `403` | Key valid but missing the scope this endpoint needs. Empty body. |
Server helper (Node 18+):
```js
const BASE = 'https://new.yourdailystay.com/api'
export async function dailyStay(path) {
const response = await fetch(BASE + path, {
headers: { 'X-Api-Key': process.env.DAILYSTAY_API_KEY },
})
if (response.status === 401) throw new Error('DailyStay rejected the key. Is it revoked?')
if (response.status === 403) throw new Error('The key is missing a scope for ' + path)
const body = await response.json()
if (!body.success) throw new Error(body.error.message)
return body.data
}
```
## Key lifetime and token refresh
An API key is not an access token. There is no login call, no expiry and nothing to refresh: a key works until the owner revokes it, or until the property leaves the Max plan.
### Rotate a key without downtime
1. Create a second key with the same scopes.
2. Deploy it to your server.
3. Watch the old key's last use stop moving in the keys list.
4. Revoke the old key. It stops working at once and stays listed, marked revoked, for the audit trail.
### Which credential refreshes
| Credential | Expires | Refresh |
| --- | --- | --- |
| API key, `ds_...` | Never, until revoked | Not needed. Rotate by creating a new key. |
| AI assistant connector (MCP, OAuth) | Access token expires | Your AI client refreshes it on its own with `grant_type=refresh_token` at `/mcp/oauth/token`. |
| DailyStay app sign-in | Yes | Internal to DailyStay's own apps. Not supported for integrations: use an API key. |
## Scopes
| Scope | Grants | Used by |
| --- | --- | --- |
| `bookings:read` | Read the property's bookings, guest contact details included. | `GET /partner/bookings`, `GET /partner/bookings/{bookingId}` |
| `bookings:write` | Create bookings, set their price and change their status. | `POST /partner/bookings`, `PUT /partner/bookings/{bookingId}/price`, `PATCH /partner/bookings/{bookingId}/status` |
| `rooms:read` | Read rooms, room classes, amenities and photos. | `GET /partner/rooms`, `GET /partner/rooms/{roomId}`, `GET /partner/room-classes` |
| `rooms:write` | Change a room's name, description, capacity, amenities and photos. Not its price. | `PUT /partner/rooms/{roomId}`, `PUT /partner/rooms/{roomId}/images` |
| `pricing:read` | Read base prices, the weekend setting and rate rules. | `GET /partner/pricing` |
| `pricing:write` | Set a room's base price. | `PUT /partner/rooms/{roomId}/price` |
| `calendar:read` | Read closed dates, restrictions, occupied nights (no guest details) and the calendar feeds. | `GET /partner/calendar`, `GET /partner/ical` |
| `calendar:write` | Close dates and open them again. | `POST /partner/calendar/blocks`, `DELETE /partner/calendar/blocks/{ruleId}` |
| `guests:read` | Reserved. Grants nothing today. | None yet. Guest details come with each booking. |
`GET /partner/whoami` needs no scope. Give each key the least it needs: a reporting tool only ever needs `bookings:read`. A website that mirrors the property needs every scope above except `guests:read`: choose **Full website sync** when you create the key. Call `GET /partner/whoami` to see which scopes a key carries and switch features off, rather than fail, when one is missing.
## Public Booking API
For the hotel's own website. No key, open to any origin (CORS), limited to 60 requests a minute per IP address. Nothing here reveals another guest or another booking.
### GET /public/booking/{propertyId}
**Property and rooms.** Everything a booking page shows before dates are picked: contact details, check-in times, deposit rule, cancellation terms and every room that is not in maintenance. Prices here are the rooms' base rates; real prices come from availability.
- URL: `GET https://new.yourdailystay.com/api/public/booking/{propertyId}`
- Auth: None
| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `propertyId` | path | string | yes | The DailyStay property id. |
| `ref` | query | string | no | A tracked booking link code, counted as a visit for that link. |
Response `200`:
```json
{
"success": true,
"data": {
"propertyId": "1042",
"name": "Villa Drita",
"intro": "Five minutes from the beach, with parking.",
"city": "Sarandë",
"country": "Albania",
"address": "Rruga Skënderbeu 14",
"phone": "+355694821937",
"email": "stay@example.com",
"currency": "EUR",
"checkInTime": "14:00",
"checkOutTime": "11:00",
"depositEnabled": true,
"depositPercentage": 30,
"rooms": [
{
"id": "8f2c1a4e-6b0d-4c1e-9a57-2d4b9e1f7c30",
"name": "Sea View Double",
"type": "double",
"capacity": 2,
"pricePerNight": 85,
"description": "Balcony over the bay.",
"images": [
"https://example.com/rooms/sea-view-1.jpg"
],
"amenities": [
"Wi-Fi",
"Air conditioning",
"Balcony"
],
"classTitle": "Double room with sea view",
"maxAdults": 2,
"maxGuests": 2
}
],
"coordinates": "39.8756,20.0053",
"websiteUrl": "https://example.com",
"facilities": [
"Air conditioning",
"Balcony",
"Wi-Fi"
],
"gallery": [
"https://example.com/rooms/sea-view-1.jpg"
],
"freeCancellationDays": 3,
"payAtPropertyAllowed": true
},
"error": null,
"pagination": null
}
```
Errors:
- `404 NOT_FOUND` The booking page is switched off, the id is unknown, or the property is not on the Max plan.
Example:
```bash
curl "https://new.yourdailystay.com/api/public/booking/1042"
```
### GET /public/booking/by-slug/{slug}
**Property by booking address.** The same response, found by the booking address name the owner chose in Property Settings (the `villa-drita` in `villa-drita.yourdailystay.com`). Read `propertyId` from the answer and use it for every other call.
- URL: `GET https://new.yourdailystay.com/api/public/booking/by-slug/{slug}`
- Auth: None
| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `slug` | path | string | yes | The booking address name. |
| `ref` | query | string | no | A tracked booking link code. |
Response `200`:
```json
{
"success": true,
"data": {
"propertyId": "1042",
"name": "Villa Drita",
"intro": "Five minutes from the beach, with parking.",
"city": "Sarandë",
"country": "Albania",
"address": "Rruga Skënderbeu 14",
"phone": "+355694821937",
"email": "stay@example.com",
"currency": "EUR",
"checkInTime": "14:00",
"checkOutTime": "11:00",
"depositEnabled": true,
"depositPercentage": 30,
"rooms": [
"..."
],
"coordinates": "39.8756,20.0053",
"websiteUrl": "https://example.com",
"facilities": [
"..."
],
"gallery": [
"..."
],
"freeCancellationDays": 3,
"payAtPropertyAllowed": true
},
"error": null,
"pagination": null
}
```
Errors:
- `404 NOT_FOUND` No published page with that name.
Example:
```bash
curl "https://new.yourdailystay.com/api/public/booking/by-slug/villa-drita"
```
### GET /public/booking/{propertyId}/availability
**Search availability.** Rooms free for the whole stay that fit the party, priced with the property's rates, minimum stays and closed dates applied. When nothing fits, `rooms` is empty, `notes` says why in plain words, and `suggestions` offers up to three nearby windows that do have rooms.
- URL: `GET https://new.yourdailystay.com/api/public/booking/{propertyId}/availability`
- Auth: None
| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `propertyId` | path | string | yes | The property id. |
| `checkIn` | query | date | yes | Arrival, `YYYY-MM-DD`. Not in the past, at most 730 days ahead. |
| `checkOut` | query | date | yes | Departure, `YYYY-MM-DD`. 1 to 60 nights after arrival. |
| `guests` | query | integer | yes | People staying. Values below 1 are read as 1. |
Response `200`:
```json
{
"success": true,
"data": {
"checkIn": "2026-11-14",
"checkOut": "2026-11-17",
"nights": 3,
"guests": 2,
"currency": "EUR",
"rooms": [
{
"roomId": "8f2c1a4e-6b0d-4c1e-9a57-2d4b9e1f7c30",
"name": "Sea View Double",
"type": "double",
"capacity": 2,
"description": "Balcony over the bay.",
"total": 235,
"averagePerNight": 78.33,
"deposit": 70.5,
"images": [
"https://example.com/rooms/sea-view-1.jpg"
],
"amenities": [
"Wi-Fi",
"Air conditioning",
"Balcony"
],
"classTitle": "Double room with sea view",
"maxGuests": 2,
"standardPerNight": 85
}
],
"notes": [],
"suggestions": []
},
"error": null,
"pagination": null
}
```
Errors:
- `404 NOT_FOUND` No published booking page for this property.
Example:
```bash
curl "https://new.yourdailystay.com/api/public/booking/1042/availability?checkIn=2026-11-14&checkOut=2026-11-17&guests=2"
```
### POST /public/booking/{propertyId}
**Create a reservation.** Saves the reservation in DailyStay as `pending` with reservation source Direct, and notifies the property, who confirms it in the app. Availability, price, minimum stay and capacity are all checked again on the server, so a stale search cannot double-book a room.
- URL: `POST https://new.yourdailystay.com/api/public/booking/{propertyId}`
- Auth: None
| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `propertyId` | path | string | yes | The property id. |
| `roomId` | body | string | yes | A `roomId` from availability. |
| `checkIn` | body | date | yes | `YYYY-MM-DD`. |
| `checkOut` | body | date | yes | `YYYY-MM-DD`. |
| `guests` | body | integer | yes | 1 to 20, and no more than the room sleeps. |
| `guestName` | body | string | yes | Up to 100 characters. |
| `guestEmail` | body | string | yes | A valid email, up to 100 characters. |
| `guestPhone` | body | string | no | Up to 20 characters. |
| `guestCountry` | body | string | no | Up to 50 characters. |
| `notes` | body | string | no | Message for the property, up to 1000 characters. |
| `consentGiven` | body | boolean | yes | Must be `true`: the guest accepted the data-processing notice. |
| `payAtProperty` | body | boolean | no | Guest wants to pay on arrival. Honoured only when `payAtPropertyAllowed` is true. |
| `whatsAppOptIn` | body | boolean | no | Guest agreed to WhatsApp messages about this booking. |
| `ref` | body | string | no | Tracked booking link code, credits the booking to that link. |
Request body:
```json
{
"roomId": "8f2c1a4e-6b0d-4c1e-9a57-2d4b9e1f7c30",
"checkIn": "2026-11-14",
"checkOut": "2026-11-17",
"guests": 2,
"guestName": "Elira Hoxha",
"guestEmail": "guest@example.com",
"guestPhone": "+355694821937",
"guestCountry": "Albania",
"notes": "Arriving late, around 22:00",
"consentGiven": true,
"payAtProperty": false,
"whatsAppOptIn": false
}
```
Response `200`:
```json
{
"success": true,
"data": {
"bookingId": "482913",
"reference": "482913",
"roomName": "Sea View Double",
"checkIn": "2026-11-14",
"checkOut": "2026-11-17",
"nights": 3,
"guests": 2,
"total": 235,
"deposit": 70.5,
"currency": "EUR",
"status": "pending",
"payAtProperty": false,
"freeCancellationUntil": "2026-11-11"
},
"error": null,
"pagination": null
}
```
Errors:
- `400 VALIDATION_ERROR` A rule was broken, for example consent missing, dates in the past, or more guests than the room sleeps. `message` says which.
- `400` Field validation failed (problem details body, see Errors).
- `409 UNAVAILABLE` The room was taken meanwhile, a closed date or minimum stay applies, or the page is not live.
Example:
```bash
curl -X POST "https://new.yourdailystay.com/api/public/booking/1042" \
-H "Content-Type: application/json" \
-d '{"roomId":"8f2c1a4e-6b0d-4c1e-9a57-2d4b9e1f7c30","checkIn":"2026-11-14","checkOut":"2026-11-17","guests":2,"guestName":"Elira Hoxha","guestEmail":"guest@example.com","consentGiven":true}'
```
> **Note:** Calling these from your server instead of the browser? Every guest then shares your server's IP address and its 60 requests a minute. Cache the property response and keep availability calls to real searches.
## Online payments
Optional. Lets a guest pay the deposit, or the whole stay, right after booking. Needs Stripe or PayPal connected in Property Settings, Payments tab.
1. Read the providers. Empty means no online payment.
2. Start a payment for the `bookingId` you got back from the reservation.
3. Finish it in the browser with Stripe.js (`clientToken` is the client secret) or the PayPal buttons (`clientToken` is the order id).
4. Confirm the payment so DailyStay records it against the booking.
### GET /public/payments/{propertyId}/providers
**Payment providers.** Which ways this property can be paid online. An empty list means no online payment: show the booking as pending and let the property collect.
- URL: `GET https://new.yourdailystay.com/api/public/payments/{propertyId}/providers`
- Auth: None
| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `propertyId` | path | string | yes | The property id. |
Response `200`:
```json
{
"success": true,
"data": [
"stripe",
"paypal"
],
"error": null,
"pagination": null
}
```
Example:
```bash
curl "https://new.yourdailystay.com/api/public/payments/1042/providers"
```
### POST /public/payments/{propertyId}/booking/{bookingId}
**Start a payment.** Opens a payment for a booking and returns what the browser needs to finish it. The amount is worked out on the server: the deposit percentage when the property takes deposits, otherwise the whole stay. For Stripe, `clientToken` is the PaymentIntent client secret and `publishableKey` the Stripe publishable key. For PayPal, `clientToken` is the order id and `publishableKey` the PayPal client id.
- URL: `POST https://new.yourdailystay.com/api/public/payments/{propertyId}/booking/{bookingId}`
- Auth: None
| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `propertyId` | path | string | yes | The property id. |
| `bookingId` | path | string | yes | `bookingId` from the reservation response. |
| `provider` | body | string | yes | `stripe` or `paypal`. |
Request body:
```json
{
"provider": "stripe"
}
```
Response `200`:
```json
{
"success": true,
"data": {
"paymentId": "c41d7e02-93b5-4f6a-8e1c-5a9b2f0d7e64",
"provider": "stripe",
"clientToken": "pi_3Q8...secret_...",
"publishableKey": "pk_live_...",
"amount": 70.5,
"currency": "EUR",
"isLive": true
},
"error": null,
"pagination": null
}
```
Errors:
- `400 PAYMENT_ERROR` Online payments are off, the booking is cancelled, or there is nothing to pay.
Example:
```bash
curl -X POST "https://new.yourdailystay.com/api/public/payments/1042/booking/482913" \
-H "Content-Type: application/json" \
-d '{"provider":"stripe"}'
```
### POST /public/payments/confirm/{paymentId}
**Confirm a payment.** Call when the guest comes back from paying. DailyStay asks the provider directly whether the money arrived, so this call cannot mark a booking paid by itself. Stripe payments are also confirmed by Stripe's own webhook if the guest closes the tab.
- URL: `POST https://new.yourdailystay.com/api/public/payments/confirm/{paymentId}`
- Auth: None
| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `paymentId` | path | string | yes | `paymentId` from Start a payment. |
Response `200`:
```json
{
"success": true,
"data": {
"id": "c41d7e02-93b5-4f6a-8e1c-5a9b2f0d7e64",
"bookingId": "482913",
"provider": "stripe",
"amount": 70.5,
"refundedAmount": 0,
"currency": "EUR",
"status": "paid",
"kind": "deposit",
"failureReason": null,
"createdAt": "2026-10-21T09:13:02.104",
"capturedAt": "2026-10-21T09:13:40.881",
"refundedAt": null
},
"error": null,
"pagination": null
}
```
Errors:
- `404 NOT_FOUND` No such payment.
- `400 PAYMENT_ERROR` The provider refused or could not be reached.
Example:
```bash
curl -X POST "https://new.yourdailystay.com/api/public/payments/confirm/c41d7e02-93b5-4f6a-8e1c-5a9b2f0d7e64"
```
## Partner API
Everything your server needs to run against one property: read and change its rooms, prices, calendar and bookings. The key fixes the property, so no id appears in the URL, and each endpoint asks for the one scope it needs.
### GET /partner/whoami
**Check a key.** Returns the property this key belongs to and the scopes it carries. Call it once at start-up to fail fast on a wrong or revoked key.
- URL: `GET https://new.yourdailystay.com/api/partner/whoami`
- Auth: API key, no scope needed
| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Api-Key` | header | string | yes | Your key, `ds_...`. |
Response `200`:
```json
{
"success": true,
"data": {
"propertyId": "1042",
"scopes": [
"bookings:read"
]
},
"error": null,
"pagination": null
}
```
Errors:
- `401` Missing, unknown or revoked key, or the property is no longer on the Max plan.
Example:
```bash
curl "https://new.yourdailystay.com/api/partner/whoami" \
-H "X-Api-Key: $DAILYSTAY_API_KEY"
```
### GET /partner/bookings
**List bookings.** Bookings of the key's property, newest created first. `pagination` is `null`: keep raising `offset` until a page comes back shorter than `limit`.
- URL: `GET https://new.yourdailystay.com/api/partner/bookings`
- Auth: API key, scope `bookings:read`
| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Api-Key` | header | string | yes | Your key. |
| `status` | query | string | no | Only bookings in this status: `pending`, `confirmed`, `checked_in`, `checked_out`, `cancelled` or `rejected`. |
| `limit` | query | integer | no | Page size. Default `100`, clamped to 1-200. |
| `offset` | query | integer | no | How many to skip. Default `0`. |
Response `200`:
```json
{
"success": true,
"data": [
{
"id": "482913",
"displayId": null,
"bookingGroupId": null,
"roomId": "8f2c1a4e-6b0d-4c1e-9a57-2d4b9e1f7c30",
"propertyId": "1042",
"reservationSourceId": null,
"reservationSource": "Direct",
"effectivePricePerNight": 78.33,
"appliedRateRuleId": null,
"appliedRateRuleName": null,
"pricingMode": null,
"emailDisabled": false,
"guestName": "Elira Hoxha",
"guestEmail": "guest@example.com",
"guestPhone": "+355694821937",
"guestCountry": "Albania",
"guestPassportId": null,
"checkIn": "2026-11-14T00:00:00",
"checkOut": "2026-11-17T00:00:00",
"guests": 2,
"totalAmount": 235,
"depositAmount": 0,
"remainingAmount": 235,
"status": "pending",
"paymentStatus": "pending",
"depositSent": false,
"depositDate": null,
"depositPaymentMethod": null,
"notes": "Arriving late, around 22:00",
"extraOptions": [],
"emailStatuses": [],
"createdAt": "2026-10-21T09:12:44.517",
"updatedAt": "2026-10-21T09:12:44.517"
}
],
"error": null,
"pagination": null
}
```
Errors:
- `401` Key rejected.
- `403` Key lacks `bookings:read`.
Example:
```bash
curl "https://new.yourdailystay.com/api/partner/bookings?status=confirmed&limit=50&offset=0" \
-H "X-Api-Key: $DAILYSTAY_API_KEY"
```
### GET /partner/bookings/{bookingId}
**Get one booking.** A single booking by id. Use it after a webhook arrives to read the current state.
- URL: `GET https://new.yourdailystay.com/api/partner/bookings/{bookingId}`
- Auth: API key, scope `bookings:read`
| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Api-Key` | header | string | yes | Your key. |
| `bookingId` | path | string | yes | The booking id, for example `482913`. |
Response `200`:
```json
{
"success": true,
"data": {
"id": "482913",
"displayId": null,
"bookingGroupId": null,
"roomId": "8f2c1a4e-6b0d-4c1e-9a57-2d4b9e1f7c30",
"propertyId": "1042",
"reservationSourceId": null,
"reservationSource": "Direct",
"effectivePricePerNight": 78.33,
"appliedRateRuleId": null,
"appliedRateRuleName": null,
"pricingMode": null,
"emailDisabled": false,
"guestName": "Elira Hoxha",
"guestEmail": "guest@example.com",
"guestPhone": "+355694821937",
"guestCountry": "Albania",
"guestPassportId": null,
"checkIn": "2026-11-14T00:00:00",
"checkOut": "2026-11-17T00:00:00",
"guests": 2,
"totalAmount": 235,
"depositAmount": 0,
"remainingAmount": 235,
"status": "confirmed",
"paymentStatus": "pending",
"depositSent": false,
"depositDate": null,
"depositPaymentMethod": null,
"notes": "Arriving late, around 22:00",
"extraOptions": [],
"emailStatuses": [],
"createdAt": "2026-10-21T09:12:44.517",
"updatedAt": "2026-10-21T09:12:44.517"
},
"error": null,
"pagination": null
}
```
Errors:
- `401` Key rejected.
- `403` Key lacks `bookings:read`.
- `404 NOT_FOUND` No such booking on this property.
Example:
```bash
curl "https://new.yourdailystay.com/api/partner/bookings/482913" \
-H "X-Api-Key: $DAILYSTAY_API_KEY"
```
### POST /partner/bookings
**Create a booking.** Adds a booking to the calendar, for a reservation your system took itself (a direct sale, a phone call). Send `totalAmount` to book at an agreed price (a quote you gave the guest); leave it out and DailyStay prices the stay from the room's rates and rate rules. The deposit follows the property's deposit setting. The same checks apply as in the dashboard: dates already taken, closed ranges and minimum stays are refused. Guests are not emailed unless you set `sendEmails`. For a reservation made by a visitor on a hotel website, use the Public Booking API instead: it starts as `pending` and is accepted by the property.
- URL: `POST https://new.yourdailystay.com/api/partner/bookings`
- Auth: API key, scope `bookings:write`
| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Api-Key` | header | string | yes | Your key. |
| `roomId` | body | string | yes | The room to book. |
| `guestName` | body | string | yes | Up to 200 characters. |
| `guestEmail` | body | string | no | Without one a placeholder is stored and no email can be sent. |
| `guestPhone` | body | string | no | International format. |
| `guestCountry` | body | string | no | Country name. |
| `checkIn` | body | date | yes | Arrival, `YYYY-MM-DD`. |
| `checkOut` | body | date | yes | Departure, `YYYY-MM-DD`. |
| `guests` | body | integer | no | 1-20. Default 1. |
| `notes` | body | string | no | Free text. |
| `source` | body | string | no | Where it came from, shown as the booking source. Default `Partner API`. |
| `status` | body | string | no | One of the booking statuses. Default `confirmed`. |
| `totalAmount` | body | number | no | Agreed total for the whole stay. Without it the stay is priced from the rates. |
| `sendEmails` | body | boolean | no | Let DailyStay email the guest. Default `false`. |
Request body:
```json
{
"roomId": "8f2c1a4e-6b0d-4c1e-9a57-2d4b9e1f7c30",
"guestName": "Elira Hoxha",
"guestEmail": "guest@example.com",
"checkIn": "2026-11-14",
"checkOut": "2026-11-17",
"guests": 2,
"source": "Joy Apartments website",
"status": "confirmed"
}
```
Response `200`:
```json
{
"success": true,
"data": {
"id": "482913",
"displayId": null,
"bookingGroupId": null,
"roomId": "8f2c1a4e-6b0d-4c1e-9a57-2d4b9e1f7c30",
"propertyId": "1042",
"reservationSourceId": null,
"reservationSource": "Joy Apartments website",
"effectivePricePerNight": 78.33,
"appliedRateRuleId": null,
"appliedRateRuleName": null,
"pricingMode": null,
"emailDisabled": false,
"guestName": "Elira Hoxha",
"guestEmail": "guest@example.com",
"guestPhone": "+355694821937",
"guestCountry": "Albania",
"guestPassportId": null,
"checkIn": "2026-11-14T00:00:00",
"checkOut": "2026-11-17T00:00:00",
"guests": 2,
"totalAmount": 235,
"depositAmount": 0,
"remainingAmount": 235,
"status": "confirmed",
"paymentStatus": "pending",
"depositSent": false,
"depositDate": null,
"depositPaymentMethod": null,
"notes": "Arriving late, around 22:00",
"extraOptions": [],
"emailStatuses": [],
"createdAt": "2026-10-21T09:12:44.517",
"updatedAt": "2026-10-21T09:12:44.517"
},
"error": null,
"pagination": null
}
```
Errors:
- `400 VALIDATION_ERROR` Unknown status or an invalid field.
- `403` Key lacks `bookings:write`.
- `409 BOOKING_ERROR` Dates taken, a closed range, a stay below the minimum, or an unknown room. `message` says which.
Example:
```bash
curl -X POST "https://new.yourdailystay.com/api/partner/bookings" \
-H "X-Api-Key: $DAILYSTAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"roomId":"8f2c1a4e-6b0d-4c1e-9a57-2d4b9e1f7c30","guestName":"Elira Hoxha","checkIn":"2026-11-14","checkOut":"2026-11-17","guests":2}'
```
### PATCH /partner/bookings/{bookingId}/status
**Change a booking's status.** Moves a booking to a new status: confirm a pending one, cancel or reject one (the nights become free again), or mark it checked in or out. Returns the booking.
- URL: `PATCH https://new.yourdailystay.com/api/partner/bookings/{bookingId}/status`
- Auth: API key, scope `bookings:write`
| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Api-Key` | header | string | yes | Your key. |
| `bookingId` | path | string | yes | The booking id, for example `482913`. |
| `status` | body | string | yes | `pending`, `confirmed`, `checked_in`, `checked_out`, `cancelled` or `rejected`. |
Request body:
```json
{
"status": "confirmed"
}
```
Response `200`:
```json
{
"success": true,
"data": {
"id": "482913",
"displayId": null,
"bookingGroupId": null,
"roomId": "8f2c1a4e-6b0d-4c1e-9a57-2d4b9e1f7c30",
"propertyId": "1042",
"reservationSourceId": null,
"reservationSource": "Direct",
"effectivePricePerNight": 78.33,
"appliedRateRuleId": null,
"appliedRateRuleName": null,
"pricingMode": null,
"emailDisabled": false,
"guestName": "Elira Hoxha",
"guestEmail": "guest@example.com",
"guestPhone": "+355694821937",
"guestCountry": "Albania",
"guestPassportId": null,
"checkIn": "2026-11-14T00:00:00",
"checkOut": "2026-11-17T00:00:00",
"guests": 2,
"totalAmount": 235,
"depositAmount": 0,
"remainingAmount": 235,
"status": "confirmed",
"paymentStatus": "pending",
"depositSent": false,
"depositDate": null,
"depositPaymentMethod": null,
"notes": "Arriving late, around 22:00",
"extraOptions": [],
"emailStatuses": [],
"createdAt": "2026-10-21T09:12:44.517",
"updatedAt": "2026-10-21T09:12:44.517"
},
"error": null,
"pagination": null
}
```
Errors:
- `400 VALIDATION_ERROR` Unknown status.
- `403` Key lacks `bookings:write`.
- `404 NOT_FOUND` No such booking.
Example:
```bash
curl -X PATCH "https://new.yourdailystay.com/api/partner/bookings/482913/status" \
-H "X-Api-Key: $DAILYSTAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"status":"confirmed"}'
```
### PUT /partner/bookings/{bookingId}/price
**Set a booking's price.** Sets the agreed total of a booking, for example after the guest accepted a price you quoted. A deposit already paid is kept; otherwise the deposit follows the property's deposit setting. Combine it with Change a booking's status to confirm the stay at that price.
- URL: `PUT https://new.yourdailystay.com/api/partner/bookings/{bookingId}/price`
- Auth: API key, scope `bookings:write`
| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Api-Key` | header | string | yes | Your key. |
| `bookingId` | path | string | yes | The booking id. |
| `totalAmount` | body | number | yes | The agreed total for the whole stay. |
Request body:
```json
{
"totalAmount": 420
}
```
Response `200`:
```json
{
"success": true,
"data": {
"id": "482913",
"displayId": null,
"bookingGroupId": null,
"roomId": "8f2c1a4e-6b0d-4c1e-9a57-2d4b9e1f7c30",
"propertyId": "1042",
"reservationSourceId": null,
"reservationSource": "Direct",
"effectivePricePerNight": 140,
"appliedRateRuleId": null,
"appliedRateRuleName": null,
"pricingMode": "manual",
"emailDisabled": false,
"guestName": "Elira Hoxha",
"guestEmail": "guest@example.com",
"guestPhone": "+355694821937",
"guestCountry": "Albania",
"guestPassportId": null,
"checkIn": "2026-11-14T00:00:00",
"checkOut": "2026-11-17T00:00:00",
"guests": 2,
"totalAmount": 420,
"depositAmount": 126,
"remainingAmount": 294,
"status": "pending",
"paymentStatus": "pending",
"depositSent": false,
"depositDate": null,
"depositPaymentMethod": null,
"notes": "Arriving late, around 22:00",
"extraOptions": [],
"emailStatuses": [],
"createdAt": "2026-10-21T09:12:44.517",
"updatedAt": "2026-10-21T09:12:44.517"
},
"error": null,
"pagination": null
}
```
Errors:
- `403` Key lacks `bookings:write`.
- `404 NOT_FOUND` No such booking.
Example:
```bash
curl -X PUT "https://new.yourdailystay.com/api/partner/bookings/482913/price" \
-H "X-Api-Key: $DAILYSTAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"totalAmount":420}'
```
### Rooms and photos
### GET /partner/rooms
**List rooms.** Every room of the key's property with what its room class adds (guest limits, class title), its base price, amenities and photos. `name` is shared by rooms of the same kind, so show `name` with `number` ("Deluxe Apartment 101") to tell them apart. `type` names the room class; it matches `type` on `GET /partner/room-classes`. Read it to build or refresh your own room list, then match rooms by `id`, which never changes.
- URL: `GET https://new.yourdailystay.com/api/partner/rooms`
- Auth: API key, scope `rooms:read`
| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Api-Key` | header | string | yes | Your key. |
Response `200`:
```json
{
"success": true,
"data": [
{
"id": "8f2c1a4e-6b0d-4c1e-9a57-2d4b9e1f7c30",
"name": "Deluxe Apartment",
"number": "101",
"floor": "1",
"disabledAccess": false,
"type": "deluxe",
"classTitle": "Deluxe apartment with sea view",
"capacity": 4,
"maxGuests": 4,
"maxAdults": 3,
"pricePerNight": 150,
"description": "Corner apartment with a balcony over the harbour.",
"status": "available",
"amenities": [
"Wi-Fi",
"Air conditioning",
"Balcony"
],
"images": [
"https://example.com/media/deluxe-1.jpg",
"https://example.com/media/deluxe-2.jpg"
],
"updatedAt": "2026-10-21T09:12:44.517"
}
],
"error": null,
"pagination": null
}
```
Errors:
- `401` Key rejected.
- `403` Key lacks `rooms:read`.
Example:
```bash
curl "https://new.yourdailystay.com/api/partner/rooms" \
-H "X-Api-Key: $DAILYSTAY_API_KEY"
```
### GET /partner/rooms/{roomId}
**Get one room.** A single room, in the same shape as the list.
- URL: `GET https://new.yourdailystay.com/api/partner/rooms/{roomId}`
- Auth: API key, scope `rooms:read`
| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Api-Key` | header | string | yes | Your key. |
| `roomId` | path | string | yes | The room id, for example `8f2c1a4e-6b0d-4c1e-9a57-2d4b9e1f7c30`. |
Response `200`:
```json
{
"success": true,
"data": {
"id": "8f2c1a4e-6b0d-4c1e-9a57-2d4b9e1f7c30",
"name": "Deluxe Apartment",
"number": "101",
"floor": "1",
"disabledAccess": false,
"type": "deluxe",
"classTitle": "Deluxe apartment with sea view",
"capacity": 4,
"maxGuests": 4,
"maxAdults": 3,
"pricePerNight": 150,
"description": "Corner apartment with a balcony over the harbour.",
"status": "available",
"amenities": [
"Wi-Fi",
"Air conditioning",
"Balcony"
],
"images": [
"https://example.com/media/deluxe-1.jpg",
"https://example.com/media/deluxe-2.jpg"
],
"updatedAt": "2026-10-21T09:12:44.517"
},
"error": null,
"pagination": null
}
```
Errors:
- `401` Key rejected.
- `403` Key lacks `rooms:read`.
- `404 NOT_FOUND` No such room on this property.
Example:
```bash
curl "https://new.yourdailystay.com/api/partner/rooms/8f2c1a4e-6b0d-4c1e-9a57-2d4b9e1f7c30" \
-H "X-Api-Key: $DAILYSTAY_API_KEY"
```
### GET /partner/room-classes
**List room classes.** The classes (types) rooms belong to, with their guest limits, layout (`sizeSqm` in m², `beds` as guests read it, `bathrooms`, `sofaBeds`; each `null` until the property fills it in), selling copy, amenities and photos (`images`, the first is the cover; `imageUrl` repeats the cover). Use it to group rooms the way the property does, and show the class photos where a room has none of its own.
- URL: `GET https://new.yourdailystay.com/api/partner/room-classes`
- Auth: API key, scope `rooms:read`
| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Api-Key` | header | string | yes | Your key. |
Response `200`:
```json
{
"success": true,
"data": [
{
"id": "b3d2a7c1-5e84-4f19-a6c0-9d1e2f3a4b50",
"propertyId": "1042",
"type": "deluxe",
"generalType": "apartment",
"title": "Deluxe apartment with sea view",
"shortTitle": "Deluxe",
"description": "Spacious apartment for up to four guests.",
"minGuests": 1,
"maxGuests": 4,
"maxAdults": 3,
"allowChildren": true,
"sizeSqm": 32,
"beds": "1 double bed, 2 sofa beds",
"bathrooms": 1,
"sofaBeds": 2,
"amenities": [
"Wi-Fi",
"Air conditioning"
],
"imageUrl": "https://example.com/media/deluxe-1.jpg",
"images": [
"https://example.com/media/deluxe-1.jpg",
"https://example.com/media/deluxe-2.jpg"
],
"sortOrder": 0,
"createdAt": "2026-08-02T10:00:00",
"updatedAt": "2026-10-01T10:00:00"
}
],
"error": null,
"pagination": null
}
```
Errors:
- `401` Key rejected.
- `403` Key lacks `rooms:read`.
Example:
```bash
curl "https://new.yourdailystay.com/api/partner/room-classes" \
-H "X-Api-Key: $DAILYSTAY_API_KEY"
```
### PUT /partner/rooms/{roomId}
**Update a room.** Changes a room's name, description, capacity, amenities or photos. Send only what changes; anything left out is kept. Prices are separate (see Set a room's price) so a key that may edit text cannot reprice the property. Returns the room as it is now.
- URL: `PUT https://new.yourdailystay.com/api/partner/rooms/{roomId}`
- Auth: API key, scope `rooms:write`
| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Api-Key` | header | string | yes | Your key. |
| `roomId` | path | string | yes | The room id, for example `8f2c1a4e-6b0d-4c1e-9a57-2d4b9e1f7c30`. |
| `name` | body | string | no | Up to 100 characters. Two rooms cannot share a name. |
| `description` | body | string | no | Up to 1000 characters. |
| `capacity` | body | integer | no | 1-20. |
| `amenities` | body | string[] | no | Replaces the amenity list. |
| `images` | body | string[] | no | Replaces the photos. Public https addresses only, at most 30. See Replace photos. |
Request body:
```json
{
"description": "Corner apartment with a balcony over the harbour.",
"amenities": [
"Wi-Fi",
"Air conditioning",
"Balcony"
]
}
```
Response `200`:
```json
{
"success": true,
"data": {
"id": "8f2c1a4e-6b0d-4c1e-9a57-2d4b9e1f7c30",
"name": "Deluxe Apartment",
"number": "101",
"floor": "1",
"disabledAccess": false,
"type": "deluxe",
"classTitle": "Deluxe apartment with sea view",
"capacity": 4,
"maxGuests": 4,
"maxAdults": 3,
"pricePerNight": 150,
"description": "Corner apartment with a balcony over the harbour.",
"status": "available",
"amenities": [
"Wi-Fi",
"Air conditioning",
"Balcony"
],
"images": [
"https://example.com/media/deluxe-1.jpg",
"https://example.com/media/deluxe-2.jpg"
],
"updatedAt": "2026-10-21T09:12:44.517"
},
"error": null,
"pagination": null
}
```
Errors:
- `400 VALIDATION_ERROR` A photo address is not a public https URL, or there are more than 30.
- `400 UPDATE_ROOM_ERROR` The change was refused, for example a duplicate room name.
- `403` Key lacks `rooms:write`.
- `404 NOT_FOUND` No such room.
Example:
```bash
curl -X PUT "https://new.yourdailystay.com/api/partner/rooms/8f2c1a4e-6b0d-4c1e-9a57-2d4b9e1f7c30" \
-H "X-Api-Key: $DAILYSTAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"description":"Corner apartment with a balcony over the harbour."}'
```
### PUT /partner/rooms/{roomId}/images
**Replace photos.** Replaces the room's photos with these addresses, in this order; the first one leads on the booking page. DailyStay stores the addresses and shows each photo from where it is hosted, so host the files yourself on a public https address and keep them there. Send an empty list to remove every photo. Duplicates are dropped.
- URL: `PUT https://new.yourdailystay.com/api/partner/rooms/{roomId}/images`
- Auth: API key, scope `rooms:write`
| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Api-Key` | header | string | yes | Your key. |
| `roomId` | path | string | yes | The room id, for example `8f2c1a4e-6b0d-4c1e-9a57-2d4b9e1f7c30`. |
| `images` | body | string[] | yes | Absolute `https://` URLs without a user name or password, each up to 2048 characters, at most 30. |
Request body:
```json
{
"images": [
"https://example.com/media/deluxe-1.jpg",
"https://example.com/media/deluxe-2.jpg"
]
}
```
Response `200`:
```json
{
"success": true,
"data": {
"id": "8f2c1a4e-6b0d-4c1e-9a57-2d4b9e1f7c30",
"name": "Deluxe Apartment",
"number": "101",
"floor": "1",
"disabledAccess": false,
"type": "deluxe",
"classTitle": "Deluxe apartment with sea view",
"capacity": 4,
"maxGuests": 4,
"maxAdults": 3,
"pricePerNight": 150,
"description": "Corner apartment with a balcony over the harbour.",
"status": "available",
"amenities": [
"Wi-Fi",
"Air conditioning",
"Balcony"
],
"images": [
"https://example.com/media/deluxe-1.jpg",
"https://example.com/media/deluxe-2.jpg"
],
"updatedAt": "2026-10-21T09:12:44.517"
},
"error": null,
"pagination": null
}
```
Errors:
- `400 VALIDATION_ERROR` An address is not public https (`data:`, `http:` and `javascript:` are refused), or there are more than 30.
- `403` Key lacks `rooms:write`.
- `404 NOT_FOUND` No such room.
Example:
```bash
curl -X PUT "https://new.yourdailystay.com/api/partner/rooms/8f2c1a4e-6b0d-4c1e-9a57-2d4b9e1f7c30/images" \
-H "X-Api-Key: $DAILYSTAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"images":["https://example.com/media/deluxe-1.jpg"]}'
```
### Prices
### GET /partner/pricing
**Read prices.** Base price per room, the weekend setting, and the rate rules. A rule applies to one room class between two dates and multiplies the base price: `weekdayMultiplier` on weekdays and `weekendMultiplier` on the days listed in `configuration.weekendDays` (0 is Sunday). Where rules overlap the highest `priority` wins. To get a total for exact dates, ask `GET /public/booking/{propertyId}/availability`: it applies every rule for you.
- URL: `GET https://new.yourdailystay.com/api/partner/pricing`
- Auth: API key, scope `pricing:read`
| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Api-Key` | header | string | yes | Your key. |
Response `200`:
```json
{
"success": true,
"data": {
"configuration": {
"propertyId": "1042",
"autoApplyRates": true,
"allowManualOverride": true,
"roundingMode": "nearest_0_01",
"weekendDays": [
5,
6
],
"updatedAt": "2026-10-01T10:00:00"
},
"rules": [
{
"id": "e5a1c9d3-27b4-4c68-8f0a-3b6d9e1a2c47",
"propertyId": "1042",
"name": "High season",
"roomClassType": "deluxe",
"startDate": "2027-07-01T00:00:00",
"endDate": "2027-08-31T00:00:00",
"weekdayMultiplier": 1.4,
"weekendMultiplier": 1.6,
"reservationSourceId": null,
"reservationSource": null,
"sourceAdjustmentType": "none",
"sourceAdjustmentValue": 0,
"minStayNights": 3,
"maxStayNights": null,
"priority": 10,
"isActive": true,
"createdAt": "2026-10-01T10:00:00",
"updatedAt": "2026-10-01T10:00:00"
}
],
"rooms": [
{
"id": "8f2c1a4e-6b0d-4c1e-9a57-2d4b9e1f7c30",
"name": "Deluxe Apartment",
"type": "deluxe",
"pricePerNight": 150
}
]
},
"error": null,
"pagination": null
}
```
Errors:
- `403` Key lacks `pricing:read`.
Example:
```bash
curl "https://new.yourdailystay.com/api/partner/pricing" \
-H "X-Api-Key: $DAILYSTAY_API_KEY"
```
### PUT /partner/rooms/{roomId}/price
**Set a room's price.** Sets the room's base price per night. Seasonal and weekend rates are rules applied on top of this base (see Read prices), so changing the base moves every rate that is a multiple of it.
- URL: `PUT https://new.yourdailystay.com/api/partner/rooms/{roomId}/price`
- Auth: API key, scope `pricing:write`
| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Api-Key` | header | string | yes | Your key. |
| `roomId` | path | string | yes | The room id, for example `8f2c1a4e-6b0d-4c1e-9a57-2d4b9e1f7c30`. |
| `pricePerNight` | body | number | yes | More than 0, in the property's currency. |
Request body:
```json
{
"pricePerNight": 160
}
```
Response `200`:
```json
{
"success": true,
"data": {
"id": "8f2c1a4e-6b0d-4c1e-9a57-2d4b9e1f7c30",
"name": "Deluxe Apartment",
"number": "101",
"floor": "1",
"disabledAccess": false,
"type": "deluxe",
"classTitle": "Deluxe apartment with sea view",
"capacity": 4,
"maxGuests": 4,
"maxAdults": 3,
"pricePerNight": 160,
"description": "Corner apartment with a balcony over the harbour.",
"status": "available",
"amenities": [
"Wi-Fi",
"Air conditioning",
"Balcony"
],
"images": [
"https://example.com/media/deluxe-1.jpg",
"https://example.com/media/deluxe-2.jpg"
],
"updatedAt": "2026-10-21T09:12:44.517"
},
"error": null,
"pagination": null
}
```
Errors:
- `403` Key lacks `pricing:write`.
- `404 NOT_FOUND` No such room.
Example:
```bash
curl -X PUT "https://new.yourdailystay.com/api/partner/rooms/8f2c1a4e-6b0d-4c1e-9a57-2d4b9e1f7c30/price" \
-H "X-Api-Key: $DAILYSTAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"pricePerNight":160}'
```
### Calendar
### GET /partner/calendar
**Read the calendar.** What is closed and what is taken in a window. `rules` are closed dates (`kind: "block"`) and stay restrictions (`kind: "restriction"`: minimum nights, closed to arrival or departure). `bookings` are the nights held by stays that are not cancelled or rejected, with no guest details; use `bookings:read` for those. A rule with `roomId: null` applies to every room. `endDate` of a rule is its last night, inclusive; a booking's `checkOut` is the morning the guest leaves.
- URL: `GET https://new.yourdailystay.com/api/partner/calendar`
- Auth: API key, scope `calendar:read`
| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Api-Key` | header | string | yes | Your key. |
| `from` | query | date | no | First day, `YYYY-MM-DD`. Default today. |
| `to` | query | date | no | Last day, `YYYY-MM-DD`. Default a year after `from`; windows are cut at two years. |
Response `200`:
```json
{
"success": true,
"data": {
"from": "2026-11-01",
"to": "2027-10-31",
"rules": [
{
"id": "f7b2d4a6-1c83-49e5-b0d7-6a8c2e4f1b93",
"propertyId": "1042",
"roomId": "8f2c1a4e-6b0d-4c1e-9a57-2d4b9e1f7c30",
"kind": "block",
"startDate": "2026-12-01",
"endDate": "2026-12-05",
"note": "Painting",
"minNights": null,
"closedToArrival": false,
"closedToDeparture": false,
"createdAt": "2026-10-20T08:00:00"
}
],
"bookings": [
{
"id": "482913",
"roomId": "8f2c1a4e-6b0d-4c1e-9a57-2d4b9e1f7c30",
"checkIn": "2026-11-14",
"checkOut": "2026-11-17",
"status": "confirmed"
}
]
},
"error": null,
"pagination": null
}
```
Errors:
- `400 VALIDATION_ERROR` `to` is before `from`.
- `403` Key lacks `calendar:read`.
Example:
```bash
curl "https://new.yourdailystay.com/api/partner/calendar?from=2026-11-01&to=2027-10-31" \
-H "X-Api-Key: $DAILYSTAY_API_KEY"
```
### POST /partner/calendar/blocks
**Close dates.** Closes a range of nights for one room, or for every room when `roomId` is empty. The change goes to the connected booking channels like a closure made in the dashboard. Nights that are already sold cannot be closed.
- URL: `POST https://new.yourdailystay.com/api/partner/calendar/blocks`
- Auth: API key, scope `calendar:write`
| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Api-Key` | header | string | yes | Your key. |
| `roomId` | body | string | no | The room to close. Empty closes every room. |
| `startDate` | body | date | yes | First night closed, `YYYY-MM-DD`. |
| `endDate` | body | date | yes | Last night closed, inclusive. |
| `note` | body | string | no | Up to 200 characters, shown on the calendar. |
Request body:
```json
{
"roomId": "8f2c1a4e-6b0d-4c1e-9a57-2d4b9e1f7c30",
"startDate": "2026-12-01",
"endDate": "2026-12-05",
"note": "Painting"
}
```
Response `200`:
```json
{
"success": true,
"data": {
"id": "f7b2d4a6-1c83-49e5-b0d7-6a8c2e4f1b93",
"propertyId": "1042",
"roomId": "8f2c1a4e-6b0d-4c1e-9a57-2d4b9e1f7c30",
"kind": "block",
"startDate": "2026-12-01",
"endDate": "2026-12-05",
"note": "Painting",
"minNights": null,
"closedToArrival": false,
"closedToDeparture": false,
"createdAt": "2026-10-21T09:12:44.517"
},
"error": null,
"pagination": null
}
```
Errors:
- `400 VALIDATION_ERROR` The range is not valid.
- `403` Key lacks `calendar:write`.
- `409 DATES_BOOKED` Some of those nights already have a stay. `message` says which.
Example:
```bash
curl -X POST "https://new.yourdailystay.com/api/partner/calendar/blocks" \
-H "X-Api-Key: $DAILYSTAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"roomId":"8f2c1a4e-6b0d-4c1e-9a57-2d4b9e1f7c30","startDate":"2026-12-01","endDate":"2026-12-05","note":"Painting"}'
```
### DELETE /partner/calendar/blocks/{ruleId}
**Open dates again.** Removes a closed range, using the `id` from the calendar or from Close dates. A closure that comes from a synced calendar feed cannot be removed here: the next import would put it back.
- URL: `DELETE https://new.yourdailystay.com/api/partner/calendar/blocks/{ruleId}`
- Auth: API key, scope `calendar:write`
| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Api-Key` | header | string | yes | Your key. |
| `ruleId` | path | string | yes | The id of the closed range. |
Response `200`:
```json
{
"success": true,
"data": true,
"error": null,
"pagination": null
}
```
Errors:
- `403` Key lacks `calendar:write`.
- `404 NOT_FOUND` No such closed range.
- `409 SYNCED_BLOCK` The closure belongs to a synced calendar feed.
Example:
```bash
curl -X DELETE "https://new.yourdailystay.com/api/partner/calendar/blocks/f7b2d4a6-1c83-49e5-b0d7-6a8c2e4f1b93" \
-H "X-Api-Key: $DAILYSTAY_API_KEY"
```
### GET /partner/ical
**List calendar feeds.** The iCal feeds the property has set up per room. `direction: "export"` feeds carry `url`, the address to give another system (Booking.com, Airbnb, your own site) so it can read this room's availability. `direction: "import"` feeds are the channel calendars DailyStay reads; their address is never returned, because it usually holds a private token. Use `lastImportedAt` and `lastError` to show whether a channel is healthy.
- URL: `GET https://new.yourdailystay.com/api/partner/ical`
- Auth: API key, scope `calendar:read`
| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Api-Key` | header | string | yes | Your key. |
Response `200`:
```json
{
"success": true,
"data": [
{
"id": "a9c3e5b7-4d12-4f68-90ab-1c2d3e4f5a67",
"roomId": "8f2c1a4e-6b0d-4c1e-9a57-2d4b9e1f7c30",
"roomName": "Deluxe Apartment",
"direction": "export",
"label": "Availability for partners",
"channel": "other",
"url": "https://new.yourdailystay.com/api/ical/q7Zk2mXw9d.ics",
"isActive": true,
"lastImportedAt": null,
"lastImportedCount": 0,
"lastError": null
},
{
"id": "b1d4f6a8-5e23-4a79-81bc-2d3e4f5a6b78",
"roomId": "8f2c1a4e-6b0d-4c1e-9a57-2d4b9e1f7c30",
"roomName": "Deluxe Apartment",
"direction": "import",
"label": "Airbnb",
"channel": "airbnb",
"url": null,
"isActive": true,
"lastImportedAt": "2026-10-21T09:00:00",
"lastImportedCount": 12,
"lastError": null
}
],
"error": null,
"pagination": null
}
```
Errors:
- `403` Key lacks `calendar:read`.
Example:
```bash
curl "https://new.yourdailystay.com/api/partner/ical" \
-H "X-Api-Key: $DAILYSTAY_API_KEY"
```
### Booking statuses
| Status | Meaning |
| --- | --- |
| `pending` | Request received, not yet accepted by the property. Every public booking starts here. |
| `confirmed` | Accepted by the property. The room is held. |
| `checked_in` | The guest has arrived. |
| `checked_out` | The stay is over. |
| `cancelled` | Cancelled. The nights are free again. |
| `rejected` | Declined by the property. The nights are free again. |
Read every booking, 200 at a time:
```js
const all = []
for (let offset = 0; ; offset += 200) {
const page = await dailyStay('/partner/bookings?limit=200&offset=' + offset)
all.push(...page)
if (page.length < 200) break
}
```
## Keep a website in sync
The same steps work for any property and any system: a hotel website, a channel manager, a booking app. DailyStay stays the source of truth for rooms, prices, the calendar and bookings; your system keeps its own presentation (display names, copy, layout) and sends back what only it has (photos, reservations it takes itself).
1. Create a key with **Full website sync** (or only the scopes you need) and call `GET /partner/whoami`. Turn off whatever a missing scope would need instead of failing later.
2. Read the catalogue: `GET /partner/rooms` and `GET /partner/room-classes`. Store each room's `id`; it never changes, and it is how you match a room to your own record.
3. Match rooms. Pair rooms you already have with DailyStay rooms by `id` (suggest pairs whose names match), let the owner confirm, and offer to create a record for any room that is new. Run this step again whenever a refresh brings a room you have not seen.
4. Keep your own display name per room if you want one. Treat `name` as DailyStay's name and never write it back unless the owner asks.
5. Read prices (`GET /partner/pricing`) for a "from" price, and ask `GET /public/booking/{propertyId}/availability` for the real total of a stay: it applies every rule.
6. Mirror the calendar: `GET /partner/calendar` for closed dates and occupied nights, or the iCal feeds from `GET /partner/ical` for systems that only speak iCal. Refresh on a timer and after each webhook.
7. Push what you own: photos with `PUT /partner/rooms/{roomId}/images` (public https addresses you host), closures with `POST /partner/calendar/blocks`, and reservations your system took with `POST /partner/bookings`.
8. Import history once with `GET /partner/bookings?limit=200&offset=...` (past stays included), then follow `booking.created`, `booking.updated` and `booking.cancelled` webhooks. Treat a webhook as a nudge and read the booking again.
Start-up check and catalogue refresh:
```js
const { scopes } = await dailyStay('/partner/whoami')
const can = (scope) => scopes.includes(scope)
const rooms = can('rooms:read') ? await dailyStay('/partner/rooms') : []
const unseen = rooms.filter((room) => !myRooms.has(room.id))
if (unseen.length) askOwnerToMatch(unseen) // show your "match rooms" step
// Push photos you host. Public https addresses only.
if (can('rooms:write')) {
await dailyStay('/partner/rooms/' + roomId + '/images', {
method: 'PUT',
body: { images: photoUrls },
})
}
```
> **Note:** Photos are stored as addresses, not files. Keep the files on your own public https address for as long as they should show on the booking page, and send the full list again whenever it changes.
## Errors
| HTTP | error.code | When |
| --- | --- | --- |
| `400` | `VALIDATION_ERROR` | The request broke a booking rule. `message` says which, in words you can show a guest. |
| `400` | none (problem details) | A body field failed validation, for example a malformed email. See below. |
| `400` | `PAYMENT_ERROR` | A payment could not be started or confirmed. |
| `401` | none (empty body) | API key missing, unknown or revoked. |
| `403` | none (empty body) | API key lacks the scope. |
| `404` | `NOT_FOUND` | Unknown booking or payment, or the booking page is off. |
| `409` | `UNAVAILABLE` | The room is no longer free, or a closed date or minimum stay applies. |
| `409` | `BOOKING_ERROR`, `DATES_BOOKED`, `SYNCED_BLOCK` | A Partner API write was refused: the dates are taken, nights are already sold, or the closure belongs to a synced calendar feed. `message` says which. |
| `429` | `RATE_LIMITED` | Too many requests. Wait for the `Retry-After` seconds. |
| `5xx` | varies | Something failed on our side. Retry with backoff. |
400 problem details:
```json
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"GuestEmail": [
"The GuestEmail field is not a valid e-mail address."
]
}
}
```
## Rate limits
| Endpoints | Limit |
| --- | --- |
| `/public/booking/...` | 60 requests a minute per IP address |
| `/public/payments/...` and `/partner/...` | 300 requests a minute per IP address |
Over the limit you get `429` with `error.code` `RATE_LIMITED` and a `Retry-After` header in seconds. Back off when you see it: an address that keeps going after `429`s is blocked for a while.
## Webhooks
Instead of polling, let DailyStay call your server when a booking changes. Add a subscription in Property Settings, Features tab, API and webhooks: an `https` URL and the events you want (none ticked means all). Copy the signing secret shown with it.
| Event | Sent when |
| --- | --- |
| `booking.created` | The property creates a booking in the DailyStay app. |
| `booking.updated` | A booking's status changes, for example a website request is confirmed, or a guest checks in. |
| `booking.cancelled` | A booking is cancelled or rejected. |
> **Note:** Reservations made through the Public Booking API do not send `booking.created`. You hear about them as `booking.updated` when the property confirms (or `booking.cancelled` when it rejects). Edits to dates or guest details do not send an event.
### Delivery
- `POST` to your URL with a JSON body and the header `Daily-Stay-Signature: t=<unix seconds>,v1=<hex HMAC>`.
- Any `2xx` within 8 seconds counts as delivered. Answer fast and do the work afterwards.
- A failed delivery is not retried. After 20 failures in a row the subscription stops being called; it stays listed with its last error. Fix the endpoint, then delete the subscription and add it again.
- Treat an event as a nudge: read the booking again with `GET /partner/bookings/{bookingId}`, and run a periodic sync to catch anything missed.
Payload:
```json
{
"event": "booking.updated",
"propertyId": "1042",
"sentAt": "2026-10-21T09:30:02.1184310Z",
"data": {
"Id": "482913",
"DisplayId": null,
"BookingGroupId": null,
"RoomId": "8f2c1a4e-6b0d-4c1e-9a57-2d4b9e1f7c30",
"PropertyId": "1042",
"ReservationSourceId": null,
"ReservationSource": null,
"EffectivePricePerNight": null,
"AppliedRateRuleId": null,
"AppliedRateRuleName": null,
"PricingMode": null,
"EmailDisabled": false,
"GuestName": "Elira Hoxha",
"GuestEmail": "guest@example.com",
"GuestPhone": "+355694821937",
"GuestCountry": "Albania",
"GuestPassportId": null,
"CheckIn": "2026-11-14T00:00:00",
"CheckOut": "2026-11-17T00:00:00",
"Guests": 2,
"TotalAmount": 235,
"DepositAmount": 0,
"RemainingAmount": 235,
"Status": "confirmed",
"PaymentStatus": "pending",
"DepositSent": false,
"DepositDate": null,
"DepositPaymentMethod": null,
"Notes": "Arriving late, around 22:00",
"ExtraOptions": [],
"EmailStatuses": [],
"CreatedAt": "2026-10-21T09:12:44.517",
"UpdatedAt": "2026-10-21T09:12:44.517"
}
}
```
> **Important:** Inside `data` the field names are PascalCase (`GuestName`, `CheckIn`), unlike the API's camelCase. `ReservationSource` is always `null` in webhooks; read the booking from the API if you need it.
### Verify the signature
Compute HMAC-SHA256, hex encoded, of `<t>.<raw request body>` with the subscription secret and compare it with `v1`. Use the raw bytes as received: parsing and re-serialising the JSON changes them. Reject timestamps older than five minutes to stop replays.
Node (Express):
```js
import crypto from 'node:crypto'
import express from 'express'
const app = express()
function verify(rawBody, header, secret) {
const parts = Object.fromEntries((header ?? '').split(',').map((part) => part.split('=')))
const timestamp = Number(parts.t)
if (!timestamp || !parts.v1) return false
if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false
const expected = crypto.createHmac('sha256', secret).update(timestamp + '.' + rawBody).digest('hex')
return expected.length === parts.v1.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
}
app.post('/webhooks/dailystay', express.raw({ type: 'application/json' }), (req, res) => {
const raw = req.body.toString('utf8')
if (!verify(raw, req.get('Daily-Stay-Signature'), process.env.DAILYSTAY_WEBHOOK_SECRET)) {
return res.sendStatus(401)
}
res.sendStatus(200)
const event = JSON.parse(raw)
console.log(event.event, event.data.Id, event.data.Status)
})
```
## AI usage
Building with ChatGPT, Claude, Cursor or Copilot? Copy this whole documentation as one Markdown file and paste it into the chat with your task. Every endpoint, field and example on this page is in it.
Prompt to start with:
```text
You are helping me connect a hotel website to the DailyStay PMS.
Use only the endpoints and fields in the DailyStay API documentation below.
Do not invent endpoints, fields or statuses. Never put the API key in browser code.
Property id: 1042
What I am building: <describe your task>
<paste the DailyStay API documentation here>
```
Prompt for a full sync:
```text
You are building a two-way sync between my website and the DailyStay PMS.
Use only the endpoints, scopes and fields in the DailyStay API documentation below.
Never put the API key in browser code. Store the key on the server.
1. Call GET /partner/whoami and read the scopes. Disable any feature whose scope is missing.
2. Read rooms and room classes. Match them to my own rooms by DailyStay room id, suggest
matches by name, and ask me before linking. List DailyStay rooms I have not matched.
3. Keep my own display name for a room if I set one. Do not write names back to DailyStay.
4. Mirror prices, the calendar and bookings from DailyStay. Import past bookings once, then
follow webhooks and re-read the booking on each one.
5. Send DailyStay what only I have: photos (public https URLs I host), closed dates, and
reservations my site took.
Property id: 1042
My stack: <describe it>
<paste the DailyStay API documentation here>
```
### Let an AI assistant work inside DailyStay
To have Claude or ChatGPT look up bookings and answer questions about the property directly, add `https://new.yourdailystay.com/mcp` as a custom connector. The assistant signs in through OAuth and refreshes its own tokens, so no API key is involved. Max plan.
Building with ChatGPT, Claude, Cursor or Copilot? Copy this whole documentation as one Markdown file and paste it into the chat with your task. Every endpoint, field and example on this page is in it.
Prompt to start with
You are helping me connect a hotel website to the DailyStay PMS.
Use only the endpoints and fields in the DailyStay API documentation below.
Do not invent endpoints, fields or statuses. Never put the API key in browser code.
Property id: 1042
What I am building: <describe your task>
<paste the DailyStay API documentation here>
Prompt for a full sync
You are building a two-way sync between my website and the DailyStay PMS.
Use only the endpoints, scopes and fields in the DailyStay API documentation below.
Never put the API key in browser code. Store the key on the server.
1. Call GET /partner/whoami and read the scopes. Disable any feature whose scope is missing.
2. Read rooms and room classes. Match them to my own rooms by DailyStay room id, suggest
matches by name, and ask me before linking. List DailyStay rooms I have not matched.
3. Keep my own display name for a room if I set one. Do not write names back to DailyStay.
4. Mirror prices, the calendar and bookings from DailyStay. Import past bookings once, then
follow webhooks and re-read the booking on each one.
5. Send DailyStay what only I have: photos (public https URLs I host), closed dates, and
reservations my site took.
Property id: 1042
My stack: <describe it>
<paste the DailyStay API documentation here>
Let an AI assistant work inside DailyStay
To have Claude or ChatGPT look up bookings and answer questions about the property directly, add https://new.yourdailystay.com/mcp as a custom connector. The assistant signs in through OAuth and refreshes its own tokens, so no API key is involved. Max plan.