Skip to content

Channex ARI publication

Channels provides guarded availability, price and restriction publication using the local ledger, effective rates and transactional outbox. Read ADR 0034 for protocol decisions, recovery bounds and remaining distributed ordering risks.

  1. Configure local room types, units, plans and prices. Connect Channex, map properties and room types, register the webhook and drain bookings.

  2. Map the plans that should publish. Direct-only plans may remain unmapped; health shows their count. Published plans must match room type, currency and sell mode and must have inheritance disabled in Channex. Existing live targets cannot be silently replaced: they are retired or replaced through their own workflow. The plan’s pricing root (a derived plan’s chain root) decides what else must match, see Occupancy-based rates.

  3. Inspect translated settings/warnings. GuestCraft reads advance/price/cut-off/automatic update rules and UI access settings without modifying them. The price limits are shown only to a member who reads the property’s rates; the warnings name a set limit to every member.

  4. For initial takeover, reconcile existing/future bookings and revoke the previous writer. Acknowledge takeover and choose the half-open window. Initial/new horizon availability uses per-plan/date verified prices or verified stop sell. One rejected price closes only its plan/dates.

  5. Make bookings or edit rates normally. Health refreshes every 15 seconds, including pending counts, sends, warnings, rejection attempts/deadlines, pause, lease and horizon. Local health remains available when live plan discovery fails.

  6. A booking signal holds availability for its property while revisions are applied; it allows zero closures immediately. Ack failures never hold locally applied revisions. A hold lasting 15 minutes closes prudently and shows an alert assigning the workspace administrator to restore intake. Quiet drain staleness warns and triggers safety work. Quarantine closes explicitly and shows an alert. A mapped plan that cannot be published exactly is withheld alone (below) and shows an alert (plans_withheld); the other plans keep publishing.

  7. A send whose response was lost is looked up at Channex up to three times, 30 seconds apart. If Channex holds exactly the send’s values, the desired state is satisfied and the send is settled as verified (“Channex holds these values, which does not prove that this send ran”). Otherwise it waits: nothing Channex lists is specific to one request, so another write proves nothing about this one. Unknown sends that lookups cannot settle still repair after a 120-second settling wait with recomputed absolute values. Closures can pass during the wait. Repeatedly rejected items back off and stop automatic retries after six attempts. A member with channex.configure can request Retry stuck publication, which records an audit acknowledgement and queues fresh computation of the horizon, including reopening after a cleared quarantine without another local edit.

  8. A failure that does not heal by itself stops the property’s publication: Channex refused the key or the property, or answered with something unusable (upstream_rejected); the Channex property’s currency is not supported (currency_unsupported); or a bug or another permanent failure (internal_failure). Health shows Publication has stopped with what to fix; nothing is read or sent until the cause is fixed and a member with channex.configure selects Retry stuck publication, realigns, or reconnects Channex. Changes made meanwhile, and uncertain sends, stay pending and are published after the resume. 429, 5xx and network failures still pause and resume by themselves.

  9. Every night, from 02:00 property time, GuestCraft compares Channex’s availability, rates and restrictions over the verified horizon with the values it last published, and republishes only what differs (for example a price changed in the Channex extranet). Channels → health shows the last check, the differences found, those still being republished and those Channex rejected. The check waits while booking intake is unsettled or a send is open, and a failed read is retried three times, 10 minutes apart, without pausing publication.

  10. Problems that need an operator become alerts on the property: a notice on Today, Planning, Reservations and Channels with a link to the fix, and the webhook events channex.alert_opened@1 and channex.alert_resolved@1. A sync failure (publication stopped, booking intake held past 15 minutes, failing acknowledgements, changes rejected past the retry limit), unresolved overbookings, drift that could not be repaired and quarantined revisions each open one alert, resolved automatically once the cause is gone. A screen left open reads its alerts again every minute and their openings and resolutions every 15 seconds, and shows each one once as a notice. There is no e-mail or SMS.

