Salta ai contenuti

Channex guest messaging

Questi contenuti non sono ancora disponibili nella tua lingua.

GuestCraft exposes encrypted guest conversations through workspace and property inboxes. Channex charges extra for the Messages application, per property and by property type; GuestCraft cannot show or cover that price. Activation therefore needs an explicit acknowledgement, and the operator should check the price in the Channex account first.

Operation Resource Grant
Messages state of every mapped property GET /channex/messages messages.read
Messages state of one property (ETag: its revision) GET /channex/mappings/{property_id}/messages messages.read
Activate ({active: true, acknowledge_cost: true}) or deactivate ({active: false}), If-Match PUT /channex/mappings/{property_id}/messages messages.configure; shared-account activation also requires channex.manage_account_mode
Confirm that an unconfirmed installation is GuestCraft’s ({installation_id}, If-Match, audited) POST /channex/mappings/{property_id}/messages/install_attempt/confirm messages.configure; on a shared account also channex.manage_account_mode
Leave an unconfirmed installation alone ({installation_id}, If-Match, audited) POST /channex/mappings/{property_id}/messages/install_attempt/forget messages.configure
Request a synchronization (202) POST /channex/messages/synchronizations messages.configure
List threads: property_id, filter=all|to_answer|unread|requests, limit, cursor GET /message_threads messages.read
Unread and to-answer counts GET /message_threads/summary messages.read
Thread with its latest 1,000 messages, outbox and requests (ETag) GET /message_threads/{thread_id} messages.read
Older messages, page by page: limit, cursor GET /message_threads/{thread_id}/messages messages.read
Move the shared read marker, If-Match PUT /message_threads/{thread_id}/read_state messages.read
Composer variables for a language GET /message_threads/{thread_id}/variables messages.send
Queue a reply (202, Location, optional If-Match) POST /message_threads/{thread_id}/messages messages.send
Queue a file (202, Location) POST /message_threads/{thread_id}/attachments messages.send
Delivery state of a queued item (ETag) GET /message_threads/{thread_id}/outbox/{outbox_id} messages.read
Send a definite failure again POST …/outbox/{outbox_id}/retry messages.send
Dismiss a failed or uncertain reply (audited) POST …/outbox/{outbox_id}/dismiss messages.send
Booking.com “no reply needed” POST /message_threads/{thread_id}/no_reply_needed messages.send
Requests, with or without a thread GET /message_requests messages.read
One request (ETag) GET /message_requests/{request_id} messages.read
Decide a request (If-Match) POST /message_requests/{request_id}/decision messages.send
Workspace retention settings (ETag) GET /message_retention, PUT /message_retention messages.read / messages.configure
Erase one guest’s message content DELETE /message_guests/{guest_id}/content messages.erase
Templates of a property, optional language GET /properties/{property_id}/message_templates messages.read
Create (201, Location), read, replace and delete with If-Match …/message_templates, …/message_templates/{template_id} messages.send (writes)

Thread, request and template grants follow their property scope; PostgreSQL row security is a second boundary. Retention updates and guest erasure are workspace-wide and require an unrestricted principal; a property-scoped member is refused. Erasure is destructive browser administration and is never grantable to API keys (ADR 0041). Of the built-in roles, admin has all Messages grants and front_desk has read and send; the others have none. Custom roles can hold any subset within the grantor’s ceiling. A role without properties.read sees the inbox without property names; reservation details still need reservations.read.

