Skip to main content

Stay API

The stay API is how a property management system (PMS) tells Centro System Manager for Crestron who is staying where. Your middleware sits between the PMS and the processor. It calls an HTTPS API on the processor with an API key to send reservations, check-ins and check-outs, and it asks the processor what has changed so that it can tell the PMS.

This page is for the developer who writes that middleware and for the engineer who sets the processor up. It needs no other page to follow.

What it is for​

Each location on the processor can have a stay: a state, the person's name and the arrival and departure dates. The processor shows the stay to staff, passes it to the User App and to the location's Crestron system through its connection, and runs the site's stay rules, such as Dirty at check-out. The stay API is how a PMS's stays reach the processor on site.

  • You call the processor. The processor never calls you. It sends no webhooks: you poll for changes.
  • Every call can be repeated. The PMS's own stay id is the key. Sending a call a second time changes nothing.
  • The PMS stays the authority for its stays. The processor holds the stay each location has now and the stays expected next. It isn't a PMS: it keeps no history, rates or bills.
  • A site can have more than one source. An Administrator ticks where the site's stays come from: Staff (entered by hand in the admin app), Stay API (your middleware) and Centro cloud (the PMS Centro cloud is linked to, set up by LCD). Each stay belongs to the source that sent it. See Who holds a location.

Before you start​

The engineer who sets the processor up does these five things in the admin app. All five need an Administrator.

1. Check the licences​

The processor needs the API Licence and an active Manager Licence. A Manager Licence on trial counts; one that's read-only doesn't. Stays have no licence of their own: the stay API is part of the processor's API, sold as the API Licence.

Settings → Settings → Licence shows both under What this processor has. Without them, every call is refused with 423, reads included.

2. Switch on Stays​

  1. Open Settings → Settings → General.
  2. Turn on Stays. It saves at once.

Stays are off on a new processor, and on one upgraded from an earlier program. Until they're on, every call is refused with 423 and the code stays_off, and Settings → Settings → Stays isn't in the menu.

3. Tick Stay API as a source​

  1. Open Settings → Settings → Stays.
  2. Under Where stays come from, tick Stay API. It saves at once.

Nothing is ticked on a new processor. Until Stay API is ticked, every call is refused with 409 and the reason sourceOff.

Stay API can be ticked before the API Licence is added. Until it is, a line under it reads Needs the API Licence.

Once it's ticked, the section under it shows the address to call (see The address and HTTPS) and how many keys may send stays. No API key may check in and check out yet: the middleware cannot send stays. means none can.

4. Create an API key​

  1. Open Settings → Access → API keys, or select Manage API keys under Stay API.
  2. Select Create API key.
  3. Enter a Name, such as the name of the system that will call. Under For, choose API and MCP.
  4. Under What the key may do, choose Read and control. It holds Read stays and Check in and check out. Or open Choose one by one and tick the permissions the middleware needs.
  5. If the key should stop working on a date, tick This key expires and set Expires at.
  6. Select Create API key. The key is shown once, under Your new API key. Copy it and give it to the developer, then select Done.

What each permission allows:

PermissionAllows
Read staysGET api/stays, GET api/stays/{stayId} and the change feed. Read only holds it.
Check in and check outEvery call that writes: reserve or change, check in, check out, cancel and replace all. Read and control holds it, with Read stays.

A key without the permission a call needs is refused with 403.

Things to know about keys:

  • A key starts with cbk_. The processor keeps no copy that can be read, so it can't show the key again. If a key is lost, select Regenerate on its row: a new key replaces it, with the same name, permissions and expiry, and the old one stops working at once. Or create another.
  • A key reaches the processor's API as its permissions allow — locations, location control and schedules too, if it holds those permissions — and never the processor's settings, the stay settings or the Stays switch.
  • A processor holds up to 10 live keys. Revoked and expired keys don't count. Create one key for each system that calls, so one can be revoked without stopping the others.
  • A revoked key is refused from its next call. An expired key is refused too; Edit can give it a later expiry.
  • Last heard from on the key's row shows when the processor last accepted it. It's updated every few minutes.

5. Give each location a Reference​

