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.
● DRAFTv1.0.0travel.book_tour_guide

travel.book_tour_guide — Full Intent Specification

INTENT NAMESPACE: travel
INTENT NAME:      book_tour_guide
FULL ID:          travel.book_tour_guide
VERSION:          v1.0.0
STATUS:           draft
TTBS WEIGHTS:     time 0.15 · taste 0.40 · budget 0.20 · safety 0.25
LAST UPDATED:     2026-05-14

User books a licensed local tour guide for a heritage walk, city tour, museum tour, food tour, photography tour, or destination-specific guided experience. Distinct from travel.book_package (multi-component bundles), travel.book_adventure_activity (specific physical activity), and from generic translation services. Partner exemplars: GetYourGuide, Viator, Klook, Headout, Tripoto, Withlocals, Ministry of Tourism RLG (Regional Level Guide) registries, local guide cooperatives.


1. NATURAL LANGUAGE COVERAGE

Classifies IN

  • "local guide for Hampi tour"
  • "Old City Hyderabad heritage walk"
  • "Mumbai food tour guide"
  • "Delhi history tour with a licensed guide"
  • "private guide Taj Mahal"
  • "Goa Portuguese heritage walk"
  • "photography tour Varanasi"
  • "Sri Lanka local guide"
  • "museum tour Bangalore"
  • "wine tour Nashik with guide"

Classifies OUT — borderline NO

  • "package tour" → travel.book_package
  • "trek with guide" → travel.book_adventure_activity (when activity dominates)
  • "language translator" — out of scope v1
  • "audio guide rental" — out of scope v1
  • "rental car with driver" → mobility.book_chauffeur_hourly
  • "city tour bus" → travel.book_bus (when bus is the booked item) or this intent (when guide is the booked item)

MULTI-INTENT TRIGGERS

  • "Hampi tour + hotel + train" → travel.book_tour_guide + travel.book_hotel + travel.book_train
  • "Heritage walk + lunch reservation" → travel.book_tour_guide + food.book_dine_in
  • "Photography tour + early-morning cab" → travel.book_tour_guide + mobility.book_chauffeur_hourly

2. INPUT — TOMO → PROVIDER

{
  "intent":          "travel.book_tour_guide",
  "intent_version":  "v1.0.0",
  "request_id":      "req_grd_2h4k_2026-05-14T10:00:00+05:30",
  "user_session_id": "anon_user_token_or_uid",

  "tour_request": {
    "destination_city":  "Hampi",
    "destination_state": "Karnataka",
    "country_code":      "IN",
    "tour_kind":         "heritage_walk",
    "tour_themes":       ["history", "architecture"],
    "tour_date":         "2026-08-12",
    "start_time_window": {
      "start": "2026-08-12T07:00:00+05:30",
      "end":   "2026-08-12T10:00:00+05:30"
    },
    "duration_hours":    4,
    "party": {
      "adult_count":     2,
      "children_with_age": [],
      "seniors_count":   0,
      "disabled_count":  0
    },
    "preferred_language":     "en",
    "language_alternates":    ["hi"],
    "private_tour_required":  true,
    "transport_included_required": false,
    "entry_tickets_included_required": true,
    "mobility_walking_floor_km": 3,
    "mobility_walking_ceiling_km": 8,
    "guide_gender_preference":   "no_preference",
    "budget_band":               "good",
    "budget_max_inr_total":      4500
  },

  "context": {
    "user_locale":          "en-IN",
    "user_currency_pref":   "INR",
    "trip_purpose":         "leisure",
    "trust_signals": {
      "is_repeat_customer":          false,
      "user_account_age_days":       312
    }
  }
}
Field Type Constraint Notes
intent string REQUIRED, equals "travel.book_tour_guide"
tour_request.destination_city string REQUIRED Validated against partner's coverage
tour_request.tour_kind enum REQUIRED, STRICT §6 heritage_walk / city_tour / food_tour / museum_tour / photography / wine / pilgrimage / hidden_gems / family_friendly
tour_request.tour_themes array REQUIRED, STRICT §6, ≥1
tour_request.tour_date ISO_DATE REQUIRED Up to 90 days advance
tour_request.start_time_window object REQUIRED start, end ISO_DATETIME
tour_request.duration_hours int REQUIRED, 1-12
tour_request.party.adult_count int REQUIRED, ≥1, ≤25 Above 25 = group tour separate intent
tour_request.party.disabled_count int REQUIRED Triggers accessibility filter
tour_request.preferred_language enum REQUIRED, STRICT §6
tour_request.private_tour_required bool REQUIRED Hard filter on shared vs private
tour_request.transport_included_required bool REQUIRED Vehicle + guide combo when true
tour_request.entry_tickets_included_required bool REQUIRED Drives total quote
tour_request.mobility_walking_floor_km float REQUIRED, ≥0 Lower bound of walking
tour_request.mobility_walking_ceiling_km float REQUIRED, ≥0 Upper bound; hard filter
tour_request.guide_gender_preference enum REQUIRED, STRICT §6 male / female / no_preference