Resident restarts, new hosted deployments and sustained feed failures can start durable historical/future-booking catch-up. It holds availability while scanning, resumes across workers and reopens after the final drain automatically, independently of full-sync quota. Initial takeover remains an explicit operator responsibility. Hosted workers rely on the stable Vercel deployment ID; ordinary Function invocations do not count as restarts.

Built-in members inspect health and mappings; built-in administrators map, resync and request repair. Custom roles require channex.view_health for health, mappings and remote plan discovery, and channex.configure for mapping, resync and repair. Health answers Channex’s price limits (settings.values.min_price and max_price) only to a caller who also holds rates.read for the property, so housekeeping and a role with channex.view_health alone receive the other settings without them (API revision 4.1.1). The Channels screen shows such a member the health with a permission notice in place of the rate plan mapping, and requests neither plan list. All Channex permissions are workspace permissions that require unrestricted property access: a member limited to selected properties is refused them. Inventory configuration alone does not grant ARI configuration; see roles and property access. Foreign resources and invalid mappings are refused. Drafts survive failed writes, and mapping concurrency prevents one member overwriting another’s saved selections.

A per-person plan publishes one price per adult count through the same publisher: the same outbox, leases, pacing, coalescing, 512 KiB budget, drift repair and provider lookup. The distribution matrix says what reaches each channel.

  • Mapping. A plan whose pricing root sells per person maps only to a Channex per_person plan with rate_mode manual, exactly one occupancy option for each adult count from 1 to the root’s maximum, exactly one main (is_primary) option, and no option whose rate Channex derives (derived_option rate rules or an inherited rate). A per-room plan maps only to a per_room plan. When the plan’s root has age bands, the property’s Channex hotel policy must give every age from 0 to 21 the category the bands give it (below), and Channex’s children_fee must equal the child band’s nightly fee and infant_fee the infant band’s, 0 for a missing band, exactly in the property’s currency. The plan must also be distributable on the channels it declares: the save-time assessment distributionProblem (distribution matrix) is applied again with the hotel policy just read, and a plan sold direct only never maps. A mapping write that a local setting breaks answers 422 rate_plan.not_distributable with the plan (plan_id) and its setting (field, as saving the plan names it: occupancy, occupancy/curve, ageBands, derivation, distribution or sellMode), the error pointing at the plan’s mapping, and the mapping screen shows it on that plan’s choice; a Channex plan no setting can match (missing, inherited, of another currency or room type) answers 400 request.invalid.
  • Withheld plans. When the Channex plan, its hotel policy or the local plan changes later, the next run withholds that mapping (one plan on one room type) and only it. Health names the cause, plan_occupancy_mismatch, plan_age_policy_mismatch, plan_fees_mismatch, plan_channels_mismatch or plan_incompatible, with plan_incompatible also in the warnings while any plan is withheld; its settings list each withheld mapping in planIssues (planId, roomTypeId, code and the plan’s field), and the channels screen names the plans and room types that still publish. Each issue says how its mapping is withheld (withheld): a stop sell without prices or other restrictions (stop_sell), also for a Channex plan of another currency, room type or sale basis, which a stop sell to its own plan still reaches; nothing for a Channex or local plan that no longer exists, which sells nothing (not_sent); nothing, with its room type’s availability closed, for a Channex plan that inherits values GuestCraft does not send, whose stop sell cannot be trusted (room_type_closed). Every other plan and room type keeps publishing availability, rates and restrictions, and the nightly drift check goes on, comparing a stop-sold plan’s stop sell and leaving out a mapping that is sent nothing. A room type otherwise closes only when no healthy plan remains on it. Saving the mapping again keeps the issues until the next run assesses them. Only property-wide failures close the whole property: an unsupported currency, refused credentials, degraded booking intake or an internal failure. The mapping is published again over the horizon when it is withheld and once the next upstream refresh finds it agrees.
  • Hotel policy ages. Channex adds the fees “based at Hotel Age Policy” (rate plans). The property’s policy is Properties.Property.hotel_policy_id (“UUID of the property’s hotel policy, or null”), read with GET /hotel_policies/{id} (hotel policies): a guest counts as an infant up to infant_max_age (at most 10) and as a child up to children_max_age (at most 21), each nullable (OpenAPI HotelPolicies.HotelPolicy). GuestCraft reads it with the property settings on every run, at mapping and at plan discovery, and stores it with the settings (agePolicy, null without a policy), which saving a plan compares too. A policy not read yet, a property without one, or a null age a band needs is not an exact projection, so it is refused. A failed policy read fails the settings read.
  • Values. Each night sends rates: [{ occupancy, rate }] for every adult count, ordered by occupancy, as decimal strings, and no rate (Channex: “Alternative to rate for Rate Plans with multiple Occupancy Options”, ARI). If any count has no positive price on a night, the night is sent as stop sell without rates: Channex keeps the rate of an occupancy a change leaves out, so a partial list would sell the missing counts at stale prices. Equal neighbouring nights coalesce by value; the verified cache compares the lists by value.
  • Drift repair. Channex documents reading one rate per plan and night (Restrictions.RestrictionValues) and no per-occupancy read. GuestCraft assumes it is the main option’s rate (unverified, see the certification checklist) and compares the published list through that occupancy only; a difference republishes the whole list. A change to any other occupancy made outside GuestCraft cannot be detected, so it is overwritten instead: each completed nightly check also forgets and queues the published rates of a rotating share of the nights of every mapped plan with more than one occupancy, through the usual changes, leases, pacing and verified cache. The calendar is cut into runs of 28 consecutive dates, one run in seven is due each night (occupancyRefreshDays, occupancyRefreshCycle), so every published night is sent again at least once every seven completed checks, about a seventh of the horizon a night. Health warns drift_partial_occupancies while such a plan is mapped: the check’s differences count only what the read can show, and drift.pending includes the republished nights until sent.
  • Provider lookup. No read can show that the non-main occupancies were applied, so a lost send with per-occupancy rates is never settled by a lookup (and makes no read): it waits for the bounded repair, which sends recomputed values again after the settling wait.
  • Children. Channex sends children’s ages with an OTA booking; the import stores them as the room’s children’s ages only when there is one age from 0 to 17 per child, otherwise none. Imported prices stay the OTA’s: ages are for the record and later re-quotes, never invented.

