Channex-compatible API
Repull serves the Channex API as a drop-in replacement. Point an integration written against Channex at Repull's base URL, send a Repull API key, and it keeps working: the same paths, request and response bodies, filters, pagination, envelopes, error shapes, webhooks and HMAC signatures. Nothing has to be rewritten. Every operation in Channex's published API has a route; the coverage table shows which work fully, which work with a named limit, and which return a Channex-shaped error.
| Base URL | https://api.repull.dev/channex/api/v1 (instead of https://app.channex.io/api/v1) |
| Auth | Your Repull API key in the user-api-key header |
| Channels | Booking.com, Airbnb and Vrbo (beta). Creating any other Channex channel returns a Channex-shaped error. |
| Moving an existing Channex account | Switch from Channex: one import call keeps your Channex ids |
Quick start
- Create an API key in the Repull dashboard.
- Replace the base URL.
https://app.channex.io/api/v1becomeshttps://api.repull.dev/channex/api/v1. - Send the Repull key in the
user-api-keyheader, where your Channex key was.
BASE=https://api.repull.dev/channex/api/v1 curl "$BASE/properties?pagination[page]=1&pagination[limit]=10" \ -H "user-api-key: sk_live_YOUR_KEY"
Everything else is unchanged: filter[...], pagination[...] and order[...] query parameters, { data, meta } on success, { errors: { code, title, details } } on failure with the same HTTP status. A missing or unknown key returns Channex's 401; an unknown path returns 404 resource_not_found; a wrong method returns 405 with an Allow header. The base URL without /api (https://api.repull.dev/channex/v1) works too.
Read meta.warnings
meta.warnings, for example a rate that a channel cannot sell. Log them; they are how a partial endpoint tells you what it did.How Channex concepts map
| Channex | On Repull |
|---|---|
| Property | A hotel or a home. A hotel groups its room types; a multi-room Booking.com property is one property; a single home is its own property. One rule assigns the property_id, so bookings, revisions, messages, reviews, webhooks and channels all print the same id for the same place. |
| Room type | A sellable unit type. count_of_rooms is how many identical units it has. |
| Rate plan | Real rate plans. Every room type has a default plan, and you can add more: manual, derived from a parent (with modifiers), cascade or auto, with occupancy options, meals and a cancellation policy. Each plan can be mapped to a Booking.com rate. Airbnb and Vrbo sell one price per night, taken from the primary plan. |
| ARI | POST /availability sets rooms to sell per night, as a count. POST /restrictions sets rate, min_stay_arrival, min_stay_through, max_stay, closed_to_arrival, closed_to_departure and stop_sell per plan and date. A derived plan is computed from its parent. |
| Task | Every ARI or inventory write returns a task. GET /tasks/:id shows the dates written, queued and refused, with an error per refused item. |
curl -X POST "$BASE/restrictions" \
-H "user-api-key: sk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "values": [
{ "property_id": "PROPERTY_ID", "rate_plan_id": "RATE_PLAN_ID",
"date_from": "2026-12-01", "date_to": "2026-12-14",
"rate": "189.00", "min_stay_arrival": 3, "closed_to_arrival": false }
] }'Channels API and the mapping iframe
The Channels API (/channels, /channels/:id/mappings, activate, deactivate, /channels/list, /channels/codes, test_connection, mapping_details) is available for Booking.com (BDC), Airbnb (ABB) and Vrbo (VRB). Every connection in your workspace appears as a channel, including ones made from the dashboard.
If your app embeds Channex's channel mapping screen, keep the button and change the server. Request a one-time token as before, then load https://api.repull.dev/channex/auth/exchange with the same query string. The token is single-use and lasts 15 minutes. It opens the hosted mapping screen for that workspace, limited to the channels in channels or available_channels. Only redirect_to=/channels exists; other pages are refused before the token is spent.
curl -X POST "$BASE/auth/one_time_token" \
-H "user-api-key: sk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "one_time_token": { "property_id": "PROPERTY_ID", "username": "jane@pms.example.com" } }'
# { "data": { "token": "..." }, "meta": { "message": "..." } }Webhooks
Register webhooks with POST /webhooks exactly as on Channex: callback_url, event_mask, property_id or is_global, send_data, headers, request_params, is_signed and signing_key_id, with Channex's defaults. Deliveries use Channex's event names and envelope. A property webhook wins over a global one on the same URL, so the endpoint gets one delivery, not two. Delivery is retried, and GET /webhooks/:id/logs shows every attempt.
{
"event": "booking",
"payload": {
"booking_id": "9c1a2b3c-...",
"property_id": "4f6e7d8c-...",
"revision_id": "e2d4f6a8-..."
},
"property_id": "4f6e7d8c-...",
"user_id": null,
"timestamp": "2026-10-22T14:03:11.204Z"
}HMAC signing
Signing is byte-for-byte Channex's, so your existing verifier works unchanged. A signed delivery carries:
x-hmac-timestamp: the Unix time used to sign.x-hmac-signature-256: lowercase hex HMAC-SHA256 of<timestamp>.<raw body>. During a rotation it holds two signatures, comma-separated, current first.x-hmac-keys-256:...plus the last six characters of each secret, in the same order.
Your secrets carry over. The import recreates each signing key under its Channex id; install the secret your endpoints already verify with through POST /signing_keys/:id/rotate and { "signing_key": { "secret": "..." } }. POST /signing_keys and generate accept a secret too, of 32 to 256 characters; leave it out and Repull generates one, shown once. generate adds a pending secret and rotate promotes it. Where Channex asks for billing_account_id, use your workspace id; the response names it if you send your old one.
import crypto from 'node:crypto'
// rawBody must be the exact bytes received, before JSON parsing.
export function verifyChannexWebhook(rawBody: string, headers: Headers, secret: string): boolean {
const ts = headers.get('x-hmac-timestamp')
const sigs = (headers.get('x-hmac-signature-256') ?? '').split(',')
if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false
const expected = crypto.createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex')
return sigs.some(s =>
s.length === expected.length && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)),
)
}Events
Every event Channex lists is accepted in event_mask (GET /webhooks/events). These are accepted but not sent, and registering one returns a warning:
published_review: review fires when a review is first seen; the publish moment is not observed.updated_channel: No event is sent when a connected account’s settings change.activate_channel, deactivate_channel: Use new_channel and disconnect_channel.booking_invalid_guarantee: Cards are not validated on arrival.airbnb_notification: Airbnb platform notifications are not relayed.booking_unmapped_rate: Accepted, but not produced yet.
Bookings
The revision feed works as on Channex. A booking webhook carries booking_id, property_id and revision_id, and the revision exists before the webhook is sent, so the loop needs no change: receive the webhook, fetch the revision, store it, ack it.
# 1. Fetch the revision named by the webhook curl "$BASE/booking_revisions/REVISION_ID" -H "user-api-key: sk_live_YOUR_KEY" # 2. Acknowledge it once stored curl -X POST "$BASE/booking_revisions/REVISION_ID/ack" -H "user-api-key: sk_live_YOUR_KEY" # Catch-up: every revision not yet acknowledged curl "$BASE/booking_revisions/feed" -H "user-api-key: sk_live_YOUR_KEY"
booking_new,booking_modificationandbooking_cancellationfollow the revision's status, so a cancellation always arrives asbooking_cancellation.- A change that leaves the booking's content unchanged creates no revision and sends no webhook.
non_acked_bookingfires for revisions left unacknowledged, and is dropped if you ack first. - A revision shows the booking's current content. Revisions are kept for 90 days; ack is idempotent.
amountisnullwhen the channel reported no money, never"0.00". The booking'sguaranteecarries the card where the channel provides one; no PCI app is needed. Webhooks never carry a card.reservation_request,accepted_reservationanddeclined_reservationcarrybmsin the booking revision structure.
Booking CRS
POST /bookings and PUT /bookings/:idcreate and change real reservations, which appear in the feed as CRS revisions. One room per booking: send a multi-room booking as one booking per room. The stay is priced by the property's rates; if your days total differs, the response names both amounts in meta.warnings. Fields such as meta, services, deposits and notes are stored and read back as written.
Messages and reviews
/message_threads, /message_threads/:id/messages, /bookings/:id/messages, /attachments, /reviews, /reviews/:id/reply and /scores work as on Channex, with message, review and updated_review webhooks. Replies go out on the channel the guest wrote on. The limits are in the table below.
Endpoint coverage
Every operation in Channex's published API has a route. Full behaves as on Channex. Partial works, with the limit named. Not available returns a Channex-shaped error with the reason, never a fake success. A value Repull does not hold is null, never 0, false or [].
Inventory and ARI
| Endpoint | Status | Notes |
|---|---|---|
/properties (CRUD, options) | Full | A property is a hotel, a multi-room Booking.com property or a home. DELETE deactivates it. email, phone, website and logo_url are not stored and read as null. A property assembled from a Booking.com hotel is edited per room type (422 otherwise). |
/room_types (CRUD, options) | Full | count_of_rooms is the number of units. Room counts owned by a hotel PMS refuse writes with 409. Dorm beds are not sold individually. |
/rate_plans (CRUD, options) | Full | Every room type has a default plan. Extra plans support manual, derived, cascade and auto modes, a parent plan, modifiers, occupancy options, meals and cancellation policy. DELETE archives. |
GET/POST /availability | Full | Rooms to sell are stored and pushed as counts, never collapsed to open/closed. |
GET/POST /restrictions | Partial | Rates, min stay (arrival and through), max stay, CTA, CTD and stop-sell per plan and date. Airbnb and Vrbo take one nightly price, from the primary plan. An extra plan not mapped to a Booking.com rate is stored but not sold. |
GET /tasks, /tasks/:id | Full | Every ARI and inventory write: dates written, queued and refused, with per-item errors. |
/property_facilities, /room_facilities | Partial | Channex does not publish its catalogue ids, so map facilities by title. Facilities with no equivalent are not listed. |
/hotel_policies (CRUD, options) | Partial | Check-in and check-out times, pets, smoking, adults-only and max guests reach the listing. Internet, parking, pet fees, child ages and cleaning practices are stored and read back only. |
/taxes, /tax_sets (CRUD, options, test) | Partial | Stored and calculated, but not pushed to any channel. Airbnb pass-through taxes are read-only. |
/photos (CRUD, upload) | Full | Photos of a listing managed by a connected PMS cannot be replaced as a set. |
/currencies, /timezones, /whoami | Full | whoami returns the API key acting for the workspace (system_role: api). |
Bookings
| Endpoint | Status | Notes |
|---|---|---|
GET /bookings, /bookings/:id | Full | amount and currency are null when the channel reported no money. guarantee carries the card where the channel provides one. |
POST /bookings, PUT /bookings/:id (Booking CRS) | Partial | Creates or changes a real reservation and a CRS revision. One room per booking (422 otherwise). Priced by the property’s rates; a different days total is named in meta.warnings. |
/booking_revisions, /feed, /:id, POST /:id/ack | Full | A revision is a content change. Kept 90 days. It shows the booking’s current content; historical snapshots are not kept. Ack is idempotent. |
GET /bookings/:id/payments | Full | Charges, holds and refunds. |
price_breakdown, payout_details, bank_transfer_payout | Partial | Booking.com bookings only. Other channels return 422 method_not_supported. |
charge_payment, pre_auth_payment, settle_payment, void_payment, refund_payment | Not available | Card charging is not available through this API (403). |
stripe_payment_method, stripe_token, authorize_net_customer | Partial | Returns the payment method already on the property’s connected account; otherwise 422 or 403. |
no_show, invalid_card, cancel_due_invalid_card, guest_misconduct, stay_change, misconduct_categories | Not available | Booking.com guest reporting is not connected (422 method_not_supported). |
Messages and reviews
| Endpoint | Status | Notes |
|---|---|---|
/message_threads, /:id, /:id/messages | Full | message_count is null in the list and exact on a single thread. |
/bookings/:id/messages, POST …/messages/pull | Partial | pull returns the stored thread; it does not trigger a fresh channel fetch. A booking without a conversation returns 422. |
POST /attachments, GET /attachments/:id/:file | Full | Kept for 24 hours. |
POST /message_threads/:id/no_reply_needed | Partial | Booking.com conversations on a connectivity-provider connection only. |
POST /message_threads/:id/close | Not available | Threads have no closed state (422). |
/reviews, /:id, POST /:id/reply | Full | — |
POST /reviews/:id/guest_review | Partial | Airbnb only. |
GET /scores/:property_id, /detailed | Full | Computed from the reviews held, out of 10 (Airbnb scores doubled). |
Channels, iframe and accounts
| Endpoint | Status | Notes |
|---|---|---|
/channels (CRUD, options) | Partial | Every connection is a channel: each Booking.com hotel, Extranet login, Airbnb host and Vrbo login. Deleting a Vrbo or Extranet login is done in the dashboard, not the API. |
POST /channels/:id/activate, /deactivate, /check_readiness | Full | Turns rate and availability pushes on or off. Activate is refused, with the reasons, while a channel is unconnected or unmapped. |
POST /channels/:id/mappings, DELETE …/mappings/:id | Partial | An Airbnb listing cannot be unmapped through the API; deactivate the channel instead. |
/channels/:id/execute/:action | Not available | The actions list is empty: reservations import continuously. |
GET /channels/:id/action/listings | Full | Airbnb. Other channels return 400, as on Channex. |
/channels/list, /adapter, /codes, test_connection, connection_details, mapping_details | Full | Booking.com (BDC), Airbnb (ABB) and Vrbo (VRB). |
POST /meta/airbnb/connection_link | Full | Returns a hosted Connect URL that runs Airbnb authorisation, then redirects to redirect_uri. |
POST /auth/one_time_token, {server}/auth/exchange | Partial | Opens the hosted channel mapping screen. Only redirect_to=/channels exists. |
/channel_availability_rules (CRUD, options) | Partial | Close-outs are enforced on Booking.com and Airbnb, and stored only on Vrbo and Extranet logins. availability_offset and max_availability are stored, not enforced. |
GET /live_feed, /:id, POST /:id/resolve | Partial | booking_revision_id is null until that revision has been created. |
/groups (CRUD, options) | Partial | A group is a workspace. A property lives in exactly one, so moving it between groups is refused. |
/group_users, /property_users | Partial | Real members are read-only. Invitations are stored but create no account, send no email and grant no access. |
/applications, /installed, install, uninstall | Partial | Messages and Booking CRS are always on. PCI is not needed (cards are on the booking). Payment apps and PMS bridges are refused with a reason. |
/applications/payment_app/* | Not available | There is no installable payment app (404). |
/applications/stripe_tokenization_app/:id/* | Partial | Answered from the booking, as stripe_payment_method above. |
Webhooks
| Endpoint | Status | Notes |
|---|---|---|
/webhooks (CRUD), GET /webhooks/events | Full | Every Channex attribute, with Channex’s defaults. A property webhook wins over a global one on the same URL. |
POST /webhooks/test, /webhooks/:id/test | Full | — |
GET /webhooks/:id/logs | Full | One entry per attempt, newest first. Custom header values are redacted. |
/signing_keys (CRUD, options, generate, rotate) | Full | Channex’s HMAC headers, verified with Channex’s own Node and Python code. |
Differences to know
- Revision ids start fresh. When you import a Channex account, property, room type, rate plan, webhook and channel ids carry over; booking revision ids do not. Unacknowledged Channex revisions should be acked on Channex before you switch.
- New channels start deactivated. A channel you create is not pushed to until you call
activate. - Airbnb and Vrbo sell one rate. Extra rate plans reach Booking.com when mapped to a Booking.com rate; on Airbnb and Vrbo the primary plan sets the price.
- No card charging. Cards are readable on the booking; charge, pre-authorise, settle, void and refund are not available.
- Rate limits are Repull's. A
429returnshttp_too_many_requestswithRetry-After. See Rate limits.