Anti-fabrication preamble: no paid placement; guide ordering is TTBS-only. Government-licensed guides (Ministry of Tourism RLG / Regional Level Guide / archaeological-site-specific) MUST be flagged as such. Unlicensed self-styled "guides" at protected monuments = ingest reject.


3. PROVIDER TOOLS

Tool 1: search_guides

PURPOSE:        return guides matching destination + date + language + theme
INPUT:          §2 request body
OUTPUT:         { guides: GuideOption[], result_token, expires_at }
SLA:            p50 < 800ms, p95 < 2500ms, p99 < 4500ms
RATE LIMIT:     ≤ 1/sec per (user_session_id, partner)
RESULT SET:     up to 20 guides

Tool 2: get_guide_detail

PURPOSE:        full guide bio + license + sample tour photos + reviews
INPUT:          { guide_id, request_id }
OUTPUT:         GuideDetail (§5)
SLA:            p95 < 1500ms

Tool 3: hold_tour_slot

PURPOSE:        soft-lock for 10 minutes
INPUT:          { guide_id, tour_date, start_time, duration_hours, request_id }
OUTPUT:         { hold_id, hold_expires_at, locked_price_inr }
SLA:            p95 < 1200ms

Tool 4: confirm_tour

PURPOSE:        confirm booking + issue voucher + e-tickets when applicable
INPUT:          { hold_id, party, contact, payment_method_id, request_id }
OUTPUT:         ConfirmedTour (§5)
SLA:            p95 < 3500ms
IDEMPOTENCY:    request_id

Tool 5: cancel_tour

PURPOSE:        cancel + refund per policy
INPUT:          { booking_id, reason_code, request_id }
OUTPUT:         CancellationResult (§5)
SLA:            p95 < 2500ms

4. RESPONSE SHAPE

GuideOption