The stay API names a location by its Reference, the number or short code set on Settings → System → Locations. Set each location's Reference to the PMS's own number for it, and give no two locations the same one. A location with no Reference can't be addressed.

The address and HTTPS​

The address to call is shown under the ticked Stay API source on Settings → Settings → Stays, as Address for the PMS. It looks like https://processor.example:8443/api/stays. Other addresses lists the rest when the processor has more than one. Host names come first, and an address only the processor itself can reach is never listed.

The addresses are built from the names on the certificate the processor presents, so a client that trusts the certificate can check any of them. Use one of the addresses listed. An address that isn't on the certificate fails the client's certificate check.

  • HTTPS only, on the admin port. The default port is 8443.
  • The plain HTTP port doesn't serve the stay API. It answers 400 and doesn't redirect, so a wrong address fails at once. A key sent to the plain HTTP port gets "Use HTTPS. Keys are not taken over plain HTTP." A key sent to the User App port gets "Use the admin HTTPS port. Keys are not taken on this port."
  • Your HTTP client must trust the processor's certificate. By default the processor makes its own certificate. Download it from Settings → Deployment → Downloads in the admin app (Processor certificate), or from http://processor.example:8080/box-ca.crt, and give the file (centro-box-ca.crt) to your HTTP client as a trusted certificate authority. If the site uses its organisation's certificate instead, a client that already trusts that certificate authority needs nothing more.
warning

Don't switch certificate checks off in your client. The key travels in every call.

The processor also serves an interactive reference for its API at https://processor.example:8443/docs/public. Calls made from it are real: they change the site.

A first call​

Send the key in the X-Api-Key header on every call. Send bodies as JSON with Content-Type: application/json.

The examples on this page use these two values:

BASE="https://processor.example:8443"
KEY="cbk_EXAMPLE-not-a-real-key"

List the stays the processor holds from the stay API:

curl --cacert centro-box-ca.crt -H "X-Api-Key: $KEY" "$BASE/api/stays"

On a processor that has been sent nothing yet, the answer is 200 with an empty list:

{ "stays": [] }

If you get anything else, see Troubleshooting.

The life of one stay​

This follows one stay, PMS-48213, in the location whose Reference is 204, from reservation to check-out.

Expected arrival​

Send the reservation with PUT api/stays/{stayId}:

curl --cacert centro-box-ca.crt -X PUT "$BASE/api/stays/PMS-48213" \
-H "X-Api-Key: $KEY" -H "Content-Type: application/json" \
-d '{ "locationReference": "204", "guestName": "Ada Lovelace", "arrival": "2026-11-02", "departure": "2026-11-05" }'

The answer is 200 with the stay:

{
"stayId": "PMS-48213",
"locationReference": "204",
"locationName": "Suite 204",
"state": "Expected",
"guestName": "Ada Lovelace",
"arrival": "2026-11-02",
"departure": "2026-11-05",
"checkedInUtc": null,
"endedUtc": null,
"endReason": null,
"updatedUtc": "2026-10-28T09:14:03.512Z",
"source": "Api"
}

In the admin app: before the arrival day, the location shows Booked. On the arrival day it shows Pre check-in, with the name and the dates, and From the PMS in its stay card.

Check-in​

A stay that's already reserved needs no body:

curl --cacert centro-box-ca.crt -X POST "$BASE/api/stays/PMS-48213/check-in" -H "X-Api-Key: $KEY"

The answer is 200 with the stay. Its state is now InUse and checkedInUtc is set:

{
"stayId": "PMS-48213",
"locationReference": "204",
"locationName": "Suite 204",
"state": "InUse",
"guestName": "Ada Lovelace",
"arrival": "2026-11-02",
"departure": "2026-11-05",
"checkedInUtc": "2026-11-02T14:02:11.482Z",
"endedUtc": null,
"endReason": null,
"updatedUtc": "2026-11-02T14:02:11.482Z",
"source": "Api"
}

In the admin app: the location shows Checked in. The stay is passed on to the location's Crestron system, and to the User App where the site uses it.

To check in a person with no reservation, send the stay with the check-in. See Check in.

Change​

To change the name, the dates or the location, send the stay again with PUT. This call moves the departure to 6 November:

curl --cacert centro-box-ca.crt -X PUT "$BASE/api/stays/PMS-48213" \
-H "X-Api-Key: $KEY" -H "Content-Type: application/json" \
-d '{ "locationReference": "204", "arrival": "2026-11-02", "departure": "2026-11-06" }'

The answer is 200 with the stay, which is still InUse and now has "departure": "2026-11-06". guestName was left out, so the stay keeps the name it has.

A PUT with another locationReference moves the stay to that location. The stay keeps its id.

In the admin app: the location shows the new date. After a move, the location the person left shows No current stay and the new one shows Checked in.

Check-out​

curl --cacert centro-box-ca.crt -X POST "$BASE/api/stays/PMS-48213/check-out" -H "X-Api-Key: $KEY"

The answer is 200 with the stay. Its state is Ended:

{
"stayId": "PMS-48213",
"locationReference": "204",
"locationName": "Suite 204",
"state": "Ended",
"guestName": "Ada Lovelace",
"arrival": "2026-11-02",
"departure": "2026-11-06",
"checkedInUtc": "2026-11-02T14:02:11.482Z",
"endedUtc": "2026-11-06T10:41:27.093Z",
"endReason": "CheckedOut",
"updatedUtc": "2026-11-06T10:41:27.093Z",
"source": "Api"
}

In the admin app: the location shows No current stay. The site's check-out rules run, each if it is switched on under Settings → Settings → Stays: Dirty at check-out marks the location Dirty, Clear requests at check-out clears do not disturb and make up room, and End sessions at check-out signs the user out of the User App. Touch screens stay signed in.

A stay id is used for one stay. Once a stay has been checked out, its id can't be used for a new stay.

Reference​

Every route is under /api/stays on the address above, and every call carries the X-Api-Key header.

Method and pathWhat it doesPermission
PUT api/stays/{stayId}Reserve, or changeCheck in and check out
POST api/stays/{stayId}/check-inCheck inCheck in and check out
POST api/stays/{stayId}/check-outCheck outCheck in and check out
DELETE api/stays/{stayId}Cancel a reservationCheck in and check out
GET api/stays/{stayId}Read one stayRead stays
GET api/staysList your staysRead stays
PUT api/staysReplace allCheck in and check out
GET api/stays/changesThe change feedRead stays

The stay object​

Every route that answers a stay answers this object.

FieldTypeMeaning
stayIdstringThe PMS's own id for the stay. null in the change feed for a stay that staff entered by hand.
locationReferencestring or nullThe location's Reference. null when the location has none.
locationNamestringThe location's name on the processor.
statestringThe stay's own state: Expected (reserved, not checked in), InUse (checked in) or Ended.
guestNamestring or nullThe person's name, as one string, as you sent it.
arrival, departurestringThe site's local dates, as yyyy-MM-dd.
checkedInUtcstring or nullWhen the stay was checked in. UTC, ending in Z.
endedUtcstring or nullWhen the stay ended. UTC, ending in Z.
endReasonstring or nullWhy the stay ended: CheckedOut, Canceled, NoShow, Overridden or, for a stay entered by hand, Superseded. See Who holds a location.
updatedUtcstringWhen the stay last changed. UTC, ending in Z.
sourcestringApi for your stays. The change feed also carries stays entered by hand, with Manual.

Rules for stayId:

  • 1 to 64 characters, with no slash.
  • Compared exactly. Upper and lower case are different.
  • Don't use the id changes. GET api/stays/changes is the change feed.

Names are always included for a key. The settings that hide names from the Manager role and from the User App don't apply to the stay API.

States​

A stay has its own state, in the stay object's state. A location has a state too, which follows its stays and the clock. The change feed sends the location's state, and the admin app shows it in these words:

Location stateShown asWhen
NotInUseNo current stay, or Booked when a stay is expected on a later dateNo stay today.
ArrivingPre check-inA stay is expected today and isn't checked in.
InUseChecked inA stay is checked in.
DepartingPre check-outA stay is checked in, its departure date is today and the site's morning time has passed.

A stay that is checked in past its departure date stays InUse. The admin app marks it Overstay.

Naming the location​

locationReference is the location's Reference. It's trimmed and matched without regard to case.

