12306 (中国铁路 China Railway)
May 18, 2026 · View on GitHub
Mode: 🌐 Public (stations, trains, train, price) · 🖥️ Browser + Cookie (me, passengers, orders)
Domain: kyfw.12306.cn
Read-only adapter against the 12306 (China Railway) site. Anonymous queries hit the public ticket-search endpoints; authenticated queries reuse the user's existing browser login state. No CAPTCHA / slider / SMS / anti-abuse bypass is performed, no credentials are stored, and no booking / payment / ticket-sniping is implemented.
Commands
| Command | Mode | Description |
|---|---|---|
opencli 12306 stations | Public | Search the station bundle by Chinese name, telecode, full pinyin, or short alias |
opencli 12306 trains | Public | Trains between two stations on a given date with per-seat-class availability |
opencli 12306 train | Public | Stop list (arrive / depart / stopover time) for one train segment |
opencli 12306 price | Public | Ticket prices by seat class for one train segment + date |
opencli 12306 me | Browser (cookie) | Logged-in account summary (sensitive fields masked by default) |
opencli 12306 passengers | Browser (cookie) | Saved passenger list (sensitive fields masked by default) |
opencli 12306 orders | Browser (cookie) | In-progress orders (not yet ridden / refunded / completed) |
Usage Examples
# Search stations
opencli 12306 stations 上海 --limit 5
opencli 12306 stations beijing
opencli 12306 stations AOH # by telecode
# List trains between two stations
opencli 12306 trains 北京 上海 --date 2026-05-22 --limit 20
opencli 12306 trains BJP AOH --date 2026-05-22
# Show stops for one train
opencli 12306 train 24000000G10L --from 北京南 --to 上海虹桥 --date 2026-05-22
# Ticket prices by seat class
opencli 12306 price 24000000G10L --from 北京南 --to 上海虹桥 --date 2026-05-22
# Authenticated commands (require login on kyfw.12306.cn first)
opencli 12306 me
opencli 12306 me --include-sensitive
opencli 12306 passengers --limit 10
opencli 12306 orders
opencli 12306 orders --include-sensitive
# JSON output
opencli 12306 trains 北京 上海 --date 2026-05-22 -f json
Station Resolution
The <from> and <to> arguments on trains, train, and price accept
any of the following forms; lookup is anchored against the public
station_name.js bundle 12306 ships:
- Chinese name (
上海虹桥) - Telecode (
AOH, 3-4 uppercase letters, the wire format used in 12306's own API) - Full pinyin (
shanghaihongqiao, case-insensitive) - Short alias / abbr (
shhq)
Anything else raises ArgumentError with a hint pointing to the accepted
forms. Match order is exact-name first, so 北京 does not accidentally
resolve to 北京北 by substring.
Columns
stations
| Column | Notes |
|---|---|
name | Chinese station name |
code | 3-4 letter telecode (use in API URLs) |
pinyin | Full pinyin |
abbr | Short alias |
city | Chinese city name |
trains
| Column | Notes |
|---|---|
code | Public train code (G1, D301, ...) |
from_station / to_station | Chinese station names |
from_code / to_code | Telecodes |
start_time / arrive_time / duration | HH:MM format |
available | true when 12306 shows 预订 status |
business_seat / first_seat / second_seat | Availability for 商务座 / 一等座 / 二等座 (有 / 无 / number string) |
soft_sleeper / hard_sleeper / hard_seat / no_seat | Same shape for 软卧 / 硬卧 / 硬座 / 无座 |
train_no | Internal id used by train and price commands |
train
| Column | Notes |
|---|---|
station_no | 1-based stop index within this train's route |
station_name | Chinese name |
arrive_time / start_time / stopover_time | Empty string for the origin (no arrive) and destination (no stopover) |
price
| Column | Notes |
|---|---|
seat_code | 12306 seat letter (A9 商务座, M 一等座, O 二等座, WZ 无座, A1 硬座, A3 硬卧, A4 软卧, F 动卧, P 特等座) |
seat_name | Localised Chinese label |
price | Decimal string (CNY) |
currency | Always CNY |
Rows are sorted by descending price. 12306 double-encodes some prices as
bare numeric keys (e.g. "9": "21580" mirroring "A9": "¥2158.0" in
no-decimal form); the bare-numeric duplicates are filtered out so each
seat class appears exactly once.
me
| Column | Notes |
|---|---|
username | 12306 login name (loginUserDTO.user_name) |
real_name | Masked by default; --include-sensitive to opt in |
email | Local-part masked by default |
mobile | 12306 already masks server-side (xxx****xxxx) and the adapter preserves that |
birth_date | Year only by default |
sex, country, user_type | 男 / 女, ISO-3, e.g. 成人 |
member, active | Boolean flags |
passengers
| Column | Notes |
|---|---|
name | Masked by default to <surname>*<...> |
sex | 男 / 女 |
born_year | Year only by default |
id_type | e.g. 居民身份证 |
id_no | 12306 masks server-side; the adapter never decodes |
mobile | Same as me |
passenger_type | 成人, 儿童, 学生, 残军 |
country | ISO-3 |
orders
| Column | Notes |
|---|---|
order_id | 12306 sequence_no |
order_date | Order placement timestamp |
train_code | Public train code |
from_station / to_station | Chinese names |
departure | Departure timestamp |
passengers | Comma-separated passenger names from the order; masked by default, --include-sensitive to opt in |
status | 12306 ticket / order status name |
amount | Total price (CNY) |
Login
The three authenticated commands (me, passengers, orders) require a
logged-in 12306 session in the same Chrome instance that OpenCLI's bridge
talks to. Sign in once at https://kyfw.12306.cn; the adapter then reads
the resulting tk / JSESSIONID cookies on subsequent runs.
Login is detected via document.cookie rather than
page.getCookies({url}), because 12306 sets the auth cookies with
Path=/otn and CDP Network.getCookies filters by URL path - a bare
https://kyfw.12306.cn URL filter would otherwise hide them. If tk or
JSESSIONID is missing, the adapter raises AuthRequiredError.
Privacy
Authenticated commands mask sensitive fields by default:
- Email: local part is
<first>***<last>@<domain> - Mobile: 12306's own mask is preserved (the adapter never tries to decode it)
- Real name / passenger/order passenger name: Chinese names are masked to
<first-char>*<last-char>(or<first-char>*for two-character names) - Birth date: year only
Pass --include-sensitive on me, passengers, or orders to opt back into
the unmasked fields the user is entitled to see on their own account. The
12306-side ID number mask is server-side and is never decoded.
Limit Validation
All --limit arguments use a strict validator that throws ArgumentError
on non-integer / out-of-range input rather than silently clamping. The
caps are:
stations: 1-50 (default 20)trains: 1-100 (default 50)passengers: 1-50 (default 20)
Endpoint Notes
/otn/leftTicket/initis hit first to mint anonymous session cookies (JSESSIONID,route,BIGipServerotn).- 12306 rotates the train-query endpoint name (
queryO,queryZ,queryA,queryG, ...) every few weeks. The adapter walks a list of known names; when the server responds with{c_url: "leftTicket/queryX"}it captures the suggested name and retries. - The
|-separated train wire record includes a base64 booking-handshakesecrettoken at position 0. This adapter parses but does not surface that field (a unit test asserts it cannot leak via the row shape).
Non-Goals
- No ticket sniping
- No order submission / payment
- No CAPTCHA / slider / SMS / anti-abuse bypass
- No password storage
- No order history beyond the in-progress slice (left as a follow-up)