GuideOption:
  guide_id: { type: string, constraint: REQUIRED, opaque }
  display_name: { type: string, constraint: REQUIRED }
  license:
    license_kind: { type: enum, constraint: REQUIRED, STRICT §6, values: [moT_RLG, asi_site_specific, state_tourism_licensed, unlicensed_local_expert, international_certified] }
    license_number: { type: string, constraint: REQUIRED, semantics: "Empty when license_kind=unlicensed_local_expert" }
    license_valid_until: { type: string, constraint: REQUIRED, ISO_DATE, semantics: "Empty when unlicensed" }
    issuing_authority: { type: string, constraint: REQUIRED, semantics: "Empty when unlicensed" }

  bio_short: { type: string, constraint: REQUIRED, max 280 chars }
  years_guiding: { type: int, constraint: REQUIRED, ≥0 }
  tours_completed_count: { type: int, constraint: REQUIRED, ≥0 }

  languages_spoken: { type: array<enum>, constraint: REQUIRED, STRICT §6, ≥1 }
  fluency_levels:
    type: array<object>
    constraint: REQUIRED
    shape:
      language: { type: enum, constraint: REQUIRED, STRICT §6 }
      level: { type: enum, constraint: REQUIRED, STRICT §6, values: [conversational, professional, native] }

  specialties: { type: array<enum>, constraint: REQUIRED, STRICT §6, ≥1 }
  themes_covered: { type: array<enum>, constraint: REQUIRED, STRICT §6, ≥1 }

  duration_offered_hours:
    min: { type: float, constraint: REQUIRED, ≥0.5 }
    max: { type: float, constraint: REQUIRED, ≤12 }

  pricing:
    base_per_hour_inr: { type: int, constraint: REQUIRED, INR_INTEGER, ≥0 }
    flat_for_duration_inr: { type: int, constraint: REQUIRED, INR_INTEGER, ≥0, semantics: "Calculated for requested duration" }
    private_premium_inr: { type: int, constraint: REQUIRED, INR_INTEGER, ≥0, semantics: "Add'l for private tour" }
    entry_tickets_inr: { type: int, constraint: REQUIRED, INR_INTEGER, ≥0, semantics: "Site entry fees pass-through" }
    transport_inr: { type: int, constraint: REQUIRED, INR_INTEGER, ≥0, semantics: "Vehicle pass-through when included" }
    gst_inr: { type: int, constraint: REQUIRED, INR_INTEGER, ≥0 }
    total_for_party_inr: { type: int, constraint: REQUIRED, INR_INTEGER }

  inclusions: { type: array<enum>, constraint: REQUIRED, STRICT §6, may be empty }
  exclusions_text: { type: array<string>, constraint: REQUIRED, may be empty }

  walking_distance_estimate_km: { type: float, constraint: REQUIRED, ≥0 }
  walking_pace_kmph: { type: float, constraint: REQUIRED, ≥0 }
  rest_stops_planned: { type: int, constraint: REQUIRED, ≥0 }
  wheelchair_accessible: { type: boolean, constraint: REQUIRED }
  kid_friendly: { type: boolean, constraint: REQUIRED }
  senior_friendly: { type: boolean, constraint: REQUIRED }

  meeting_point:
    name: { type: string, constraint: REQUIRED }
    address: { type: string, constraint: REQUIRED }
    lat: { type: float, constraint: REQUIRED }
    lng: { type: float, constraint: REQUIRED }
    landmark: { type: string, constraint: REQUIRED }

  cancellation_policy:
    type: array<object>
    constraint: REQUIRED, may NOT be empty
    shape:
      hours_before_start_min: { type: int, constraint: REQUIRED }
      hours_before_start_max: { type: int, constraint: REQUIRED }
      refund_pct: { type: int, constraint: REQUIRED, 0-100 }

  rating:
    average_score: { type: float, constraint: REQUIRED, 0.0-5.0 }
    review_count: { type: int, constraint: REQUIRED, ≥0 }
    response_time_minutes_median: { type: int, constraint: REQUIRED, ≥0 }

  guide_gender: { type: enum, constraint: REQUIRED, STRICT §6, values: [male, female, other, prefer_not_to_say] }

  partner_reference:
    source: { type: string, constraint: REQUIRED }
    deeplink: { type: string, constraint: REQUIRED, HTTPS URL }

GuideDetail

GuideDetail:
  guide_id: { type: string, constraint: REQUIRED }
  bio_long: { type: string, constraint: REQUIRED }
  sample_itineraries:
    type: array<object>
    constraint: REQUIRED, ≥1
    shape:
      title: { type: string, constraint: REQUIRED }
      duration_hours: { type: float, constraint: REQUIRED }
      stops_in_order: { type: array<string>, constraint: REQUIRED, ≥1 }
  sample_tour_photos: { type: array<string>, constraint: REQUIRED, ≥3, HTTPS URLs }
  recent_reviews:
    type: array<object>
    constraint: REQUIRED, ≥3
    shape:
      reviewer_initials: { type: string, constraint: REQUIRED, semantics: "First initial + last initial only" }
      review_text_short: { type: string, constraint: REQUIRED, max 280 chars }
      rating: { type: float, constraint: REQUIRED, 0.0-5.0 }
      tour_date: { type: string, constraint: REQUIRED, ISO_DATE }
      verified_booking: { type: boolean, constraint: REQUIRED }

ConfirmedTour