CaseAnswer
Missing or blank, where it is needed422, reason blankReference.
No location has it422, reason unknownReference. The sentence names the Reference.
Two or more locations have it409, reason duplicateReference. The sentence names the locations. Nothing is written.

It's needed on every PUT api/stays/{stayId}, on every stay in a replace all, and on a check-in of a stay the processor doesn't have yet. On a check-in of a known stay it may be left out, and an empty or blank one is treated as left out: the stay keeps its location.

The stay API acts on the site's real locations, whether or not demo mode is on.

Reserve, or change​

PUT api/stays/{stayId}

Body fieldTypeRequiredMeaning
locationReferencestringYesThe location's Reference.
guestNamestringNoThe person's name, up to 200 characters. Leave it out to keep the name the stay has. An empty string is refused.
arrivalstringYesyyyy-MM-dd.
departurestringYesyyyy-MM-dd. Not before the arrival. The same day is allowed.

What it does:

  • A new id makes a reservation.
  • A known stay takes the name, the dates and the location sent. Another location is a move.
  • A stay that was cancelled is opened again.
  • A stay that was released as a no-show is opened again when the dates are new. Sent again with the dates it was released for, it changes nothing and answers the ended stay.
  • Dates in the past are accepted, so a late message isn't refused.

Answers: 200 with the stay. 422 for a body or id that can't be used. 409 with the reason ended, occupied, duplicateReference, heldByOtherSource or overlapsBooking.

A checked-in stay that moves keeps its id. The location it left gets the site's check-out rules, and the new one is in use. A move of a checked-in stay to a location where another of your stays is checked in is refused with occupied.

A reservation can be put on a location that somebody else is in, as long as its dates don't overlap the current stay there. See Who holds a location.

Check in​

POST api/stays/{stayId}/check-in

Body fieldTypeRequiredMeaning
locationReferencestringOnly for a new idThe location's Reference. For a reserved stay, another location is a move.
guestNamestringNoAs for a reservation.
arrivalstringNoyyyy-MM-dd. For a new id, today's site date unless sent.
departurestringOnly for a new idyyyy-MM-dd.

What it does:

  • A reserved stay is checked in and takes any value sent. The body can be left out.
  • A new id makes the stay and checks it in. locationReference and departure are then needed.
  • A stay that was cancelled or released as a no-show is opened again and checked in.
  • A stay that is already checked in takes any value sent that differs. With nothing different, the call changes nothing.

Answers: 200 with the stay. 422 for a body or id that can't be used; a new id with no locationReference has the reason blankReference. 409 with the reason occupied, ended, duplicateReference, heldByOtherSource or overlapsBooking.

Two of your stays can't be checked in to one location. Check the other out first.

Check out​

POST api/stays/{stayId}/check-out, with no body. Ends the stay and runs the site's check-out rules.

Answers: 200 with the stay. 404 with the reason unknownStay. 409 with the reason notCheckedIn for a stay that is only reserved, or that was cancelled: cancel a reservation instead.

Cancel a reservation​

DELETE api/stays/{stayId}, with no body.

Answers: 204 with no body. 404 with the reason unknownStay. 409 with the reason checkedIn for a stay that is checked in, and ended for one that was checked out.

Read one stay​

GET api/stays/{stayId} answers 200 with the stay, ended or not, or 404 with the reason unknownStay.

On GET, check-out and DELETE, an id that isn't 1 to 64 characters is also answered 404 unknownStay. On PUT and check-in it's 422 invalid.

List your stays​

GET api/stays answers 200 with the stays the stay API has sent that have not ended, earliest arrival first:

{ "stays": [ { "stayId": "PMS-48213", "state": "InUse", "source": "Api" } ] }

Each item is a whole stay object. The example is cut short.

Replace all​

PUT api/stays sends everything the PMS holds for today and ahead. The processor applies the list and ends every stay of yours that isn't in it. Use it on the first run, and after an outage on your side.

