Skip to content

Guest identity, duplicates and consents

Guests are workspace identities. Contact details are not unique: people may share a name, e-mail address or phone number. The Guests screen supports search, profile editing, stay history, duplicate review, consent evidence and owning-property reassignment. ADR 0045 records the identity rules.

Open Guests and choose Duplicates, or open a guest profile’s duplicate panel. Each candidate explains its matching e-mail or phone, with matching names as additional context. A name alone never proposes a pair. Stronger matches appear first. Review both profiles before deciding. “Not a duplicate” remembers the pair and writes an audit record; reload does not propose that pair again. “Show dismissed” includes remembered pairs and offers an audited undo. Repeating the current dismissal state creates no extra audit entry.

Contacts shared by more than five live profiles appear once as a shared-contact group. Open the group to review its paginated members. They are not expanded into every possible pair. Normalized keys are stored and indexed; the profile panel uses equality lookups.

The core does not collect birth dates, document numbers or country-specific legal identification for this workflow. A matching contact is evidence to review, not proof that two people are the same.

An administrator with guests.merge opens the comparison, chooses which guest survives, and selects the source of each contact field. Confirming checks the versions of both guests. The comparison labels the current Survives and Absorbed identities, shows their owning properties and reservation counts, and offers each field’s actual values. Review the latest consent decision for every purpose on each identity, including any withdrawal warning, and the final summary before confirming. A conflict requires reloading and reviewing the new values before trying again.

The survivor receives the chosen details and the live reservation references. Folios, Channex booking links and message threads remain attached to their reservations, so those reservations’ guest identity now resolves to the survivor. Immutable snapshots and historical records are preserved. The absorbed guest remains readable with a pointer to the final visible survivor and retains its own read-only consent history. Its former URL does not become a 404. The survivor’s “Merged from” links open absorbed profiles and their history. Lists and search exclude absorbed profiles by default; API clients can request include_merged=true when they need historical identities.

Each moved reservation receives a new version and its ordinary update audit/event in the same transaction. Clients holding its old ETag must refetch before changing it.

A manually merged survivor is operator-owned and remains editable even when it has OTA reservations. Later OTA revisions preserve the chosen identity and update booking facts. Their latest contact snapshot is stored separately on the reservation. When details differ, the profile shows the channel, revision time and differing fields. “Apply” uses the ordinary audited guest edit with the current version; it never automatically replaces the identity.

The current reservation model has one booker guest identity, and a split folio may name a guest as its payer. Occupant counts do not create separate guest identity links; registration of individual occupants remains a separate product capability. The merge covers every live guest reference in the current model: it moves folio payers to the survivor too, and a merged identity cannot be chosen as a payer again.

Reservation creates and guest changes may use an absorbed guest ID when the entire pointer chain is visible to the caller. They resolve to the live identity and return that guest in the successful response. A hidden or missing intermediate profile returns guest.not_found before the reservation changes; an old alias cannot widen property access.

Merge never crosses a workspace or organization. Property-restricted members cannot merge because an identity may be linked to bookings at properties hidden from them.

The profile offers marketing e-mail, marketing phone and marketing SMS purposes. A member with guests.consents can record granted or withdrawn, with a source and an optional note. Current decisions are shown per purpose independently of history pages. History is newest first and shows the actor’s display name, source, note and time in the user’s time zone. A later entry does not replace or delete earlier evidence. Server time is authoritative.

Consents remain attached to the identity that provided them. Merging never copies or combines them. A withdrawal on an absorbed identity remains in that identity’s history; it does not silently change the survivor’s independent history. An absorbed profile accepts no writes: consent, dismissal, undo, merge, edits and ownership changes answer 409 guest.absorbed with a final survivor pointer. Open that profile before recording a new decision. Its tombstone page has no write actions or booking call to action. This panel records operator evidence and does not authorize a marketing delivery or apply national rules.

An unrestricted administrator with guests.reassign can change a profile’s owning property to another property in the same workspace. The operation checks the guest version, saves the new owner and records audit/event history atomically. This can change visibility for property-restricted members. Legacy unowned guests are not automatically backfilled; an administrator may explicitly assign their owner.