Booking and change requests can be accepted or declined from GuestCraft before their deadline. Inquiries offer Pre-approve, Send special offer and Let it lapse, with no inquiry Decline. Offers use the booking currency supplied by Channex, with a localized money field converted to integer minor units in GuestCraft; the adapter converts back to the upstream integer major-unit amount. Channex requires whole major units for special offers, so fractional amounts show a field error and cannot be sent. Without that currency, special offers are unavailable. Let it lapse sends nothing and shows a lapsed outcome after a known deadline. The documented inquiry payload currently supplies no deadline, so GuestCraft does not invent a lapse timer: Let it lapse sends nothing and its explanation remains visible; a lapsed outcome can only be shown when an authoritative deadline or channel outcome is available. Every transmitted decision requires confirmation with the guest and stay visible; special offers repeat the formatted amount and currency. Reservation declines offer a reason and distinct optional guest and Airbnb messages. The decision is admitted under the property stop barrier and a stop returns 503 messages.replies_in_flight while a decision is in flight. Retry the stop after settlement; the drain retries a refusal-triggered stop. The request enters resolving so a repeat cannot send it twice; an uncertain outcome is re-read by event ID and settles to the channel result or reopens. A stale ETag, a retired or stopped property, a resolved request or a passed deadline is refused with a stable problem code. The simulator follows Channex finality: an extranet decision wins and is audited separately from the operator’s requested decision. The durable claim records the requested action, money and actor. If a lost answer is later confirmed to match that claim exactly, reconciliation attributes the decision to the operator. A differing resolution, or one without a pending claim, is attributed to the channel. Exhausted read recovery reopens the request with visible unknown-outcome metadata; check the channel before trying again.

Problems are specific: messages.thread_not_found, messages.not_retryable, messages.attachment_too_large (413 with max_bytes: a file is at most 2,000,000 bytes, decoded), messages.not_active, messages.decision_refused (409: the channel definitely refused the attempted decision), messages.retired (read-only history of an upstream property that is no longer linked), messages.install_pending (503), messages.install_unconfirmed (409), messages.install_attempt_changed (409), messages.replies_in_flight (503), messages.variable_missing (with variable), messages.thread_changed and messages.version_mismatch (both 412 with the current ETag), among others in the API reference. A decision about a property’s Messages, like its unlink, waits at most two seconds for each lock another decision of the same property holds, once its transaction began (PostgreSQL’s lock_timeout; it bounds neither the whole request nor the wait for a pooled connection): then a request that changed nothing answers channex.property_busy (503, Retry-After: 2). An activation that already changed something (it finished an earlier pending stop, ended stale replies, or recorded or advanced its install attempt) answers messages.activation_incomplete instead (503, Retry-After: 2, with the current ETag): the Messages card, which reloads, shows what stands, and activating again with that ETag continues. Activation records its install attempt before it registers the webhook or installs, so whatever it made at Channex has a durable record that a stop, an unlink or the operator settles.

The Messages state’s ETag. A property’s Messages state has one revision, its version and strong ETag. Every change of the activation (activated, stopping, stopped, its installation removed), every change of the install attempt (begun, its install requested or refused, installed, unconfirmed, confirmed, ended or forgotten), every stop, and every mapping change that changes the linked upstream property the state shows (a link, a relink to another upstream property, a finished unlink) advances it. The synchronization status reported with it (health, failure_code, failed_at, last_sync) does not, so a drain never invalidates an operator’s view. Activation, deactivation, confirm and forget take it in If-Match; a stale one answers 412 messages.version_mismatch with the current ETag. Confirm and forget also name the installation decided about in installation_id.

A thread’s detail holds its latest 1,000 messages, oldest first, and messages_next_cursor: null when they begin the conversation, otherwise the cursor of the messages before them. GET /message_threads/{thread_id}/messages?cursor=… answers the page right before that position, oldest first, as every list does ({ data, next_cursor, has_more }, limit 1–100, 20 by default); its next_cursor continues further back, and null means the page holds the conversation’s first message. Without a cursor it answers the latest messages.

Messages are ordered by Channex time, then by ingestion sequence for the same time. The cursor names a message of the thread, and a page holds the messages ordered strictly before it, so messages that arrive between two pages never shift one: each message is listed once. A message Channex dates before the position, ingested late, appears on the page where its time puts it. The cursor is bound to the caller and to the thread: another thread’s cursor, or a forged one, is refused with cursor.invalid (400).

Content removed by retention or erased on request stays removed on every page: the message is listed with readable: false, no body and its redaction_reason, never its ciphertext. Paging only reads; it changes no read marker. In the browser, “Load older messages” above the oldest message loads 100 at a time, keeps the message being read in place, and offers to try again after a failure; “This is the start of the conversation.” follows the last page. Messages that arrive meanwhile still appear at the bottom.

What GuestCraft reads and writes in Channex

Section titled “What GuestCraft reads and writes in Channex”