{
"stays": [
{ "stayId": "PMS-48213", "locationReference": "204", "guestName": "Ada Lovelace",
"arrival": "2026-11-02", "departure": "2026-11-06", "checkedIn": true },
{ "stayId": "PMS-48377", "locationReference": "310", "guestName": "Grace Hopper",
"arrival": "2026-11-03", "departure": "2026-11-04", "checkedIn": false }
],
"confirmEmpty": false
}
Body fieldTypeRequiredMeaning
stayslistYesAt most 2,000 stays.
stays[].stayIdstringYesThe PMS's id for the stay.
stays[].locationReferencestringYesThe location's Reference.
stays[].guestNamestringNoAs for a reservation.
stays[].arrival, stays[].departurestringYesyyyy-MM-dd.
stays[].checkedInbooleanNotrue when the person is in the location. Left out, it is false.
confirmEmptybooleanNoMust be true to send an empty list.

The answer is 200 with a summary:

{ "created": 1, "changed": 0, "checkedIn": 0, "checkedOut": 0, "cancelled": 0, "unchanged": 1, "rejected": [] }
  • Each stay in the list is applied as a reservation, or as a check-in when checkedIn is true.
  • Each stay is counted exactly once as created, changed or unchanged, or is listed in rejected, so those four add up to the length of the list. checkedIn also counts the stays this call checked in.
  • A stay that fails is skipped and listed in rejected with its stayId, a reason and a message, and with heldBy, or bookingSource and bookingArrival, where the reason has them. The others are still applied. Read rejected on every call.
  • Every stay of yours that isn't in the list is ended. A stay that was checked in is checked out, and the site's check-out rules run. A stay that was only reserved is cancelled. They are counted in checkedOut and cancelled.
  • A stay listed in rejected isn't taken for one that left: it stays on the processor as it was.
  • It only touches stays the stay API sent. A stay entered by hand isn't ended because it is missing from the list.
  • Sending the same list again changes nothing and counts every stay under unchanged.

The whole call is refused with 422, and nothing is changed, when:

  • stays is missing;
  • the list holds more than 2,000 stays;
  • a stay id is in the list twice;
  • the list is empty and confirmEmpty isn't true. An empty list checks everybody out and cancels every reservation.
warning

The list must be complete. A stay left out by mistake is checked out or cancelled.

Errors​

A refusal with a body has this shape. reason is for your code. errorDetails.title is one plain sentence for a person.

{
"success": false,
"errorDetails": { "title": "No location has the Reference 999.", "messages": [] },
"reason": "unknownReference"
}

A 409 heldByOtherSource also carries heldBy. A 409 overlapsBooking carries bookingSource and bookingArrival. A 423 has feature or code in place of reason. A 401 and a 403 have no body.

StatusreasonMeaningWhat to do
400noneThe call went to the plain HTTP port, or a key went to the User App port.Use the HTTPS address.
401noneNo key, a key the processor doesn't know, or an expired or revoked key. No body.Check the header name and the key.
403noneThe key doesn't hold the permission the route needs: Read stays for a read, Check in and check out for a write. No body.An Administrator edits the key's permissions.
404unknownStayNo stay has this id.
405noneThe route doesn't take this method, HEAD included. No body; the Allow header lists the methods it takes.
409sourceOffStay API isn't ticked under Where stays come from.An Administrator ticks it.
409heldByOtherSourceAnother source's stay holds the location until it is checked out. heldBy says which. Nothing was written.Send it again once the location is free.
409overlapsBookingThe new departure of a checked-in stay would run into another source's booking. bookingSource and bookingArrival say which. Nothing was written.Agree the dates with whoever entered the booking.
409duplicateReferenceTwo or more locations share the Reference.Give each location its own Reference.
409occupiedAnother of your stays is checked in to the location.Check that one out first.
409endedThe stay was checked out, by you or by staff.Use a new stay id for a new stay.
409checkedInThe stay is checked in, so it can't be cancelled.Check it out.
409notCheckedInThe stay isn't checked in, so it can't be checked out.Cancel it.
410cursorExpiredThe cursor can't be carried on from.Make a full read of the change feed.
415noneA body was sent that isn't marked Content-Type: application/json. The body of the answer is the web server's own, with no reason.Send the header.
422blankReferenceThe location's Reference is missing.
422unknownReferenceNo location has the Reference.Check the Reference on the location.
422invalidThe id or the body isn't what the route takes, or the request can't be read at all: a body that isn't JSON, a value of the wrong type, or limit=abc. The sentence says what.
423noneA licence is missing or Stays are off. The body has feature api (no API Licence, or no Manager or User App Licence beside it), feature rooms (no active Manager Licence) or code stays_off (Stays is off in Settings → Settings → General).See Before you start.
429rateLimitedToo many calls.Wait for the number of seconds in the Retry-After header.