A plan’s sales cap is published as max_sell, documented by Channex as “Maximum rooms that may be sold on the date. null removes the limit” (Restrictions.RestrictionUpdate.max_sell).

  • The remaining count, never the cap. Channex does not document whether its max_sell counts the bookings it receives itself. GuestCraft’s cap counts every sale of the plan, direct ones included, which Channex never sees. It therefore sends max(0, cap − sold): if Channex does not count its own bookings this is exact once GuestCraft has imported them, and if it does, the plan can at worst under-sell by the OTA rooms counted twice; it never oversells. Which of the two Channex does is a question for the certification session.
  • Never degraded. A full write of a plan’s restrictions always carries max_sell, null when the plan has no cap, so a stale limit cannot survive; zero stays zero. A plan with nothing left is also sent stop_sell: true, so a channel that ignores max_sell cannot sell it, and its room type closes when no other plan of it sells. An OTA room whose Channex plan is not mapped is unattributed: it counts against no cap and is reported in health (unattributed_nights) so that the operator maps that plan.
  • Following the sales. Saving a reservation and importing an OTA revision queue a rates change with the field sales for each plan whose sales change, which sends only the plan’s stop sell and, when it has a cap, its remaining max_sell; a plan switch on the same nights republishes both plans. Both that change and a cap edit (maxSell in the bulk change) take the fast path like a stop sell.
  • Read back and repaired. The read-back asks GET /restrictions for max_sell and keeps null as a value. The nightly drift check compares it with what was published, and a difference is repaired like any other field.
  • Channel availability rules (availability_offset, max_availability, close_out; “Set through Channel Availability Rules, not here”) are read with the property’s settings and shown read-only in Channels health; GuestCraft never writes them and never counts them as sales.

