travel.book_hotel — Full Intent Specification
INTENT NAMESPACE: travel
INTENT NAME: book_hotel
FULL ID: travel.book_hotel
VERSION: v1.0.0
STATUS: live
LAST UPDATED: 2026-05-09
TTBS WEIGHTS: time 0.20 · taste 0.30 · budget 0.30 · safety 0.20
This is the gold-reference intent spec. Other intent specs follow this exact shape.
1. NATURAL LANGUAGE COVERAGE
Classifies IN
- "hotel in Goa for next weekend"
- "I need a place to stay in Bangalore Friday to Sunday"
- "book me an Airbnb in Hyderabad for 2 nights"
- "find a homestay near MG Road for 4 people"
- "weekend stay in Lonavala under 5k a night"
- "room for tonight near Hyderabad airport"
- "boutique hotel in Pondicherry for our anniversary, sea view"
- "resort in Coorg with a pool, family of 4"
- "service apartment in Whitefield for the next 2 weeks"
- "guest house in Dharamshala for 5 nights"
Classifies OUT — borderline NO
- "PG in Koramangala" →
real_estate.find_pg - "rent a flat in Whitefield" →
real_estate.lease_residential - "book a meeting room" →
lifestyle.book_workspace - "where can I stay with friends in Mumbai" (free stay) →
social.couchsurf(not v1) - "hotel for a wedding venue" →
entertainment.book_venue
MULTI-INTENT TRIGGERS
- "Goa trip for 3 days" → fans out to
travel.book_hotel+travel.book_flight+mobility.book_intercity - "honeymoon package to Bali" →
travel.book_package(which internally orchestratesbook_hotel+book_flight) - "weekend in Pondicherry, hotel and a car" →
travel.book_hotel+mobility.book_self_drive
2. INPUT — TOMO → PROVIDER
The exact request body sent to the provider's search_availability tool.
{
"intent": "travel.book_hotel",
"intent_version": "v1.0.0",
"request_id": "req_8f3k2m_2026-05-09T14:32:00Z",
"user_session_id": "anon_user_token_or_uid",
"destination": {
"kind": "city",
"city": "Bangalore",
"lat": 12.9716,
"lng": 77.5946,
"address": "MG Road, Bangalore",
"country_code": "IN",
"search_radius_km": 8
},
"dates": {
"check_in": "2026-05-15",
"check_out": "2026-05-17",
"nights": 2,
"timezone": "Asia/Kolkata",
"flexible_days": 0
},
"party": {
"adult_count": 2,
"children_ages": [],
"infants": 0,
"room_count": 1,
"guest_count": 2
},
"preferences": {
"budget_band": "good",
"budget_max_inr_per_night": 5000,
"budget_max_inr_total": 10000,
"kind_filter": ["hotel", "homestay"],
"star_rating_min": null,
"amenities_must_have": ["wifi"],
"amenities_nice_to_have": ["breakfast", "parking"],
"free_cancellation_required": false,
"pay_at_property_acceptable": true,
"verified_property_required": true,
"lgbtq_welcoming_required": false,
"female_traveler_safety_required": false,
"accessibility_step_free_required": false,
"pet_friendly_required": false
},
"context": {
"user_locale": "en-IN",
"user_currency_pref": "INR",
"trip_purpose": "leisure",
"trust_signals": {
"is_repeat_traveler": false,
"prior_bookings_with_partner": 0,
"user_account_age_days": 312
}
}
}
Field-by-field contract
| Field | Type | Constraint | Notes |
|---|---|---|---|
intent |
string | REQUIRED, must equal "travel.book_hotel" |
partner uses to route internally |
intent_version |
string | REQUIRED, semver | partner rejects unknown major versions |
request_id |
string | REQUIRED, unique per call | partner echoes in error responses |
user_session_id |
string | REQUIRED, opaque to partner | used for rate-limit accounting |
destination.kind |
enum | REQUIRED, STRICT ENUM city | lat_lng | address |
exactly one resolution kind |
destination.city |
string | REQUIRED if kind=city |
always non-null when supplied |
destination.lat |
float | REQUIRED if kind=lat_lng |
-90 to 90 |
destination.lng |
float | REQUIRED if kind=lat_lng |
-180 to 180 |
destination.address |
string | REQUIRED if kind=address |
passed verbatim |
destination.country_code |
string | REQUIRED, ISO_3166_2, always IN for v1 |
locked |
destination.search_radius_km |
int | REQUIRED, 1-50 | filter scope |
dates.check_in |
date | REQUIRED, ISO_DATE, today or future | partner rejects past |
dates.check_out |
date | REQUIRED, ISO_DATE, > check_in | partner rejects same-day |
dates.nights |
int | REQUIRED, derived | match check_out − check_in |
dates.timezone |
string | REQUIRED, IANA, always Asia/Kolkata for v1 |
locked |
dates.flexible_days |
int | REQUIRED, 0-7, 0 means strict dates | partner may show alternates if >0 |
party.adult_count |
int | REQUIRED, ≥1 | |
party.children_ages |
int[] | REQUIRED, may be empty array | each age 0-17 |
party.infants |
int | REQUIRED, ≥0 | under 2 yrs |
party.room_count |
int | REQUIRED, ≥1 | |
party.guest_count |
int | REQUIRED, derived = adult_count + children + infants | |
preferences.budget_band |
enum | REQUIRED, STRICT ENUM ok | good | great |
|
preferences.budget_max_inr_per_night |
int | REQUIRED, INR_INTEGER | hard ceiling, partner must filter |
preferences.budget_max_inr_total |
int | REQUIRED, INR_INTEGER | hard ceiling for stay total |
preferences.kind_filter |
array |
REQUIRED, non-empty subset of §6 kind enum |
|
preferences.star_rating_min |
int or null | REQUIRED, 0-5 or null | null = no filter |
preferences.amenities_must_have |
array |
REQUIRED, may be empty | filtered hard, see §6 |
preferences.amenities_nice_to_have |
array |
REQUIRED, may be empty | scored softly |
preferences.free_cancellation_required |
boolean | REQUIRED | hard filter |
preferences.pay_at_property_acceptable |
boolean | REQUIRED | filter or scoring hint |
preferences.verified_property_required |
boolean | REQUIRED | hard filter |
preferences.lgbtq_welcoming_required |
boolean | REQUIRED | hard filter |
preferences.female_traveler_safety_required |
boolean | REQUIRED | hard filter |
preferences.accessibility_step_free_required |
boolean | REQUIRED | hard filter |
preferences.pet_friendly_required |
boolean | REQUIRED | hard filter |
context.user_locale |
string | REQUIRED, RFC_3066_LOCALE, always en-IN for v1 |
|
context.user_currency_pref |
string | REQUIRED, always INR for v1 |
partner converts |
context.trip_purpose |
enum | REQUIRED, STRICT ENUM leisure | business | medical | family_emergency |
|
context.trust_signals.is_repeat_traveler |
boolean | REQUIRED | TOMO-side cohort label |
context.trust_signals.prior_bookings_with_partner |
int | REQUIRED, ≥0 | TOMO-known count |
context.trust_signals.user_account_age_days |
int | REQUIRED, ≥0 |
Anti-fabrication preamble (universal)
TOMO will never inject paid_placement signals, urgency text, or commission-influenced fields into this payload. Provider may not reject the request based on TOMO's commission rate. Provider may not return ad units, sponsored placements, or non-organic listings.
3. PROVIDER TOOLS
Tool 1: search_availability
PURPOSE: return all listings matching the request
INPUT: §2 request body
OUTPUT: { listings: Listing[], result_token, expires_at }
SLA: p50 < 600ms, p95 < 1500ms, p99 < 3000ms
RATE LIMIT: TOMO sends ≤ 1/sec per (user_session_id, partner)
IDEMPOTENCY: not required (read-only)
RETRY: TOMO retries once on 5xx with exponential backoff (1s, 4s)
RESULT SET: up to 50 listings, partner ordering ignored (TOMO re-ranks)
Tool 2: get_listing
PURPOSE: return full ListingDetail for one listing
INPUT: { listing_id, request_id, user_session_id, dates, party }
OUTPUT: ListingDetail (§5)
SLA: p50 < 300ms, p95 < 800ms
RATE LIMIT: ≤ 5/sec per user_session_id
IDEMPOTENCY: not required
RETRY: retry once on 5xx
INVALIDATION: listing_id from a search_availability call expires at
expires_at; partner returns LISTING_EXPIRED after that
Tool 3: create_booking
PURPOSE: commit a booking
INPUT: { listing_id, room_id, dates, party, payment_token,
request_id, idempotency_key, guest_details }
OUTPUT: { booking_ref, status, confirmation_email_sent,
total_amount_inr, currency, cancellation_until,
partner_support_phone, partner_support_email }
SLA: p50 < 2000ms, p95 < 5000ms, p99 < 10000ms
IDEMPOTENCY: REQUIRED — duplicate idempotency_key returns same booking_ref
RETRY: TOMO never retries automatically; user-driven retry only
PAYMENT: payment_token is one-time-use; partner debits user via token
TOMO never holds funds
Tool 4: cancel_booking
PURPOSE: cancel an existing booking
INPUT: { booking_ref, reason, request_id, user_session_id }
OUTPUT: { status, refund_amount_inr, refund_eta_days,
refund_method, cancellation_confirmation_id }
SLA: p95 < 3000ms
IDEMPOTENCY: REQUIRED — duplicate cancel returns same outcome
RETRY: TOMO retries once on 5xx
Tool 5: modify_booking
PURPOSE: change dates, party size, or room within a booking
INPUT: { booking_ref, change_set, request_id }
OUTPUT: { status, new_total_inr, fees_inr, new_cancellation_until }
SLA: p95 < 3000ms
IDEMPOTENCY: REQUIRED
All five tools MUST be implemented. No partial implementations accepted.
4. RESPONSE SHAPE — THE LISTING CONTRACT
Every listing returned by search_availability MUST contain every field below. Partners that lack a field must collect, derive, or contract a third-party data provider. Listings missing any field are REJECTED at TOMO ingest — the partner sees a structured rejection log.
Listing (returned by search_availability)
id: string, REQUIRED # partner's internal id, opaque to TOMO
merchant_id: string, REQUIRED # canonical id (Google Place ID preferred)
listing_token: string, REQUIRED # opaque, used in get_listing/create_booking
expires_at: ISO_DATETIME, REQUIRED # token validity
name: string, REQUIRED # "Taj West End"
official_name: string, REQUIRED # legal property name on registration
brand: string, REQUIRED # parent chain or "Independent"
kind: STRICT ENUM, REQUIRED # see §6
sub_kind: STRICT ENUM, REQUIRED # see §6
price:
total_inr: INR_INTEGER, REQUIRED
per_night_inr: INR_INTEGER, REQUIRED
per_room_per_night_inr: INR_INTEGER, REQUIRED
currency: string, REQUIRED, always "INR"
taxes_included: boolean, REQUIRED
fees_breakdown: array, REQUIRED, ≥1 entry
- label: string, REQUIRED # "Room subtotal"
amount_inr: INR_INTEGER, REQUIRED
kind: STRICT ENUM, REQUIRED # see §6
base_rate_inr: INR_INTEGER, REQUIRED # before discounts
discount_inr: INR_INTEGER, REQUIRED, ≥0
discount_reason: string, REQUIRED, "" if no discount
payable_now_inr: INR_INTEGER, REQUIRED
payable_at_property_inr: INR_INTEGER, REQUIRED
refundable_amount_inr: INR_INTEGER, REQUIRED # if cancelled now
conversion_rate_used: float, REQUIRED # 1.0 if natively INR
location:
address_line_1: string, REQUIRED
address_line_2: string, REQUIRED, "" allowed
neighborhood: string, REQUIRED
city: string, REQUIRED
state: string, REQUIRED
pincode: string, REQUIRED
country_code: string, REQUIRED, ISO_3166_2
lat: float, REQUIRED
lng: float, REQUIRED
what3words: string, REQUIRED # 3-word location code
google_place_id: string, REQUIRED
distance_from_user_km: float, REQUIRED
distance_to_nearest_metro_km: float, REQUIRED
distance_to_nearest_metro_name: string, REQUIRED
distance_to_nearest_airport_km: float, REQUIRED
distance_to_nearest_airport_iata: string, REQUIRED
distance_to_nearest_railway_km: float, REQUIRED
distance_to_nearest_railway_name: string, REQUIRED
distance_to_nearest_hospital_km: float, REQUIRED
distance_to_nearest_hospital_name: string, REQUIRED
distance_to_nearest_pharmacy_km: float, REQUIRED
distance_to_nearest_atm_km: float, REQUIRED
distance_to_nearest_grocery_km: float, REQUIRED
distance_to_nearest_petrol_pump_km: float, REQUIRED
distance_to_nearest_ev_charger_km: float, REQUIRED
walk_score: int, REQUIRED, 0-100
transit_score: int, REQUIRED, 0-100
media:
thumbnail_url: URL, REQUIRED
thumbnail_width_px: int, REQUIRED
thumbnail_height_px: int, REQUIRED
hero_url: URL, REQUIRED
photo_count: int, REQUIRED, ≥1
photos_url: URL, REQUIRED # JSON manifest endpoint
virtual_tour_url: URL, REQUIRED # may be the photos_url if no tour
video_walkthrough_url: URL, REQUIRED # may be empty-string sentinel
last_photos_updated: ISO_DATETIME, REQUIRED
ratings:
star_rating: int, REQUIRED, 0-5 # 0 = unrated
star_rating_authority: STRICT ENUM, REQUIRED # see §6
guest_review_score: float, REQUIRED, 0-10
guest_review_count: int, REQUIRED, ≥0
review_score_label: STRICT ENUM, REQUIRED # see §6
recent_30day_review_count: int, REQUIRED, ≥0
recent_30day_score: float, REQUIRED, 0-10
recent_90day_score: float, REQUIRED, 0-10
recent_365day_score: float, REQUIRED, 0-10
solo_traveler_score: float, REQUIRED, 0-10
solo_traveler_count: int, REQUIRED, ≥0
family_score: float, REQUIRED, 0-10
family_count: int, REQUIRED, ≥0
business_score: float, REQUIRED, 0-10
business_count: int, REQUIRED, ≥0
couples_score: float, REQUIRED, 0-10
couples_count: int, REQUIRED, ≥0
group_score: float, REQUIRED, 0-10
group_count: int, REQUIRED, ≥0
category_scores: object, REQUIRED # see structure below
cleanliness: float, REQUIRED, 0-10
comfort: float, REQUIRED, 0-10
location: float, REQUIRED, 0-10
facilities: float, REQUIRED, 0-10
staff: float, REQUIRED, 0-10
value_for_money: float, REQUIRED, 0-10
free_wifi: float, REQUIRED, 0-10
amenities: array<enum>, REQUIRED, ≥1
amenities_freshness_date: ISO_DATETIME, REQUIRED
amenities_verification_method: STRICT ENUM, REQUIRED # see §6
policy:
cancellation: STRICT ENUM, REQUIRED # free | partial | non_refundable
cancellation_policy_text: string, REQUIRED
free_cancel_until: ISO_DATETIME, REQUIRED # may be epoch sentinel if non_refundable
partial_cancel_schedule: array, REQUIRED, may be empty
- cutoff_iso: ISO_DATETIME, REQUIRED
refund_pct: int, REQUIRED, 0-100
pay_at_property: boolean, REQUIRED
deposit_required: boolean, REQUIRED
deposit_amount_inr: INR_INTEGER, REQUIRED
deposit_refundable: boolean, REQUIRED
minimum_age_check_in: int, REQUIRED, 0-100 # 18 in India typically
unmarried_couples_allowed: boolean, REQUIRED # locally relevant
pet_friendly: boolean, REQUIRED
pets_max_count: int, REQUIRED # 0 if not pet_friendly
pets_size_limit: STRICT ENUM, REQUIRED # none | small | medium | large
pets_fee_inr: INR_INTEGER, REQUIRED # 0 if not allowed
smoking_allowed: boolean, REQUIRED
smoking_zones: STRICT ENUM, REQUIRED # none | designated_outdoor | balcony | full_property
alcohol_allowed: boolean, REQUIRED
alcohol_served: boolean, REQUIRED
vegetarian_only: boolean, REQUIRED
jain_food_available: boolean, REQUIRED
halal_food_available: boolean, REQUIRED
lgbtq_welcoming: boolean, REQUIRED
lgbtq_welcoming_self_declared: boolean, REQUIRED # property declared vs verified
female_staff_on_site_24x7: boolean, REQUIRED
female_only_floor_available: boolean, REQUIRED
child_policy_max_age_free: int, REQUIRED # 0 if no free child policy
extra_bed_available: boolean, REQUIRED
extra_bed_inr: INR_INTEGER, REQUIRED
parking_charges_inr_per_night: INR_INTEGER, REQUIRED
parking_for_two_wheelers: boolean, REQUIRED
parking_for_four_wheelers: boolean, REQUIRED
ev_charging_charges_inr: INR_INTEGER, REQUIRED # 0 if free or absent
early_check_in_charges_inr: INR_INTEGER, REQUIRED
late_check_out_charges_inr: INR_INTEGER, REQUIRED
check_in_time: string, REQUIRED # "14:00"
check_out_time: string, REQUIRED # "11:00"
trust:
verified_property: boolean, REQUIRED
verification_method: STRICT ENUM, REQUIRED # see §6
partner_account_age_days: int, REQUIRED
last_property_audit_date: ISO_DATETIME, REQUIRED
tomo_field_team_audited: boolean, REQUIRED # set by TOMO ops, not partner
property_registration_certificate_present: boolean, REQUIRED
property_registration_authority: STRICT ENUM, REQUIRED # see §6
fire_safety_certified: boolean, REQUIRED
fire_safety_last_inspected: ISO_DATETIME, REQUIRED
emergency_exit_count: int, REQUIRED
cctv_in_common_areas: boolean, REQUIRED
cctv_storage_days: int, REQUIRED
staff_kyc_completed_pct: int, REQUIRED, 0-100
emergency_response_avg_minutes: int, REQUIRED
property:
year_built: int, REQUIRED
year_last_renovated: int, REQUIRED
total_rooms: int, REQUIRED
total_floors: int, REQUIRED
has_elevator: boolean, REQUIRED
has_generator_backup: boolean, REQUIRED
generator_backup_capacity_pct: int, REQUIRED, 0-100 # 100 = full hotel
water_supply: STRICT ENUM, REQUIRED # municipal | borewell | tanker | mixed
water_24x7: boolean, REQUIRED
ro_water_in_rooms: boolean, REQUIRED
hot_water_24x7: boolean, REQUIRED
power_backup_for_rooms: boolean, REQUIRED
air_quality_aqi_avg_30day: int, REQUIRED, ≥0 # local AQI average
noise_level_db_day_avg: float, REQUIRED, ≥0
noise_level_db_night_avg: float, REQUIRED, ≥0
room_summary:
size_sqft_min: int, REQUIRED
size_sqft_max: int, REQUIRED
bed_configurations_offered: array<string>, REQUIRED, ≥1
max_occupancy: int, REQUIRED
ac_type: STRICT ENUM, REQUIRED # split | central | window | none
wifi_speed_mbps_avg: int, REQUIRED, ≥0
wifi_complimentary: boolean, REQUIRED
power_outlets_per_room_avg: int, REQUIRED
power_outlets_near_bed_avg: int, REQUIRED
usb_outlets_per_room_avg: int, REQUIRED
smart_tv_with_otts: array<STRICT ENUM>, REQUIRED # see §6, may be empty array
blackout_curtains: boolean, REQUIRED
soundproofing_rating: STRICT ENUM, REQUIRED # see §6
natural_light_orientation: STRICT ENUM, REQUIRED # north | south | east | west | mixed
view_kind: STRICT ENUM, REQUIRED # see §6
bathroom_kind: STRICT ENUM, REQUIRED # attached_private | shared_floor | common
bath_or_shower: STRICT ENUM, REQUIRED # bath | shower | both
hot_water_type: STRICT ENUM, REQUIRED # solar | electric | gas | central
toiletries_provided: array<enum>, REQUIRED # see §6 (may be empty)
hair_dryer_available: boolean, REQUIRED
iron_available: boolean, REQUIRED
in_room_safe: boolean, REQUIRED
mini_fridge: boolean, REQUIRED
electric_kettle: boolean, REQUIRED
tea_coffee_complimentary: boolean, REQUIRED
bottled_water_complimentary_per_day_count: int, REQUIRED
food:
breakfast_included: boolean, REQUIRED
breakfast_kind: STRICT ENUM, REQUIRED # buffet | continental | indian | mixed | none
breakfast_inr_if_not_included: INR_INTEGER, REQUIRED # 0 if included or unavailable
in_house_restaurant_count: int, REQUIRED
room_service_available: boolean, REQUIRED
room_service_24x7: boolean, REQUIRED
cuisines_offered: array<enum>, REQUIRED, may be empty # see §6
veg_only_kitchen: boolean, REQUIRED
jain_meals_available: boolean, REQUIRED
halal_meals_available: boolean, REQUIRED
facilities:
pool: boolean, REQUIRED
pool_kind: STRICT ENUM, REQUIRED # outdoor | indoor | rooftop | infinity | none
pool_temperature_controlled: boolean, REQUIRED
gym: boolean, REQUIRED
gym_24x7: boolean, REQUIRED
spa: boolean, REQUIRED
conference_rooms_count: int, REQUIRED
business_center: boolean, REQUIRED
laundry_service: boolean, REQUIRED
dry_cleaning_service: boolean, REQUIRED
childcare_available: boolean, REQUIRED
kids_play_area: boolean, REQUIRED
garden_or_lawn: boolean, REQUIRED
rooftop_access: boolean, REQUIRED
airport_shuttle: boolean, REQUIRED
airport_shuttle_inr: INR_INTEGER, REQUIRED # 0 if free or absent
doctor_on_call: boolean, REQUIRED
doctor_response_time_minutes: int, REQUIRED # 0 if absent
in_house_pharmacy: boolean, REQUIRED
accessibility:
step_free_entrance: boolean, REQUIRED
elevator_to_all_floors: boolean, REQUIRED
wheelchair_accessible_room_count: int, REQUIRED
wheelchair_accessible_bathroom_count: int, REQUIRED
braille_signage: boolean, REQUIRED
hearing_loop_in_reception: boolean, REQUIRED
service_animals_welcome: boolean, REQUIRED
visual_fire_alarms: boolean, REQUIRED
sustainability:
carbon_kg_per_night_per_room: float, REQUIRED, ≥0
solar_powered_pct: int, REQUIRED, 0-100
rainwater_harvesting: boolean, REQUIRED
greywater_recycling: boolean, REQUIRED
linen_change_policy: STRICT ENUM, REQUIRED # daily | on_request | every_3_days
single_use_plastic_free: boolean, REQUIRED
green_certified: boolean, REQUIRED
green_certification_authority: STRICT ENUM, REQUIRED # see §6 ("none" if not certified)
host:
name: string, REQUIRED # owner / mgmt company
kind: STRICT ENUM, REQUIRED # individual | company | chain
kyc_verified: boolean, REQUIRED
kyc_verification_method: STRICT ENUM, REQUIRED # see §6
identity_proof_type: STRICT ENUM, REQUIRED # see §6
pan_verified: boolean, REQUIRED
gstin_verified: boolean, REQUIRED
response_rate_pct: int, REQUIRED, 0-100
response_time_hours: float, REQUIRED, ≥0
languages_spoken: array<RFC_3066_LOCALE>, REQUIRED, ≥1
account_age_days: int, REQUIRED, ≥0
total_listings_managed: int, REQUIRED, ≥1
availability:
rooms_left: int, REQUIRED, ≥0
this_is_the_last_room: boolean, REQUIRED
last_booked_minutes_ago: int, REQUIRED, ≥0 # 9999 if never
last_searched_minutes_ago: int, REQUIRED, ≥0
high_demand: boolean, REQUIRED # backed by inventory data, not marketing
high_demand_reason: STRICT ENUM, REQUIRED # see §6 ("none" if not high demand)
freshness:
last_cleaned_iso: ISO_DATETIME, REQUIRED
last_inspected_iso: ISO_DATETIME, REQUIRED
last_review_added_iso: ISO_DATETIME, REQUIRED
data_last_synced_iso: ISO_DATETIME, REQUIRED # partner side
_provider:
name: string, REQUIRED # "Booking.com"
tomo_partner_id: string, REQUIRED
partner_tier: STRICT ENUM, REQUIRED # tier1_path_a | tier1_path_b | tier1_path_c
deep_link: URL, REQUIRED
partner_property_url: URL, REQUIRED # the canonical partner page
customer_support_phone: string, REQUIRED
customer_support_email: string, REQUIRED
customer_support_24x7: boolean, REQUIRED
in_app_chat_supported: boolean, REQUIRED
ListingDetail (returned by get_listing)
Superset of Listing. ALL Listing fields PLUS:
description_full: string, REQUIRED # markdown-safe partner copy
description_language: RFC_3066_LOCALE, REQUIRED
house_rules: array<string>, REQUIRED, ≥0
nearby_landmarks: array, REQUIRED, ≥0
- name: string, REQUIRED
distance_km: float, REQUIRED
kind: STRICT ENUM, REQUIRED # see §6
rooms_offered: array<RoomDetail>, REQUIRED, ≥1
# see RoomDetail below
photos: array<PhotoMeta>, REQUIRED, ≥1
# see PhotoMeta below
review_excerpts: array, REQUIRED, ≥0
- excerpt: string, REQUIRED
score_out_of_10: float, REQUIRED
reviewer_segment: STRICT ENUM, REQUIRED # solo | family | business | couple | group
review_date: ISO_DATE, REQUIRED
verified_stay: boolean, REQUIRED
language: RFC_3066_LOCALE, REQUIRED
policies_full:
cancellation_policy_text: string, REQUIRED
child_policy_text: string, REQUIRED
pet_policy_text: string, REQUIRED
damage_deposit_text: string, REQUIRED
visitor_policy_text: string, REQUIRED
faqs: array, REQUIRED, ≥0
- question: string, REQUIRED
answer: string, REQUIRED
local_info:
weather_avg_high_celsius_check_in_month: float, REQUIRED
weather_avg_low_celsius_check_in_month: float, REQUIRED
rainfall_avg_mm_check_in_month: float, REQUIRED
local_phrases_useful: array<string>, REQUIRED, ≥0
RoomDetail
room_id: string, REQUIRED
room_type: string, REQUIRED # "Deluxe King"
max_occupancy: int, REQUIRED
adult_max_occupancy: int, REQUIRED
child_max_occupancy: int, REQUIRED
infant_max_occupancy: int, REQUIRED
bed_config: string, REQUIRED # "1 king bed"
extra_bed_available: boolean, REQUIRED
size_sqft: int, REQUIRED
floor_number: int, REQUIRED
floor_kind: STRICT ENUM, REQUIRED # ground | mid | top | basement | rooftop
view_kind: STRICT ENUM, REQUIRED
window_orientation: STRICT ENUM, REQUIRED
balcony: boolean, REQUIRED
balcony_size_sqft: int, REQUIRED # 0 if no balcony
sound_proofing_rating: STRICT ENUM, REQUIRED
ac_type: STRICT ENUM, REQUIRED
wifi_speed_mbps: int, REQUIRED, ≥0
mattress_age_years: int, REQUIRED, ≥0
mattress_kind: STRICT ENUM, REQUIRED # spring | foam | hybrid | latex | unknown_legacy
pillow_count: int, REQUIRED
pillow_options_available: array<enum>, REQUIRED # see §6, may be empty
amenities_in_room: array<enum>, REQUIRED # see §6
last_renovated_iso: ISO_DATE, REQUIRED
photos: array<URL>, REQUIRED, ≥1
price_total_inr: INR_INTEGER, REQUIRED
price_per_night_inr: INR_INTEGER, REQUIRED
cancellation: STRICT ENUM, REQUIRED
free_cancel_until: ISO_DATETIME, REQUIRED
breakfast_included: boolean, REQUIRED
breakfast_kind: STRICT ENUM, REQUIRED
PhotoMeta
url: URL, REQUIRED
width_px: int, REQUIRED
height_px: int, REQUIRED
caption: string, REQUIRED # "" allowed
photographer: string, REQUIRED # "" allowed
photo_kind: STRICT ENUM, REQUIRED # see §6
captured_iso: ISO_DATETIME, REQUIRED
authenticity_verified: boolean, REQUIRED
ai_generated: boolean, REQUIRED # MUST be false for v1
Forbidden fields (TOMO rejects entire response if any present)
paid_placement_score
ad_bid
sponsored_rank
promotion_priority
kickback_amount
referral_fee_kickback
_partner_revenue_share
artificial_urgency_text
fake_scarcity_count
auto_inflate_score
seasonal_marketing_label
fake_recent_booking_text
5. CONTROLLED VOCABULARIES
Listing.kind
hotel
homestay
resort
service_apartment
guest_house
boutique_hotel
heritage_property
hostel_private_room
Listing.sub_kind
budget_chain | luxury_chain | independent_boutique | family_run_homestay |
beach_resort | mountain_resort | wellness_resort | farm_stay |
serviced_apartment_short_term | serviced_apartment_long_term |
backpacker_private | heritage_haveli | heritage_palace | heritage_fort |
heritage_courtyard
policy.cancellation
free // 100% refund up to free_cancel_until
partial // partial refund per partial_cancel_schedule
non_refundable // no refund regardless of date
price.fees_breakdown[].kind
room_subtotal | gst | service_fee | cleaning_fee | resort_fee |
local_tax | tourism_tax | platform_fee | early_check_in_fee |
late_check_out_fee | extra_person_fee | extra_bed_fee
amenities (Listing.amenities + room.amenities_in_room)
wifi | fast_wifi | breakfast | parking | car_parking | bike_parking |
ev_charging | pool | gym | spa | sauna | jacuzzi | ac | heater |
kitchen | kitchenette | restaurant | bar | room_service |
laundry | iron | dry_cleaning | shoe_polish |
business_center | conference_room | coworking_space |
pet_friendly | airport_shuttle | local_shuttle |
family_friendly | child_care | kids_pool | kids_play_area |
accessible | accessible_bathroom | hearing_loop |
balcony | private_balcony | terrace | garden_view |
sea_view | mountain_view | city_view | pool_view |
beach_access | private_beach |
veg_only | jain_meals | halal_meals |
24x7_reception | luggage_storage | concierge | bellboy |
elevator | generator_backup | ro_water | hot_water_24x7 |
in_room_safe | mini_fridge | electric_kettle | tea_coffee |
hair_dryer | bathrobe | toiletries_premium | bath_amenities_basic |
smart_tv | streaming_apps | newspaper_complimentary |
honeymoon_setup | anniversary_setup | birthday_setup |
female_only_floor | female_only_dorm
amenities_verification_method
partner_self_declared | partner_inspection_report | property_uploaded_photos |
tomo_field_audit | third_party_inspection | guest_review_corroboration
ratings.star_rating_authority
ministry_of_tourism_india | hrawi | independent_rating_body |
internal_partner_grade | unrated
ratings.review_score_label
exceptional | excellent | very_good | good | fair | poor | unrated
trust.verification_method
ownership_documents | partner_inspection | tomo_field_audit |
user_review_corroboration | third_party_inspection
trust.property_registration_authority
state_tourism_department | municipality | gram_panchayat |
hrawi_member | mots_classification | none
room.smart_tv_with_otts
netflix | prime | hotstar | disney_plus | sony_liv | zee5 | jio_cinema |
youtube | apple_tv | bbc_iplayer
room.soundproofing_rating
excellent | good | average | poor
room.view_kind
sea_view | mountain_view | city_view | garden_view | pool_view |
courtyard_view | street_view | parking_view | no_view
room.window_orientation
north | south | east | west | northeast | northwest | southeast | southwest |
mixed | no_window
room.bathroom_kind
attached_private | shared_floor | common
room.hot_water_type
solar | electric_geyser | gas_geyser | central | none
room.toiletries_provided
soap | shampoo | conditioner | body_wash | shower_gel | hair_dryer |
shaving_kit | dental_kit | sanitary_pads | comb | shower_cap |
moisturizer | sunscreen | mosquito_repellent
room.pillow_options_available
soft | medium | firm | memory_foam | hypoallergenic | buckwheat | feather
food.breakfast_kind
buffet | continental | indian | south_indian | north_indian | mixed | none
food.cuisines_offered
north_indian | south_indian | hyderabadi | bengali | punjabi | gujarati |
maharashtrian | rajasthani | kerala | tamilian | chinese | thai |
italian | french | continental | mediterranean | mexican | japanese |
korean | lebanese | mughlai | jain | vegan | live_grill | bbq | tandoor
facilities.pool_kind
outdoor | indoor | rooftop | infinity | private_villa_pool | none
host.kind
individual | company | chain
host.kyc_verification_method
aadhaar_offline | aadhaar_online | digilocker | pan_only | gstin_only |
in_person_office_visit | none
host.identity_proof_type
aadhaar | pan | passport | driving_license | voter_id |
company_incorporation | partnership_deed | gstin
availability.high_demand_reason
none | school_holidays | weekend | local_event | religious_festival |
long_weekend | wedding_season | conference_in_city
nearby_landmarks[].kind
transit | airport | railway | metro | bus_terminal |
hospital | pharmacy | atm | grocery | mall |
restaurant | cafe | bar | nightclub |
park | beach | lake | viewpoint | temple | church | mosque | gurudwara |
museum | gallery | theatre | cinema | sports_stadium |
police_station | fire_station |
embassy | consulate
media.photos[].photo_kind
exterior | lobby | room_interior | bathroom | dining | pool | gym | spa |
view_from_room | balcony | breakfast_spread | pool_area | kids_area |
gardens | rooftop | other
room.mattress_kind
spring | foam | hybrid | latex | air | unknown_legacy
room.floor_kind
ground | mid | top | basement | rooftop
room.ac_type
split | central | window | tower | none
sustainability.linen_change_policy
daily | on_request | every_3_days | every_5_days
sustainability.green_certification_authority
none | leed | iso_14001 | green_globe | earthcheck | tourism_for_tomorrow
context.trip_purpose
leisure | business | medical | family_emergency | religious_pilgrimage |
education | wedding | conference
merchant_id resolution order (preferred → fallback)
1. Google Place ID
2. ISO 3166-2 + lat/lng to 4 decimals
3. partner_id + ":" + listing_id (no cross-dedup possible)
6. TTBS DIMENSIONS
Per-domain weights (locked in server/lib/domain-agent-map.ts)
travel: { time: 0.20, taste: 0.30, budget: 0.30, safety: 0.20 }
TIME
SIGNALS USED:
- confirmation_speed_seconds (lower = better) weight 0.30
- location.distance_from_user_km weight 0.30
- availability.last_booked_minutes_ago weight 0.10
- availability.high_demand weight 0.15
- availability.this_is_the_last_room weight 0.15
USER BAND HANDLING:
- "fast" = TOMO penalizes properties > 1500ms p95 search latency
- "flexible" = no time penalty, distance still counted
TASTE
SIGNALS USED:
- ratings.guest_review_score weight 0.30
- ratings.recent_30day_score weight 0.20
- ratings.star_rating / 5 weight 0.10
- amenities_match_pct (intersection w/ user) weight 0.20
- kind_preference_match weight 0.10
- ratings.<segment>_score (matches trip purpose) weight 0.10
USER BAND HANDLING:
- solo_traveler users → ratings.solo_traveler_score weighted 2x
- family users → ratings.family_score weighted 2x
- business users → ratings.business_score weighted 2x
- couple users → ratings.couples_score weighted 2x
BUDGET
SIGNALS USED:
- price.per_night_inr vs user budget_band:
ok → 0 ≤ price ≤ 33rd percentile of (kind, city) market
good → 33rd–66th percentile
great → 66th+ percentile
- price.fees_breakdown sum / per_night_inr ratio (lower = better)
- price.discount_inr / base_rate_inr (higher = better)
HARD FILTERS (drop before TTBS, don't penalize):
- listings above preferences.budget_max_inr_per_night
- total above preferences.budget_max_inr_total
USER BAND HANDLING:
- users persistently in "ok" band see boosted budget weight
- "great" band users see budget de-weighted, taste up-weighted
SAFETY
SIGNALS USED:
- policy.cancellation == "free" weight 0.20
- policy.free_cancel_until > 24h before check_in weight 0.10
- trust.verified_property weight 0.15
- trust.tomo_field_team_audited weight 0.10
- trust.fire_safety_certified weight 0.10
- trust.cctv_in_common_areas weight 0.05
- host.kyc_verified weight 0.10
- ratings.guest_review_count >= 50 weight 0.05
- policy.female_staff_on_site_24x7
(boosted if female_traveler_safety_required) weight 0.10
- policy.lgbtq_welcoming
(boosted if lgbtq_welcoming_required) weight 0.05
HARD FILTERS:
- preferences.verified_property_required → drop unverified
- preferences.female_traveler_safety_required → drop without female staff 24x7
- preferences.lgbtq_welcoming_required → drop without lgbtq_welcoming
- preferences.accessibility_step_free_required → drop without step_free_entrance
Hidden ranking factor
information_completeness_score = pct of all fields populated with non-default values. This is computed by TOMO ingest and added to the merged-pool score with weight 0.10. Partners returning richer data rank higher. Not a TTBS letter — TTBS stays the 4-axis user-facing brand.
User cannot see this score. Partner cannot influence beyond providing data.
7. COMPLETION CONTRACT
When booking confirms (or terminally fails):
POST /api/v1/cpc/mcp_provider/{tomo_partner_id}
Content-Type: application/json
X-TOMO-Timestamp: 1715257923000
X-TOMO-Signature: sha256=<hex>
{
"intent": "travel.book_hotel",
"intent_version": "v1.0.0",
"external_id": "BOOKING-CONFIRMATION-12345",
"amount_inr": 8400,
"closed_at": "2026-05-09T14:35:00+05:30",
"request_id": "req_8f3k2m_...",
"status": "confirmed",
"booking_ref": "BOOK-12345",
"merchant_id": "ChIJxxxxx",
"check_in": "2026-05-15",
"check_out": "2026-05-17",
"rooms": 1,
"guests": 2,
"currency": "INR",
"fees_breakdown_total_inr": 1200,
"cancellation_until": "2026-05-13T18:00:00+05:30",
"notes": ""
}
Status enum: confirmed | failed_payment | cancelled_by_user | rejected_by_provider
Signing: HMAC-SHA256(signing_key, "${X-TOMO-Timestamp}.${rawBodyJSON}")
Replay window: 5 minutes
Retry policy: Partner retries up to 5x with exponential backoff (1s, 2s, 4s, 8s, 16s) on TOMO 5xx. TOMO never retries the partner's POST (idempotent on external_id).
8. WIDGET
WIDGET TYPE: travel_listing_results
SOURCE FILE: src/widgets/types.ts
TYPE NAME: TravelListingResultsPayload
RENDERED IN: components/widgets/TravelListingResultsWidget.tsx
Default rendering
- 3-row collapsed preview, expandable to full result set
- Each row: thumbnail · name · per_night_inr (total small) · distance · free_cancel pill · review_score · kind
- Tap row → fetches
get_listing→ expanded card → "Book now" - "Book now" →
create_booking→ completion →BookingConfirmationCard
Widget cannot be styled by partner. Locked.
9. CACHING POLICY
| Call | TTL | Rationale |
|---|---|---|
search_availability results |
60s | pricing/availability shifts fast |
get_listing ListingDetail |
5min | descriptions/amenities are static-ish |
cancel_booking outcomes |
0s | always re-call partner |
| Failure responses | 0s | always retry |
| Photo URLs | 24h | partner CDN-backed, rarely changes |
10. ERROR CODES
| Code | HTTP | Meaning | TOMO retry behavior |
|---|---|---|---|
INVALID_REQUEST |
400 | payload malformed | none, surfaces to user |
INVALID_AUTH |
401 | bad credentials | partner re-auth required |
LISTING_EXPIRED |
410 | listing_id invalid | re-search |
OUT_OF_INVENTORY |
409 | rooms gone since search | drop listing, show others |
PAYMENT_DECLINED |
402 | gateway rejected token | surface to user |
INVALID_DATES |
400 | dates malformed/unavailable | surface to user |
RATE_LIMITED |
429 | partner-side throttle | exponential backoff |
INTERNAL_ERROR |
500 | partner-side failure | drop listing, fall back to other providers |
IDEMPOTENCY_CONFLICT |
409 | duplicate idempotency_key with different body | surface to user as "already booked" |
SIGNATURE_INVALID |
401 | (CPC webhook only) HMAC mismatch | partner re-sends |
BOOKING_NOT_FOUND |
404 | (cancel/modify) booking_ref doesn't exist | surface to user |
MODIFICATION_NOT_ALLOWED |
409 | (modify) past free_cancel_until | surface to user |
PARTNER_MAINTENANCE |
503 | scheduled downtime | drop partner from pool until next probe |
Free-text error strings are forbidden. Every error MUST map to one of these codes.
11. SANDBOX → PRODUCTION CHECKLIST
[ ] All §2 input fields validated (request_id echo, country_code=IN, currency=INR)
[ ] search_availability returns ≥1 valid listing for "Bangalore + 7 days out + 2 guests"
[ ] All §4/5 required Listing fields populated with real production data
[ ] No test fixtures, no Lorem Ipsum, no placeholder photos
[ ] merchant_id present + Google Place ID format on ≥80% of listings
[ ] price.fees_breakdown sum equals price.total_inr exactly
[ ] All §6 controlled vocabularies respected (no free-text drift)
[ ] get_listing returns valid ListingDetail for any id from a live search response
[ ] create_booking returns booking_ref within SLA p95
[ ] cancel_booking returns refund schedule honoring partial_cancel_schedule
[ ] modify_booking returns new total honoring policies
[ ] CPC webhook arrives within 60s of booking, signed correctly
[ ] HMAC verification passes, replay window enforced
[ ] No forbidden fields in any response (TOMO ingest scans + rejects)
[ ] SLA latency p95 met across 100-call sandbox test
[ ] photos.ai_generated == false on every photo
[ ] customer_support_phone reachable + responds within published SLA
[ ] Compliance docs uploaded:
[ ] GSTIN
[ ] Tourism dept registration (or HRAWI / state body)
[ ] Privacy policy URL (live + accessible)
[ ] Fire safety certificate (per property)
[ ] Property registration certificate (per property)
[ ] LGBTQ+ welcoming + female_safety claims independently verifiable
[ ] No artificial urgency claims (high_demand backed by inventory data only)
Reviewed by TOMO admin within 24-72h. Approval flips status from sandbox → live.
12. ANTI-FABRICATION RULES
RULE 1 — No paid placement signals
No paid_placement_score, ad_bid, sponsored_rank, promotion_priority,
kickback_amount, referral_fee_kickback, _partner_revenue_share fields
anywhere in any response. TOMO ingest scans for these field names AND
semantic equivalents (lowercased, snake_case'd). Listing rejected if found.
RULE 2 — No artificial urgency
high_demand=true requires availability.high_demand_reason ∈ valid enum
AND availability.rooms_left ≤ 3 OR demonstrable booking-velocity proof.
this_is_the_last_room=true requires availability.rooms_left=1.
Fake scarcity = listing rejection + partner warning. Three offenses = suspension.
RULE 3 — Review scores must be unrounded
ratings.guest_review_score = unrounded float from raw user reviews.
No "rounded up to 4.5 for marketing." TOMO checks scores against
historical distribution; outliers flagged.
RULE 4 — No AI-generated photos
photos[].ai_generated MUST be false. Partner declares; TOMO field audit
spot-checks. AI-generated photos = listing rejection.
RULE 5 — No fake review excerpts
review_excerpts[].verified_stay = true required for excerpts to render.
TOMO sample-tests excerpts against partner's full review database.
RULE 6 — Self-declared safety claims must be marked
policy.lgbtq_welcoming_self_declared field exists separately from
policy.lgbtq_welcoming. If self_declared=true, badge in widget shows
"self-declared" qualifier.
RULE 7 — TTBS scores are not visible to partners
Partners cannot see how individual listings rank. They see only the
aggregate dispatch volume per intent + their CPC ledger.
RULE 8 — No commission-based response shaping
Partner cannot vary listings shown to TOMO based on TOMO's commission
rate. TOMO randomly samples 1% of search responses against the partner's
public website results to detect divergence.
RULE 9 — Customer support fields must be honest
customer_support_24x7=true means a human answers 24x7. Voicemail-only
fails the claim. TOMO field-tests support during sandbox.
RULE 10 — Prices must include all mandatory fees
price.total_inr = sum of all fees_breakdown[].amount_inr exactly.
Any "resort fee mentioned at check-in" is breach. Property delisted.
VERSION HISTORY
v1.0.0 — 2026-05-09 — Initial spec (gold reference for all other intents)
PARTNER QUICKSTART (TL;DR)
- Read
_TEMPLATE.mdfor the contract structure - Read this file for the
travel.book_hotelcontract specifically - Implement 5 tools:
search_availability,get_listing,create_booking,cancel_booking,modify_booking - Return Listings matching §4 schema, no fields skipped
- Use only §6 vocabularies, no free text in enum slots
- POST signed CPC webhook (§7) on booking close
- Pass §11 checklist
- Get
status: live - Receive traffic, get paid 90% of
amount_inr, TOMO keeps 10%