The processor checks a call in this order: the key (401), the API Licence (423 api), the rate limit (429), the key's permission (403), the Manager Licence (423 rooms), the Stays switch (423 stays_off), the Stay API source (409 sourceOff), then the route's own rules.

Rate limit​

A key can make 300 calls a minute. The count covers every call the key makes to the processor's API, not only stay calls. Each key has its own count, so one system that runs away doesn't shut out another.

An address that sends more than 10 calls a minute with keys the processor doesn't know is refused with 429 until the minute has passed.

Keeping in step​

Repeating calls​

Every call can be sent again. A check-in of a checked-in stay with the same values, a check-out of a checked-out stay, a cancel of a cancelled stay and a PUT with the values the stay has all answer as the first call did, 200 or 204, with the stay as it stands. Nothing is written and nothing new appears in the change feed.

When a call times out, or you don't know whether it arrived, send it again.

After an outage​

When your middleware has been away and no longer knows what it sent:

  1. Send replace all with everything the PMS holds.
  2. Carry on polling the change feed with the cursor you had. If the answer is 410, make a full read and carry on from there.

The change feed​

GET api/stays/changes?since={cursor}&limit={n} answers what has changed on the processor since your last call.

  1. Call it once with no since. The answer is a full read: how every location stands now, and a cursor.
  2. Call it again with since set to the cursor you were last given. Pass the cursor back exactly as you received it. Don't read meaning into it.
  3. Keep the new cursor from each answer. When more is true, call again at once.
{
"cursor": "k3v9x2ab.4812",
"more": false,
"changes": [
{
"seq": 4811,
"occurredUtc": "2026-11-02T14:02:11.513Z",
"kind": "Stay",
"locationReference": "204",
"locationName": "Suite 204",
"value": "InUse",
"stay": { "stayId": "PMS-48213", "state": "InUse", "source": "Api" }
},
{
"seq": 4812,
"occurredUtc": "2026-11-02T14:05:40.120Z",
"kind": "Housekeeping",
"locationReference": "204",
"locationName": "Suite 204",
"value": "Dirty"
}
]
}

In the feed, stay is a whole stay object. The example is cut short.

FieldMeaning
cursorWhere to carry on from. Send it as since on your next call.
moretrue when there are more changes than this answer holds.
changes[].seqThe entry's number. null in a full read.
changes[].occurredUtcWhen it happened. UTC, ending in Z.
changes[].kindStay, Housekeeping or Request.
changes[].locationReference, changes[].locationNameThe location. A location with no Reference is still listed, with locationReference as null.
changes[].valueThe new value. See the next table.
changes[].stayFor Stay: the stay the entry is about, as it stood then. Left out when the location has no stay.
changes[].overridetrue when staff changed, checked in, checked out or cancelled one of your stays by hand. Left out otherwise.
changes[].tookOvertrue on your check-in when it ended a stay entered by hand. Left out otherwise.
kindvalueNotes
StayThe location's state: NotInUse, Arriving, InUse or DepartingSent when a stay changes and when the state moves with the clock: Arriving at the start of the arrival day, Departing at the site's morning time on the departure day. A move sends an entry for each of the two locations.
HousekeepingClean, Dirty or InProgressWhoever set it: the location's Crestron system, a member of staff or the processor's own rules.
RequestNone, DoNotDisturb or MakeUpRoom

Things to know:

  • limit is 200 unless you send it. The most is 1,000, and a value below 1 is taken as 1.
  • With nothing new, changes is empty and cursor is the one you sent.
  • If you always send the cursor you were last given, you miss nothing and get nothing twice. An older cursor gives the same entries again, which is safe.
  • A full read has one entry of each kind for every location. seq is null, because the entries are how things stand, not changes. value is null where the processor holds no value yet.
  • A change made during a full read is delivered again by your next call. It's never lost.
  • Stays entered by hand are in the feed too, with stayId as null and source as Manual. Ignore them if the PMS doesn't want them.
  • Entries are kept for 7 days, and at most 20,000 of them.

