Developer Specification

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 whomPMS calls HUIDUNHUIDUN calls the PMS
ProtocolOpenTravel 2003/05 XML, HTTP Basic over TLSJSON with HMAC-SHA256 request signatures
Inventory ownerPMS (once connected, manual rate and availability edits on the HUIDUN side are blocked)PMS
What the PMS implementsFour messages: products, rates, availability, reservation pullEndpoints 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/cm

Production

Production credentials are issued after certification. Paths are identical to the sandbox; only the host name changes.

https://app.hui-dun.com/ota/cm

2. 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_id when 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/RoomTypeCode and rate plan codes RatePlanCode are 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/End are stay dates (inclusive) in YYYY-MM-DD format, up to 365 days per range.
  • AmountAfterTax is the tax-inclusive price per room per night. The entry without NumberOfGuests is the base rate; entries with it are occupancy-based rates.
  • CurrencyCode must match the hotel's configured currency (currently TWD); otherwise you receive 61.
  • One request may contain several RateAmountMessage elements. If any entry is invalid, nothing in the request is written and Errors lists 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". Without Restriction the room is closed; Arrival means no check-in on that date and Departure means no check-out on that date.
  • LengthsOfStay: SetMinLOS and SetMaxLOS (by arrival date), SetMinLOS_Through and SetMaxLOS_Through (applied to every night of the stay).
  • Restrictions sent without RatePlanCode apply 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.

ParameterDescription
hotel_idRequired. Hotel code
from/toISO 8601 times, filtered by the reservation's last-modified time; required when id is not given
limit10 to 200, default 10; sorted by modification time, oldest first
idOptional. Reservation number (ResID_Value)
  • ResStatus: Book for a new reservation (including held ones, see section 7), Modify for a change, Cancel for a cancellation.
  • The reservation number is in HotelReservationID ResID_Value and 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.

StageWhat the hub doesWhat the PMS sees when pulling
HoldConditional decrement night by night; if any night is short the whole booking fails. Holds last 10 minutes by default, 30 minutes at mostBook (you can close the room on other channels during payment)
ConfirmConfirmed after successful payment; the booking is created and recorded against the hotel's service-fee accountSame reservation, status unchanged
Expiry or cancellationInventory is restoredA 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).
  • idemKey is globally unique: resending the same idemKey returns 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 problems are 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.

MethodPathPurposeResponse
GET/availability?unit=&plan=&from=&to=&guests=Nightly availability and price{ "items": [{ "unit", "date", "available", "priceMinor" }] }
POST/quoteQuote{ "ok", "amountMinor", "currency", "quoteId", "expiresAt", "problems" }
POST/bookingsHold or book{ "ref", "status": "held" | "confirmed" | "pending", "holdUntil", "amountMinor", "currency" }
POST/bookings/{ref}/confirmConfirm a hold after payment{ "status": "confirmed" }
POST/bookings/{ref}/cancelCancel{ "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"}}
CodeMeaningWhat to do
497Authentication failedCheck credentials; do not retry automatically
450Unparseable or unsupported message, or body too largeFix the request
366Per-minute request limit exceededBack off and retry
448System errorRetry 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>
CodeMeaning
392Invalid hotel code, or this hotel has not selected your PMS
402Unknown room type code
425Unknown rate plan code, or not valid for this room type
15Invalid date format, end before start, or range longer than 365 days
61Currency does not match the hotel's setting
321Required field missing
448Too many segments in one request; split it into batches

Channel API (JSON)

HTTPerrorMeaning
401unauthorizedInvalid key
403scope_denied/channel_forbiddenThe key lacks this permission, or belongs to another channel
409quote_failedSold out, closed, minimum stay not met and so on; reasons are listed in problems
409idem_key_conflictThis idemKey is already used by another channel
400need_contact/bad_requestThe guest has neither phone nor email, or the request is malformed
404booking_not_foundThe 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.

  1. Product retrieval (OTA_HotelProduct) succeeded
  2. Rate push (OTA_HotelRateAmountNotif) succeeded
  3. Availability push (OTA_HotelAvailNotif) succeeded
  4. A channel hold created a booking
  5. A hold was confirmed
  6. A booking was cancelled, or a hold expired and was released
  7. 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.