PMS・チャネルと HUIDUN 客室在庫ミドルプラットフォームの連携
本仕様は PMS ベンダーおよび技術担当者向けです。PMS 側で実装が必要なのは、OpenTravel 2003/05 標準メッセージ 4 種類のみです。予約の受付、仮押さえ、オーバーブッキング防止、照合はミドルプラットフォームが処理します。掲載しているサンプルはすべてサンドボックスの実際のリクエストとレスポンスです。
1. 概要
HUIDUN には 2 つの連携モードがあります。大半の PMS はモード A を使用し、ご自身で完了できます。モード B は、リアルタイムの照会のみ提供でき、送信機能を持たないシステム向けで、案件ごとに開通します。
| モード A:送信と予約取得(標準) | モード B:リアルタイムコネクタ(案件ごと) | |
|---|---|---|
| 呼び出しの方向 | PMS が HUIDUN を呼び出す | HUIDUN が PMS を呼び出す |
| プロトコル | OpenTravel 2003/05 XML、TLS 上の HTTP Basic | JSON、HMAC-SHA256 によるマシン署名 |
| 在庫の持ち主 | PMS(連携後は HUIDUN 側での手動による料金・在庫の変更は拒否されます) | PMS |
| PMS 側で実装するもの | 客室タイプの取得、料金の送信、在庫の送信、予約の取得の 4 種類のメッセージ | 照会、見積り、仮押さえ、予約、キャンセル、予約照会の各エンドポイントの提供 |
サンドボックス
独立したテスト用データベースです。お申し込みと同時に開通し、30 日後に停止します。
https://huidun-demo.luma-e61.workers.dev/ota/cm本番環境
認定に合格すると本番用の認証情報を発行します。パスはサンドボックスと同じで、ホスト名のみが異なります。
https://app.hui-dun.com/ota/cm2. 認証
- すべてのリクエストに
Authorization: Basic base64(ユーザー名:パスワード)を付与し、通信は常に HTTPS とします。HUIDUN はパスワードのハッシュ値のみを保存するため、紛失した場合は再設定のみ可能です。 HotelCode(予約取得ではhotel_id)は、ホテルが HUIDUN の管理画面「チャネル連携」で貴社の PMS を選択した際に設定するコードです。ホテルが貴社の PMS を選択していない場合、このコードは貴社のアカウントには存在しないものとして扱われます(392 を返します)。- 客室タイプコード
InvTypeCode/RoomTypeCodeとプランコードRatePlanCodeは、客室タイプ取得で返されたものが正となり、対応表は PMS 側で作成します。 - アカウントごとに 1 分あたり 1,200 リクエストが上限で、超過すると 366 を返します。1 リクエストの本文は 1 MB が上限です。
3. 客室タイプとプランの取得 OTA_HotelProduct
このホテルの HUIDUN 上の客室タイプとプランの組み合わせを取得します。「客室タイプ × プラン」の組ごとに 1 つの HotelProduct が返ります。PaymentCollection は HotelCollect(現地払い)または OTACollect(オンライン事前決済)です。
POST https://huidun-demo.luma-e61.workers.dev/ota/cm/OTA_HotelProduct
Authorization: Basic base64(ユーザー名:パスワード)
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>
<!-- 「客室タイプ × プラン」の組ごとに 1 つの HotelProduct -->
</HotelProducts>
<TPA_Extensions><Pricing Type="Standard" /></TPA_Extensions>
</OTA_HotelProductRS>4. 日別料金の送信 OTA_HotelRateAmountNotif
Start/Endは宿泊日(開始日と終了日を含む)で、形式はYYYY-MM-DD、1 区間は最大 365 日です。AmountAfterTaxは 1 室 1 泊あたりの税込料金です。NumberOfGuestsを指定しないものが基本料金で、指定した場合はその人数の料金となります。CurrencyCodeはホテルに設定された通貨(現在は TWD)と一致している必要があり、一致しない場合は 61 を返します。- 1 つのリクエストに複数の
RateAmountMessageを含められます。いずれか 1 件でもエラーがあると、リクエスト全体が書き込まれず、Errorsに各エラーが列挙されます(最大 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. 在庫と制限の送信 OTA_HotelAvailNotif
BookingLimit:その期間に 1 泊あたり何室販売できるか(0 から 9999)。ミドルプラットフォームは予約の成立時に、この数値から宿泊日ごとに減算します。RestrictionStatus:Status="Close"/"Open"。Restrictionを指定しない場合は販売停止、Arrivalはその日のチェックイン不可、Departureはその日のチェックアウト不可を表します。LengthsOfStay:SetMinLOS、SetMaxLOS(チェックイン日を基準)、SetMinLOS_Through、SetMaxLOS_Through(宿泊期間の各泊を基準)。- 制限系で
RatePlanCodeを指定しない場合、その客室タイプで現在適用可能なすべてのプランに適用されます。
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">
<!-- 在庫:BookingLimit=この期間の1泊あたりの販売可能室数 -->
<AvailStatusMessage BookingLimit="3">
<StatusApplicationControl Start="2026-11-04" End="2026-11-05" InvTypeCode="STD"/>
</AvailStatusMessage>
<!-- 販売停止/チェックイン不可/最低宿泊数(RatePlanCode を指定しない場合は、その客室タイプのすべてのプランに適用) -->
<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. 予約の取得 OTA_HotelResNotif
「最終更新日時」の範囲を指定して予約を取得するか、id で単一の予約を照会します。1 分から 5 分ごとの取得を推奨します。範囲の前後を少し重ねて指定し、予約番号で重複を除いてください。
| パラメータ | 説明 |
|---|---|
hotel_id | 必須。ホテルコード |
from/to | ISO 8601 形式の日時。予約の最終更新日時で絞り込みます。id を指定しない場合は必須 |
limit | 10 から 200、既定値は 10。更新日時の古い順に並びます |
id | 任意。予約番号(ResID_Value) |
ResStatus:Bookは成立(仮押さえ中を含む。第 7 節参照)、Modifyは変更済み、Cancelはキャンセル済みを表します。- 予約番号は
HotelReservationID ResID_Valueにあり、同じ予約の番号は変わりません。変更やキャンセルの際も、同じ番号でもう一度現れます。 PrepaidIndicator="true"は、旅行者がオンラインで決済済みであることを示し、ホテルが旅行者から宿泊料金を別途受け取る必要はありません。
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(ユーザー名:パスワード)
# 単一の予約のみを照会する場合:
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. 仮押さえ、確定、オーバーブッキング防止
チャネルからの予約は常に三段階方式で処理され、在庫は仮押さえの時点で確保されます。2 つのチャネルが最後の 1 室を同時に争った場合、負けた側は決済の前に「満室」の応答を受け取るため、旅行者が支払ったのに部屋がないという事態は起こりません。
| 段階 | ミドルプラットフォームの処理 | PMS が予約取得で見えるもの |
|---|---|---|
| 仮押さえ | 宿泊日ごとに条件付きで在庫を減算し、1 泊でも不足すれば予約全体が成立しません。仮押さえの既定は 10 分、最長 30 分です | Book(決済待ちの数分間でも、他のチャネルの在庫を先に減らせます) |
| 確定 | 決済の成功後に確定し、予約が成立してホテルのサービス手数料の勘定に計上されます | 同じ予約で、ステータスは変わりません |
| 時間切れまたはキャンセル | 在庫が戻されます | すでに取得した予約は Cancel として再度現れます。一度も取得されていない時間切れの仮押さえは現れません |
- ホテルごとに確保室数(バッファ。既定は 1 室)を設定できます。PMS から送信された在庫のうち、この室数は販売せずに確保し、双方の同期の間に生じる時間差を吸収します。
- 連携後の在庫の持ち主は PMS です。HUIDUN 管理画面での手動による料金・在庫の変更は拒否されますので、PMS 側で変更したうえで送信してください。
- PMS が予約を取得した後に在庫を送信しても、ミドルプラットフォームは取得済みの予約を識別するため、二重に減算されることはありません。
8. チャネル API(販売側が使用。サンドボックスでは予約の模擬に使用)
販売側のチャネルは /api/mp を通じて、在庫の照会、見積り、予約を行います。PMS がこの節を実装する必要はありません。サンドボックスでは、テスト用のチャネルキーを発行しますので、実際の予約を待たなくても、予約が予約取得の結果にどのように現れるかを確認できます。
POST /api/mp/quote
X-API-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
{ ...上記と同じ, "hold":true, "holdMinutes":10, "idemKey":"order-10086",
"guest":{"name":"王小明","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"}- 金額の単位は最小通貨単位です(TWD の場合は元)。
idemKeyは全体で一意です。同じidemKeyで再送すると同じ予約が返り(reused:true)、2 件にはなりません。- 宿泊者には、氏名と、電話番号またはメールアドレスのいずれかが少なくとも必要です。
- 客室名や
problemsなどの文字列は、ホテルが設定した言語で返されます。
9. リアルタイムコネクタとマシン署名(モード B)
PMS がリアルタイムの照会のみ提供できる場合は、HUIDUN が PMS のエンドポイントを呼び出します。PMS には、販売可能数の照会、見積り、仮押さえ、予約、キャンセル、予約照会の各機能に加え、対応機能の宣言(仮押さえ、キャンセル、即時確定に対応しているか)を提供していただく必要があります。このモードは案件ごとの開通となりますので、申込フォームで「当社でリアルタイムの照会・予約 API を提供する」を選択してください。
HUIDUN が送信するすべてのリクエストには Authorization: Bearer トークン と 2 つの署名ヘッダーが付与されます。PMS 側で検証を行ってから処理してください。
x-hd-timestamp: 1792908100105 (Unix ミリ秒)
x-hd-signature: hex( HMAC-SHA256( 共有シークレット, timestamp + "." + リクエスト本文の原文 ) )
検証ルール:
1. |現在時刻 − timestamp| ≤ 5 分。超過した場合は拒否する(リプレイ攻撃の防止)
2. 受信した生のバイト列で計算する。JSON を解析してから再度シリアライズしない
3. 署名文字列は定数時間で比較する// Node.js での検証例
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));
}エンドポイント
HUIDUN は開通時に登録した API ベース URL を基準に、上記の署名を付けて次のエンドポイントを呼び出します。リクエストとレスポンスは JSON です。金額は最小単位の整数で、新台湾ドル・円・ウォンは 1 単位、その他の通貨はセント単位です。日付はホテル現地の YYYY-MM-DD、to は当日を含まず、1 回の照会は最大 31 泊です。
| メソッド | パス | 用途 | レスポンス |
|---|---|---|---|
GET | /availability?unit=&plan=&from=&to=&guests= | 泊ごとの在庫と料金 | { "items": [{ "unit", "date", "available", "priceMinor" }] } |
POST | /quote | 見積 | { "ok", "amountMinor", "currency", "quoteId", "expiresAt", "problems" } |
POST | /bookings | 仮押さえまたは確定予約 | { "ref", "status": "held" | "confirmed" | "pending", "holdUntil", "amountMinor", "currency" } |
POST | /bookings/{ref}/confirm | 決済後に仮押さえを確定 | { "status": "confirmed" } |
POST | /bookings/{ref}/cancel | キャンセル | { "status": "cancelled" } |
GET | /bookings/{ref} | 予約照会 | { "status" } |
/quote の本文:{ unit, plan, day, nights, guests }。/bookings の本文には quoteId、guest { name, phone, email }、idemKey、hold(真偽値)と仮押さえ分数 holdMinutes を加えます。キャンセルの本文には idemKey を含めます。
同じ idemKey で再送された場合は同じ予約を返し、在庫を二重に引き当てないでください。期限までに確定されなかった仮押さえは自動で解放し、その後の確定要求には 409 と hold_expired を返してください。仮押さえとキャンセルへの対応可否は開通時に登録します。
失敗時は 2xx 以外のステータスと { "error": "コード", "message": "説明" } を返してください。次の業務エラー(ステータス 404、409、422)はコードのまま販売チャネルへ伝えます:sold_out、stay_restricted、price_changed、quote_expired、hold_expired、invalid_guests、not_cancellable、booking_not_found。それ以外のコード、401、429、5xx、タイムアウト(8 秒)は一時的な障害として扱い、チャネルには再試行を案内します。メッセージ本文は転送しません。
共有シークレットは開通時に別途お渡しします。ページやメール本文に記載されることはありません。
10. エラーコード
解析前のエラー:HTTP 505+JSON
HTTP/1.1 505
Content-Type: application/json
{"error":{"code":"497","message":"Authentication error","timestamp":"2026-10-25T14:01:40+08:00"}}| コード | 意味 | 対処方法 |
|---|---|---|
| 497 | 認証失敗 | 認証情報を確認してください。自動での再試行はしないでください |
| 450 | 解析不能、非対応のメッセージ、または本文が大きすぎる | リクエスト内容を修正してください |
| 366 | 1 分あたりのリクエスト上限を超過 | 間隔をあけて再試行してください |
| 448 | システムエラー | しばらくしてから再試行してください |
業務エラー:HTTP 200+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>| コード | 意味 |
|---|---|
| 392 | ホテルコードが無効、またはこのホテルが貴社の PMS を選択していない |
| 402 | 客室タイプコードが存在しない |
| 425 | プランコードが存在しない、またはこの客室タイプに適用できない |
| 15 | 日付の形式が誤っている、終了日が開始日より前、または期間が 365 日を超えている |
| 61 | 通貨がホテルの設定と一致しない |
| 321 | 必須項目が不足している |
| 448 | 1 つのリクエストに含まれる区間が多すぎるため、分割してください |
チャネル API(JSON)
| HTTP | error | 意味 |
|---|---|---|
| 401 | unauthorized | キーが無効 |
| 403 | scope_denied/channel_forbidden | キーにこの操作の権限がない、またはこのチャネルのキーではない |
| 409 | quote_failed | 満室、販売停止、最低宿泊数を満たさないなど。理由は problems に列挙されます |
| 409 | idem_key_conflict | この idemKey はすでに別のチャネルで使用されています |
| 400 | need_contact/bad_request | 宿泊者の電話番号とメールアドレスがない、またはリクエスト形式が誤っている |
| 404 | booking_not_found | 予約が存在しない、またはこのチャネルの予約ではない |
11. 認定検証 7 項目
サンドボックスの検証表は、サンドボックスデータベースの実際の記録に基づいて自動で判定されます。7 項目すべてが完了すれば、本番認定を申請できます。
- 客室タイプとプランの取得(OTA_HotelProduct)の成功
- 料金の送信(OTA_HotelRateAmountNotif)の成功
- 在庫の送信(OTA_HotelAvailNotif)の成功
- チャネルでの仮押さえによる予約 1 件の成立
- 仮押さえの確定の成立
- 予約 1 件のキャンセル、または仮押さえの時間切れによる解放
- 予約の成立後の予約取得(OTA_HotelResNotif)の成功
サンドボックスをワンクリックで実行 のページで、実行のたびにこのチェック表を表示します。JSON でも取得できます:https://huidun-demo.luma-e61.workers.dev/sandbox/pms/progress?ref=ホテルコード&format=json(完了の有無と日時のみを返し、認証情報や個人情報は含まれません)。
12. サンドボックスの制限
- 開通のたびにサンドボックス用ホテルを 1 軒作成します。客室タイプ 2 種類(STD、DLX)× プラン 2 種類(RO は素泊まり・現地払い、BB は朝食付き・オンライン事前決済)で、今後 30 日分の料金と在庫があらかじめ設定され、確保室数は 1 室です。
- サンドボックスは 30 日後に停止します。同じネットワークからは 1 時間あたり最大 3 つまで開通できます。
- サンドボックスのチャネルのサービス手数料は 0 であり、本番の料金体系を示すものではありません。
- サンドボックスは独立したテスト用データベースで、本番環境とのデータのやり取りは一切ありません。実在する旅行者のデータは入力しないでください。