Developing a Mobile App for Train Tickets
Train ticket sales app requires integration with carrier API—in Russia this is JSC RZD via Express-3 IS or through intermediary platforms (Tutu.ru API, UFS, Ticketcloud). Direct Express-3 access available only to authorized agents, so most independent apps use aggregators.
Rail API Integration Specifics
Express-3 route search returns trains with all cars, seat types, and availability. Response is heavy—up to 500 KB XML on popular routes like Moscow–St. Petersburg. Main-thread deserialization—guaranteed ANR on Android or freeze on iOS. Parse in background queue: DispatchQueue.global(qos: .userInitiated) on iOS, Dispatchers.IO on Android.
Seat selection in car—interactive schema like aviation but with compartments, hard seating, and SV (superior). Each schema different; data about car layout comes from API as array of seats with coordinates. Render via Canvas—custom component with zone select mode support (lower/upper berth for hard seating).
// Carriage type filtering
data class CarriageFilter(
val type: CarriageType, // PLATZKART, COUPE, SV, SITTING
val onlyLowerBerths: Boolean = false,
val withBedding: Boolean = true
)
// Composable for carriage type selection
@Composable
fun CarriageTypeSelector(
available: List<CarriageType>,
selected: CarriageType,
onSelect: (CarriageType) -> Unit
) {
LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
items(available) { type ->
FilterChip(
selected = type == selected,
onClick = { onSelect(type) },
label = { Text(type.localizedName()) }
)
}
}
}
E-ticket and Offline Access
After payment—user gets travel receipt as PDF. RZD conductor accepts QR code—must be stored locally, available without internet. On iOS—WKWebView with locally loaded PDF, or native rendering via PDFKit. On Android—PdfRenderer from android.graphics.pdf.
Push notifications via FCM/APNs: schedule change, train delay (data via RZD train status API), 24-hour reminder before departure.
Payment and Refund
SBP is the preferred option for most users: instant, no commission for individuals. YooKassa or PSB (Promsvyazbank, official RZD acquirer). Apple Pay via PKPaymentRequest and Google Pay via PaymentsClient mandatory for high conversion.
Ticket refund—separate API call with penalty calculated by tariff: more than 8 hours before departure—full refund minus service fee, less than 8 hours—50% of cost. Don't implement penalty logic on client—only show data from server. Some aggregators (UFS, Tutu) return refund amount directly in cancellation response.
Search and Travel History
Route search: from-to field with autocomplete—for Tutu API this is GET /v1/cities?q=mosk. Station directory loads on first run and caches locally (around 2500 RZD stations).
Typical search returns 10–30 trains with different departure times. Sorting: by departure time, by travel time, by price (lowest class). Filtering: carriage type, lower berths only, departure time (day/night).
"My Tickets" history: upcoming, archive. For each trip—"Refund Ticket" button with refund sum calculation and "Show QR" button for offline access.
Dynamic Prices and Seat "Lottery"
Express-3 has dynamic pricing: lower compartment berth price rises as free seats decrease and departure approaches. Moscow–Sochi search a month out might be 3500 rubles, a week out—5800, a day out—7200. Show this info in train card.
Timeline
6–8 weeks for app with search, seat selection, payment, refund, and travel history via aggregator API. Pricing is calculated individually after requirements analysis.







