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_train

travel.book_train — Full Intent Specification

INTENT NAMESPACE: travel
INTENT NAME:      book_train
FULL ID:          travel.book_train
VERSION:          v1.0.0
STATUS:           live
TTBS WEIGHTS:     time 0.30 · taste 0.20 · budget 0.30 · safety 0.20
LAST UPDATED:     2026-05-11

Indian Railways (IRCTC) booking. Distinct from travel.book_flight because: (a) IRCTC is a regulated monopoly issuer — partners are aggregators, not direct sellers; (b) quota/Tatkal/waitlist/RAC mechanics dominate UX; (c) ticket has multiple states (CNF, RAC, WL_x, PQWL_x) that change between booking and journey; (d) PRS PNR is the canonical reference, not partner ref.


1. NATURAL LANGUAGE COVERAGE

Classifies IN

  • "train ticket Hyderabad to Mumbai tomorrow"
  • "Rajdhani to Delhi, AC 2 tier"
  • "Tatkal book for Bangalore Vande Bharat"
  • "IRCTC tickets for next Friday"
  • "lower berth Hyderabad to Vijayawada"
  • "ladies quota train ticket"
  • "senior citizen quota Mumbai Pune"
  • "foreign tourist quota train"
  • "premium Tatkal Jaipur"
  • "side lower 3AC Bangalore Chennai"

