宿泊予約サイト連携
概要
楽天トラベル・agoda・trip.com・booking.com の4社からAPIで宿泊情報を取得し、横断比較する。
Phase 2 実装状況(モック)
Phase 2 時点では、外部プロバイダー API ではなく自前の hotels テーブル(データベース設計 参照)からデータを返すモック実装になっています。フロントエンドから呼び出される実エンドポイントは以下のとおりです。
| メソッド | パス | 説明 |
|---|---|---|
| GET | /api/v1/hotels?destination=OKA | 目的地空港コードに紐付くホテル一覧を返却 |
hotels テーブルには name / description / image_url / price_per_night / rating / review_count / provider / booking_url / amenities が格納されており、Phase 3 で以下の 4 社 API 連携に置き換えます。それまでは /api/v1/accommodation/* の仕様書(本ページ以降)は Phase 3 の設計ガイド として参照してください。
対応サービス一覧
| サービス | API種別 | 申請要否 | 特徴 |
|---|---|---|---|
| 楽天トラベル | Rakuten Developers API | 要(無料) | 国内に強い、日本語対応完全 |
| agoda | Partner API (YCS) | 要(パートナー登録) | アジア圏に強い |
| trip.com | Affiliate API | 要(アフィリエイト登録) | 価格競争力高い |
| booking.com | Affiliate Partner API | 要(パートナー登録) | 世界最大の在庫 |
楽天トラベル API(Phase 1 で実装)
利用するAPI
| API | エンドポイント | 用途 |
|---|---|---|
| 施設検索API | /Travel/SimpleHotelSearch | エリア・キーワードでホテル検索 |
| 空室検索API | /Travel/VacantHotelSearch | 日程指定で空室・料金取得 |
| エリアコードAPI | /Travel/GetAreaClass | 都道府県・市区町村コード取得 |
リクエスト例(空室検索)
javascript
const params = {
applicationId: process.env.RAKUTEN_APP_ID,
checkinDate: '2024-07-20',
checkoutDate: '2024-07-22',
largeClassCode: 'japan',
middleClassCode: 'hokkaido',
smallClassCode: 'sapporo',
adultNum: 2,
hits: 20,
sort: '+roomCharge', // 料金安い順
};
const response = await fetch(
`https://app.rakuten.co.jp/services/api/Travel/VacantHotelSearch/20170426?${new URLSearchParams(params)}`
);レスポンスマッピング
javascript
function mapRakutenResponse(hotel) {
return {
provider: 'rakuten',
hotelName: hotel.hotelBasicInfo.hotelName,
pricePerNight: hotel.roomInfo[0].dailyCharge.price,
totalPrice: hotel.roomInfo[0].totalCharge,
rating: hotel.hotelBasicInfo.reviewAverage,
imageUrl: hotel.hotelBasicInfo.hotelImageUrl,
bookingUrl: hotel.hotelBasicInfo.planListUrl,
address: hotel.hotelBasicInfo.address1 + hotel.hotelBasicInfo.address2,
latitude: hotel.hotelBasicInfo.latitude,
longitude: hotel.hotelBasicInfo.longitude,
};
}agoda API(Phase 2)
Partner API (YCS)
- パートナー登録が必要
- REST API でホテル検索・料金取得
- アフィリエイト収益モデル
主要エンドポイント
| API | 用途 |
|---|---|
| Property Search | 施設検索 |
| Availability | 空室・料金取得 |
| Property Details | 施設詳細 |
trip.com API(Phase 2)
Affiliate API
- アフィリエイトパートナー登録が必要
- ホテル検索・料金比較
booking.com API(Phase 2)
Affiliate Partner API (via RapidAPI)
- RapidAPI 経由でアクセス可能
- 施設検索・料金・空室・レビュー取得
主要エンドポイント
| API | 用途 |
|---|---|
| /v1/hotels/search | ホテル検索 |
| /v1/hotels/data | ホテル詳細 |
| /v1/hotels/reviews | レビュー取得 |
統一レスポンス形式
全サービスのレスポンスを以下の統一フォーマットに変換する。
typescript
interface AccommodationResult {
// 基本情報
provider: 'rakuten' | 'agoda' | 'tripcom' | 'bookingcom';
hotelId: string;
hotelName: string;
// 料金
pricePerNight: number; // 1泊料金(税込・円)
totalPrice: number; // 合計料金(税込・円)
currency: 'JPY';
// 評価
rating: number; // 0-5 に正規化
reviewCount: number;
// 位置情報
address: string;
latitude: number;
longitude: number;
// メディア
imageUrl: string;
// 予約
bookingUrl: string; // アフィリエイトリンク
cancelPolicy: string; // キャンセルポリシー
// メタデータ
fetchedAt: string; // 取得日時
}キャッシュ戦略
外部APIの呼び出し回数を抑えるため、検索結果をキャッシュする。
| 項目 | 設定 |
|---|---|
| キャッシュ有効期間 | 1時間 |
| キャッシュキー | provider + destination + checkIn + checkOut + guests |
| 保存先 | SQLite (accommodation_cache テーブル) |
| 更新タイミング | キャッシュ期限切れ時に再取得 |
javascript
async function searchAccommodation(params) {
// キャッシュ確認
const cached = db.prepare(`
SELECT response_json FROM accommodation_cache
WHERE provider = ? AND destination = ?
AND check_in = ? AND check_out = ?
AND guests = ? AND expires_at > datetime('now')
`).get(params.provider, params.destination,
params.checkIn, params.checkOut, params.guests);
if (cached) {
return JSON.parse(cached.response_json);
}
// API 呼び出し
const result = await fetchFromProvider(params);
// キャッシュ保存
db.prepare(`
INSERT INTO accommodation_cache
(provider, destination, check_in, check_out, guests, response_json, expires_at)
VALUES (?, ?, ?, ?, ?, ?, datetime('now', '+1 hour'))
`).run(params.provider, params.destination,
params.checkIn, params.checkOut, params.guests,
JSON.stringify(result));
return result;
}料金比較ロジック
javascript
async function compareAccommodations(destination, checkIn, checkOut, guests) {
// 全プロバイダーに並行リクエスト
const results = await Promise.allSettled([
searchAccommodation({ provider: 'rakuten', destination, checkIn, checkOut, guests }),
searchAccommodation({ provider: 'agoda', destination, checkIn, checkOut, guests }),
searchAccommodation({ provider: 'tripcom', destination, checkIn, checkOut, guests }),
searchAccommodation({ provider: 'bookingcom', destination, checkIn, checkOut, guests }),
]);
// 成功したレスポンスのみ統合
const allHotels = results
.filter(r => r.status === 'fulfilled')
.flatMap(r => r.value);
// 同一ホテルをグループ化(名前の類似度で判定)
// 最安値順にソート
return allHotels.sort((a, b) => a.pricePerNight - b.pricePerNight);
}