ConfirmedTour:
  booking_id: { type: string, constraint: REQUIRED, immutable }
  guide_id: { type: string, constraint: REQUIRED }
  guide_name: { type: string, constraint: REQUIRED }
  tour_date: { type: string, constraint: REQUIRED, ISO_DATE }
  starts_at: { type: string, constraint: REQUIRED, ISO_DATETIME }
  ends_at: { type: string, constraint: REQUIRED, ISO_DATETIME }
  meeting_point: { type: object, constraint: REQUIRED, semantics: "Same shape as GuideOption.meeting_point" }
  total_paid_inr: { type: int, constraint: REQUIRED, INR_INTEGER }
  voucher_url: { type: string, constraint: REQUIRED, HTTPS URL }
  e_tickets:
    type: array<object>
    constraint: REQUIRED, may be empty
    shape:
      site_name: { type: string, constraint: REQUIRED }
      ticket_url: { type: string, constraint: REQUIRED, HTTPS URL }
      valid_for: { type: string, constraint: REQUIRED, ISO_DATE }
  guide_phone_at_t_minus_2h: { type: string, constraint: REQUIRED, E.164, semantics: "Empty until 2h before" }
  emergency_contact_phone: { type: string, constraint: REQUIRED, E.164 }
  cancellation_window:
    full_refund_until: { type: string, constraint: REQUIRED, ISO_DATETIME }
    last_cancellable_at: { type: string, constraint: REQUIRED, ISO_DATETIME }

CancellationResult

CancellationResult:
  booking_id: { type: string, constraint: REQUIRED }
  cancelled_at: { type: string, constraint: REQUIRED, ISO_DATETIME }
  refund_amount_inr: { type: int, constraint: REQUIRED, INR_INTEGER, ≥0 }
  refund_method: { type: enum, constraint: REQUIRED, STRICT §6, values: [original_payment, bank_transfer] }
  refund_eta_days: { type: int, constraint: REQUIRED, 0-10 }
  non_refundable_components: { type: array<string>, constraint: REQUIRED, may be empty }

FORBIDDEN FIELDS

  • paid_placement_score, ad_bid, sponsored_rank, featured_guide
  • editor_pick, tomo_recommended, top_choice
  • fake_review_block, inflated_tours_completed_count
  • ai_generated_guide_photo, ai_generated_tour_photo (real photos required)
  • unlicensed_at_protected_site — ingest reject for monuments with ASI/state-licence requirements
  • commission_padded_per_hour_inr, kickback_from_site_authority
  • client_real_names in reviews — initials only

5. CONTROLLED VOCABULARIES

tour_request.tour_kind:
  values:
    heritage_walk:      "Historical / architectural walking tour"
    city_tour:          "General city orientation"
    food_tour:          "Cuisine + eateries tasting"
    museum_tour:        "Museum-focused"
    photography:        "Photography composition tour"
    wine:               "Vineyard / winery tour"
    pilgrimage:         "Religious / spiritual site tour"
    hidden_gems:        "Off-the-beaten-track local"
    family_friendly:    "Kid-friendly pace + content"
    pub_crawl:          "Adult evening tour"
    wildlife_birding:   "Bird / wildlife in urban / peri-urban"

tour_request.tour_themes / themes_covered / specialties:
  values:
    history, architecture, religion, art, food, photography, music,
    literature, film, fashion, crafts, markets, street_culture,
    colonial, mughal, dravidian, indo_islamic, mauryan, colonial_portuguese,
    british_raj, modern_india, freedom_movement, women_in_history,
    science_industry, gandhian, ambedkar_buddhist, sufism, vaishnav,
    shaiva, jain_heritage, sikh_heritage

tour_request.preferred_language / languages_spoken:
  values: [en, hi, te, ta, kn, ml, mr, bn, gu, pa, or, as, ur, fr, es, de, it, ja, zh, ko, ru, ar, pt]

fluency_levels.level:
  values: [conversational, professional, native]

GuideOption.license.license_kind:
  values:
    moT_RLG:                  "Ministry of Tourism Regional Level Guide"
    asi_site_specific:        "Archaeological Survey of India site-specific guide"
    state_tourism_licensed:   "State tourism department licensed guide"
    unlicensed_local_expert:  "Local expert; not government-licensed (cannot operate at protected sites)"
    international_certified:  "International association certified (WFTGA, etc.)"

inclusions:
  values:
    entry_tickets, transport_pickup, transport_drop, transport_intra_tour,
    bottled_water, snacks, meal, headset_audio, museum_skip_line, fast_track_entry

guide_gender / guide_gender_preference:
  values: [male, female, other, prefer_not_to_say, no_preference]

CancellationResult.refund_method:
  values: [original_payment, bank_transfer]

6. TTBS DIMENSIONS

