Documentation · HOS Events
HOS Events 0.1
Immutable, CloudEvents-compatible records of what happened in hospitality operations: one envelope profile, fifteen event types in five families, and the delivery rules every producer and consumer share.
Draft for review.
Names and fields may change before 0.1 is final. The specification text is published under CC BY 4.0; schemas are Apache-2.0 (see licensing). Every example is synthetic. Comments and counter-proposals are welcome through the technical contributor path.
Principles
Facts before intentions.
Immutable facts
An event records what happened. A correction, a released unit, a reverted check-in or a withdrawn maintenance window is a new event; nothing is edited in place.
Named for meaning
Types are domain.resource.past_tense, singular and vendor-neutral: housekeeping.task.completed, not a vendor's webhook name.
One dimension at a time
A status change states only the changed dimension, the previous value when known, the new value, the authority source and an optional reason.
Explicit snapshots
A snapshot is sent only when deltas cannot recover the state. It is declared in hosdatamode, allowed by the producer's manifest and sensitivity-classified.
Plans are not states
unit.maintenance_scheduled plans when a unit will be out of service or out of sale. The unit's statuses change only through unit.status_changed, when the window begins.
Honest time and actor
time is when the fact occurred. When the source only knows the entity's last modification, hostimebasis says modified; when it knows nothing, recorded. hosactor names who acted, pseudonymously, when the source knows.
Envelope profile
Every event is a CloudEvent with HOS context.
Draft. CloudEvents 1.0 in structured JSON mode with HOS extension attributes. Every HOS event, and every reference situation, uses this envelope. Attribute names follow CloudEvents rules: lower-case letters and digits, at most 20 characters, scalar values only.
| Attribute | Meaning |
|---|---|
| specversion | CloudEvents specification version. |
| id | Producer-scoped, immutable event identity. With source, it is the duplicate key: a redelivered event keeps its id. |
| source | The producer. Must equal the producer declared in its Event Producer manifest. |
| type | Vendor-neutral HOS event type, named domain.resource.past_tense in the singular. |
| time | When the fact occurred, when known. Otherwise the best time the producer has, qualified by hostimebasis. |
| datacontenttype | HOS 0.1 data is always JSON. |
| dataschemaoptional | Optional reference to the schema of data. |
| hosschemaversion | HOS version the event conforms to. |
| hosrecordedat | When the HOS implementation (producer or adapter) recorded the event. HOS always carries it. |
| hostimebasisoptional | What time represents. Absent means occurred. modified means the source's last modification of the entity, when it does not date the change itself: the fact occurred at or before time. recorded means the occurrence time is unknown and time is when the producer or adapter recorded the fact. |
| hostenant | Tenant: the security, policy and data-autonomy boundary. |
| hosproperty | Property the fact belongs to: the operating context. |
| hospropertytimezone | IANA time zone of the Property. Local times are derived from it, never assumed. |
| hosbusinessdateoptional | Local operating (business) date the fact belongs to, when it matters. It can differ from the calendar date of time. |
| hossubjects | Typed HOS entity references implicated by the fact, as space-separated type:id pairs, for example stay:stay_1042 unit:unit_204. A string because CloudEvents attributes cannot hold arrays. |
| hosdatamodeoptional | How to read data. Absent means delta. A snapshot must be declared here, be allowed for its type and be sensitivity-classified. |
| hossensitivityoptional | Sensitivity class of data. Required for snapshots. |
| hoscausationsourceoptional | Source of the event that caused this one, when known. |
| hoscausationidoptional | Id of the event that caused this one, when known. Used with hoscausationsource. |
| hosactoroptional | Who made the change in the producer, when it knows: user (a staff member), guest, system (the producer itself) or integration (another system through its API), with a pseudonymous, producer-scoped reference, for example user:staff_k3. Never a name or contact detail. |
| data | The normalized payload. Its shape is defined by type; vendor detail goes in data.extensions under an inverted domain namespace. |
{
"specversion": "1.0",
"id": "hk-002231",
"source": "urn:hos:housekeeping:demo",
"type": "unit.status_changed",
"time": "2026-07-30T06:40:00Z",
"datacontenttype": "application/json",
"hosschemaversion": "0.1",
"hosrecordedat": "2026-07-30T06:40:02Z",
"hostenant": "tenant_demo",
"hosproperty": "prop_demo",
"hospropertytimezone": "Europe/Paris",
"hosbusinessdate": "2026-07-30",
"hossubjects": "unit:unit_204",
"data": {
"unit_id": "unit_204",
"dimension": "housekeeping",
"previous": "inspected",
"current": "dirty",
"authority_source": "urn:hos:housekeeping:demo",
"reason": "departure"
}
}Event catalogue
Fifteen event types in five families.
Required data is listed per type; every data object also accepts namespaced extensions. Each type has a downloadable example.
| Type | Family · authority | Required data | Meaning | Example |
|---|---|---|---|---|
| reservation.created | ReservationReservation system (PMS) | reservation_id, status, planned_arrival_date, planned_departure_date | A reservation was created. | reservation.created.json |
| reservation.updated | ReservationReservation system (PMS) | reservation_id, changed_fields | A reservation changed. changed_fields lists what changed; only those fields are carried, with their new values. | reservation.updated.json |
| reservation.cancelled | ReservationReservation system (PMS) | reservation_id | A reservation was cancelled. | reservation.cancelled.json |
| stay.expected | StayPMS | stay_id, reservation_id, planned_arrival_at, planned_departure_at | A stay is expected: the operational visit that fulfils a reservation, one per booked unit. It is published as soon as the stay is committed, however early, and again whenever its planned times change; the latest occurrence wins. hosbusinessdate is the business date of the planned arrival. Unit assignment is a separate fact. | stay.expected.json |
| stay.checked_in | StayPMS | stay_id, unit_id | The guest checked in. time is the actual arrival. | stay.checked_in.json |
| stay.check_in_reverted | StayPMS | stay_id | A check-in was reverted, for example because it was recorded in error. The stay is expected again; time is when the check-in was reverted. | stay.check_in_reverted.json |
| stay.checked_out | StayPMS | stay_id, unit_id | The guest checked out. time is the actual departure. | stay.checked_out.json |
| stay.unit_assigned | StayPMS | stay_id, unit_id, previous_unit_id | A unit was assigned to a stay. Successive assignments, and the releases stay.unit_unassigned records, form the stay's assignment history. | stay.unit_assigned.json |
| stay.unit_unassigned | StayPMS | stay_id, unit_id | A unit was released from a stay, which has no unit until the next stay.unit_assigned. unit_id is the released unit; its entry in the assignment history ends at time. | stay.unit_unassigned.json |
| unit.status_changed | UnitDeclared per dimension (housekeeping system, PMS, maintenance) | unit_id, dimension, current, authority_source | One dimension of a unit changed. The event states only that dimension, the previous value when known, the new value, the authority source and an optional reason; it never alters the other dimensions. | unit.status_changed.json |
| unit.status_changed (snapshot) | UnitDeclared per dimension | unit_id, statuses, authority_source, reason | Snapshot mode, only with hosdatamode = snapshot and when the producer declares snapshots: the current value of each dimension the producer reports, sent when deltas cannot recover the state, for example after an outage. | unit.status_changed.snapshot.json |
| unit.maintenance_scheduled | UnitDeclared: maintenance system or PMS | maintenance_id, unit_id, starts_at, ends_at, statuses | A maintenance window was planned for a unit, or its plan changed. It is published again whenever the window changes; the latest occurrence for a maintenance_id wins. It does not change the unit's current statuses: unit.status_changed does, when the window begins. | unit.maintenance_scheduled.json |
| unit.maintenance_cancelled | UnitDeclared: maintenance system or PMS | maintenance_id, unit_id | A maintenance window was withdrawn. A window that ends as planned needs no event; one cut short is published again with its new end. | unit.maintenance_cancelled.json |
| housekeeping.task.created | HousekeepingHousekeeping system | task_id, task_type, priority | A housekeeping task was created for a unit or a stay. | housekeeping.task.created.json |
| housekeeping.task.completed | HousekeepingHousekeeping system | task_id, task_type | A housekeeping task was reported complete. Completing a task does not change a unit status by itself. | housekeeping.task.completed.json |
| guest.message.received | Guest communicationGuest messaging system | message_id, channel, sensitivity | An inbound guest message was received. Only metadata and extracted signals travel; the content stays in the messaging system. | guest.message.received.json |
Delivery, ordering and replay
Six rules every producer and consumer apply the same way.
01At-least-once delivery
The same event can arrive more than once. Consumers deduplicate on source + id.
02No global order
Ordering holds only when a producer declares it, per subject or in total. Otherwise consumers order by time, then source, then id; delivery order never decides.
03Declared capability
A consumer processes only what the producer's manifest declares for the property. Undeclared means denied.
04Source authority
Only the declared system of record changes a value. Other facts are recorded and shown as conflicts, never merged into a single truth.
05Replay
Producers declare replay support and retention. Replays are JSON Lines, one event per line; a certified Event Producer profile provides at least 30 days.
06Minimal data
Data objects are closed. People appear only as pseudonymous references, message content never enters HOS, and vendor detail lives in namespaced extensions.
Event Producer manifest
A system is not simply compatible: it declares exactly what it produces.
A public system publishes its manifest at /.well-known/hos/manifest.json; a private one may use a configured, authenticated URL.
What it declares
The events it emits for which properties, per dimension where relevant, whether it is the system of record for each, whether it may send snapshots, its delivery mechanisms and ordering, replay, retention and known limitations.
One authority
At most one producer is authoritative for a given property, event type and dimension. A PMS can mirror housekeeping status without being its authority.
Signed by its producer
A signature beside the manifest shows who declared these capabilities and that nobody changed them. It expires, and the producer's keys rotate without breaking it.
Signed manifests
A consumer can check who declared the capabilities, and that nobody changed them.
The manifest stays readable JSON. Its producer signs it with a key it publishes, and the signature travels beside it.
01Signed by its producer
A producer signs its manifest, so a consumer can check who declared its capabilities and that nobody has changed them since. A consumer verifies a manifest it retrieves before trusting any declaration in it. A manifest that fails counts as no manifest: nothing is declared, so nothing is processed. A manifest an operator installs by hand, as the conformance scenarios do, is trusted as configured.
02Three files
A public producer serves, on one HTTPS origin, its manifest at /.well-known/hos/manifest.json, the signature at /.well-known/hos/manifest.jws and its public keys at /.well-known/hos/jwks.json. A private producer gives the three through configured, authenticated URLs.
03Detached signature
manifest.jws is a JWS in compact serialization with a detached payload (RFC 7515, appendix F): the header and the signature, with nothing between the two dots. The payload is the manifest in its canonical form (RFC 8785), in UTF-8, so spacing and member order in manifest.json do not matter. The manifest itself carries no signature.
04Header
The protected header holds alg, Ed25519 (RFC 9864) or ES256; kid, the signing key's id in the producer's key set; iat, when the manifest was signed; and exp, when the signature expires, both in whole seconds since the epoch. Every other algorithm is refused, none and EdDSA included, and so are crit and b64.
05Keys
jwks.json is a JSON Web Key Set (RFC 7517) of public keys only: an OKP key on Ed25519 for Ed25519, an EC key on P-256 for ES256. Each kid is unique in the set. A key that states alg, use or key_ops states its algorithm, sig and verify.
06Verification
A consumer canonicalizes the manifest it received, finds the key named by kid, checks that the key fits alg, and verifies the signature. It then checks that iat has come and exp has not, allowing at most 60 seconds of clock skew.
07Rotation
A producer signs its manifest again before exp. To change keys, it adds the new key to its key set, signs with it, and keeps the old key until every manifest signed with the old key has expired. A compromised key is removed at once, and whatever it signed then fails verification.
{
"alg": "Ed25519",
"kid": "Rmvn6iiK5EPEKShAwybtwHfST8XHnptFp5kdveGjsfk",
"iat": 1790380800,
"exp": 1821916800
}A fictional producer, signed
What https://housekeeping.example would serve under /.well-known/hos/. The domain is reserved for examples; the producer is not a real system.
Test vectors in /spec/0.1/conformance/signing/ pair a manifest, its signature and a key set with the verdict a verifier must reach: valid, expired, not yet valid, signed with another key, changed after signing, unknown kid, alg none, or a payload that is not detached.
Reference situation · non-normative
What a consumer can derive: an arrival room-readiness risk.
Situations are not part of the event catalogue. The reference projection emits them with the same envelope so they can be replayed and audited.
arrival.room_readiness_at_risk
Raised when a stay is still expected and its assigned unit may not be ready when the guest comes: the guest is expected before the planned check-in and the unit is not ready, a maintenance window blocks the unit at that time, or another guest in house in the unit is not due to leave before then. A unit is not ready when its authoritative housekeeping status is not clean or inspected (in the scenarios), a window blocks it, or another guest still holds it. It carries the evidence, the conflicts, the latest task, and the window or the stay that holds the unit.
arrival.room_readiness_resolved
Emitted when the risk stops holding, with the reason: the stay was given another unit, the unit is ready, the window went, the guest in house left or is now due to leave first, the arrival is no longer early, the reservation is inactive or the stay has started. Situations are emitted on transitions only, and HOS 0.1 never acts on them.
Conformance corpus
Prove an implementation against three scenarios and 42 synthetic deliveries.
Each scenario is a property with its own producers and manifests. An implementation conforms when it reproduces every scenario's expected.json.
13 deliveries · Europe/Paris
Early arrival, unit not ready
- Nominal early-arrival, dirty-unit scenario
- Duplicate event (source + id deduplication)
- Out-of-order event
- Conflicting source facts
- Snapshot recovery with explicit classification
- Missing capability (undeclared means denied)
/spec/0.1/conformance/arrival-readiness/
14 deliveries · Europe/Lisbon
Assigned unit out of order
- Maintenance window blocking an assigned arrival
- A plan is not a state: the window and the maintenance status travel separately
- Mirrored maintenance plan from a non-authoritative producer
- Latest plan wins: an older revision delivered late is superseded
- Reassignment resolves the risk
- Causation across producers
/spec/0.1/conformance/room-out-of-order/
15 deliveries · America/Toronto
Late check-out on a same-day turnover
- A unit held by another guest in house is not ready
- Late check-out overlapping a same-day arrival
- Occupancy observed by a non-authoritative producer
- Latest plan wins: an older stay plan replayed late is superseded
- Delivery order differs from occurrence order
- Check-out resolves the risk
/spec/0.1/conformance/late-checkout/