Connecting a PMS or channel to the HUIDUN inventory hub
This specification is for PMS vendors and their technical teams. On the PMS side you implement only four standard OpenTravel 2003/05 messages; booking, holds, overbooking protection and reconciliation are handled by the hub. All examples are real requests and responses from the sandbox.
1. Overview
HUIDUN offers two integration modes. Most PMS vendors use Mode A, which is self-service. Mode B is for systems that can only answer real-time queries and cannot push updates; it is enabled case by case.
| Mode A: push and pull (default) | Mode B: real-time connector (case by case) | |
|---|---|---|
| Who calls whom | PMS calls HUIDUN | HUIDUN calls the PMS |
| Protocol | OpenTravel 2003/05 XML, HTTP Basic over TLS | JSON with HMAC-SHA256 request signatures |
| Inventory owner | PMS (once connected, manual rate and availability edits on the HUIDUN side are blocked) | PMS |
| What the PMS implements | Four messages: products, rates, availability, reservation pull | Endpoints for availability, quote, hold, booking, cancellation and booking lookup |
Sandbox
A separate test database, issued the moment you apply and disabled after 30 days.
https://huidun-demo.luma-e61.workers.dev/ota/cmProduction
Production credentials are issued after certification. Paths are identical to the sandbox; only the host name changes.
https://app.hui-dun.com/ota/cm2. Authentication
- Every request carries
Authorization: Basic base64(username:password)over HTTPS. HUIDUN stores only a hash of the password, so a lost password can only be reset. HotelCode(hotel_idwhen pulling reservations) is the code a hotel sets when it selects your PMS under channel connections in the HUIDUN console. If the hotel has not selected your PMS, the code does not exist for your account (error 392).- Room type codes
InvTypeCode/RoomTypeCodeand rate plan codesRatePlanCodeare those returned by product retrieval; keep the mapping on the PMS side. - Each account may send up to 1,200 requests per minute; beyond that you receive 366. The maximum request body is 1 MB.
3. Products and rate plans: OTA_HotelProduct
Returns the hotel's room type and rate plan combinations on HUIDUN, one HotelProduct per room type and rate plan pair. PaymentCollection is HotelCollect (pay at the hotel) or OTACollect (prepaid online).
POST https://huidun-demo.luma-e61.workers.dev/ota/cm/OTA_HotelProduct
Authorization: Basic base64(username:password)
Content-Type: application/xml
<OTA_HotelProductRQ xmlns="http://www.opentravel.org/OTA/2003/05" Version="1.0">
<HotelProducts>
<HotelProduct HotelCode="SBX-LUVQ9U"/>
</HotelProducts>
</OTA_HotelProductRQ><OTA_HotelProductRS Version="1.0" TimeStamp="2026-10-25T06:01:40Z" xmlns="http://www.opentravel.org/OTA/2003/05">
<Success />
<HotelProducts HotelCode="SBX-LUVQ9U">
<HotelProduct>
<RoomTypes><RoomType RoomTypeCode="STD" RoomTypeName="標準雙人房" MaxOccupancy="2" /></RoomTypes>
<RatePlans><RatePlan RatePlanCode="RO" RatePlanName="純住宿" PaymentCollection="HotelCollect" /></RatePlans>
</HotelProduct>
<HotelProduct>
<RoomTypes><RoomType RoomTypeCode="STD" RoomTypeName="標準雙人房" MaxOccupancy="2" /></RoomTypes>
<RatePlans><RatePlan RatePlanCode="BB" RatePlanName="含早餐" PaymentCollection="OTACollect" /></RatePlans>
</HotelProduct>
<!-- One HotelProduct per room type and rate plan pair -->
</HotelProducts>
<TPA_Extensions><Pricing Type="Standard" /></TPA_Extensions>
</OTA_HotelProductRS>4. Daily rates: OTA_HotelRateAmountNotif
Start/Endare stay dates (inclusive) inYYYY-MM-DDformat, up to 365 days per range.AmountAfterTaxis the tax-inclusive price per room per night. The entry withoutNumberOfGuestsis the base rate; entries with it are occupancy-based rates.CurrencyCodemust match the hotel's configured currency (currently TWD); otherwise you receive 61.- One request may contain several
RateAmountMessageelements. If any entry is invalid, nothing in the request is written andErrorslists each problem (up to 20).
POST https://huidun-demo.luma-e61.workers.dev/ota/cm/OTA_HotelRateAmountNotif
<OTA_HotelRateAmountNotifRQ xmlns="http://www.opentravel.org/OTA/2003/05" Version="1.0">
<RateAmountMessages HotelCode="SBX-LUVQ9U">
<RateAmountMessage>
<StatusApplicationControl InvTypeCode="STD" RatePlanCode="BB"/>
<Rates>
<Rate CurrencyCode="TWD" Start="2026-11-04" End="2026-11-05">
<BaseByGuestAmts>
<BaseByGuestAmt AmountAfterTax="3500"/>
<BaseByGuestAmt NumberOfGuests="1" AmountAfterTax="3200"/>
</BaseByGuestAmts>
</Rate>
</Rates>
</RateAmountMessage>
</RateAmountMessages>
</OTA_HotelRateAmountNotifRQ><OTA_HotelRateAmountNotifRS Version="1.0" TimeStamp="2026-10-25T06:01:40Z" xmlns="http://www.opentravel.org/OTA/2003/05">
<Success />
<UniqueID ID="1792908100105976" Type="506" />
</OTA_HotelRateAmountNotifRS>5. Availability and restrictions: OTA_HotelAvailNotif
BookingLimit: rooms available to sell per night in the range (0 to 9999). The hub decrements this number night by night when a booking is made.RestrictionStatus:Status="Close"/"Open". WithoutRestrictionthe room is closed;Arrivalmeans no check-in on that date andDeparturemeans no check-out on that date.LengthsOfStay:SetMinLOSandSetMaxLOS(by arrival date),SetMinLOS_ThroughandSetMaxLOS_Through(applied to every night of the stay).- Restrictions sent without
RatePlanCodeapply to every rate plan currently valid for that room type.
POST https://huidun-demo.luma-e61.workers.dev/ota/cm/OTA_HotelAvailNotif
<OTA_HotelAvailNotifRQ xmlns="http://www.opentravel.org/OTA/2003/05" Version="1.0">
<AvailStatusMessages HotelCode="SBX-LUVQ9U">
<!-- Availability: BookingLimit = rooms sellable per night for these dates -->
<AvailStatusMessage BookingLimit="3">
<StatusApplicationControl Start="2026-11-04" End="2026-11-05" InvTypeCode="STD"/>
</AvailStatusMessage>
<!-- Close, no arrival, minimum stay (without RatePlanCode it applies to every rate plan of the room type) -->
<AvailStatusMessage>
<StatusApplicationControl Start="2026-12-31" End="2026-12-31" InvTypeCode="DLX" RatePlanCode="RO"/>
<RestrictionStatus Status="Close" Restriction="Arrival"/>
<LengthsOfStay><LengthOfStay MinMaxMessageType="SetMinLOS" Time="2"/></LengthsOfStay>
</AvailStatusMessage>
</AvailStatusMessages>
</OTA_HotelAvailNotifRQ><OTA_HotelAvailNotifRS Version="1.0" TimeStamp="2026-10-25T06:01:40Z" xmlns="http://www.opentravel.org/OTA/2003/05">
<Success />
<UniqueID ID="1792908100106824" Type="505" />
</OTA_HotelAvailNotifRS>6. Reservation pull: OTA_HotelResNotif
Pull reservations by last-modified time range, or look up a single reservation by id. We recommend pulling every 1 to 5 minutes with slightly overlapping ranges and de-duplicating by reservation number.
| Parameter | Description |
|---|---|
hotel_id | Required. Hotel code |
from/to | ISO 8601 times, filtered by the reservation's last-modified time; required when id is not given |
limit | 10 to 200, default 10; sorted by modification time, oldest first |
id | Optional. Reservation number (ResID_Value) |
ResStatus:Bookfor a new reservation (including held ones, see section 7),Modifyfor a change,Cancelfor a cancellation.- The reservation number is in
HotelReservationID ResID_Valueand never changes; modifications and cancellations reappear under the same number. PrepaidIndicator="true"means the guest has paid online and the hotel should not collect the room charge again.
GET https://huidun-demo.luma-e61.workers.dev/ota/cm/OTA_HotelResNotif?hotel_id=SBX-LUVQ9U&from=2026-10-25T06:00:00Z&to=2026-10-25T07:00:00Z&limit=50
Authorization: Basic base64(username:password)
# Look up a single reservation:
GET .../ota/cm/OTA_HotelResNotif?hotel_id=SBX-LUVQ9U&id=6193082730<OTA_HotelResNotifRS Version="1.0" TimeStamp="2026-10-25T06:01:40Z" xmlns="http://www.opentravel.org/OTA/2017/05">
<HotelReservations>
<HotelReservation CreateDateTime="2026-10-25T06:01:40+00:00" LastModifyDateTime="2026-10-25T06:01:40+00:00" ResStatus="Book">
<RoomStays><RoomStay>
<RoomTypes><RoomType RoomTypeCode="STD" NonSmoking="true"><RoomDescription Name="標準雙人房" /></RoomType></RoomTypes>
<RatePlans><RatePlan RatePlanCode="BB" PrepaidIndicator="true"><RatePlanDescription Name="含早餐" /><MealsIncluded MealPlanCodes="19" /></RatePlan></RatePlans>
<RoomRates><RoomRate RoomTypeCode="STD" RatePlanCode="BB" NumberOfUnits="1"><Rates>
<Rate UnitMultiplier="1" RateTimeUnit="Day" EffectiveDate="2026-11-04" ExpireDate="2026-11-05"><Total AmountAfterTax="3500.00" CurrencyCode="TWD" /></Rate>
<Rate UnitMultiplier="1" RateTimeUnit="Day" EffectiveDate="2026-11-05" ExpireDate="2026-11-06"><Total AmountAfterTax="3500.00" CurrencyCode="TWD" /></Rate>
</Rates></RoomRate></RoomRates>
<BasicPropertyInfo HotelCode="SBX-LUVQ9U" HotelName="沙盒飯店 LUVQ9U" />
</RoomStay></RoomStays>
<ResGuests><ResGuest><Profiles><ProfileInfo><Profile><Customer>
<PersonName><GivenName>旅客</GivenName><Surname>測</Surname></PersonName>
<ContactPerson><PersonName><GivenName>旅客</GivenName><Surname>測</Surname></PersonName><Telephone PhoneNumber="0900000000" /><Email /></ContactPerson>
</Customer></Profile></ProfileInfo></Profiles></ResGuest></ResGuests>
<ResGlobalInfo>
<GuestCounts><GuestCount AgeQualifyingCode="10" Count="2" /></GuestCounts>
<TimeSpan Start="2026-11-04" End="2026-11-06" />
<Total AmountAfterTax="7000.00" CurrencyCode="TWD" />
<HotelReservationIDs><HotelReservationID ResID_Type="14" ResID_Value="6193082730" /></HotelReservationIDs>
</ResGlobalInfo>
</HotelReservation>
</HotelReservations>
</OTA_HotelResNotifRS>7. Holds, confirmation and overbooking protection
Every channel booking goes through three stages, and inventory is taken at the moment of the hold. When two channels compete for the last room, the losing side is told the hotel is full before payment, so a guest never pays for a room that does not exist.
| Stage | What the hub does | What the PMS sees when pulling |
|---|---|---|
| Hold | Conditional decrement night by night; if any night is short the whole booking fails. Holds last 10 minutes by default, 30 minutes at most | Book (you can close the room on other channels during payment) |
| Confirm | Confirmed after successful payment; the booking is created and recorded against the hotel's service-fee account | Same reservation, status unchanged |
| Expiry or cancellation | Inventory is restored | A reservation you have already seen reappears as Cancel; an expired hold that was never pulled does not appear |
- Each hotel can set a buffer (default 1 room): that many rooms of the availability you push are kept unsold to absorb the delay between the two systems syncing.
- Once connected, the PMS owns inventory: manual rate and availability edits in the HUIDUN console are blocked. Change them in the PMS and push again.
- When the PMS pushes availability after pulling reservations, the hub recognises reservations already pulled and does not decrement them twice.
8. Channel API (for sellers; used in the sandbox to simulate bookings)
Selling channels use /api/mp for availability, quotes and bookings. A PMS does not need to implement this section. The sandbox gives you a test channel key so you can see how bookings appear in your reservation pull without waiting for real orders.
POST /api/mp/quote
X-API-Key: sandbox channel key
{"channel":"sbx_luvq9u","source":"huidun","merchant":"PROP-SBX-LUVQ9U",
"unit":"STD","plan":"BB","day":"2026-11-04","nights":2,"guests":2}
200 {"ok":true,"quote":{"ok":true,"sellMinor":7000,"currency":"TWD",
"detail":{"roomTypeName":"標準雙人房","ratePlanName":"含早餐","cancelPolicy":"不可取消",
"nightly":[{"date":"2026-11-04","sell":3500},{"date":"2026-11-05","sell":3500}]}}}
POST /api/mp/bookings
{ ...same as above, "hold":true, "holdMinutes":10, "idemKey":"order-10086",
"guest":{"name":"Alex Chen","phone":"0900000000"} }
201 {"ok":true,"booking":{"bookingKey":"mpb-2cf63b70-…","status":"held",
"holdUntil":"2026-10-25T06:11:40.112Z","grossMinor":7000,"currency":"TWD","reused":false}}
409 {"ok":false,"error":"quote_failed","problems":["已客滿"]}
POST /api/mp/bookings/{bookingKey}/confirm → 200 {"ok":true,"status":"confirmed","already":false}
POST /api/mp/bookings/{bookingKey}/cancel → 200 {"ok":true,"status":"cancelled"}- Amounts are in the smallest currency unit (for TWD, one dollar).
idemKeyis globally unique: resending the sameidemKeyreturns the same booking (reused:true) instead of creating a second one.- The guest needs at least a name plus either a phone number or an email address.
- Text values such as room names and
problemsare returned in the hotel's own language.
9. Real-time connector and request signatures (Mode B)
When a PMS can only answer real-time queries, HUIDUN calls the PMS endpoints. The PMS provides: availability, quote, hold, booking, cancellation and booking lookup, plus a capability declaration (whether holds, cancellation and instant confirmation are supported). This mode is enabled case by case; choose "We provide a real-time availability and booking API" in the application form.
Every request HUIDUN sends carries Authorization: Bearer token and two signature headers, which the PMS verifies before processing:
x-hd-timestamp: 1792908100105 (Unix milliseconds)
x-hd-signature: hex( HMAC-SHA256( shared secret, timestamp + "." + raw request body ) )
Verification rules:
1. Reject if |now − timestamp| > 5 minutes (replay protection)
2. Compute over the raw bytes; do not parse and re-serialise the JSON first
3. Compare signature strings in constant time// Node.js verification example
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(secret, ts, body, sig) {
if (Math.abs(Date.now() - Number(ts)) > 300000) return false;
const want = createHmac("sha256", secret).update(ts + "." + body).digest("hex");
return want.length === sig.length && timingSafeEqual(Buffer.from(want), Buffer.from(sig));
}Endpoints
HUIDUN calls the endpoints below relative to the API base URL registered at enablement, signed as described above. Request and response bodies are JSON. Amounts are integers in the smallest unit: whole units for TWD, JPY and KRW, cents for other currencies. Dates are the hotel's local date YYYY-MM-DD; to is exclusive and a request covers at most 31 nights.
| Method | Path | Purpose | Response |
|---|---|---|---|
GET | /availability?unit=&plan=&from=&to=&guests= | Nightly availability and price | { "items": [{ "unit", "date", "available", "priceMinor" }] } |
POST | /quote | Quote | { "ok", "amountMinor", "currency", "quoteId", "expiresAt", "problems" } |
POST | /bookings | Hold or book | { "ref", "status": "held" | "confirmed" | "pending", "holdUntil", "amountMinor", "currency" } |
POST | /bookings/{ref}/confirm | Confirm a hold after payment | { "status": "confirmed" } |
POST | /bookings/{ref}/cancel | Cancel | { "status": "cancelled" } |
GET | /bookings/{ref} | Look up a booking | { "status" } |
/quote body: { unit, plan, day, nights, guests }. The /bookings body adds quoteId, guest { name, phone, email }, idemKey, hold (boolean) and the hold length in minutes holdMinutes; the cancel body carries idemKey.
A repeated idemKey must return the same booking and must not take another room. A hold that is not confirmed in time must be released automatically; a later confirmation must receive 409 with hold_expired. Support for holds and cancellation is registered at enablement.
On failure, return a non-2xx status with { "error": "code", "message": "text" }. These business errors (status 404, 409 or 422) are passed on to the channel by code: sold_out, stay_restricted, price_changed, quote_expired, hold_expired, invalid_guests, not_cancellable, booking_not_found. Any other code, 401, 429, 5xx and timeouts (8 seconds) are treated as a temporary outage: the channel is asked to retry later and your message text is not forwarded.
The shared secret is delivered separately when the connector is enabled and never appears on any web page or in an email body.
10. Error codes
Pre-parse errors: HTTP 505 with JSON
HTTP/1.1 505
Content-Type: application/json
{"error":{"code":"497","message":"Authentication error","timestamp":"2026-10-25T14:01:40+08:00"}}| Code | Meaning | What to do |
|---|---|---|
| 497 | Authentication failed | Check credentials; do not retry automatically |
| 450 | Unparseable or unsupported message, or body too large | Fix the request |
| 366 | Per-minute request limit exceeded | Back off and retry |
| 448 | System error | Retry later |
Business errors: HTTP 200 with Errors
HTTP/1.1 200
<OTA_HotelRateAmountNotifRS Version="1.0" TimeStamp="2026-10-25T06:01:40Z" xmlns="http://www.opentravel.org/OTA/2003/05">
<Errors>
<Error Type="3" ShortText="Invalid room type" Code="402">Room type SUITE is not defined for this hotel.</Error>
</Errors>
<UniqueID ID="1792908100109112" Type="506" />
</OTA_HotelRateAmountNotifRS>| Code | Meaning |
|---|---|
| 392 | Invalid hotel code, or this hotel has not selected your PMS |
| 402 | Unknown room type code |
| 425 | Unknown rate plan code, or not valid for this room type |
| 15 | Invalid date format, end before start, or range longer than 365 days |
| 61 | Currency does not match the hotel's setting |
| 321 | Required field missing |
| 448 | Too many segments in one request; split it into batches |
Channel API (JSON)
| HTTP | error | Meaning |
|---|---|---|
| 401 | unauthorized | Invalid key |
| 403 | scope_denied/channel_forbidden | The key lacks this permission, or belongs to another channel |
| 409 | quote_failed | Sold out, closed, minimum stay not met and so on; reasons are listed in problems |
| 409 | idem_key_conflict | This idemKey is already used by another channel |
| 400 | need_contact/bad_request | The guest has neither phone nor email, or the request is malformed |
| 404 | booking_not_found | The booking does not exist or belongs to another channel |
11. The seven certification checks
The sandbox checklist is evaluated automatically from what the sandbox database actually recorded. Once all seven are complete, you can request certification.
- Product retrieval (OTA_HotelProduct) succeeded
- Rate push (OTA_HotelRateAmountNotif) succeeded
- Availability push (OTA_HotelAvailNotif) succeeded
- A channel hold created a booking
- A hold was confirmed
- A booking was cancelled, or a hold expired and was released
- A reservation pull (OTA_HotelResNotif) succeeded after a booking existed
The Sandbox Runner shows this checklist after each run. It is also available as JSON at https://huidun-demo.luma-e61.workers.dev/sandbox/pms/progress?ref=YOUR_HOTEL_CODE&format=json (completion and timestamps only; no credentials or personal data).
12. Sandbox limits
- Each signup creates one sandbox hotel: 2 room types (STD, DLX) × 2 rate plans (RO room only, pay at hotel; BB with breakfast, prepaid online), preloaded with rates and availability for the next 30 days and a buffer of 1 room.
- Sandboxes are disabled after 30 days; a single network can open at most 3 sandboxes per hour.
- The sandbox channel's service fee is 0 and does not reflect production pricing.
- The sandbox is a separate test database with no data exchange with production; do not enter real guest data.