Channex booking import
This workflow imports OTA bookings into the existing local front desk. Connecting a key
alone checks access. A member with channex.configure must also map a local property and its
active room types in Channels, then register the webhook. Room types not mapped on either side
remain visible. Imported mappings cannot be removed or replaced; adding missing room
mappings is supported. Rate-plan mapping and guarded outbound ARI are described in the publication workflow.
Operator workflow
Section titled “Operator workflow”- Connect a customer-owned Channex key and configure local room types/units.
- In Channels select the local and Channex properties, select matching room types and save.
- Register/check the webhook. GuestCraft configures the booking event with active=true, send_data=false and a random authentication header. The secret never reaches the browser.
- Request a drain or let the worker react to a verified webhook. Refresh sync health to inspect the last completed drain, oldest pending acknowledgement and revision receipts.
- Find imported reservations in Today, Planning and Reservations with their channel. Assign a ready unit and check in/out as usual. Local notes remain editable. Dates, room type, occupancy, OTA guest details, prices and cancellation belong to the OTA.
- Review overbookings and quarantined revisions. Reservations has separate overbooking
and quarantine filters. Add a missing mapping and retry the revision in Channels.
Unknown properties appear in the non-routable queue and may be retried after mapping.
A revision for a Channex room type retired through the ARI retirement workflow waits as
retired target (
retired_target): Apply to former room type applies it to the room type it was mapped to (POST /channex/retrywithapply_to_former_room_type, audited), or map that Channex room type again and retry (ADR 0028). - To bring in the bookings a property already had before it was linked, choose Import
existing bookings for the property in Channels (
channex.configure). The preview reads the property’s 1,000 most recent Channex revisions and shows how many bookings still to depart would be imported, are already in GuestCraft, are skipped (cancellations of bookings GuestCraft never had, unreadable revisions), would go to the review queue (with the first reasons) and would be above capacity. Confirm to queue the import; its status and counts stay in Channels. Each booking’s current revision goes through the same import as the live feed, so running it twice, or while the feed delivers the same bookings, changes nothing more. Conflicts appear in the review queue, overbookings in the overbooking filter. A revision Channex no longer offers in its feed is recorded as acknowledged, without changing what the live feed recorded for it meanwhile. When the 1,000 revisions do not reach the property’s oldest one, the preview says so. One runner at a time holds an import, by a renewable claim: a runner whose claim lapsed and was taken over, or whose import ended asstalled, stops without writing, so the counts and the outcome are those of the runner that holds it. - A booking the channel cancelled and later made active again (reinstated) is active again in
GuestCraft: its rooms sell their nights again, without a unit until one is assigned, it is
flagged as an overbooking when the rooms are gone meanwhile, the audit log records
reservation.reinstatedand the reservation shows when it was reinstated. A cancelled stay that happened stays as it happened.
Unknown-room-type receipts for an active linked property are acknowledged after their encrypted archive is saved; acknowledgement confirms receipt, not local application. New quarantines are replayed automatically once per connection version after a room mapping is available. Receipts that were waiting before migration 0024 remain manual-only: Channels marks them “Retry by hand after mapping”; use Retry import after mapping. Retry accepts only quarantined and non-routable revisions.
A replay that fails because of its archive or Channex never stops intake of other revisions or properties. Channels shows why in the Automatic retry column:
- Encrypted copy unreadable: the key that wrote the archive is no longer configured. See secret rotation.
- Channex no longer returns the booking: for example, Channex deleted it six months after checkout. Retry after checking it in Channex.
- Channex did not answer: automatic delays are 1, 2, 4 and 8 minutes. After five failures in a row it waits for a manual retry or a mapping change. A mapping change also clears any active delay and attempt count. When Channex asks for a pause (429), the drain stops replaying until the next drain.
Any other failure, such as the database failing, stops that drain without recording a replay attempt; a later drain tries the receipt again.
When Channex holds a newer revision of the booking, only the replayed one becomes stale; the newer archived revision is applied on its own.
In exclusive mode, a linked property without mapped room types keeps its bookings unacknowledged. Sync health counts them under “Awaiting room mapping”. Mapping a room type applies and acknowledges them; until then Channex may resend them.
Changing the API key
Section titled “Changing the API key”Use a different API key in Channels validates the new key before saving it. GuestCraft still has webhooks or acknowledgements upstream for every linked property and for every unlinked property whose webhook removal is pending. The new key must see all of them in its own property list; GuestCraft never sends it a property ID to ask. Otherwise the form says so, nothing changes, and Open property links leads to the properties to unlink and the webhook removals to finish (Retry Channex cleanup) with the current key first. A rotated key of the same account sees them and is accepted.
GuestCraft reads the key’s list page by page and stops as soon as it has seen every one of those properties. It reads at most the first 5,000 properties (50 pages of 100). If they are not all among them, the form says that the key could not be verified and nothing changes: try again, use the key of a Channex user with access to fewer properties, or unlink properties first.
After a key change, retrying a webhook removal that was left pending under the old key completes only if the new key lists GuestCraft’s webhook and removes it. If it lists none, the removal stays pending (“Webhook removal not confirmed”): connect with the key used when the property was unlinked, then retry. A retry that answers “Webhook removal already ended” recorded nothing: another request finished or forgot the removal, or a newer unlink replaced it, while Channex answered. Reload to see the current state.
If a removal can never be confirmed, for example because the webhook is already gone, a
member with channex.configure and channex.manage_account_mode can choose Forget webhook removal on its row and type the property’s name to
confirm. GuestCraft cannot verify the cleanup in Channex: remove any remaining GuestCraft
webhook for that property in Channex yourself. Bookings from that Channex property stay
blocked until it is linked again, imported reservations and their history are kept, and the
audit log records the forget (channex.webhook_removal_forgotten). The property can then be
linked again. The unlink’s Messages stop must have finished first (ADR 0044): while
GuestCraft’s Messages installation of the property is not uninstalled, or an unfinished
activation’s install attempt is not settled, forgetting answers “Finish the unlink first”
(channex.messages_stop_pending) and changes nothing; retry the unlink until its Messages part
is done.
A key of another Channex account that also lists this account’s properties is accepted. A removal retry then asks that key for the webhooks of a property it listed itself; its empty answer proves nothing, so the removal stays pending until a key that lists the webhook removes it, or a member allowed to unlink forgets it.
The check proves that the new key can read those properties, not that it may write: a read-only key is accepted, then each drain fails to acknowledge and sync health shows the failure until a key that can write is connected. While Channex is confirming a booking or removing a webhook, the form asks to try again in a few seconds; nothing was changed.
Each operation checks a permission; Public API lists them. With the built-in
roles, every role holds channex.view_health and only the administrator holds
channex.configure and channex.manage_account_mode; a custom role holds exactly the codes
it grants. All three are workspace permissions that require unrestricted property access: a
member limited to selected properties is refused (403 auth.forbidden) whatever their role
(roles and property access). UI preferences and formatting use the shared English
catalogs. Operational drafts stay in memory; a reload retrieves saved mapping and health.
Runtime and ownership
Section titled “Runtime and ownership”Docker Compose starts the worker image target alongside web. The same server worker
runs budgeted jobs from GET /api/internal/channex/drain, authenticated with
Authorization: Bearer <GUESTCRAFT_CRON_SECRET>. Configure a random secret of at least
32 characters on the server and in the deployer’s scheduler; do not put it in a URL.
Call the route every five minutes with a host timeout exceeding 120 seconds. Resident
workers evaluate the safety schedule themselves. A production environment and an
initialized database whose instance identifier matches configuration are both required;
the connection also retains its owning instance. Vercel previews explicitly disable
outbound writes even when their general application environment is production.
Development, preview and restored-instance environments never acknowledge or register
webhooks. No end-user key is required by CI. The browser harness replaces transport in
the test process with a loopback HTTP simulator, leaving production URL policy intact.
POST /api/channex/signal/:organization/:workspace checks
X-GuestCraft-Channex-Secret, ignores the body and persists only a fixed event,
verification result and receive time. Bad authentication returns 401. Persistence faults
return 200; the safety net recovers missed signals. Successful signals transactionally
coalesce a pg-boss drain request. Queue startup happens outside the tenant transaction so
that even a single-server-connection transaction pooler cannot deadlock on queue metadata.
Transaction and retry contract
Section titled “Transaction and retry contract”The connection row and existing property writer gate serialize local effects. External HTTP calls happen outside transactions. The immutable revision sequence has a unique (workspace, revision) key; personal content is in a separate erasable ciphertext table. The envelope binds instance, organization, workspace, key version and revision ID, using the independent integration keyring. No raw card or unknown response fields enter it. Mutable receipts distinguish applied, stale, quarantined and non-routable from the acknowledgement timestamp. Inventory, reservation, receipt, audit and event commit together.
The connection row, then the property row, is also the per-property serialization point of
Messages (ADR 0044). A mapping save (channex.mapping.put) and revision retry
(channex.revision.retry) wait at most two seconds for each of these locks: while another change holds
them longer, they answer 503 channex.property_busy with Retry-After: 2, roll back with
nothing applied, and the same request can be repeated. The drain’s job keeps waiting, bounded
only by the statement timeout.
A drain allows 24 upstream calls, including authoritative reads and acknowledgements. It requests page one of each feed, ordered by insertion time, at most 100 records. Acks remove rows, so after any acknowledgement it returns to page one; only a full page that makes no progress (entries deliberately left upstream) advances to the next page of the same feed. Remaining work enqueues a durable continuation. Pending local acknowledgement receipts are retried before reading the feed, including when Channex applied an ack but its response was lost. They use at most half the budget; a refused one is marked failed and the feed is still read, but the drain does not count as completed until every acknowledgement succeeds. A wake-up request older than twenty minutes is treated as lost and renewed by the next signal or safety pass. A revision returning in the feed cannot repeat its local effects. A safety job runs every five minutes and wakes connections without a successful empty-feed drain for twenty minutes. Failed attempts are retained by pg-boss and its dead-letter policy; later safety passes can recover them.
Projection ordering compares insertion instants, including submillisecond precision. Equal timestamps fetch the current booking’s revision ID and reread that revision; unresolved ambiguity quarantines rather than choosing by UUID. A cancellation is undone only by a newer non-cancelled revision of the booking: its rooms the channel cancelled become active again, except stayed nights, which remain a fact (reinstatement, ADR 0048). The archive watermark prevents older creates from bypassing newer quarantined cancellations. Per-room cancellation uses stable booking_room_id; omitted rooms remain cancelled history. An OTA fact may exceed local capacity; the ledger records it and an overbooking event and visible state identify the conflict. Direct sales still reject insufficient inventory. A missing type, incompatible currency or invalid normalized payload is quarantined. Active assignment/date conflicts, including cancellation of an in-house stay, require local resolution before retry; imported data never silently moves a local stay. A cancellation or omission of a departed room is applied as a channel fact: the room shows Cancelled by channel but keeps its stayed nights at their frozen prices, release, unit history, sold inventory and charges, whatever amounts the cancellation carries, and later revisions do not change it. Its reservation stays confirmed, so the folio and reports keep working. Checking an in-house guest out and retrying the quarantined cancellation ends the same way (ADR 0028, Cancelling a stay that happened). A newer successfully applied revision supersedes earlier quarantine.
Each feed entry is parsed on its own. A body that fails parsing but keeps its revision,
booking, property and timestamp metadata becomes an invalid_revision quarantine, archived
and acknowledged only when its property is linked, room-mapped and not unlinked, in either
account mode. An entry without that metadata (or with a null property) is never archived or
acknowledged: it stays upstream, import health shows a failure, and that property’s
availability barrier stays held, while later bookings and other properties continue. ADR
0028 lists the guarantee per path (feed, recovery, retry, tie reads, signals).
Rate plans and packages
Section titled “Rate plans and packages”Each booking room names the Channex rate plan it was sold on (rate_plan_id). Import resolves
it through the ARI rate-plan mapping (publication) to the local plan of
the room’s type, and the room keeps that plan. The channel’s nightly prices stay authoritative
and unchanged (the room stays price_overridden, so no quote or re-rate replaces them). When the
plan includes a package (ADR 0029, Packages), import carves it out of each night: the included
services are frozen with the night at their catalog prices, under the item’s basis, with the
source channel. The folio then posts the accommodation (the price less the package) and each
service apart. Posting an included extra again is refused (folio.included_in_package) unless
it is beyond_package. The revenue report shows room and package revenue apart.
Example: two adults, two nights at 100.00 on a mapped plan with breakfast at 6.00 per guest per night. Each night stays 100.00 and includes 12.00 of breakfast, so the folio posts 88.00 accommodation and 12.00 breakfast per night.
GuestCraft keeps no history of a plan’s settings, so it uses the package and catalog prices the plan has when it imports the revision. A later revision of the same sale (same plan and guest count, already split) keeps what was sold:
- a night with the same date and price keeps its frozen services, even after the catalog price changes;
- the once-per-stay services sold stay exactly once, on the first night, at their sold value;
- new or repriced nights take the plan’s package as it is at that revision.
Any other revision, such as one with another plan, another guest count or a room that was left for review, is split again as a new sale. The split is identical on replay, retry and recovery. This is because it depends only on the revision, the stored reservation and the local plan.
Import never refuses a booking, or drops its inventory, because its price cannot be split. The
room keeps its whole price as accommodation, and the reservation’s OTA state lists it for review
(packageReview, one entry per booking room) with a reason:
unknown_rate_plan: the booking names no Channex rate plan, or one not mapped to a local plan of the room’s type, while that type sells an active plan with a package. When none of the type’s plans includes a service, a whole-price night is what the property sells, so nothing is listed;package_not_allocated: the mapped plan’s package is worth the night’s price or more, or more than a safe amount. The room keeps the plan.
A later revision that can be split clears its entry. GuestCraft does not split stored bookings again when a mapping or a plan changes: map the Channex rate plan (or correct the plan) for the bookings to come, and check the listed rooms against the channel’s booking, because the folio still has no package line to protect from an extra charged twice.
Public API
Section titled “Public API”The workspace header applies to these OpenAPI/SDK operations. Sync health accepts any principal with the permission; the others accept only a browser session. Each operation names the permission it requires.
channex.sync.get: GET/api/v1/channex/sync;channex.view_health.channex.roomTypes.list: GET/api/v1/channex/room_types?property_id=…;channex.view_health.channex.property.get: GET/api/v1/channex/properties/{id};channex.manage_account_mode.channex.mapping.put: PUT/api/v1/channex/mapping;channex.configure, and alsochannex.manage_account_modefor the first link of a property on a shared account.channex.mapping.delete: DELETE/api/v1/channex/mappings/{property_id}(unlink);channex.configureandchannex.manage_account_mode.channex.mapping.cleanup.delete: DELETE/api/v1/channex/mappings/{property_id}/cleanup;channex.configureandchannex.manage_account_mode.channex.webhook.register: POST/api/v1/channex/webhook;channex.configure.channex.drain.request: POST/api/v1/channex/drain;channex.configure.channex.revision.retry: POST/api/v1/channex/retry;channex.configure.channex.bookingHistory.preview: GET/api/v1/properties/{property_id}/channex/booking_history;channex.configure. The preview reads Channex synchronously: Channex refusing the read ischannex.history_read_refused(409), Channex unavailable isserver.unavailable(503), and an unlinked or unmapped property ischannex.mapping_inactive(409).channex.bookingHistory.import: POST/api/v1/properties/{property_id}/channex/booking_history_imports(201);channex.configure. It only queues the import, so it never reads Channex: another import of the property while one runs ischannex.history_import_running(409), and an unlinked or unmapped property ischannex.mapping_inactive(409). A refusal during the queued run ends that import asfailedwith the codeupstream_rejected(orupstream_unavailable), shown by the list below.channex.bookingHistory.list: GET/api/v1/properties/{property_id}/channex/booking_history_imports;channex.view_health, any principal.channex.alerts.list: GET/api/v1/properties/{property_id}/channex/alerts;channex.view_health, any principal (see publication for the alerts).
Outputs include ChannexMapping, ChannexSyncHealth, ChannexRevisionReceipt and
OtaBooking. Reservation/calendar/summary source includes ota; direct creation does
not accept it. Reservation lists accept overbooking=true. OtaBooking.reinstated_at says when a channel last reinstated rooms (API revision 6.3.0). OtaBooking.package_review lists the rooms left for review by the package split above, each booking_room_id with its reason (API revision 6.7.0). OTA rooms expose
booking_room_id and ota_cancelled, in reservation detail and in summaries; OTA guest detail exposes ota_owned, and an edit of such a guest is guest.ota_owned (409). Existing optimistic
versions still protect local mutations. Health returns up to 100 receipts, prioritizing
unresolved issues; aggregate pending count, oldest receipt and degraded property IDs are
computed over the whole queue. ARI consults the uncapped quarantine state and durable signal barrier before positive publication.
Rollback
Section titled “Rollback”Migration 0024 keeps retired upstream targets in their own table and is declared expand, so
the compatibility check lets the previous runtime start against it. That runtime does not
maintain the separate retired-target fence: a later exclusive drain could consume a formerly
unlinked property’s bookings. Do not roll back across 0024 while any workspace is in exclusive
account mode; restore the pre-upgrade database backup if a rollback is required.
Limits
Section titled “Limits”Historical import (ADR 0048) reads at most 1,000 revisions per property; reinstated rooms come back without a unit. The erasure user interface remains planned. An OTA date or room-type change of a departed stay the channel has not cancelled stays quarantined as a local-stay conflict that no local action resolves. ARI takeover/recovery limitations are documented separately. Ciphertext separation permits a future erasure job without deleting sequence metadata. The queue view is bounded to 100 receipts; bulk historical browsing/export is planned. A channel price is split with the plan’s package as it is at import, not as it was when the channel sold it; a booking left for package review is not split again until its next revision. Automated axe/browser checks complement, rather than replace, human assistive-technology review and Channex certification. See ADR 0028.
Shared accounts default to mapped-property booking intake/recovery and ARI publication. See the Shared accounts sections of ADRs 0028 and 0034 for the call-site audit, mode transition and webhook guarantees. Link a specific property ID before room discovery; exclusive booking consumption needs an audited confirmation by a member with channex.manage_account_mode.
Manually merged guest contacts
Section titled “Manually merged guest contacts”A manually merged guest identity becomes operator-owned. Its profile can be edited even when an OTA reservation references it. Subsequent OTA revisions preserve the chosen identity while still updating booking facts. Their latest normalized contact details remain a separate reservation snapshot; the profile shows differing fields with the channel and revision time. Applying selected fields uses the normal audited guest edit and current ETag. Absorbed profiles remain read-only and point to the final visible survivor. See guest identity and ADR 0045.