410 with the reason cursorExpired means the cursor can't be carried on from. That happens when Stay API was unticked and ticked again since the cursor was given, when the cursor is older than the oldest entry kept, when the value sent isn't a cursor, and when its number is above the highest the processor has given out. Make a full read, by calling with no since, and carry on from the cursor it gives.

How often to poll​

Poll the change feed every 10 to 30 seconds. At one call every 10 seconds, polling uses 6 of the 300 calls a key can make in a minute, which leaves the rest for the calls the PMS's own events cause. If the same key is also used for other calls to the processor's API, they share the 300.

Housekeeping and requests​

The change feed is how housekeeping and requests go back to the PMS. A Housekeeping entry carries the location's housekeeping status. A Request entry carries do not disturb and make up room, the two requests a user can make from the location.

Two settings under Settings → Settings → Stays, in the PMS section, switch them. Both are on to start with.

SettingWhen it is off
Send housekeeping to the PMSHousekeeping entries are left out of the change feed and of a full read.
Send requests to the PMSRequest entries are left out of the change feed and of a full read.

Entries left out while a setting was off may not reach a cursor that has moved past them. After a setting is switched back on, make a full read to be sure of the current values.

The stay API has no route that sets housekeeping or a request. They are set at the location, by staff or by the site's rules.

Who holds a location​

A site can take stays from more than one source: Staff, Stay API and Centro cloud, each ticked under Where stays come from. These rules decide which source may change a location.

Each stay belongs to its source​

Your calls only ever act on your own stays, found by your stay id. A stay entered by hand, or sent by another source, is never changed by your calls.

The current stay holds its location​

A location whose current stay belongs to another source is held by that source until the stay is checked out. The current stay is the one checked in or, when nobody is in, the one arriving today.

  • Only the current stay holds, and only for its own dates. A reservation of yours that arrives on or after the current stay's departure day is taken: that's the turnaround. One whose dates overlap the current stay's is refused.
  • An overstay holds until it is checked out. While the current stay is checked in past its departure date it has no end, so every reservation of yours there overlaps it.
  • A check-in there is refused whatever the dates.

While it holds, these are refused with 409 heldByOtherSource, and heldBy says who holds it: Manual for staff, Api, Cloud or Demo. That covers a check-in there; a new reservation, a move, a date change or a cancelled stay opened again there whose dates overlap the current stay's; and a stay of yours whose dates would end an arrival staff entered by hand. Nothing is written: a reservation whose new dates were refused keeps the dates it had. Send it again once the location is free — the replace all is a good way.

{ "success": false, "errorDetails": { "title": "This location has a stay entered by staff, which holds it until it is checked out, so this stay was not written. Nothing was changed.", "messages": [] }, "reason": "heldByOtherSource", "heldBy": "Manual" }

Staff see a warning on the location, A stay was refused: the location is held by another source, for 24 hours or until the stay that holds it ends.

A checked-in stay isn't extended into another source's booking​

When you move the departure of a checked-in stay of yours to a later date that runs into a booking from another source on the same location, the change is refused with 409 overlapsBooking. bookingSource and bookingArrival say whose booking and when it arrives. Nothing is written: the stay keeps the departure it had.

{ "success": false, "errorDetails": { "title": "This stay would run into a booking entered by staff arriving on 2026-11-20, so its new departure was not written. Nothing was changed.", "messages": [] }, "reason": "overlapsBooking", "bookingSource": "Manual", "bookingArrival": "2026-11-20" }

A departure on the booking's arrival day is the turnaround, and is taken. A change of the name, or an earlier departure, isn't checked. Staff are refused the same way when they extend a stay into one of your reservations.

Your stays hold their locations against staff​

Staff can't change, check in, check out or cancel your stays, or put a stay on a location over the dates of your current stay there. The admin app says The PMS holds this location. Change the stay in the PMS.

