T
TOMO
Developer Docs
BETA These docs are under partner review. Some features described are roadmap items, not yet shipped. Verify against your sandbox before relying on any contract.
● LIVEv1.0.0travel.book_hotel

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 orchestrates book_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 sandboxlive.


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)

  1. Read _TEMPLATE.md for the contract structure
  2. Read this file for the travel.book_hotel contract specifically
  3. Implement 5 tools: search_availability, get_listing, create_booking, cancel_booking, modify_booking
  4. Return Listings matching §4 schema, no fields skipped
  5. Use only §6 vocabularies, no free text in enum slots
  6. POST signed CPC webhook (§7) on booking close
  7. Pass §11 checklist
  8. Get status: live
  9. Receive traffic, get paid 90% of amount_inr, TOMO keeps 10%