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.
| 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) |
# 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 jsonThe <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.
| 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 |
| 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 |
| 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) |
| 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.
| 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 |
| 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 |
| 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) |
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.
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.
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)
/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).
- 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)