On an account shared with another system, GuestCraft only reads and writes what belongs to a confirmed, linked property with at least one room type mapped and Messages activated:

  • Discovery: GET /live_feed with filter[property_id] and filter[event], one call per kind (message, inquiry, reservation_request, alteration_request) from a stored cursor (filter[inserted_at][gte], ascending). A message event names its thread.
  • Threads and messages: GET /message_threads/{id} for a new thread and GET /message_threads/{id}/messages for announced threads only, from the newest messages until a page holds a message stored before the read began (messages the read stored itself, shifted onto later pages by new arrivals, never count). A history longer than ten pages keeps its next page, order and that boundary, and continues in follow-up drains until it reaches the boundary or its end. The conversation view opens on the latest 1,000 messages; older ones stay stored and load page by page (see History paging). An unchanged inbox costs four feed calls per property.
  • Requests: open requests are re-read by ID (GET /live_feed/{id}) every ten minutes, because a decision taken in the channel changes the existing event. GuestCraft decisions use the same simulator-backed resolve endpoint and the resolving idempotency state.
  • Fail closed: any item of another property, or another event kind, in any of these reads stops that property with the filter_violation health state and stores nothing of the page.
  • Installations: GET /applications/installed, the only documented list of installations, is account-wide and takes no parameters. GuestCraft reads it at activation (and to settle a lost install answer or an unfinished activation), keeps the IDs of the property’s own channex_messages items and drops everything else unread. A property where another system installed Messages is refused (messages.installation_foreign, 409): one property must not have two systems answering its guests.
  • Writes: the property’s message webhook, the install (POST /applications/install), sends to a known thread or booking, no_reply_needed, and on deactivation, a 403 or unlink DELETE /applications/{id}/uninstall of GuestCraft’s own installation and the removal of GuestCraft’s own webhook. GuestCraft calls POST /live_feed/{id}/resolve only while property admission is active. A stop returns 503 messages.replies_in_flight while an admitted decision is unsettled; the operator must retry. Channex’s and the OTA’s copies of threads and messages, booking revision archives and audit entries are outside GuestCraft’s erasure; the operator must ask Channex and the OTA to remove those copies.

Unlink deactivates Messages in the same transaction, fails queued replies with property_unlinked (terminal: never retried), uninstalls GuestCraft’s installation, removes GuestCraft’s property Messages webhook and finishes the Messages stop, and only then removes GuestCraft’s booking webhooks under the Channex ownership fence (ADR 0028). A cleanup failure keeps the unlink pending in Channels, with a retry; its webhook removal can be forgotten only once the Messages stop has finished.

Stopping. GuestCraft operates Messages only on an installation confirmed as its own, and a stop removes it before it reports success (ADR 0044). Every Channex Messages request is admitted by a committed check that its activation is still active. Unlink, deactivation and a 403 from Channex stop the same way: they commit a stop revision and stopping, which ends admission at once, then uninstall GuestCraft’s installation, settle the property’s unfinished install attempt, remove its webhook, commit stopped and answer success. An activation begun before the stop rechecks the stop revision before it commits active: it never becomes active after a stop that reported success, and removes what it installed instead. A stop returns 503 while admitted decisions are unsettled and must be retried, because Live Feed resolve is not protected by uninstall. The drain retries a stop triggered by a Channex 403. Other requests admitted before the stop use the installation guarantee: once it is gone, Channex refuses the property’s thread reads, text and attachment sends and “no reply needed” (so the simulator and Channex’s documentation say; the live test must confirm it), so such a request still on its way reaches nobody; one Channex accepted before is recorded as sent (or uncertain if its answer was lost), audited and never resent. Live Feed reads are not refused and reach no guest. Uploads are accepted by the simulator, while Channex’s documentation lists a 403 for them: GuestCraft relies on neither, because an uploaded file reaches a guest only through the attachment send. Late answers are discarded. What “no reply needed” does for guests is not verified. If Channex cannot be updated, the stop stays pending with nothing read or sent: deactivation answers messages.cleanup_failed (503) and the Messages state shows stop_pending; unlink answers channex.unlink_pending (503) and stays in pending_unlinks. A retry finishes it.

