@guestcraft/channex-client
MIT, portable Channex HTTP client, written by hand from provider documentation
(ADR 0006). It uses injected fetch, Web Crypto
and Zod; it has no GuestCraft domain, database or server dependency.
Public API
Section titled “Public API”createChannexClient(options) creates a reusable client for one connection. Options are
apiKey (required), environment, baseUrl, fetch, timeoutMs, getRetries,
userAgent, onResponse, sleep, random and now. onResponse receives response
headers for pacing; sleep, random and now make retry behavior injectable. Calls
accept an optional AbortSignal. The defaults are production, a ten-second attempt
timeout and two GET retries; baseUrl overrides the selected environment.
channexBaseUrl(environment) returns an environment’s base URL (https://app.channex.io/api/v1
or https://staging.channex.io/api/v1).
| Resource | Operations |
|---|---|
properties |
Paginated list, single-property get |
bookingRevisions |
Pending feed, historical list({ page, propertyId?, limit? }) (5 per page by default, at most 100), get, acknowledge |
roomTypes |
A property’s room types: list(propertyId) |
bookings |
currentRevisionId(id) and account-wide futureRevisionIds({ page, departingFrom }) |
webhooks |
register({ propertyId, callbackUrl, secret, eventMask? }), unregister({ propertyId, callbackUrl, eventMask? }); ids/delete for separately fenced writes |
ariSetup |
Property settings including channel availability rules and the ages of the property’s hotel policy (hotel_policy_id, then GET /hotel_policies/{id}: infant_max_age, children_max_age; hotelPolicy null without one), and paginated rate plans with their rate_mode, occupancy options and children_fee/infant_fee |
ari |
Absolute updateAvailability and updateRestrictions (a rate plan’s rate, or per-occupancy rates with one rate per occupancy); accepted tasks and warnings. availability({ propertyId, from, to }) and restrictions({ propertyId, from, to, restrictions }) read current values per date, both ends included |
tasks |
get asynchronous task outcome and channel events; list({ propertyId, page?, limit?, order? }) one page of a property’s processed tasks (Channex lists processed tasks only; a listed task without finished_at is refused) |
messages |
Installations, Live Feed/events, threads, messages, sending, attachments and no-reply status |
stripeTokenization |
The Stripe Tokenization App’s one documented operation, paymentMethod(bookingId) (POST /bookings/{booking_id}/stripe_payment_method: the Stripe payment method of the booking’s card, read from data.token; 403 without the app on the booking’s property). The committed contract documents no install, listing or removal of this app: the operator installs it in Channex’s interface |
bookingReports |
Booking Reporting: noShow(id, { waivedFees, penaltyAmount? }) (a non-negative decimal-string penalty, Expedia only, checked before sending), invalidCard(id) and cancelDueInvalidCard(id) sent without a body; each resolves on Channex’s success message |
@guestcraft/channex-client/webhooks |
verifyWebhookSignature, keyLabel, parseWebhookDelivery |
The client does not expose hotel administration, channel setup or the whole Channex API. A transport method alone does not implement booking ingestion, synchronization or a GuestCraft operator workflow.
verifyWebhookSignature checks x-hmac-timestamp, x-hmac-signature-256 and
x-hmac-keys-256: HMAC-SHA256 covers {timestamp}.{raw body}, with a default
300-second tolerance. It accepts current and pending secrets, matched by keyLabel
(... plus the secret’s last six characters). parseWebhookDelivery never throws;
its optional payload represents data sent with send_data enabled. GuestCraft’s
server intake uses neither helper: it authenticates the custom
X-GuestCraft-Channex-Secret header.
Validation, ownership and failures
Section titled “Validation, ownership and failures”Responses are validated and undeclared fields are dropped, including raw OTA fields.
A revision’s guarantee is reduced to an allowlist: the card type (a documented
BookingRevisions.CardType code, otherwise unknown), last4 (the last four characters of
Channex’s masked number when they are digits), the expiry (MM/YYYY or MM/YY, otherwise
null), is_virtual and the virtual card’s documented meta values. The number, security code,
holder’s name and token are never kept; a guarantee without the documented shape is null and
never rejects the revision; rejected-revision placeholders carry no card data. Revision parsing returns a validated revision, a rejected safe
identity envelope, or an unreadable marker. A consumer must durably save or quarantine
an owned revision before acknowledging it; unreadable markers must not be acknowledged.
Cosmetic customer fields may be null or omitted. See the
booking import contract.
Revision history supports a property filter. bookings.futureRevisionIds reads the
account-wide current-booking list and is suitable only for an exclusive account.
Webhook matching requires the documented relationships object and checks its property,
callback and event mask (default: booking); attribute-only legacy lists fail with
invalid_response before any write. Registration sets active=true, global=false and
send_data=false with a custom authentication header.
Deletion tolerates a concurrent 404. Messages installations are discovered account-wide
and filtered locally to the requested property; request-decision writes are absent.
ChannexError exposes kind, retryability, optional pacing delay and unknown write
outcome, plus the provider error’s code, title and details when present, and a
transport cause for network failures/timeouts. Error message text omits request and
response bodies; the exposed fields and cause are not redacted. Only GETs retry network
errors, timeouts, HTTP 408 and 5xx responses. Writes and 429 responses are not retried
by the client. Collection readers that gather all pages are bounded to 50 pages of 100
and refuse partial results.
An accepted ARI task does not prove delivery to every channel.
Verification
Section titled “Verification”pnpm --filter @guestcraft/channex-client testpnpm --filter @guestcraft/channex-client typecheckTests use scripted transports, synthetic fixtures and the independent simulator.
Webhook signature vectors are computed independently with Node crypto. Offline captured
request/response contracts live in channex-simulator; they do not use client schemas.
The optional upstream drift suite needs the public specification in .cache/:
pnpm channex:spec fetches it without an account. Without that cache the drift and
Messages contract tests report skipped; the nightly channex-drift workflow runs both. The
drift comparison checks the fields the client reads, their nulls, value types and enums, and
tuples member by member against OpenAPI prefixItems.
Ordinary tests require no customer credentials. The full specification
is not committed; see ADR 0006 for the
specification and snapshot policy.