Classifies OUT — borderline NO

  • "flight to Mumbai" → travel.book_flight
  • "bus to Goa" → travel.book_bus
  • "intercity cab to Vizag" → mobility.book_intercity_ride
  • "PNR status check" → utility lookup (read-only, not this intent)
  • "cancel my IRCTC ticket" → travel.cancel_train (handled inside this intent's tool 4)
  • "metro from MG Road" → not a TOMO intent v1

MULTI-INTENT TRIGGERS

  • "Mumbai trip — train + hotel + cab to station" → travel.book_train + travel.book_hotel + mobility.book_intracity_ride
  • "weekend in Goa, train + scooter rental" → travel.book_train + mobility.book_two_wheeler_rental
  • "family trip — train and car at the destination" → travel.book_train + mobility.book_self_drive

2. INPUT — TOMO → PROVIDER

{
  "intent":          "travel.book_train",
  "intent_version":  "v1.0.0",
  "request_id":      "req_tr_8f2k_2026-05-11T07:14:00Z",
  "user_session_id": "anon_user_token_or_uid",

  "origin": {
    "station_code":     "SC",
    "station_name":     "Secunderabad Junction",
    "city":             "Hyderabad",
    "state":            "Telangana",
    "country_code":     "IN",
    "lat":              17.4344,
    "lng":              78.5013
  },

  "destination": {
    "station_code":     "CSMT",
    "station_name":     "Chhatrapati Shivaji Maharaj Terminus",
    "city":             "Mumbai",
    "state":            "Maharashtra",
    "country_code":     "IN",
    "lat":              18.9398,
    "lng":              72.8355
  },

  "journey_date":         "2026-05-15",
  "intent_kind":          "scheduled",
  "flexible_days_window": 0,
  "departure_window":     "any",

  "party": {
    "passenger_count":    2,
    "adult_count":        2,
    "children_with_age":  [],
    "children_no_berth":  [],
    "infants":            0,
    "senior_count":       0,
    "ladies_count":       0,
    "disabled_count":     0,
    "armed_forces_count": 0
  },

  "preferences": {
    "class_acceptable":         ["3A", "2A", "1A"],
    "berth_preferences":        ["lower", "side_lower"],
    "quota_acceptable":         ["GN", "TQ", "PT"],
    "train_kind_acceptable":    ["mail_express", "rajdhani", "vande_bharat", "shatabdi", "duronto"],
    "max_journey_hours":        18,
    "budget_band":              "good",
    "budget_max_inr_per_passenger": 3500,
    "budget_max_inr_total":      7000,
    "veg_meal_required":         false,
    "jain_meal_required":        false,
    "wheelchair_accessible_required": false,
    "ladies_compartment_required":   false,
    "boarding_station_other_than_origin": null,
    "auto_upgrade_acceptable":   true,
    "tatkal_acceptable":         true,
    "premium_tatkal_acceptable": true,
    "waitlist_acceptable":       true,
    "rac_acceptable":            true,
    "no_split_bookings":         true
  },

  "context": {
    "user_locale":          "en-IN",
    "user_currency_pref":   "INR",
    "trip_purpose":         "leisure",
    "trust_signals": {
      "is_repeat_customer":          true,
      "prior_train_with_partner":    8,
      "user_account_age_days":       312,
      "irctc_user_id_linked":        true,
      "irctc_user_id_masked":        "kee••••",
      "is_irctc_loyalty_member":     false
    }
  }
}
Field Type Constraint Notes
intent string REQUIRED, equals "travel.book_train"
origin.station_code string REQUIRED, IRCTC station code 2-5 chars
destination.station_code string REQUIRED, IRCTC station code
origin.country_code / destination.country_code ISO_3166_2 REQUIRED, always IN v1 India only
journey_date ISO_DATE REQUIRED up to ARP (Advance Reservation Period, 120 days)
intent_kind enum REQUIRED, STRICT tatkal | premium_tatkal | scheduled
flexible_days_window int REQUIRED, 0-7 0 = strict date
departure_window enum REQUIRED, see §6 early_morning / morning / afternoon / evening / night / any
party.children_with_age array REQUIRED, may be empty each age 5-11, gets full berth
party.children_no_berth array REQUIRED, may be empty each age 0-4, no berth
party.senior_count int REQUIRED drives senior citizen quota
party.ladies_count int REQUIRED drives ladies quota
party.disabled_count int REQUIRED drives DP quota
party.armed_forces_count int REQUIRED drives DF quota
preferences.class_acceptable array REQUIRED, ≥1 see §6
preferences.berth_preferences array REQUIRED, ≥1 see §6
preferences.quota_acceptable array REQUIRED, ≥1 see §6
preferences.train_kind_acceptable array REQUIRED, ≥1 see §6
preferences.boarding_station_other_than_origin string or null REQUIRED for boarding from intermediate station
preferences.auto_upgrade_acceptable bool REQUIRED IRCTC AU scheme
preferences.tatkal_acceptable bool REQUIRED drives Tatkal window booking
preferences.premium_tatkal_acceptable bool REQUIRED drives PT (dynamic-fare Tatkal)
preferences.waitlist_acceptable bool REQUIRED hard filter on WL listings
preferences.rac_acceptable bool REQUIRED hard filter on RAC listings
preferences.no_split_bookings bool REQUIRED reject "couldn't get all in one PNR" results
context.trust_signals.irctc_user_id_linked bool REQUIRED IRCTC user account linkage required for booking

Anti-fabrication preamble: no paid placement, no commission-based train ordering, TOMO never holds the rail — money flows user → IRCTC via partner.


3. PROVIDER TOOLS

Tool 1: search_trains

PURPOSE:        return trains running origin → destination on journey_date
INPUT:          §2 request body
OUTPUT:         { trains: TrainOption[], result_token, expires_at }
SLA:            p50 < 1500ms, p95 < 3500ms, p99 < 6000ms (IRCTC dependency)
RATE LIMIT:     ≤ 1/sec per (user_session_id, partner)
RESULT SET:     up to 30 trains (typical route has 5-15)

Tool 2: get_train_availability

PURPOSE:        live availability per class per quota for one specific train
INPUT:          { train_number, journey_date, origin_code, destination_code,
                  classes_to_check[], quotas_to_check[], request_id }
OUTPUT:         AvailabilityMatrix (§5)
SLA:            p95 < 1500ms
RATE LIMIT:     ≤ 1/sec per train+date

Tool 3: get_fare_breakdown

PURPOSE:        full fare breakdown (base + reservation + GST + IRCTC fee + cat fee)
INPUT:          { train_number, journey_date, class, quota, party, boarding_station, request_id }
OUTPUT:         FareBreakdown (§5)
SLA:            p95 < 1000ms

Tool 4: book_train

PURPOSE:        commit booking against IRCTC
INPUT:          { train_number, journey_date, class, quota, party_details,
                  boarding_station, payment_token, irctc_user_id_token,
                  preferences, request_id, idempotency_key }
OUTPUT:         BookingResult (§5)
SLA:            p95 < 12000ms (IRCTC PRS handshake; high variance)
IDEMPOTENCY:    REQUIRED on idempotency_key
PAYMENT:        TOMO never holds funds. payment_token routes to UPI/card.
                Partner debits user via the token; partner remits to IRCTC.

Tool 5: get_pnr_status

PURPOSE:        live PNR state — useful between booking and journey
INPUT:          { pnr_number, request_id, user_session_id }
OUTPUT:         PnrStatus (§5)
SLA:            p95 < 1500ms
RATE LIMIT:     ≤ 1/min per PNR

Tool 6: cancel_train_ticket

PURPOSE:        cancel booking + refund per IRCTC schedule
INPUT:          { pnr_number, passenger_indices_to_cancel[],
                  reason, request_id, user_consent_token }
OUTPUT:         { status, refund_amount_inr, refund_breakdown, refund_eta_days }
SLA:            p95 < 8000ms

Tool 7: get_chart_status

PURPOSE:        check if chart prepared (final berth assignment)
INPUT:          { pnr_number, request_id }
OUTPUT:         { chart_prepared: bool, chart_prepared_iso, allocated_seats[] }
SLA:            p95 < 800ms

Tool 8: change_boarding_station

PURPOSE:        modify boarding station (within IRCTC rules)
INPUT:          { pnr_number, new_boarding_station_code, request_id }
OUTPUT:         { status, change_charge_inr }
SLA:            p95 < 4000ms

All eight REQUIRED.


4. RESPONSE SHAPE

TrainOption (returned by search_trains)

id:                               string, REQUIRED
result_token:                     string, REQUIRED
expires_at:                       ISO_DATETIME, REQUIRED

train:
  train_number:                   string, REQUIRED                # 5-digit IRCTC number
  train_name:                     string, REQUIRED                # "Hyd-Mum Vande Bharat"
  train_kind:                     STRICT ENUM, REQUIRED           # see §6
  is_premium:                     boolean, REQUIRED
  is_special:                     boolean, REQUIRED               # festival/special trains
  is_superfast:                   boolean, REQUIRED
  zone:                           STRICT ENUM, REQUIRED           # see §6
  rake_kind:                      STRICT ENUM, REQUIRED           # see §6 (LHB, ICF, Vande_Bharat)
  is_lhb_coach:                   boolean, REQUIRED
  has_pantry_car:                 boolean, REQUIRED
  has_food_on_board:              boolean, REQUIRED
  food_inclusive_in_fare:         boolean, REQUIRED               # Rajdhani/VB/Shatabdi typically
  runs_on_days:                   array<enum>, REQUIRED, ≥1       # see §6 (mon..sun)

journey:
  origin_station_code:            string, REQUIRED
  origin_station_name:            string, REQUIRED
  destination_station_code:       string, REQUIRED
  destination_station_name:       string, REQUIRED
  departure_iso:                  ISO_DATETIME, REQUIRED
  arrival_iso:                    ISO_DATETIME, REQUIRED
  duration_minutes:               int, REQUIRED
  distance_km:                    int, REQUIRED
  intermediate_stops_count:       int, REQUIRED
  is_direct:                      boolean, REQUIRED               # no train change
  halt_minutes_at_origin:         int, REQUIRED
  arrives_next_day:               boolean, REQUIRED
  arrives_day_after_next:         boolean, REQUIRED
  punctuality_30day_pct:          float, REQUIRED, 0-100          # historical on-time
  average_delay_minutes_30day:    int, REQUIRED

classes_offered:                  array<TrainClass>, REQUIRED, ≥1   # see TrainClass below

reservation_status:
  arp_window_open:                boolean, REQUIRED               # 120-day ARP active
  tatkal_window_open:             boolean, REQUIRED               # T-1 day Tatkal window
  premium_tatkal_window_open:     boolean, REQUIRED
  chart_prepared:                 boolean, REQUIRED               # final chart done?
  chart_prepared_iso:             ISO_DATETIME, REQUIRED          # epoch sentinel if not yet

amenities_on_board:
  bed_roll_provided:              boolean, REQUIRED
  bed_roll_charge_inr:            INR_INTEGER, REQUIRED           # 0 if free
  charging_socket_at_seat:        boolean, REQUIRED
  reading_light_at_seat:          boolean, REQUIRED
  wifi_in_train:                  boolean, REQUIRED
  catering_kind:                  STRICT ENUM, REQUIRED           # see §6
  veg_meals_available:            boolean, REQUIRED
  jain_meals_available:           boolean, REQUIRED
  bottled_water_provided:         boolean, REQUIRED
  newspaper_provided:             boolean, REQUIRED
  blankets_provided:              boolean, REQUIRED
  pillow_provided:                boolean, REQUIRED
  toilet_kind:                    STRICT ENUM, REQUIRED           # see §6 (bio_toilet | indian | western)
  cctv_in_coaches:                boolean, REQUIRED
  emergency_alarm:                boolean, REQUIRED
  fire_extinguisher_count_per_coach: int, REQUIRED
  rpf_security_on_board:          boolean, REQUIRED               # Railway Protection Force

intermediate_halts:               array, REQUIRED, ≥0
  - halt_station_code:            string, REQUIRED
    halt_station_name:            string, REQUIRED
    arrival_iso:                  ISO_DATETIME, REQUIRED
    departure_iso:                ISO_DATETIME, REQUIRED
    halt_minutes:                 int, REQUIRED
    is_boarding_eligible:         boolean, REQUIRED               # can user board here
    distance_from_origin_km:      int, REQUIRED
    food_available_on_platform:   boolean, REQUIRED               # IRCTC e-catering applicable

TrainClass

class_code:                       STRICT ENUM, REQUIRED           # see §6 (1A, 2A, 3A, 3E, SL, 2S, CC, EC, etc.)
class_name:                       string, REQUIRED                # "AC 3 Tier"
total_seats_in_class:             int, REQUIRED
quotas_offered:                   array<QuotaAvailability>, REQUIRED, ≥1   # see QuotaAvailability below

base_fare_inr:                    INR_INTEGER, REQUIRED
reservation_charge_inr:           INR_INTEGER, REQUIRED
superfast_charge_inr:             INR_INTEGER, REQUIRED           # 0 if not superfast
catering_charge_inr:              INR_INTEGER, REQUIRED           # 0 if not inclusive
gst_inr:                          INR_INTEGER, REQUIRED
irctc_service_charge_inr:         INR_INTEGER, REQUIRED
total_fare_per_adult_inr:         INR_INTEGER, REQUIRED

dynamic_fare_active:              boolean, REQUIRED               # flexi-fare schemes
dynamic_fare_multiplier:          float, REQUIRED                 # 1.0 if not active

amenities_in_class:
  ac:                             boolean, REQUIRED
  berth_kinds_available:          array<enum>, REQUIRED, ≥1       # see §6
  side_berths:                    boolean, REQUIRED
  privacy_curtains:               boolean, REQUIRED
  dedicated_charging_per_berth:   boolean, REQUIRED
  reading_light_per_berth:        boolean, REQUIRED
  width_per_berth_inches:         int, REQUIRED
  length_per_berth_inches:        int, REQUIRED
  reclining_seat:                 boolean, REQUIRED               # CC / EC only
  meal_included:                  boolean, REQUIRED
  bedroll_included:               boolean, REQUIRED

QuotaAvailability

quota_code:                       STRICT ENUM, REQUIRED           # see §6 (GN, TQ, PT, LD, SS, FT, DP, etc.)
quota_name:                       string, REQUIRED                # "General"
total_quota_seats:                int, REQUIRED
available_seats:                  int, REQUIRED
status_kind:                      STRICT ENUM, REQUIRED           # see §6
status_text:                      string, REQUIRED                # "AVAILABLE-12", "WL/142", "RAC/8"
waitlist_position:                int, REQUIRED                   # 0 if AVAILABLE / RAC
rac_position:                     int, REQUIRED                   # 0 if not RAC
predicted_confirmation_pct:       int, REQUIRED, 0-100            # AI-predicted
predicted_chart_status:           STRICT ENUM, REQUIRED           # see §6
chart_prepared:                   boolean, REQUIRED
booking_window_status:            STRICT ENUM, REQUIRED           # see §6
booking_opens_iso:                ISO_DATETIME, REQUIRED          # for Tatkal
booking_closes_iso:               ISO_DATETIME, REQUIRED          # at chart preparation

quota_eligibility:
  eligibility_required:           boolean, REQUIRED
  documents_required:             array<enum>, REQUIRED, may be empty   # see §6
  age_min:                        int, REQUIRED                   # 0 if N/A
  age_max:                        int, REQUIRED                   # 0 if N/A
  gender_restriction:             STRICT ENUM, REQUIRED           # none | female_only | male_only

dynamic_fare_in_quota:            boolean, REQUIRED               # PT especially
fare_multiplier_in_quota:         float, REQUIRED

AvailabilityMatrix (returned by get_train_availability)

train_number:                     string, REQUIRED
journey_date:                     ISO_DATE, REQUIRED
fetched_at_iso:                   ISO_DATETIME, REQUIRED
availability_by_class:            array<TrainClass>, REQUIRED, ≥1
                                                                  # full detail per class+quota matrix
multi_date_outlook:               array, REQUIRED, may be empty   # ±3 day outlook
  - date:                         ISO_DATE, REQUIRED
    summary:                      STRICT ENUM, REQUIRED           # see §6 (lots_avail | tight | only_wl | sold_out)

FareBreakdown (returned by get_fare_breakdown)

train_number:                     string, REQUIRED
journey_date:                     ISO_DATE, REQUIRED
class_code:                       STRICT ENUM, REQUIRED
quota_code:                       STRICT ENUM, REQUIRED

per_passenger:
  adult:
    base_fare_inr:                INR_INTEGER, REQUIRED
    reservation_charge_inr:       INR_INTEGER, REQUIRED
    superfast_charge_inr:         INR_INTEGER, REQUIRED
    catering_charge_inr:          INR_INTEGER, REQUIRED
    dynamic_fare_premium_inr:     INR_INTEGER, REQUIRED           # 0 if not dynamic
    tatkal_charge_inr:            INR_INTEGER, REQUIRED           # 0 if not TQ
    premium_tatkal_charge_inr:    INR_INTEGER, REQUIRED           # 0 if not PT
    gst_inr:                      INR_INTEGER, REQUIRED
    irctc_service_charge_inr:     INR_INTEGER, REQUIRED
    total_inr:                    INR_INTEGER, REQUIRED
  child_with_berth:               same shape as adult              # half-fare logic + reservation full
  child_no_berth:                 same shape as adult              # 0 base, only reservation
  senior_male:                    same shape as adult              # post-2020 senior subsidy abolished but flag retained
  senior_female:                  same shape as adult
  disabled:                       same shape as adult              # subsidized fare per IR rules

partner_fees:
  irctc_service_charge_inr:       INR_INTEGER, REQUIRED            # already in per-pax above; flagged for transparency
  partner_convenience_fee_inr:    INR_INTEGER, REQUIRED           # ideally 0; capped per IRCTC
  payment_gateway_charge_inr:     INR_INTEGER, REQUIRED           # 0 for UPI; 1.8% for credit cards typically
  total_partner_fees_inr:         INR_INTEGER, REQUIRED

totals:
  amount_payable_to_irctc_inr:    INR_INTEGER, REQUIRED
  amount_charged_to_user_inr:     INR_INTEGER, REQUIRED            # = irctc + partner fees
  refund_eligible_inr:            INR_INTEGER, REQUIRED            # if cancelled now
  refund_charges_inr_breakdown:   array, REQUIRED, ≥1
    - cancel_window_label:        string, REQUIRED                 # e.g. "≥48h", "12-48h"
      flat_charge_inr:            INR_INTEGER, REQUIRED
      pct_charge_pct:             int, REQUIRED, 0-100
      net_refund_inr:             INR_INTEGER, REQUIRED

travel_insurance:
  available:                      boolean, REQUIRED
  charge_per_passenger_inr:       INR_INTEGER, REQUIRED            # ₹0.49 typical IRCTC
  insurance_provider:             string, REQUIRED
  coverage_summary_text:          string, REQUIRED
  is_optional:                    boolean, REQUIRED                # MUST be true (IRCTC rule)

BookingResult (returned by book_train)

booking_ref:                      string, REQUIRED                 # partner ref
pnr_number:                       string, REQUIRED                 # 10-digit IRCTC PNR (canonical)
status:                           STRICT ENUM, REQUIRED            # see §6
booked_at_iso:                    ISO_DATETIME, REQUIRED

train:                            TrainOption.train, REQUIRED      # snapshot
journey:                          TrainOption.journey, REQUIRED    # snapshot

passengers:                       array, REQUIRED, ≥1
  - passenger_index:              int, REQUIRED
    name_redacted:                string, REQUIRED                 # always REDACTED at TOMO ingest
    age:                          int, REQUIRED
    gender:                       STRICT ENUM, REQUIRED            # see §6
    berth_preference_requested:   STRICT ENUM, REQUIRED
    berth_allocated:              STRICT ENUM, REQUIRED            # see §6 (or "TBD_AT_CHART")
    coach_number:                 string, REQUIRED                 # "S5", "B3" etc; "" if pending chart
    seat_number:                  string, REQUIRED                 # "32", "" if pending chart
    booking_status:               STRICT ENUM, REQUIRED            # see §6 (CNF, RAC_x, WL_x, PQWL_x, RLWL_x, GNWL_x)
    quota_used:                   STRICT ENUM, REQUIRED            # see §6
    is_concession_applied:        boolean, REQUIRED                # senior / disabled / etc.
    concession_kind:              STRICT ENUM, REQUIRED            # see §6 ("none" if not)
    travel_insurance_opted:       boolean, REQUIRED

class_booked:                     STRICT ENUM, REQUIRED            # see §6
quota_booked:                     STRICT ENUM, REQUIRED            # see §6
boarding_station_code:            string, REQUIRED
food_choice:                      STRICT ENUM, REQUIRED            # see §6 (veg | non_veg | jain | none_to_be_added)

financials:
  total_fare_inr:                 INR_INTEGER, REQUIRED
  irctc_total_inr:                INR_INTEGER, REQUIRED
  partner_fee_inr:                INR_INTEGER, REQUIRED
  payment_gateway_fee_inr:        INR_INTEGER, REQUIRED
  travel_insurance_inr:           INR_INTEGER, REQUIRED            # 0 if not opted
  amount_charged_inr:             INR_INTEGER, REQUIRED
  irctc_transaction_id:           string, REQUIRED                 # IRCTC bank ref
  partner_payment_ref:            string, REQUIRED

refund_policy:
  cancellation_charges_schedule:  array, REQUIRED, ≥1              # IRCTC cancellation tiers
    - window_label:               string, REQUIRED
      flat_charge_inr:            INR_INTEGER, REQUIRED
      pct_charge_pct:             int, REQUIRED, 0-100
      net_refund_per_pax_inr:     INR_INTEGER, REQUIRED
  partner_fee_refundable:         boolean, REQUIRED                # ideally true
  no_refund_after_iso:            ISO_DATETIME, REQUIRED            # chart prepared

irctc_e_ticket:
  e_ticket_pdf_url:               URL, REQUIRED                    # downloadable
  e_ticket_share_url:             URL, REQUIRED
  qr_code_url:                    URL, REQUIRED                    # for TTE verification
  m_ticket_eligible:              boolean, REQUIRED                # SMS-based ticket
  paperless_eligible:             boolean, REQUIRED                # show-on-phone

trust:
  partner_irctc_b2b_authorized:   boolean, REQUIRED                # IRCTC B2B partnership
  partner_b2b_id:                 string, REQUIRED
  partner_b2b_authorization_iso:  ISO_DATE, REQUIRED
  partner_pci_dss_compliant:      boolean, REQUIRED
  partner_iso_27001_certified:    boolean, REQUIRED

_provider:
  name:                           string, REQUIRED                 # "Confirmtkt", "Ixigo Trains", "RailYatri"
  tomo_partner_id:                string, REQUIRED
  partner_tier:                   STRICT ENUM, REQUIRED
  customer_support_phone:         string, REQUIRED
  customer_support_24x7:          boolean, REQUIRED
  in_app_chat_supported:          boolean, REQUIRED
  customer_support_irctc_dispute_specialist: boolean, REQUIRED      # for chart-day disputes

PnrStatus (returned by get_pnr_status)

pnr_number:                       string, REQUIRED
fetched_at_iso:                   ISO_DATETIME, REQUIRED
chart_prepared:                   boolean, REQUIRED
chart_prepared_iso:               ISO_DATETIME, REQUIRED            # epoch sentinel if not

current_status:
  passengers:                     array, REQUIRED, ≥1
    - passenger_index:            int, REQUIRED
      booked_status:              STRICT ENUM, REQUIRED            # original at booking
      current_status:             STRICT ENUM, REQUIRED            # latest CNF/RAC/WL movement
      current_position_in_quota:  int, REQUIRED                    # WL_x or RAC_x
      coach_number:               string, REQUIRED                 # "" if not yet allocated
      seat_number:                string, REQUIRED
      berth_allocated:            STRICT ENUM, REQUIRED
      is_confirmed:               boolean, REQUIRED
      will_travel:                boolean, REQUIRED                # false if WL not cleared by chart

prediction:
  confirmation_probability_pct:   int, REQUIRED, 0-100              # AI-predicted
  prediction_confidence:          STRICT ENUM, REQUIRED            # high | medium | low
  prediction_basis:               STRICT ENUM, REQUIRED            # see §6 (historical | api_quota_movement | hybrid)

train_running_status:
  current_running_status:         STRICT ENUM, REQUIRED            # see §6
  running_late_minutes:           int, REQUIRED                    # 0 if on time
  current_station_name:           string, REQUIRED                 # last reported
  next_station_name:              string, REQUIRED
  expected_at_origin_iso:         ISO_DATETIME, REQUIRED
  expected_at_destination_iso:    ISO_DATETIME, REQUIRED
  diversion_in_effect:            boolean, REQUIRED
  cancellation_in_effect:         boolean, REQUIRED                # full train cancelled

Forbidden fields

paid_placement_score | sponsored_rank | promotion_priority |
ad_bid | hidden_irctc_fee | inflated_dynamic_fare_multiplier |
fake_predicted_confirmation_pct | unmasked_passenger_pan |
unmasked_passenger_aadhaar | scanned_id_proof_url |
fake_chart_prepared_status | inflated_partner_fee_disguised_as_irctc

5. CONTROLLED VOCABULARIES

train.train_kind

mail_express | rajdhani | shatabdi | duronto | vande_bharat |
tejas | humsafar | garib_rath | jan_shatabdi | sampark_kranti |
intercity | superfast_express | passenger | memu | demu |
toy_train | suvidha_special | special_festival | suburban

train.zone

CR | WR | NR | SR | ER | NER | NWR | NCR | SCR | SECR | SER | SWR |
ECR | ECoR | WCR | NFR | KR | metro

train.rake_kind

LHB | ICF | Vande_Bharat | Tejas | Garib_Rath_LHB | Humsafar_LHB |
Memu_8_coach | Demu_3_coach | Special_Tourist | Antyodaya_LHB

train.runs_on_days

mon | tue | wed | thu | fri | sat | sun | daily

class_code / class_booked

1A | 2A | 3A | 3E | SL | CC | EC | 2S | FC | EA | EV |
1AC | 2AC | 3AC | first_class | executive_anubhuti | vistadome

quota_code / quota_used

GN | TQ | PT | LD | SS | FT | DP | DF | HP | HQ | YU | PH |
LDS | RD | RC | NR | OS | RS | YOGA | TC | EX | DS

(GN=General, TQ=Tatkal, PT=Premium Tatkal, LD=Ladies, SS=Senior, FT=Foreign Tourist, DP=Disabled, DF=Defence, HP=Headquarters, HQ=Headquarters Quota, YU=Yuva, PH=Physically Handicapped, etc.)

QuotaAvailability.status_kind

available | rac | wl | pqwl | rlwl | gnwl | regret | sold_out | not_yet_open | chart_prepared

QuotaAvailability.predicted_chart_status

likely_confirmed | likely_rac | likely_wl_partial_clear | likely_wl_no_clear | unknown

QuotaAvailability.booking_window_status

open_now | tatkal_opens_in_24h | premium_tatkal_opens_in_24h |
chart_prepared_no_booking | full_quota_exhausted | not_yet_arp_window

QuotaAvailability.quota_eligibility.documents_required

none | senior_age_proof | ladies_age_proof | aadhaar | pan | passport |
disability_certificate | armed_forces_id | foreign_tourist_passport_visa |
press_card | yuva_age_proof | concession_form | doctor_certificate

amenities_on_board.catering_kind

none | meals_inclusive_rajdhani | meals_inclusive_shatabdi |
meals_inclusive_vande_bharat | tea_coffee_only | pantry_car |
e_catering_only | platform_catering_only

amenities_on_board.toilet_kind

bio_toilet | bio_vacuum_toilet | indian | western | mixed

TrainClass.amenities_in_class.berth_kinds_available

upper | middle | lower | side_upper | side_lower | side_middle |
seat_only | sleeper_only | reclining_seat | executive_chair_car

BookingResult.passengers[].berth_preference_requested / berth_allocated

Same as berth_kinds_available plus TBD_AT_CHART.

BookingResult.passengers[].gender

male | female | other | not_disclosed

BookingResult.passengers[].booking_status

CNF | RAC_1 | RAC_2 | RAC_3 | RAC_4 | RAC_5 | RAC_6 | RAC_7 | RAC_8 |
WL | PQWL | RLWL | GNWL | TQWL | RLGN | RAC_TQ | CAN

(Indian Railways waitlist taxonomy. Numbers append: WL/142 = Waitlist position 142.)

BookingResult.passengers[].concession_kind

none | senior_male | senior_female | press | journalist | armed_forces |
disabled_blind | disabled_orthopedic | disabled_other | yuva |
foreign_tourist | doctor_emergency | student | unemployed_jobseeker

BookingResult.food_choice

veg | non_veg | jain | none_to_be_added | not_applicable

BookingResult.status

confirmed | wl_pending_chart | rac_pending_chart |
booking_failed_payment | booking_failed_quota_exhausted |
booking_failed_irctc_downtime | booking_failed_partner_error |
cancelled_by_user | cancelled_by_irctc | cancelled_train_cancelled |
manual_review_pending

PnrStatus.prediction.prediction_confidence

high | medium | low

PnrStatus.prediction.prediction_basis

historical_pattern | api_quota_movement | hybrid_ai_model | irctc_official_only

PnrStatus.train_running_status.current_running_status

not_yet_started | running_on_time | running_late_under_30min |
running_late_30_to_120min | running_severely_late_over_120min |
diverted | cancelled | rescheduled | terminated_short

AvailabilityMatrix.multi_date_outlook[].summary

lots_avail | tight | only_wl | sold_out | not_yet_open

cancel_train_ticket.reason

user_changed_mind | wrong_date | wrong_passengers | medical |
family_emergency | found_alternative | flight_train_clash |
weather_disruption | wl_no_clear | irctc_train_cancelled

departure_window

early_morning | morning | afternoon | evening | night | any

(early_morning=04:00-08:00, morning=08:00-12:00, afternoon=12:00-16:00, evening=16:00-20:00, night=20:00-04:00)


6. TTBS DIMENSIONS

Per-domain weights (locked)

travel (train overlay): { time: 0.30, taste: 0.20, budget: 0.30, safety: 0.20 }

TIME

SIGNALS USED:
  - journey.duration_minutes (lower = better)         weight 0.30
  - journey.is_direct                                 weight 0.20
  - journey.punctuality_30day_pct                     weight 0.30
  - QuotaAvailability.predicted_confirmation_pct (high) weight 0.20

USER BAND HANDLING:
  - departure_window strict → drop trains outside window
  - max_journey_hours HARD FILTER
  - intent_kind=tatkal → restrict search to Tatkal-eligible quotas + window

TASTE

SIGNALS USED:
  - train.train_kind (Rajdhani/VB/Shatabdi premium)   weight 0.30
  - rake_kind (LHB > ICF; Vande_Bharat highest)       weight 0.20
  - amenities_on_board (charging, wifi, food, bedroll) weight 0.20
  - amenities_in_class (privacy, width, recline)      weight 0.10
  - food_inclusive_in_fare (Rajdhani feel)            weight 0.10
  - punctuality (also informs taste perception)       weight 0.10

BUDGET

SIGNALS USED:
  - total_fare_per_adult_inr vs band:
      ok    → SL / 2S / CC
      good  → 3A / 3E / EC / CC premium
      great → 2A / 1A / EA
  - dynamic_fare_multiplier penalty if > 1.0          weight 0.20
  - tatkal_charge / premium_tatkal_charge added       weight 0.10
  - partner_convenience_fee_inr (lower better)        weight 0.20
  - refund_eligible_inr / amount_charged_inr ratio    weight 0.20

HARD FILTERS:
  - total_fare > preferences.budget_max_inr_per_passenger → drop
  - total > preferences.budget_max_inr_total → drop

SAFETY

SIGNALS USED:
  - is_lhb_coach (LHB anti-telescoping)               weight 0.20
  - amenities_on_board.cctv_in_coaches                weight 0.15
  - amenities_on_board.rpf_security_on_board          weight 0.15
  - amenities_on_board.fire_extinguisher_count_per_coach > 2 weight 0.10
  - rake_kind in (Vande_Bharat | Tejas | Rajdhani)    weight 0.10
  - trust.partner_irctc_b2b_authorized=true           HARD FILTER
  - trust.partner_pci_dss_compliant=true              HARD FILTER
  - is_special / is_premium = false-positive risk     penalty 0.10
  - punctuality_30day_pct > 80 (operational reliability) weight 0.20

HARD FILTERS:
  - ladies_compartment_required + class doesn't have one → drop
  - wheelchair_accessible_required + train lacks coach → drop

Hidden ranking factor

information_completeness_score weight 0.10. historical_partner_booking_success_rate weight 0.20 — partners with > 3% IRCTC PRS-failure rate get penalized. historical_predicted_confirmation_accuracy weight 0.10 — partners whose predicted_confirmation_pct deviates > 15% from actual chart outcomes get penalized.


7. COMPLETION CONTRACT

POST /api/v1/cpc/mcp_provider/{tomo_partner_id}
X-TOMO-Timestamp: <ms>
X-TOMO-Signature: sha256=<hex>

{
  "intent":            "travel.book_train",
  "intent_version":    "v1.0.0",
  "external_id":       "CONFIRMTKT-XYZ",
  "amount_inr":         3580,
  "closed_at":         "2026-05-15T22:18:00+05:30",
  "request_id":        "req_tr_8f2k_...",
  "status":            "journey_completed",
  "currency":          "INR",
  "booking_ref":       "CONFIRMTKT-XYZ",
  "pnr_number":        "8520473961",
  "train_number":      "12701",
  "train_name":        "Hyd-Mum Vande Bharat",
  "journey_date":      "2026-05-15",
  "departure_iso":     "2026-05-15T05:30:00+05:30",
  "arrival_iso":       "2026-05-15T22:00:00+05:30",
  "class_booked":      "EC",
  "quota_booked":      "GN",
  "passenger_count":    2,
  "all_passengers_confirmed": true,
  "irctc_total_inr":    3380,
  "partner_fee_inr":     150,
  "payment_gateway_fee_inr": 50,
  "travel_insurance_inr": 0,
  "rating_pending":      true,
  "notes":              ""
}

Status enum: journey_completed | cancelled_by_user_pre_chart | cancelled_by_user_post_chart | cancelled_by_irctc | wl_did_not_clear | failed_payment | failed_irctc_handshake

TOMO commission scenario (special): Train tickets are sold by IRCTC (government). Partners are aggregators with capped commission per IRCTC B2B agreement. TOMO commission on travel.book_train is 5% of partner_fee_inr (NOT amount_inr), locked by founder directive. This is because:

  • 90%+ of amount_inr flows directly to IRCTC (not partner)
  • Partner's commercial value is the convenience-fee margin only
  • Standard 10%-of-amount_inr would exceed partner's revenue → partner can't sustain

For a ₹3580 booking with ₹150 partner_fee:

  • TOMO commission = 5% × 150 = ₹7.50 (rounded to ₹8)
  • Partner keeps = ₹150 - ₹8 = ₹142

This is the ONE intent with bespoke commission economics. Locked at v1.


8. WIDGET

WIDGET TYPE:        train_listing_results
SOURCE:             src/widgets/types.ts
TYPE NAME:          TrainListingResultsPayload
RENDERED IN:        components/widgets/TrainListingResultsWidget.tsx

Default: 3-row preview per train showing train_number+name, kind/zone pill, departure/arrival times, duration, classes-available chips with status (AVAIL-12 / WL/142 / RAC/8 / chart prepared), best fare. Tap row → class+quota matrix + booking flow.


9. CACHING POLICY

Call TTL Rationale
search_trains 60s trains list stable, but availability shifts
get_train_availability 15s quota updates rapidly, especially Tatkal
get_fare_breakdown 5min fare structure stable for given class+quota
book_train 0s always fresh
get_pnr_status 30s PNR moves slowly, but chart-day churn is fast
cancel_train_ticket 0s
get_chart_status 0s live
change_boarding_station 0s

10. ERROR CODES

Code HTTP Meaning TOMO behavior
INVALID_REQUEST 400 Malformed surface
STATIONS_NOT_CONNECTED 404 no train origin → dest on date surface, suggest route via via
BEYOND_ARP_WINDOW 400 journey_date > 120 days surface
TATKAL_WINDOW_NOT_OPEN 409 Tatkal not yet open for date surface, give booking_opens_iso
QUOTA_EXHAUSTED 409 requested quota full surface, suggest alternates
IRCTC_USER_NOT_LINKED 401 irctc_user_id_token invalid/missing surface, prompt linkage
IRCTC_DOWNTIME 503 IRCTC PRS down retry with backoff
PRS_HANDSHAKE_FAILED 503 partner-IRCTC handshake retry once
PAYMENT_DECLINED 402 gateway rejection surface
FARE_CHANGED_DURING_BOOKING 409 dynamic fare changed mid-flow re-quote, get user re-confirm
BOOKING_TIMEOUT 408 IRCTC didn't return in time retry once
DUPLICATE_BOOKING 409 idempotency key match return existing PNR
PNR_NOT_FOUND 404 PNR doesn't exist surface
CANCELLATION_AFTER_CHART 409 chart prepared, special rules special TDR refund flow
RATE_LIMITED 429 partner-side throttle back off
INTERNAL_ERROR 500 partner-side failure drop partner

11. SANDBOX → PRODUCTION CHECKLIST

[ ] All §2 inputs validated, request_id echoed
[ ] search_trains returns ≥3 trains for "Hyderabad → Mumbai, tomorrow"
[ ] get_train_availability shows real quota counts (not stale, not stub)
[ ] get_fare_breakdown matches IRCTC official fare exactly (1% audit cross-check)
[ ] book_train returns valid 10-digit PNR within SLA
[ ] get_pnr_status reflects live IRCTC state
[ ] cancel_train_ticket honors IRCTC cancellation schedule exactly
[ ] get_chart_status returns chart timing accurately
[ ] All §4 required fields populated with REAL data (no fixtures)
[ ] No forbidden fields anywhere
[ ] No unmasked passenger PAN/Aadhaar in any response
[ ] e-ticket PDF + QR code generation works
[ ] CPC webhook arrives within 60s of journey completion (or refund)
[ ] HMAC verification passes
[ ] IRCTC B2B partnership ID + valid authorization document uploaded
[ ] PCI DSS attestation
[ ] ISO 27001 certificate
[ ] customer_support 24x7 reachable
[ ] customer_support_irctc_dispute_specialist available chart-day
[ ] No commission-based train ordering (1% audit cross-check)
[ ] Predicted confirmation pct accurate within ±15% over 100 sandbox bookings
[ ] Dynamic fare disclosure honest (sandbox vs IRCTC public site)
[ ] Tatkal window booking flow tested (T-1 day 10:00 AC, 11:00 non-AC)

12. ANTI-FABRICATION RULES

RULE 1 — No paid placement signals
RULE 2 — No fake predicted_confirmation_pct
  Predictions must derive from historical data + live quota movement.
  Inflating to drive bookings = breach + suspension.
RULE 3 — IRCTC B2B authorization mandatory
  trust.partner_irctc_b2b_authorized=true requires IRCTC partnership
  certificate on demand within 24h. False claims = listing rejection.
RULE 4 — Fare must match IRCTC official to the rupee
  base_fare + reservation + GST + IRCTC service charge must match IRCTC's
  own fare calculator. Inflating any line and disguising = breach.
RULE 5 — Convenience fee capped per IRCTC rules
  partner_convenience_fee_inr ≤ ₹50 per ticket (IRCTC B2B cap).
  Higher = breach + IRCTC partner suspension.
RULE 6 — Travel insurance must be optional
  travel_insurance.is_optional MUST be true (IRCTC mandate post-2018).
  Auto-opt-in = breach.
RULE 7 — Chart status must be authoritative
  chart_prepared from IRCTC's actual chart preparation event. Fake "chart
  prepared" to discourage cancellation = breach.
RULE 8 — Refund schedule must match IRCTC tier
  IRCTC publishes cancellation charges by class+window. Partner refund
  must match exactly. Skimming during refund = breach + IRCTC dispute.
RULE 9 — No PII leak
  No unmasked PAN / Aadhaar / passport / DL in any response. TOMO ingest
  scans for 12-digit and 10-char patterns + rejects.
RULE 10 — Customer support honest
  customer_support_irctc_dispute_specialist=true must be reachable on
  chart day for emergency disputes. TOMO field-tests.
RULE 11 — TOMO never holds the rail
  Money flows user → IRCTC via partner. TOMO orchestrates only.
RULE 12 — No commission-based train ordering
  TOMO ranks by user-fit (TTBS), not partner-paid promotion.

VERSION HISTORY

v1.0.0 — 2026-05-11 — Initial spec (Block C resume)