Activation and install attempts. Before anything at Channex, GuestCraft commits an install attempt (install_attempt in the Messages state), preparing: webhook work only. Right before the paid install it commits, under the property’s lock, that the install may be sent (pending), only while the link and the activation the operator confirmed still stand. A preparing attempt requested no install, so whatever the property lists is never offered as GuestCraft’s: after two minutes the next activation, deactivation or unlink removes GuestCraft’s webhook and ends it, and a failed removal keeps it visible for the next try. Channex’s answer to the install names GuestCraft’s installation; nothing else does, because the install takes no marker and the installed list carries no creation time or creator. So:

  • a definite refusal installed nothing: the attempt is preparing again while its webhook is removed, and an installation listed afterwards belongs to another system (messages.installation_foreign);
  • a lost answer with an installation listed makes the attempt unconfirmed (messages.install_unconfirmed): Messages stays inactive until a member with messages.configure confirms the installation as GuestCraft’s or leaves it alone, both audited;
  • a lost answer with nothing listed keeps it pending (messages.install_pending): the install may still arrive. After two minutes, the in-flight bound (each write times out after five seconds and is never repeated), the next activation, deactivation or unlink lists again: nothing listed ends the attempt, an installation listed makes it unconfirmed. PostgreSQL’s clock measures the bound, in the transaction that decides.

An installation is never adopted or uninstalled unless Channex’s answer or a member with messages.configure confirmed it. Unlink and deactivation settle an attempt that is not settled the same way, whatever the activation’s state: while its webhook or install may still be on its way they answer 503 (channex.unlink_pending, messages.install_pending); a preparing one loses its webhook and ends; an installed attempt is uninstalled by its exact ID; an unconfirmed one waits for that confirmation (unlink 503, deactivation 409 messages.install_unconfirmed). The Messages card keeps showing the property, also while its unlink waits (linked: false), with the attempt to settle; for a linked property, a preparing attempt offers “Remove webhook” and an installed one “Remove installation”, both the deactivation’s stop.

One serialization point. Activation’s intent and its final commit, deactivation, unlink, confirm, forget, every step of a stop and the in-flight sweeps lock the workspace’s Channex connection row and then the property’s row before they decide, the order in which unlink, the booking import and ARI already take them. Unlink therefore either sees an activation’s committed attempt and waits for it, or the activation sees the ended link and installs nothing. If the compensating uninstall of a losing activation fails, GuestCraft keeps its installation recorded as an installed attempt (and an ended link’s unlink pending), so the next stop removes it.

Reactivation. While a reply of an earlier activation is sending, a new installation could accept it, so activation answers messages.replies_in_flight (503). A reply sending for more than two minutes by PostgreSQL’s clock is no longer on its way: it becomes uncertain, audited, never resent, and activation proceeds. “No reply needed” keeps no durable in-flight record, so it is not part of this barrier (ADR 0044, residual risk).

Threads, requests and replies keep their upstream property: after a relink to a different one they are read-only history (retired). The other linked properties remain active. The account-wide installed-applications list is read only at activation and to settle an installation, as described above.

A drain sends the outbox first, then ingests each property separately. A failing property records its health (failing, filter_violation, or deactivated as not_active_upstream on a 403, which stops through the same path as any other stop at the end of the drain, or, if Channex could not be updated, in a later drain, an activation, a deactivation or an unlink) and never stops the others or the outbox. A thread Channex refuses keeps its own sync_failure and backoff. Only failures of Channex or the database are isolated this way. Anything else is a bug: the drain records it, then throws it to the job runner, which reports it without its text and stops (ADR 0008). A send that broke this way is uncertain, never sent again; an upload is failed with internal; a thread or property shows internal. Booking safety drains are enqueued before messages drains, and the safety job schedules Messages drains and throttled retention. A request-woken drain schedules retention only, using a durable per-workspace five-minute claim; it does not fan out Messages reads.

Channex documents no idempotency key for sends. Only the remote call is error-handled: once Channex accepted a reply, GuestCraft commits its Channex ID first and never sends it again, whatever happens to the local transaction afterwards; the row is uncertain (audited) after two minutes and becomes sent when the ingested thread contains that ID. An ambiguous answer from Channex is uncertain at once; a property message with the same text is shown as a hint only. Only a definite failure can be retried, and only while its activation holds: a definite failure that arrives after unlink ends the reply as property_unlinked, audited as channex.message_cancelled. A delivered or dismissed reply keeps no text or file in the outbox.

