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_inrflows 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)