Skip to content
visaviPublic

About

Api сайта rzd.ru

Topics

Resources

Stars

104 stars

Watchers

12 watching

Forks

Repository files navigation

Api сайта rzd.ru

Packagist Tests Coverage PHP Downloads License

Клиент API РЖД: поиск поездов, свободные места в вагонах, схемы вагонов, маршруты следования, календарь цен и справочники.

Что умеет

  • Поиск поездов — расписание, время в пути, расстояние, типы вагонов, цены и количество мест
  • Поиск с пересадками — цепочки рейсов там, где прямых поездов нет, с ожиданием и переездами между вокзалами
  • Вагоны и места — номера свободных мест по вагонам и купе, цены, услуги
  • Схемы вагонов — чертеж вагона в SVG и фотографии салона
  • Маршрут поезда — все остановки с местным и московским временем, стоянками и часовыми поясами
  • Станции — поиск кодов по названию, популярные города, коды смежных видов транспорта
  • Календарь цен — минимальные цены по датам, даты с местами и горизонт продажи
  • Справочники — тарифы, карты и абонементы, конфигурация сайта
  • Аэроэкспресс — тарифы на поездку в аэропорт

Содержание

Установка

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

About

Api сайта rzd.ru

Topics

Resources

Stars

104 stars

Watchers

12 watching

Forks

Releases

Packages

Used by

Contributors

Languages