guests.read reads profiles, duplicates and consent history. guests.manage edits contacts and dismisses candidates. The new grants appear in the custom-role editor: guests.merge and guests.reassign default to administrators; guests.consents defaults to administrators, front desk and revenue managers. All operations still require live workspace membership and their declared property scope.

Guest transitions require a strong guest If-Match. Merge also takes the absorbed guest’s version; dismissal checks both profile versions without changing them. Merge, consent recording and ownership reassignment return the resulting guest version. A stale version answers 412 guest.version_mismatch; clients must review current data before resubmitting. The API, OpenAPI and generated SDK expose the same workflow; use the guests operation module and @guestcraft/react/query/guests factories. Ordinary identity/reference writes take a shared workspace gate; merge alone takes it exclusively, so writes at different properties can proceed concurrently. Property locks precede that gate and row locks follow it.

All paths below are under /api/v1; the workspace is selected with GuestCraft-Workspace-Id.

Operation Permission Result
GET /guests?include_merged=true guests.read Include historical absorbed profiles (excluded by default)
GET /guests/duplicates guests.read Cursor page of workspace candidates and reasons
GET /guests/{id}/duplicates guests.read Cursor page restricted to that profile
PUT /guests/{id}/duplicate_dismissals/{other_id} guests.manage Remembered, audited pair dismissal; both versions checked
DELETE /guests/{id}/duplicate_dismissals/{other_id} guests.manage Audited undo; both versions checked
GET /guests/shared_contacts guests.read Paged shared-contact groups; profile subresource also available
GET /guests/shared_contact_guests guests.read Complete paged membership for a shared key
GET /guests/{id}/merges guests.read Paged visible absorbed ancestry
GET /guests/{id}/contact_notices guests.read Newest-first eligible OTA field snapshots per reservation
PUT /guests/{id}/contact_notice_dismissals/{reservation_id}/{field} guests.manage Keep the current field and remember the dismissed snapshot value; guest If-Match required
POST /guests/{id}/merge guests.merge, unrestricted properties Survivor, its new ETag, and preserved absorbed identity
GET /guests/{id}/consents guests.read Cursor page of the complete consent history
GET /guests/{id}/consents/current guests.read Latest decision for each purpose, independent of history paging
POST /guests/{id}/consents guests.consents New immutable evidence entry and updated guest ETag
PUT /guests/{id}/ownership guests.reassign Guest with corrected owner and new ETag

The existing guest read/edit and reservation-history operations remain available. Duplicate reads filter dismissals before pagination; include_dismissed=true also returns remembered pairs. Duplicate dismissal supplies other_version in its body; undo supplies it in the query. Both use the path guest’s strong If-Match. Same-identity or unrelated dismissal pairs answer 422 guest.pair_invalid. Merge supplies absorbed_id, absorbed_version, and a fields choice (survivor or absorbed) for every contact field (omitted choices retain the survivor). Ownership supplies origin_property_id. Consent supplies purpose, status, source and optional note. The OpenAPI operation descriptions define exact validation and problem responses.

Retention, erasure and planned capabilities

Section titled “Retention, erasure and planned capabilities”

Merge preserves data; it is not erasure. Retention periods, erasure, access exports and legal holds are described in guest data rights: an erasure covers the live profile and every profile merged into it, and an erased profile keeps its identifier and merge pointers. Legal identification and country compliance documents remain country-pack capabilities. Groups, online check-in, country compliance and marketing campaign delivery remain planned.

OTA notices say which channel sent the contact details and when. They contain only differing fields from snapshots later than that field’s last review, newest first. Apply and ordinary contact edits review only changed fields; notes edits do not clear unrelated notices. Keep current dismisses one exact value for one field, audited without putting contact values in the audit log. The same value remains suppressed on future revisions and other reservations; a different later value can still be reviewed. Dismissal sends the snapshot revision id and expected value; a changed snapshot returns 409 guest.contact_notice_changed for reload.