visavi / rzd-api
Клиент API РЖД: поиск поездов, вагоны и места, маршруты, цены
Requires
- php: >=8.2
- ext-json: *
- php-http/discovery: ^1.20
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/http-message: ^1.1 || ^2.0
Requires (Dev)
- guzzlehttp/guzzle: ^8.0
- guzzlehttp/psr7: ^3.0
- php-http/mock-client: ^1.6
- phpstan/phpstan: ^2.2
- phpunit/phpunit: ^11.5 || ^12.5 || ^13.0
Suggests
- guzzlehttp/guzzle: Реализация PSR-18, если в проекте ещё нет своего HTTP-клиента
README
Клиент API РЖД: поиск поездов, свободные места в вагонах, схемы вагонов, маршруты следования, календарь цен и справочники.
Что умеет
- Поиск поездов — расписание, время в пути, расстояние, типы вагонов, цены и количество мест
- Поиск с пересадками — цепочки рейсов там, где прямых поездов нет, с ожиданием и переездами между вокзалами
- Вагоны и места — номера свободных мест по вагонам и купе, цены, услуги
- Схемы вагонов — чертеж вагона в SVG и фотографии салона
- Маршрут поезда — все остановки с местным и московским временем, стоянками и часовыми поясами
- Станции — поиск кодов по названию, популярные города, коды смежных видов транспорта
- Календарь цен — минимальные цены по датам, даты с местами и горизонт продажи
- Справочники — тарифы, карты и абонементы, конфигурация сайта
- Аэроэкспресс — тарифы на поездку в аэропорт
Содержание
- Установка
- Быстрый старт
- Настройка
- Методы
- Ошибки
- Данные вне моделей
- Примеры
- Тесты
- Переход с 5.x
- Описание эндпоинтов
Установка
composer require visavi/rzd-api
Библиотека не привязана к конкретному HTTP-клиенту: ей нужна любая реализация PSR-18 и PSR-17. Если в проекте их ещё нет, достаточно Guzzle — он даёт и клиент, и фабрики:
composer require guzzlehttp/guzzle
Подойдёт любая другая пара, проверенные варианты:
| Клиент PSR-18 | Фабрики PSR-17 |
|---|---|
guzzlehttp/guzzle |
приедут вместе с ним (guzzlehttp/psr7), ставить отдельно не нужно |
symfony/http-client |
нужны отдельно: nyholm/psr7, laminas/laminas-diactoros, httpsoft/http-message |
php-http/curl-client |
нужны отдельно, те же варианты |
Реализация подхватывается автоматически через php-http/discovery, либо
передаётся в конструктор явно. Обратите внимание: symfony/http-client без
реализации PSR-17 не заработает — фабрик в нём нет.
Требования: PHP 8.2 или новее и расширение json.
Версия 6.0 работает с новым API ticket.rzd.ru и несовместима с 5.x. Если код
написан под прежний протокол pass.rzd.ru и переписывать его сейчас не нужно,
оставайтесь на пятой версии:
composer require visavi/rzd-api:^5.0
Она работоспособна, но новых данных туда не добавляется. Порядок перехода описан в docs/migration.md.
Быстрый старт
use Rzd\Client; use Rzd\Request\CarSearch; use Rzd\Request\TrainSearch; $client = new Client(); $result = $client->trains->search(new TrainSearch( origin: '2000000', // Москва destination: '2004000', // Санкт-Петербург date: new DateTimeImmutable('+7 days'), )); foreach ($result as $train) { printf( "%s %s → %s, мест %d, от %.2f\n", $train->number, $train->departure->format('d.m H:i'), $train->arrival->format('d.m H:i'), $train->freeSeats(), $train->minPrice(), ); } // Вагоны первого поезда: параметры собираются из него самого $cars = $client->cars->search(CarSearch::forTrain($result->trains[0])); foreach ($cars->withSeats() as $car) { printf("вагон %s %s, места: %s\n", $car->number, $car->typeName, $car->freePlaces); }
Коды станций ищутся по названию:
foreach ($client->stations->suggest('Чебоксары') as $station) { printf("%s %s %s\n", $station->name, $station->code, $station->timezone); }
Настройка
Сетевые настройки — таймаут, прокси, повторы, логирование — задаются в HTTP-клиенте, а не в библиотеке. Так они настраиваются один раз для всего приложения, и библиотека не дублирует возможности клиента.
Сайт принимает запросы только с российских адресов, с остальных соединение уходит в таймаут. Вне РФ нужен прокси:
use GuzzleHttp\Client as GuzzleClient; use GuzzleHttp\Psr7\HttpFactory; use Rzd\Client; use Rzd\Config; $factory = new HttpFactory(); $client = new Client( config: new Config(), httpClient: new GuzzleClient([ 'proxy' => 'socks5://127.0.0.1:1080', 'timeout' => 30, ]), requestFactory: $factory, streamFactory: $factory, );
Если клиент и фабрики не переданы, они определяются автоматически среди установленных реализаций PSR-18 и PSR-17.
Настройки самой библиотеки:
use Rzd\Config; use Rzd\Enum\Language; $config = new Config( // Язык ответов сайта language: Language::English, // Без User-Agent сайт отвечает 403, поэтому по умолчанию подставляется браузерный userAgent: 'MyApp/1.0', // Дополнительные заголовки к каждому запросу headers: ['X-Client-ID' => '22900'], );
Настройки неизменяемы, копию с другими значениями дают методы withLanguage,
withUserAgent и withHeaders.
Заголовки из настроек уходят со всеми запросами. Исключение одно: поиск с
пересадками добавляет к своему запросу куку LANG_SITE — без неё сайт отвечает
500, поэтому она важнее пользовательского Cookie.
Методы
Клиент разбит по ресурсам: trains, cars, routes, stations, prices,
references, aeroexpress, transfers.
Параметры передаются объектами запросов. Собрать такой объект можно двумя
способами: обычным конструктором с именованными аргументами либо фабрикой из
уже полученного ответа — CarSearch::forTrain($train),
RouteSearch::forTrain($train), CarSchemeSearch::forCar($car, $train),
TrainSearch::forStations($from, $to, $date),
TransferSearch::forStations($from, $to, $date),
TrainSearch::forDirection($direction, $date),
TransferSearch::forDirection($direction, $date).
Фабрики нужны не только для краткости. Например, запросу вагонов нужны коды
конкретных вокзалов (2000003 — Москва Казанская), а не города
(2000000 — Москва), которым искали поезда, плюс система бронирования поезда.
Перенося это руками, легко получить пустой ответ вместо ошибки.
Поиск поездов
$result = $client->trains->search(new TrainSearch( origin: '2000000', destination: '2004000', date: new DateTimeImmutable('2026-08-01'), adults: 2, // взрослых пассажиров children: 1, // детей без места fromSchedule: true, // добавлять поезда из расписания, у которых мест ещё нет largeFamily: false, // искать места для многодетных groupCars: false, // группировать вагоны одного типа ));
Если станции найдены подсказкой, коды доставать не нужно:
$result = $client->trains->search(TrainSearch::forStations( $client->stations->find('Москва'), $client->stations->find('Санкт-Петербург'), new DateTimeImmutable('2026-08-01'), adults: 2, ));
Фабрика принимает и ненайденную станцию: если find вернул null, будет
InvalidArgumentException вместо запроса с пустым кодом.
Популярное направление содержит обе станции сразу, подсказки ему не нужны:
$direction = $client->stations->directions()[0]; $result = $client->trains->search(TrainSearch::forDirection($direction, $date));
SearchResult перебирается как список поездов и хранит данные направления,
поэтому пустой результат отличим от отсутствия мест:
count($result); // сколько поездов найдено $result->trains; // список Train $result->withSeats(); // только поезда со свободными местами $result->cheapest(); // самый дешёвый из тех, где есть места $result->fastest(); // самый быстрый из тех, где есть места $result->originName; // название станции отправления $result->destinationName; $result->moscowTime; // текущее московское время сайта $result->partial; // сайт вернул не все поезда направления
Поезд:
$train->number; // 130Х $train->displayNumber; // номер для показа пользователю $train->name; // название фирменного поезда, иначе null $train->description; // категория: СК, ПАСС, СКОР $train->departure; // DateTimeImmutable по местному времени станции $train->arrival; $train->moscowDeparture; // то же по московскому времени $train->duration; // время в пути в минутах $train->distance; // расстояние в километрах $train->carriers; // ['ФПК'] $train->carGroups; // группы вагонов с ценами и местами $train->freeSeats(); // свободных мест по всем группам $train->minPrice(); // минимальная цена по всем группам $train->provider; // система бронирования, нужна для запроса вагонов $train->originStationCode; // код станции, а не города — тоже нужен для вагонов
Поиск туда-обратно делает два запроса: сайт не умеет искать пару маршрутов одним, его собственная страница туда-обратно поступает так же. Параметры обратного плеча повторяют первое, меняются только станции и дата:
$trip = $client->trains->searchReturn($search, new DateTimeImmutable('2026-08-05')); $trip->forward; // SearchResult туда $trip->back; // SearchResult обратно $trip->hasSeats(); // есть места в обе стороны $trip->minPrice(); // минимальная стоимость поездки целиком
Группа вагонов:
$group->type; // Compartment, Luxury, Soft, ReservedSeat, Sedentary $group->typeName; // КУПЕ, СВ, ЛЮКС, ПЛАЦ, СИДЯЧИЙ $group->serviceClasses; // ['2Э'] $group->places; // свободных мест $group->lowerPlaces; // из них нижних $group->upperPlaces; $group->minPrice; $group->maxPrice; $group->availability; // Available, LastPlaces, NotAvailable
Поиск с пересадками
Обычный поиск отдаёт только прямые поезда. Между городами без прямого
сообщения цепочку из нескольких рейсов строит отдельный метод. Города здесь
задаются идентификаторами узлов сайта, а не кодами станций: их отдаёт
подсказка станций в поле nodeId.
use Rzd\Enum\TransportProvider; use Rzd\Request\TransferSearch; $result = $client->transfers->search(new TransferSearch( origin: '5a13bdc3340c745ca1e8aa54', // Новый Уренгой destination: '5a13baab340c745ca1e7f31c', // Абакан date: new DateTimeImmutable('2026-08-20'), minTrips: 2, // наименьшее число рейсов в цепочке, 1 добавит прямые maxTrips: 4, // наибольшее, то есть пересадок плюс один maxResults: 200, // предел числа вариантов providers: [TransportProvider::Rails], // виды транспорта в поиске ));
Готовый запрос можно собрать прямо из подсказок:
$request = TransferSearch::forStations( $client->stations->find('Новый Уренгой'), $client->stations->find('Абакан'), new DateTimeImmutable('2026-08-20'), );
Подсказка возвращает и город, и его вокзалы. Годится любой узел, но выдача
отличается: город объединяет вокзалы, а отдельный вокзал даёт больше вариантов
от себя самого — Москва → Архангельск это 8 вариантов от города и 12 от
Ярославского вокзала. Узел города станции доступен как $station->cityId.
Результат перебирается как список вариантов поездки:
count($result); // сколько вариантов найдено $result->routes; // список TransferRoute $result->withSeats(); // варианты, где места есть на всех плечах $result->cheapest(); // самый дешёвый $result->fastest(); // самый быстрый
Вариант поездки перебирается как список плеч — частей, оформляемых одним билетом:
$route->changes(); // число пересадок $route->minPrice; // стоимость всей поездки по самым дешёвым местам $route->maxPrice; $route->duration(); // время в пути в минутах, вместе с ожиданием $route->waits(); // ожидание на каждой пересадке, в минутах $route->waitTotal(); // сколько всего стоять на пересадках $route->departure(); // отправление первого рейса $route->arrival(); // прибытие последнего $route->origin(); // Place начала поездки $route->destination(); $route->hasSeats(); // места есть на всех плечах $route->trips(); // все рейсы поездки подряд, list<Trip> $route->legs; // плечи, list<RouteLeg> $route->transfers; // переезды между вокзалами, list<Transfer>
Рейс:
$trip->number; // номер поезда, например 002Э $trip->transportType; // Train, Bus, Airplane $trip->origin; // Place, с названием станции и города $trip->destination; $trip->departure; $trip->arrival; $trip->duration(); // время в пути в минутах $trip->distance; // километров $trip->freePlaces; $trip->minPrice; $trip->maxPrice; $trip->products; // классы обслуживания с ценами, list<TripProduct> $trip->train(); // Train со всеми данными обычного поиска, либо null
Сайт вкладывает в рейс поезда полный ответ обычного поиска, поэтому вагоны и цены доступны без второго запроса:
foreach ($route->trips() as $trip) { foreach ($trip->train()?->carGroups ?? [] as $group) { printf("%s %d мест от %s\n", $group->typeName, $group->places, $group->minPrice); } }
Переезд между вокзалами появляется, когда цепочка приходит на один вокзал
города, а уезжает с другого. Пустой список transfers означает, что все
пересадки происходят на одном вокзале, а не что пересадок нет:
$transfer->origin?->name; // Ярославль (Московский вокзал) $transfer->destination?->name; // Ярославль-Главный $transfer->duration; // время переезда в минутах, как у рейса и поездки $transfer->seconds; // то же без округления $transfer->price; // стоимость
Вагоны и места
Запросу вагонов нужны коды конкретных станций поезда и его система бронирования. Всё это есть в найденном поезде, поэтому проще собрать параметры из него:
$cars = $client->cars->search(CarSearch::forTrain($train));
Или задать вручную:
$cars = $client->cars->search(new CarSearch( origin: '2000003', destination: '2060500', trainNumber: '130Х', departure: new DateTimeImmutable('2026-08-01 00:20'), provider: 'P1', ));
count($cars); // сколько вагонов $cars->withSeats(); // только вагоны со свободными местами $cars->cheapest(); // самый дешёвый из тех, где есть места $cars->train; // данные поезда, приходят тем же ответом $car->number; // 09 $car->typeName; // КУПЕ $car->serviceClass; // 2Э $car->freePlaces; // «2, 4» как отдаёт сайт $car->placeNumbers(); // [2, 4] числами, пометки пола отброшены $car->places; // свободных мест $car->minPrice; $car->maxPrice; $car->serviceCost; // стоимость сервисных услуг, входит в цену $car->schemeId; // идентификатор схемы вагона $car->subType; // 64К, определяет схему foreach ($car->compartments as $compartment) { printf("купе %s: %s\n", $compartment->number, implode(', ', $compartment->placeNumbers())); }
Пометка после номера места (4М, 12Ж, 22С) означает пол пассажиров в купе,
к номеру места отношения не имеет и в placeNumbers() отбрасывается.
Схема и фотографии вагона
use Rzd\Enum\SchemeView; use Rzd\Request\CarSchemeSearch; $request = CarSchemeSearch::forCar($car, $train); $scheme = $client->cars->scheme($request); $scheme->schemeId; // 567 $scheme->isTwoStorey(); $scheme->has(SchemeView::DesktopSecondStorey); // Чертёж вагона в SVG $svg = $client->cars->schemeImage($scheme->schemeId, SchemeView::DesktopFirstStorey); // Фотографии салона foreach ($client->cars->images($request) as $image) { printf("%s %s\n", $image->title, $image->content); }
Маршрут поезда
use Rzd\Request\RouteSearch; $route = $client->routes->search(RouteSearch::forTrain($train)); foreach ($route as $stop) { printf( "%s приб %s отпр %s стоянка %s мин, МСК %+d\n", $stop->stationName, $stop->arrival?->format('d.m H:i') ?? '', $stop->departure?->format('d.m H:i') ?? '', $stop->stopDuration, $stop->timeZoneDifference, ); }
У части поездов сайт отдаёт несколько вариантов маршрута, например с
прицепными вагонами. search возвращает основной, all — все.
Прежнее имя routes->forTrain() сохранено до 7.0, но помечено устаревшим:
рядом с фабрикой запроса вызов читался как
routes->forTrain(RouteSearch::forTrain($train)).
Станции
// Поиск по части названия, повторяющиеся коды отбрасываются $stations = $client->stations->suggest('ЧЕБ'); // Первая подходящая станция или null: подсказки отсортированы по близости // к запросу, поэтому для готового названия города разбирать список незачем $station = $client->stations->find('Чебоксары'); $station->name; // Чебоксары $station->code; // 2060620, он нужен для поиска поездов $station->nodeId; // идентификатор узла нового сайта, нужен для пересадок $station->cityId; // узел города станции, у самого города равен nodeId $station->isCity(); // узел города, а не отдельного вокзала $station->region; // Российская Федерация $station->type; // Город, Станция, Поселок $station->timezone; // Europe/Moscow $station->codes; // ['Railway' => ..., 'Cbdpr' => ..., 'Bus' => ..., 'Avia' => ...] $station->stationCodes; // коды всех вокзалов города // Популярные города $client->stations->popular(); // Популярные направления: готовые пары станций foreach ($client->stations->directions() as $direction) { printf("%s → %s\n", $direction->origin?->name, $direction->destination?->name); } // Город или станция по идентификатору узла $client->stations->byNodeId('5a323c29340c7441a0a556bb');
Календарь цен
// Даты, на которые между станциями есть поезда с местами $dates = $client->prices->availability( '2000000', '2004000', new DateTimeImmutable('+1 day'), new DateTimeImmutable('+21 days'), ); // Минимальные цены по датам отправления foreach ($client->prices->calendar('2000000', '2004000', new DateTimeImmutable('+1 day')) as $day) { printf("%s от %.2f\n", $day->date->format('d.m.Y'), $day->minPrice); $day->byCarType(); // ['Compartment' => 2037.30, 'Luxury' => 9043.30] $day->carriers(); // ['ФПК', 'ДОСС'] }
Отдельный вопрос — до какой даты продажа открыта вообще. Сайт отдаёт календарь примерно на тринадцать месяцев вперёд, из которых заполнены только доступные:
foreach ($client->prices->saleCalendar('2000000', '2004000') as $month) { printf("%d-%02d: дней в продаже %d\n", $month->year, $month->month, count($month->saleDays)); $month->availableDays; // числа месяца, на которые есть поезда $month->saleDays; // числа месяца, на которые открыта продажа $month->isOnSale(15); // открыта ли продажа на 15-е $month->dates(); // те же дни объектами DateTimeImmutable }
Справочники
foreach ($client->references->tariffs() as $tariff) { printf("%s %s %s\n", $tariff->sysName, $tariff->category, $tariff->isActive() ? '' : 'недействующий'); } // Конфигурация сайта отдаётся массивом: набор ключей меняется сайтом $config = $client->references->appConfig();
Карты и абонементы перевозчиков — скидка в процентах либо фиксированное число поездок:
foreach ($client->references->cards() as $card) { printf("%s %s %.2f\n", $card->code, $card->name, $card->price); $card->discount; // скидка в процентах $card->tripQuantity; // число поездок у абонемента $card->activeDays; // срок действия в днях $card->carTypes; // ['Compartment', 'Sedentary'] $card->serviceClasses; $card->isPass(); // абонемент на поездки, а не скидочная карта $card->fitsCarType('Compartment'); }
Аэроэкспресс
У аэроэкспресса свои тарифы: место обычно не фиксировано, а билет действует несколько месяцев, поэтому поиска поездов здесь нет.
foreach ($client->aeroexpress->tariffs(new DateTimeImmutable('+7 days')) as $tariff) { printf("%s %.2f\n", $tariff->name, $tariff->price); $tariff->type; // Standard, Business $tariff->description; // условия применения $tariff->maxTickets; // сколько билетов можно купить одним заказом $tariff->guaranteedSeat; $tariff->documentTypes; }
Коды станций необязательны: без них приходят тарифы, действующие на любом направлении от аэропортов и к ним.
Ошибки
Все исключения библиотеки реализуют Rzd\Exception\RzdException, поэтому
ловятся одним catch:
use Rzd\Exception\ApiException; use Rzd\Exception\ForbiddenException; use Rzd\Exception\InvalidArgumentException; use Rzd\Exception\MalformedResponseException; use Rzd\Exception\RzdException; use Rzd\Exception\TransportException; try { $client->trains->search($search); } catch (TransportException $e) { // Сайт недоступен: таймаут, обрыв соединения, ошибка прокси. // Вне РФ самая частая ошибка } catch (ForbiddenException $e) { // Запрос отбит защитой сайта, обычно из-за пустого User-Agent } catch (ApiException $e) { // Сайт ответил ошибкой $e->statusCode(); // 500 $e->errorCode(); // INTERNAL_ERROR $e->body(); // тело ответа целиком } catch (MalformedResponseException $e) { // Ответ успешный, но это не JSON } catch (InvalidArgumentException $e) { // Некорректные параметры, обнаружены до обращения к сайту } catch (RzdException $e) { // Любая ошибка библиотеки }
Данные вне моделей
Сайт отдаёт у поезда больше семидесяти полей, у вагона больше восьмидесяти. Модели описывают то, что нужно на практике, а полный ответ остаётся доступен, поэтому редкое поле не требует правки библиотеки:
$train->get('TrainBrandCode'); // 3033 $train->get('BoardingSystemTypes'); $train->raw; // весь ответ сайта по этому поезду $result->raw; // весь ответ целиком
Значения, которые присылает сайт (CarType, Provider, CarNumeration),
остаются строками, а не перечислениями: сайт может добавить новое значение,
и перечисление сломало бы клиент на ровном месте. Перечисления используются
только там, где значение выбираем мы: Language, SchemeView.
Примеры
Запускаются из корня проекта, вне РФ — с прокси:
php examples/index.php # список примеров php examples/index.php search_trains # запустить один php examples/search_trains.php # то же напрямую RZD_PROXY=socks5://127.0.0.1:1080 php examples/index.php search_trains
Либо в браузере, со страницей-навигацией по примерам:
RZD_PROXY=socks5://127.0.0.1:1080 php -S localhost:8000 -t examples
| Пример | Что показывает |
|---|---|
| search_trains.php | поиск поездов, цены, типы вагонов |
| round_trip.php | поиск туда-обратно, стоимость поездки целиком |
| transfers.php | цепочки рейсов с пересадками, ожидание |
| car_places.php | вагоны, свободные места по купе |
| car_scheme.php | схема вагона в SVG и фотографии салона |
| train_route.php | маршрут поезда по станциям |
| stations.php | коды станций, популярные города |
| price_calendar.php | горизонт продажи, наличие мест, цены по датам |
| cards.php | карты и абонементы со скидками |
| aeroexpress.php | тарифы аэроэкспресса |
| tariffs.php | справочник тарифов, конфигурация сайта |
Тесты
composer test # на моках, без обращения к сети composer test:coverage # с покрытием composer analyse # PHPStan, уровень 8
Живые запросы к сайту вынесены в группу live и исключены из обычного прогона
и из CI, поскольку сайт принимает их только с российских адресов:
RZD_PROXY=socks5://127.0.0.1:1080 composer test:live
Лицензия
MIT