Unread compares the order in which messages were ingested with the shared read marker, so a message ingested after a click stays unread. “To answer” compares the latest guest message with the latest property reply by Channex time, or a request still undecided in GuestCraft whose deadline has not passed; system messages never hide a question.

On owning Vercel production, committed activation, webhook and reply jobs wake the shared worker through Next.js after(); only verified webhooks whose event is a message kind wake the messages drain. Reads, invalid input and rolled-back transactions do not wake it. Docker retains the resident worker. The five-minute safety job schedules a drain for every workspace with active Messages.

Templates are stored per property and language. Composer variables are resolved by the server from the linked reservation, the property and an inquiry’s booking details; dates follow the template language. The check-in time is typed by the operator until properties store one.

Message records use the integration keyring. When rotating ENCRYPTION_KEYS, run credentials:reencrypt (it rewrites connections, messages, outbox payloads, request details and templates) and keep the old keys until it reports success. An unreadable record never hides the rest of the inbox. Workspace administrators configure default, inquiry, reservation and attachment retention days in Settings → Message retention. Purpose fields left empty use the workspace default; the default is required and every explicit period must be a whole number from 1 to 3650 days. Before the first save, the default is the message-content period of the workspace’s data-protection jurisdiction (its country pack), or 730 days without one (guest data rights). Erasing a guest from Personal data on the guest profile also erases this content, with the same redaction: for a guest’s erasure request use that operation, and this one only to remove conversation content while keeping the rest of the record (which to use). Successful saves and erasures are announced. A member with messages.erase can erase from a selected guest’s profile, or enter an identifier in Message retention, without needing messages.configure. Erasure requires typing ERASE in a confirmation that explains its scope. Erase-only roles can open the action without fetching retention settings.

Retention runs as its own bounded channex.messages_retention job, independent of inbox sync. Each transaction redacts at most 100 message envelopes, titles, request details and outbox payloads in total, then schedules another batch when full. The five-minute safety scheduler and request-woken discovery include stopped and disconnected workspaces with stored content, without requiring a Channex key. Stored attachments use the earlier of their attachment period and conversation-purpose period; the whole encrypted envelope is redacted together.

Redaction clears ciphertext and records its timestamp and reason, so key rotation skips removed content. The inbox distinguishes retained or erased content from content whose key is unavailable. Request details expose details_redaction_reason and details_readable: a false readability flag means present ciphertext could not be decrypted; absent upstream facts remain distinct from removal. Identifiers, ordering, erasure records and audits remain. Channex’s and the OTA’s thread/message copies, booking revision archives and audit entries are outside this operation: the operator must ask Channex and the OTA to remove their copies. Guest merge integration remains coordinated separately until guests.merged_into is present on main. Inquiry threads without an established GuestCraft guest link cannot be inferred from a name alone.

Run pnpm --filter @guestcraft/web test:e2e:messages after the web build with a disposable PostgreSQL URL. This uses the independent HTTP simulator and Vercel request pumps without a resident worker. Screenshots go to reports/screens/messages by default: English, light/dark, 1440×900 and 390×844, with axe, keyboard and horizontal-overflow assertions. With the public Channex OpenAPI downloaded (pnpm channex:spec), messages-contract.unit.test.ts checks the simulator’s Messages answers and the client’s Live Feed parameters against it; the nightly channex-drift workflow runs it. See ADR 0044.

Unknown decision outcomes are read back from Channex, with at most three failed read attempts. 403/404 and other definite read refusals end that recovery immediately. When reads cannot prove the outcome, GuestCraft reopens the request and records an uncertainty entry in Activity with the reason and attempt count. No decision is automatically resent. A pending stop can then be retried; after connection access is repaired, an operator retry first checks the channel’s final outcome before attempting a decision.

A special-offer response is compared with the requested amount and currency, as well as its action. If the channel finalized a different price first, GuestCraft preserves that offer, reports a conflict, and records both totals in minor units in Activity. A malformed or missing final total stays uncertain. An exact same-action, same-currency, same-amount decision between the preflight read and submission has no upstream origin marker, so GuestCraft cannot distinguish that channel race from its own action.