TIME (weight = 0.15):
  signals_used:
    - earliest tour slot vs start_time_window
    - response_time_minutes_median
  weighting:
    slot_fit: 0.65
    response_time: 0.35
  user_band_handling:
    fast: response time floor 60min
    balanced: standard
    flexible: 12h response acceptable for cheaper guide

TASTE (weight = 0.40):
  signals_used:
    - languages_spoken / fluency_levels match
    - tour_kind specialty match
    - themes_covered overlap with tour_themes
    - DNA repeat guide / similar tour history
    - guide_gender_preference match
    - private_tour_required match (HARD filter when TRUE)
  weighting:
    language_fluency: 0.25
    tour_kind_match: 0.20
    themes_overlap: 0.20
    dna_repeat: 0.15
    gender_preference: 0.10
    private_match: 0.10

BUDGET (weight = 0.20):
  signals_used:
    - pricing.total_for_party_inr
    - private_premium_inr
  weighting:
    total: 0.85
    private_premium: 0.15
  user_band_handling:
    ok: cheapest passing taste + safety floors
    good: balanced
    great: senior licensed guide regardless of fee

SAFETY (weight = 0.25):
  signals_used:
    - license.license_kind (HARD filter: protected sites require moT_RLG or asi_site_specific)
    - license.license_valid_until ≥ tour_date
    - years_guiding ≥3 floor
    - tours_completed_count ≥50 floor
    - rating.average_score ≥4.0 floor
    - walking_distance vs user's mobility_walking_floor/ceiling
    - kid_friendly / senior_friendly / wheelchair_accessible when applicable
  weighting:
    license_match: 0.30
    years_floor: 0.15
    tours_count: 0.15
    rating_floor: 0.20
    mobility_fit: 0.20
  user_band_handling:
    fast: relax tours_count to 20
    balanced: floor 50
    great: floor 200 + moT_RLG license

Locked weights: time 0.15 / taste 0.40 / budget 0.20 / safety 0.25. Taste dominates — guide quality + language + thematic resonance defines the experience.


7. COMPLETION CONTRACT

POST /api/v1/cpc/mcp_provider/<your_partner_id>
Body:
{
  "intent":           "travel.book_tour_guide",
  "external_id":      "<booking_id>",
  "request_id":       "<request_id>",
  "amount_inr":       300,    // NET partner commission only
  "gst_inr":          54,
  "tips_inr":         0,
  "pass_through_inr": 3500,   // remitted to guide + site entry tickets
  "closed_at":        "2026-08-12T11:00:00+05:30",
  "status":           "completed",
  "tour_kind":        "heritage_walk",
  "license_kind":     "moT_RLG",
  "duration_hours":   4,
  "party_size":       2
}

User pays full fee + tickets + transport (when included). Partner's NET commission is amount_inr. Guide fee + tickets + transport remittance is pass_through_inr. TOMO charges 10% × amount_inr. HMAC-SHA256.


8. WIDGET

GuideListingWidget.

Field mapping:
  - display_name + license_kind pill (e.g. "MoT-Licensed Guide")
  - languages_spoken intersection with user pref → language chip
  - years_guiding + tours_completed_count → "12 yrs · 420 tours"
  - specialties (top 3) → chip row
  - pricing.total_for_party_inr → big price
  - duration_offered_hours → "4-hour walk"
  - walking_distance_estimate_km → "~5 km walking" pill
  - rating.average_score + review_count → star pill
  - wheelchair_accessible / kid_friendly / senior_friendly → accessibility pills (when applicable)
  - private_tour_required match → "Private tour" badge

9. CACHING POLICY

Call TTL Rationale
search_guides 120s Calendar volatile
get_guide_detail 24h Stable profile
hold_tour_slot NO CACHE Stateful lock
confirm_tour NO CACHE Idempotent
cancel_tour NO CACHE
License verification (MoT / ASI / state) 7d Issuers update periodically

10. ERROR CODES