While publication is enabled, a mapping cannot be removed or changed (channex.ari_mapping_live): an OTA would keep selling what Channex last held. A member with channex.configure retires one target at a time from Channels → Availability and rates sync, with Retire or Replace… on a mapped rate plan or a mapped room type (ADR 0034):

  1. The dialog shows the preview first (validate_only): the nights from the property’s today to the horizon end, the Channex plans that are sent a stop sell and the room type that is sent availability 0, the sends still open, the imported channel bookings still to depart that the target sold, and that a completed retirement cannot be undone. A replacement is chosen from Channex’s plans (of the same Channex room type) or room types, with a plan list for a room type; a replacement that cannot be published names its field, like a mapping save.
  2. Starting it (with the mapping’s strong version) withholds the target: a retired plan is sent a stop sell without prices or restrictions; a retired room type availability 0 and a stop sell on each of its plans, over the whole horizon. Mapping edits wait until it ends.
  3. Once nothing is left to publish, no send is open and no booking hold is held, GuestCraft reads Channex back over the horizon. An absent value is not a closure. A target that Channex no longer lists among the property’s plans or room types (deleted in Channex) sells nothing there and counts as closed, so it can still be retired. Nights Channex does not hold are sent again; after three verifications that still differ, or if Channex returns nothing for a target it still lists (target_missing) or refuses the read (upstream_rejected), the retirement fails, keeps its target closed and opens a sync_failure alert (retirement_failed). Resume starts it again; Cancel returns the target to normal publication (the closure already sent stays until that is published).
  4. A verified closure switches the mapping in one transaction and fences the retired Channex targets. A replacement is first sent a stop sell (a replacement room type availability 0), so the old and new targets are never open together. Once GuestCraft has recorded those values as applied by Channex on every night (sends whose tasks Channex acknowledged; this is not a read-back) the retirement completes and the replacement publishes its prices, its availability opening through the usual price readiness. If nothing is left to publish while they are still missing, as when Channex rejects them past the retry limit or the replacement was deleted in Channex, the retirement fails as replacement_unpublished, the replacement still closed: Resume sends its values again; Cancel keeps the switched mapping and lets the replacement publish normally.

A revision for a retired Channex room type is held in the review queue as retired_target; an administrator applies it to its former room type, or maps that Channex room type again and retries. A booking sold on a retired rate plan keeps its local plan and still imports. Health’s retirement shows the active retirement, or the latest one, with its state, attempts, failure code and horizon.

Paths below follow /api/v1/properties/{property_id}/channex/ari. Send the workspace header; plan discovery, mapping, resync and repair accept only a browser session. SDK operations and React query factories are generated from OpenAPI.

Method and suffix Operation ID Meaning
GET / channex.ari.health Local health, including mapping version, unpublished count, stale-drain flag and retry issues
GET /plans channex.ari.plans Cached, database-coalesced upstream candidates
GET /mappings channex.ari.mappings {version,mappings} and strong ETag
PUT /mappings channex.ari.map Compatible mapping replacement/addition; strong If-Match required
POST /resync channex.ari.resync {from,to,takeover_confirmed}; manual full realignment
POST /repair channex.ari.repair Audited acknowledgement; queued worker repairs current state
POST /retirements channex.ari.retirements.create Preview (validate_only) or start a target retirement; strong If-Match to start
GET /retirements/{retirement_id} channex.ari.retirements.get One retirement and its state
POST /retirements/{retirement_id}/cancel channex.ari.retirements.cancel Cancel before the mapping switched
POST /retirements/{retirement_id}/resume channex.ari.retirements.resume Start a failed retirement again

GET /api/v1/properties/{property_id}/channex/alerts (channex.alerts.list, channex.view_health, any principal) lists the property’s open alerts, and GET /api/v1/properties/{property_id}/channex/alert_transitions (channex.alerts.transitions) their openings and resolutions since an instant, each to be shown once by its event id. Both answer an API key’s rate limit with 429 auth.rate_limited. Health’s drift reports the nightly check and each send’s lookups and looked_up_at the provider lookups (API revision 6.3.0).