An Administrator can turn on Staff can change stays sent by the PMS under Settings → Settings → Stays, for when your middleware is down. It's off to start with. While it's on:

  • What staff do to your stay keeps it yours, and the feed has a Stay entry with "override": true.
  • A stay staff check out ends with endReason as Overridden, and the site's check-out rules run. Check the person out in the PMS: until you do, the two disagree.
  • For that stay id afterwards, check-out answers 200 with the ended stay, PUT and check-in answer 409 ended, and a replace all lists it in rejected with the reason ended. Nothing you send checks the person back in.

A source that isn't ticked holds nothing​

If Staff isn't ticked, stays entered by hand earlier are kept but don't hold their locations against you:

  • Your check-in on a location where one is checked in ends it, with endReason as Superseded. No check-out rule runs, because the person hasn't left. Your stay takes over its check-in time, the feed entry for your check-in carries "tookOver": true, and staff see the alert Stay taken over by the PMS for 24 hours.
  • Expected arrivals entered by hand whose dates overlap your stay end the same way.

If Stay API is unticked, your stays are kept and staff can edit them by hand. Your calls are refused with 409 sourceOff until it is ticked again. Then your stays are still there under their ids, and you don't have to send them again. Only your cursor has expired: make a full read of the change feed and carry on.

Names and the Manager role​

Your middleware always sends and receives names. Who sees a name on screen is the site's choice:

  • An Administrator always sees the name.
  • The Manager role sees stays and names only when Managers can see stays and Managers can see names are on.
  • The User App shows the name only when Show the name in the User App is on, and only on touch screens and to a user who scanned a Session code. It is never shown from a Fixed code.

Without the licences or the Stays switch​

Every route of the stay API answers 423, reads included, until the processor has the API Licence, an active Manager Licence and Stays switched on. Stored stays are kept.

A processor can be made ready first. Keys can be created without the API Licence ("A key made now is kept, but every call it makes is refused until a licence is added."), and Stay API can be ticked before the licence is added.

Troubleshooting​

The client refuses the certificate​

Your HTTP client doesn't trust the processor's certificate, or the address you call isn't one of the names on it. Give the client the processor's certificate, and use an address shown under Address for the PMS. See The address and HTTPS.

400 "Use HTTPS"​

The call went to the plain HTTP port. Use the https:// address shown under Address for the PMS. "Use the admin HTTPS port. Keys are not taken on this port." means the call went to the User App port.

401 on every call​

  • Check the header is X-Api-Key and the key was copied whole.
  • Check the key's State on Settings → Access → API keys. An Expired key needs a later expiry; a Revoked one needs replacing. Last heard from on the key's row shows when the processor last accepted it.

403​

The key doesn't hold the permission the call needs. A Read only key can read stays and the change feed, but can't reserve, check in, check out, cancel or replace all. An Administrator edits the key and chooses Read and control, or ticks Check in and check out.

423​

A licence is missing or Stays are off. Read the body: feature api is the API Licence (or a Manager or User App Licence beside it), feature rooms is the Manager Licence, and code stays_off is the Stays switch. See Before you start.

409 with the reason sourceOff​

Stay API isn't ticked. An Administrator ticks it under Settings → Settings → Stays → Where stays come from.

409 with the reason heldByOtherSource​

Another source's current stay holds the location. Read heldBy. Agree with the site who should hold the location, then send the stay again once it's free. See The current stay holds its location.

422 unknownReference, or 409 duplicateReference​

The Reference you sent doesn't match one location. An Administrator checks the Reference of each location on Settings → System → Locations: every location the PMS addresses needs one, and no two can share one.

410 with the reason cursorExpired​

Call GET api/stays/changes with no since, then carry on from the cursor it gives. See The change feed.

429​

The key has made more than 300 calls in a minute, across all of the processor's API. Wait for the number of seconds in the Retry-After header. If it keeps happening, poll less often, or give the other system its own key.

Stays ended after a replace all​

A replace all ends every stay of yours that isn't in the list. Send the complete list, and read rejected in the answer: a stay that was refused isn't taken for one that left, and is still on the processor as it was.

409 with the reason ended on a stay the PMS still has checked in​

Staff checked the person out on the processor while Staff can change stays sent by the PMS was on. Check the person out in the PMS. To check them in again, use a new stay id.