Code HTTP Meaning Retry
INVALID_REQUEST 400 Malformed No
RATE_LIMITED 429 Throttle 1, 2s
INTERNAL_ERROR 500 Partner failure 2, exp
SIGNATURE_INVALID 401 HMAC fail No
NO_GUIDE_MATCH 200 (empty) No guide fits filters n/a
SLOT_TAKEN 409 Hold race-lost UI re-queries
HOLD_EXPIRED 410 Hold elapsed UI re-holds
LICENSE_EXPIRED 422 Guide license lapsed No
PROTECTED_SITE_REQUIRES_LICENSED 422 Site needs licensed guide; unlicensed guide returned No
LANGUAGE_NOT_FLUENT 422 No professional-level fluency in required language No (UI re-asks)
MOBILITY_OUT_OF_RANGE 422 Walking distance outside floor/ceiling No
PAYMENT_FAILED 402 Gateway declined No
WEATHER_CANCELLATION 422 Severe weather; partner cancels with refund No
SITE_CLOSED 422 Monument closed on date No
CANCELLED_TOO_LATE 410 Past last_cancellable_at No
GUIDE_NO_SHOW 200 (advisory) Auto-full-refund + investigation n/a

11. SANDBOX → PRODUCTION CHECKLIST

[ ] All five tools implemented
[ ] At least 50 guides across 15+ destinations
[ ] All moT_RLG / asi_site_specific licenses verifiable on issuing authority's records
[ ] Protected-site hard filter enforced (sample audit on 5 ASI sites)
[ ] Language fluency claims sample-audited (random call with guide)
[ ] tours_completed_count defensible (partner produces past file references)
[ ] All reviews from completed bookings; verified_booking=true for each
[ ] Walking-distance estimates measured from sample-tour GPS traces
[ ] Cancellation policy publishes before payment
[ ] HMAC signing verified on test CPC webhook
[ ] amount_inr in CPC is COMMISSION (NET); pass_through_inr = guide fee + tickets + transport
[ ] All controlled vocabularies respected
[ ] No forbidden fields anywhere
[ ] SLA p95 met
[ ] Privacy policy + grievance officer contact uploaded
[ ] Customer support: dedicated guide-tours helpline + email
[ ] Real photos required (sample audit catches AI-generated headshots)
[ ] Guide no-show policy tested with auto-full-refund

12. ANTI-FABRICATION RULES

RULE 1: At protected monuments (ASI / state-protected), only licensed
        guides (moT_RLG or asi_site_specific) may be listed. Unlicensed
        listings at protected sites = customer-harm + regulatory breach.

RULE 2: License numbers verifiable on Ministry of Tourism's RLG database
        or ASI's panel list. Re-checked weekly.

RULE 3: No paid_placement, ad_bid, sponsored_rank, featured_guide.
        Ordering is TTBS-only.

RULE 4: rating.average_score must be unrounded from completed bookings
        in past 12 months. review_count from completed bookings only.

RULE 5: Reviews must use reviewer_initials only; identifiable client names
        forbidden.

RULE 6: years_guiding = years since first license/professional engagement,
        not "years interested in history."

RULE 7: tours_completed_count defensible (CPC ledger shadow score).

RULE 8: Languages with claimed professional fluency must withstand a
        sample audio call. Partners listing "Japanese fluent" without
        capability = customer-harm + suspension.

RULE 9: AI-generated guide photos / fake guide profiles = ingest reject +
        suspension. Government-ID verified real photo at intake.

RULE 10: amount_inr is partner COMMISSION only. Guide fee + entry tickets
         + transport are pass_through_inr. Inflated commission base = audit.

RULE 11: Walking-distance estimates must come from real tour traces, not
         partner-marketing-team guesses. ±20% deviation between estimate
         and actual = customer-harm flag.

RULE 12: Site-entry tickets passed through must match the published
         government rates (ASI / state). Padded ticket pricing = breach.

RULE 13: Guide-no-show triggers automatic full refund within 24h
         regardless of policy windows; partner cannot block.

RULE 14: Information-completeness score (hidden rank factor weight 0.10)
         rewards full §4 shape.

RULE 15: Wheelchair-accessible / kid-friendly / senior-friendly flags
         must reflect verifiable route inspection. Partners marking
         accessibility falsely = customer-harm + suspension.

VERSION HISTORY

v1.0.0 — 2026-05-14 — Initial spec. MoT-RLG / ASI / state-licensed guides
                       only at protected sites. Real photos + verified
                       license at intake. Net-commission base.
                       Walking-distance estimates from real GPS traces.
                       Site-entry ticket pass-through at government rates.