A stale mapping write returns 412 channex.mapping_version_mismatch, the current version and ETag. A mapping write for a Channex property whose currency GuestCraft does not support returns 409 channex.currency_unsupported. Discovery or a mapping write whose Channex read is refused (key or property refused, unusable answer) returns 409 channex.mapping_read_refused; a temporary Channex failure returns 503 server.unavailable, with Retry-After when Channex gave one (API revision 3.8.0, which also documents the stop codes of health issues). Mapping, resync and repair wait at most two seconds for each of the connection and property locks, which the booking import and Messages share (ADR 0044): while another change holds them longer, they answer 503 channex.property_busy with Retry-After: 2 and change nothing. The push worker keeps waiting. A mapping write while a send is open, and a resync while booking intake has not settled (barrier, degraded hold, no drain yet or a failed feed), answer 409 channex.ari_busy and change nothing; a remap of a live mapping answers 409 channex.ari_mapping_live (retire it instead), a mapping write while a retirement is active answers 409 channex.ari_retirement_active, and a second manual full sync within 24 hours answers 409 channex.ari_full_sync_used (API revision 4.5.0). Malformed/missing If-Match returns the standard field error. Permission is 403; foreign property reads are 404. Resync accepts today/future dates within min(500, state_length). Manual full sync is once per 24 hours and preserves its cache-bypass flag through supersession. Recovery and stuck-state repair use no manual full-sync quota. AriPanel props remain propertyId, timeZone, currency, roomTypes and writable.

The database owns numbered leases, endpoint pacing, retries, settling deadlines and quotas. Claims and completion use exact IDs and generation. Recorded sends precede HTTP. Expired holders are adopted; late responses cannot complete another generation. Availability always sends absolute affected-date values; the verified delta cache applies to commercial fields and already verified zero-only packets during a booking hold. A closure sends zero and stop sell on every mapped plan.

Availability gathers for six seconds; commercial changes share a 45-second property window. Priority bookings bypass gathering. Requests merge equal contiguous dates and fit 512 KiB, with size-aware POST deadlines up to 16 seconds. Each endpoint/property gets one call per six seconds. 429/5xx and item rejections back off, with durable health visibility. Unknown repair recomputes values rather than replaying old payloads. Channex lacks fencing; a packet arriving after the settling wait remains a documented ordering limitation.

Docker runs the resident worker. Vercel committed writes and verified webhooks wake the same worker after the response using after()/waitUntil. Quiet publishers do not self-schedule; pumps wait only for real short delayed jobs within their budget. Nightly horizon/safety work and the daily cron remain durable; commercial hosted use requires 1–5 minute external/Pro scheduling to cover gaps between invocations. Preview, development and unowned/restored instances cannot publish. See deployment instructions.

R2 response records failing-before/passing-after probe evidence, PostgreSQL/PgBouncer and simulator tests, public contracts, built-browser checks and gate results. The analysis example uses actual imported bookings and sequential changing prices; the webhook race traverses the real HTTP handler and pg-boss worker.

Live target retirement is implemented and verified against the simulator (a live check of the closure with Channex staff is part of certification), and so are sales caps (described above); the certification session on Channex staging (the owner’s action) remains planned; occupancy-based rates are implemented as described above, with the staging checks the certification checklist lists (ADR 0048 records drift repair, provider lookup and alerts). Settings warnings need operator review. Simulator success is not provider certification: the certification checklist maps Channex’s scenarios to simulator-backed tests and lists what needs a run with Channex staff.

A failed property’s filtered feed cannot starve a healthy sibling on the same connection. Healthy intake settles independently; only unresolved properties enter failure recovery. Acknowledgement failures appear per revision and as a per-property ARI warning; they never hold or degrade availability. Successful partial drains refresh the remaining signal age, so sustained successful traffic cannot trigger an old hold’s closure.

For hosted installations, an undrained booking signal or stalled deployment recovery closes affected channels after 15 minutes. Daily cron cannot prevent this after a missed pump or redeploy; configure a 1–5 minute scheduler for a live property.

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 administrator confirmation.