Verification date: 2026-09-30 (Asia/Kolkata).
This backend is undocumented. Nothing here is a stable public contract.
Evidence labels:
- Source — string or call in the website’s public JavaScript. Response not verified.
- Request — a request was sent. No readable success body.
- Response — HTTP 200 and JSON were read.
A source string is never described as fully verified.
Homepage https://delhimetrorail.com/ returned the bundle names from the brief:
https://delhimetrorail.com/static/js/main.81213a8b.chunk.jshttps://delhimetrorail.com/static/js/10.18d6e9f4.chunk.js
Also downloaded with a browser user agent: chunks 1, 4, 11, and 15. Python’s default client received HTTP 403 for lazy chunks; that was a client block, not an API result. Not every lazy chunk was read.
Successful curls used a browser user agent, Accept: application/json, Content-Type: application/json, Origin: https://delhimetrorail.com, and Referer: https://delhimetrorail.com/. Those official-site headers describe the request that worked. They are not permission to impersonate the site from our app.
In-browser fetch of line_list from https://delhimetrorail.com/ returned 200 JSON. The same fetch from https://example.com/ failed with TypeError: Failed to fetch and no readable body.
Source and successful calls:
https://backend.delhimetrorail.com/api/v2/{lang}/{path}
lang response-verified for en and hi.
GET /api/v2/en/ → 302 to /en/api/v2/en/ with an empty body. The base URL is not a catalog.
The client constant is REACT_APP_API_BASE || "https://backend.delhimetrorail.com" plus /api/v2. Header on official fetches: Content-Type: application/json. Method is GET unless noted.
| Attempt | Result |
|---|---|
| Curl with official site Origin and Referer | 200 JSON on the operations below |
Curl with no Origin, http://localhost:5173, or https://example.com |
Cloudflare HTML 403, title “Attention Required!” |
| Browser fetch on the official site | 200 JSON for line_list |
Browser fetch on https://example.com |
Failed fetch, no body |
| English keyword search, once | TCP connection reset, then a later call returned 200 |
access-control-allow-origin: * was present on official-origin JSON responses, including the HTTP 500 HTML error. That header was not visible to the page script (get returned null) even when the official page could read the body.
Do not spoof the official origin, copy challenge cookies, or retry 403. Milestone 1 must retry from this app’s own browser origin. If that fails, show an honest error and test with fixtures. A server adapter that also gets 403 is still blocked.
cf-cache-status on line_list was DYNAMIC. Cache-Control on successful JSON was not recorded.
No pagination fields were present on these payloads.
- Status: Response.
enandhi(Hindi body was JSON; field-by-field key audit was done on English). - Purpose: Lines for search and the map legend.
- Trailing slash:
GET .../line_list/→ 302 to/en/api/v2/en/line_list/, empty body. Call it without a slash. - English shape: array of 14 objects. Fields:
id,name,line_color,line_code,primary_color_code,secondary_color_code,class_primary,class_secondary,start_station,end_station,show_in_frontend,status. - Sample object (English, real): Line 1,
line_codeLN1,line_color“Red Line”,primary_color_code#c0282c,start_station“RITHALA”,end_station“SHAHEED STHAL ( NEW BUS ADDA)”,show_in_frontendtrue,status“Normal Service”. - Hindi:
name,line_color,start_station, andend_stationwere Hindi in the first object;line_codestayedLN1. - Full code list:
docs/DATA_SOURCES.md.
- Status: Response for
enandhi, keywordsRithalaandRajiv(English). - Purpose: Station search. The journey planner calls this only when the keyword is non-empty.
- Sample:
Rithala→ one object,station_codeRI,station_name“RITHALA” in English and “रिठाला” in Hindi.Rajiv→RCK, “RAJIV CHOWK”. - Fields present:
id,station_name,station_code,station_facility[](name,class_name,image.title,image.file). - Fields absent here: line code, line color,
interchange,status. Line chips needstation_by_lineor another verified join. Do not guess the line from the name.
- Status: Response for
enandLN1. - Purpose: Stations on one line. Source passes
line_code(for exampleLN1), not the color name. - LN1: 29 stations. Fields:
id,station_name,station_code,station_facility,interchange(boolean),status(all “Station Open” in this sample). - Facility names seen: “Divyang Friendly Station”, “Parking Available”, “Lift/Escalator Available”, “Interchange Station”. Image files are paths under the backend host. A listed lift is not live lift status.
interchange: trueis not a walking-transfer graph.
- Status: Response. No trailing slash in the source URL, and the calls below had none.
- Purpose: Online journey. This is the planner the first slice should call.
- Parameters that returned JSON:
from/to: station codesRI,KG,RCKfrom the responses above. Not station names.NOPE→ HTTP 500 HTML titled “Internal server error”.mode:least-distanceandminimum-interchange. ForRI→RCK, those two responses were identical. That does not prove the modes are aliases.lowest-fare: called from the official site and from this repo. HTTP 200 with an empty body forRI→KGat2026-09-30T12:00:00.000. No JSON to save. The download skips this mode.datetime:YYYY-MM-DDTHH:mm:ss.SSSwith no offset. Source builds it withmoment.utc(valueWithoutZ).local().
- Time samples,
RI→KG,least-distance, English:- Request
2026-09-30T02:27:11.310→new_start_time06:00:02,new_end_time06:34:02. - Request
2026-09-30T12:00:00.000→new_start_time12:00:20,new_end_time12:27:24. total_timestayed0:25:57,farestayed43, platform string stayed “Platform No. 2”.- The clock time moves with the requested wall time. The early request did not echo
02:27; it returned a later service time. Send Asia/Kolkata wall time. Do not send aZtimestamp. These are schedule times from the payload, not live arrivals.
- Request
- Top-level fields:
stations,from,to,from_station_status,to_station_status(status,title,note),total_time,fare,route[],message. - Leg fields:
line,line_no,path[](name,status),path_time,map-path[],station_interchange_time,start,end,direction,towards_station,platform_name,new_start_time,new_end_time. RI→KG: 1 leg, Red Line,line_no1,stations14,pathlength 14,map-pathlength 13,directionup,towards_station“SHAHEED STHAL ( NEW BUS ADDA)”,station_interchange_time0,fare43,total_time0:25:57,message"". StatusOpenwith empty title and note.RI→RCK: 2 legs,stations18. Leg 2 is Yellow Line,line_no2,directiondown,towards_station“MILLENNIUM CITY CENTRE GURUGRAM”,platform_name“Platform No. 1”,station_interchange_time10.0,path_time0:08:37. Change at Kashmere Gate.total_time0:44:34equals the twopath_timevalues plus 10 minutes. Treat10.0as minutes for this sample.- Hindi
RI→KG: same codes inmap-path, Hindifrom,to, line, path names, towards, and platform. Status text was “स्टेशन चालू है”, not “Open”.fareandtotal_timematched English. fare: number, no currency field and no fare-type field. The official UI prefixes₹. Empty or missing fare must stay unavailable. The official bundle’sfare || 0pattern is rejected.platform_name: localized phrase when present. Show that phrase for the leg. Do not invent one when it is absent.total_timeandpath_time: strings likeH:MM:SSdurations.- Source also reads
note,note_hindi, androute[].map-pathfor the map.note/note_hindiwere absent from these successes.map-pathwas present.
- Status: Response for
en,RI,KG,least-distance, with the trailing slash the bundle uses. - Source: fare-calculator chunk. Also searches with
station_by_keyword/all/{keyword}. - Fields:
stations,from,to,total_time,weekday_fare,weekend_fare,route[]. - This sample:
weekday_fare43,weekend_fare32,total_time0:25:57,stations14. On Wednesday 2026-09-30,station_route.farefor the same pair was 43, the same number asweekday_fare. - Leg fields here:
line,line_no,path,path_time,map-path,station_interchange_time,start,end. Noplatform_name,towards_station,direction, or clock times in this sample. - No smart-card field. Do not invent a smart-card price.
- Status: Response for
en,RI,KG,least-distance, with the trailing slash the bundle uses. - Sample: first train
RI06:00:02 →KG06:34:02. Last trainRI23:00:00 →KG23:25:49. Names “RITHALA” and “KASHMERE GATE”. - Fields:
first_train.endstation_from_first_train_estimated_time,first_train.first_train_route_detail[](start_st,start_time,end_st,end_time,interchange_time,start_station_name,end_station_name), and the same shape underlast_train/last_train_route_detail. - In this one payload, first-train
interchange_timewas""and last-traininterchange_timewas0. Do not coerce either to a number of changes. - This is a filtered first/last result for one pair and mode, not a station timetable and not a live departure.
For LN1, station_by_line names and station_route path names disagree for the same map-path codes:
| Code | station_by_line.station_name |
Journey path[].name |
|---|---|---|
| RHW | DR. BABA SAHEB AMBEDKAR HOSPITAL | ROHINI WEST |
| RHE | ROHINI | ROHINI EAST |
| PTP | MADHUBAN CHOWK (earlier PITAMPURA) | PITAMPURA |
Join on codes. Show the journey’s path names inside the journey, and the line-list name in search, until a person verifies which public name to prefer. One journey path name included a trailing space (KANHAIYA NAGAR ).
| Pattern | Notes |
|---|---|
station_by_keyword/{mode}/{keyword} |
First/last screen. The mode state is initialized to least-distance, so the built path can be station_by_keyword/least-distance/{keyword}. Not called. The verified search path is station_by_keyword/all/{keyword}. |
lowest-fare |
Journey preference string next to least-distance and minimum-interchange. Called. HTTP 200 with an empty body. See the station-route note above. |
POST /api/v2/{lang}/search_by_facility_category |
JSON body. Not called. |
GET /api/v2/{lang}/service_information |
useSimpleFetch in the main bundle. Not called. |
GET /api/v2/{lang}/menus |
Curl returned 301 to /api/v2/en/menus/ and an empty body. Not a JSON success. |
GET /api/v2/{lang}/passengers/notification |
Source only. |
ws://metro.stagemyapp.com:8000/ws/station_data/ and .../metro_line_data/ |
Staging websocket in source. Not called. Do not use. |
The website route /station/:id is a page route. In the fare chunk, that id is a station_code. No separate station-detail JSON path was found in the scanned bundles.
- Unknown station code on
station_route: HTTP 500, HTML, title “Internal server error”. line_listwith a trailing slash: HTTP 302, empty body.- Non-official origins: Cloudflare HTML 403. One search call reset the connection, then succeeded.
- Official client turns a failed
station_routeparse intofalseand can storenoteJSON inlocalStorage. We will not follow that storage pattern.
The curl files in curls/ were run and the JSON bodies saved under curls/responses/. Field-by-field notes are in curls/STRUCTURE.md. New response-verified paths from that set:
GET /api/v2/en/metro_line/LN1— two-element array: a line summary, then a separate updates object.GET /api/v2/en/station_by_line_linepage/LN1— station rows plus a final count object, not another station.GET /api/v2/en/station_brief_detail/HDNRand.../NBAA— station, line, facilities, and lift rows. Liftstatusis a boolean beside a relativelast_update, not a live reading.GET /api/v2/en/station_by_keyword/all/s— 10 stations, same search fields as before.GET /api/v2/en/station_route/SAKT/RI/least-distance/2026-09-30T02:58:08.829— three-leg journey. Same route fields as the earlier two-leg sample.
scripts/download-dmrc-snapshot.py saved line_list and station_by_line for en and hi under data/ on 2026-09-30. details also saves metro_line and English station_brief_detail records. Station-to-station station_route responses are not downloaded.
Search reads this snapshot. “Show my route” uses a saved station_route file when data/{lang}/routes/least-distance/{from}/{to}.json exists, including fare and platform from that file. Otherwise it calculates a path from station order: fewer changes, then fewer stops, with fare and timing unavailable.
scripts/download-dmrc-extra.py saved facility categories, one facility search per category (toilets, water, food, and the other listed categories), station_by_line_linepage for every line, menus, and service information, in English and Hindi. scripts/download-dmrc-routes.py resumes an English download of station_route, new_fare_with_route, and first_and_last_train_with_filter for every station pair, for least-distance and minimum-interchange. Hindi downloads are not requested. lowest-fare is not requested because it returns an empty HTTP 200. The journey time sent for every route is 2026-09-30T12:00:00.000. Progress is written to data/download-status.json. LN12 stays out of search and routing. The Magenta line and Magenta extension lists do not share a station code, so the calculated fallback does not invent a link between them.
detro.archives/ holds separate collectors for a later full site archive, including Hindi and the page calls named in the public bundles. Their catalog is detro.archives/CATALOG.md. They write to detro.archives/dump/, not to data/, and they are not the app.
From the running app at http://127.0.0.1:5173/:
- The page requested
GET /api/v2/en/line_listandGET /api/v2/en/station_by_keyword/all/Rithala. fetchfailed withTypeError: Failed to fetch. No status or body was readable.- The screen showed “Station search did not answer.” It did not show a route.
- The app did not send the official site’s
OriginorReferer.
This matches the earlier non-DMRC-origin block. Live planning from this app is not working. Fixture tests cover the route text.
- Browser access from this app’s origin is unverified and currently blocked in curl.
- Fare type is on
new_fare_with_route(weekday_fare,weekend_fare), not onstation_route. - Platforms and towards-stations are on
station_routelegs in these samples, not on the fare payload. - Route display names and line-list names can differ.
- No response included a dataset version or
sourceUpdatedAt. - Redistribution permission is unknown. Fixtures used in tests should be small and sanitized. Do not check in a full station dump as the app database.