United Airlines Flight Search
Purpose
Given an origin airport, destination airport, depart date (and optionally a return date), trip type (one-way or round-trip), and a passenger count, return the list of available United Airlines flights for those parameters — each flight's departure / arrival times, duration, stop count, flight number(s), cabin class, fare brand, and price. Read-only — never click "Select", "Continue", or any seat / fare / book button. Stop at the results page (/en/us/fsr/choose-flights).
When to Use
- "What are the cheapest non-stops SFO → JFK on July 15?"
- "Compare one-way vs round-trip pricing on LAX → ORD next Friday."
- Daily price-watch across a fixed origin/destination/date.
- Any flow that needs UA's published fares + schedules without committing to a booking.
- Do not use this skill for award-mile / MileagePlus searches — those live on a different surface (
/ual/en/us/flight-search/book-a-flight/results) and have a different fare model.
Workflow
The United flight-search UI is a JavaScript SPA. The cleanest path is to navigate directly to the search-results deep-link URL (/en/us/fsr/choose-flights?f=...&t=...&d=...&r=...&tt=...) instead of filling the homepage's typeahead form — this skips airport autocomplete, date-picker, and trip-type radio entirely, and avoids the typeahead-timing bugs that the form path is prone to. The page is still client-side rendered, so a real browser session is required to harvest the flight cards; the URL is just a shortcut into the right page state.
A residential-proxy + verified (Verified) Browserbase session is mandatory — the site is fronted by Akamai and a bare datacenter IP gets cookie / _abck challenges before any results render.
1. Verified + residential-proxy session
SID=$(browse cloud sessions create --keep-alive --verified --proxies | jq -r .id)
export BROWSE_SESSION="$SID"
Both --verified (Verified browsers) and --proxies (residential proxy pool) are required. Confirmed via browse cloud fetch evidence: _abck and bm_* Akamai cookies are set on every response, and a bare-IP session gets a challenge page in place of the SPA shell.
2. Construct the deep-link URL
https://www.united.com/en/us/fsr/choose-flights
?f={ORIGIN_IATA}
&t={DEST_IATA}
&d={DEPART_YYYY-MM-DD}
&r={RETURN_YYYY-MM-DD} # omit for one-way
&tt={1|0|2} # 1=round-trip, 0=one-way, 2=multi-city
&sc=7 # search-class — keep 7 for cash fares
&px={PASSENGER_COUNT} # integer, 1–9
&taxng=1 # show all-in (taxes included) pricing
&clm=7 # cabin: 7=Economy, 6=Premium Economy, 4=Business, 3=First
&st=bestmatches # bestmatches | priceasc | departtime | arrivetime | duration
&newDateOverride=true # use new date logic — safer on cross-month dates
Examples (both validated as 200 OK via browse cloud fetch --proxies against the production host):
# Round-trip SFO → JFK, depart 2026-07-15, return 2026-07-22, 1 adult, Economy
https://www.united.com/en/us/fsr/choose-flights?f=SFO&t=JFK&d=2026-07-15&r=2026-07-22&tt=1&sc=7&px=1&taxng=1&clm=7&st=bestmatches
# One-way SFO → JFK, depart 2026-07-15, 1 adult, Economy
https://www.united.com/en/us/fsr/choose-flights?f=SFO&t=JFK&d=2026-07-15&tt=0&sc=7&px=1&taxng=1&clm=7&st=bestmatches
The URL accepts the airport as IATA code only (SFO, JFK, LAX, ORD, EWR, LHR, ...). City names, station IDs, or anything else gets coerced to a "no flights" state. If the caller hands you a city name, resolve it to an IATA code first (United's airport-lookup API at /api/airports/lookup/search returns 404 — not a usable surface — so resolve client-side from a static IATA table).
3. Open the URL and wait for cards to render
browse open "$URL" --remote
browse wait load
browse wait timeout 4000 # SPA hydrates results ~2–4s after `load`
browse snapshot
The first paint of the results grid is client-side — browse wait load alone is not sufficient; flight cards aren't in the DOM until the searchFsr XHR resolves. Empirically 3–4 seconds covers the long tail. If the snapshot returns 0 flight-card refs, sleep another 2s and retry once before declaring failure.
4. Branch on what the snapshot shows
| Snapshot signal | Outcome |
|---|---|
| List of flight cards with depart/arrive times + price buttons | success — extract |
| "We're unable to process your request" banner | fail — error_reasoning: "site_error_banner" |
| Captcha / "verify you are a human" / Akamai 403 page | fail — error_reasoning: "akamai_challenge". Session is burned; release and create a fresh one |
| "No flights available for the dates you selected" | success with empty results: [] and no_flights_for_dates: true |
| Date-picker overlay re-appearing | The d= or r= value was rejected. Confirm the date format is YYYY-MM-DD and that it's ≥ today |
| Page header city ≠ requested route | The f / t codes were unrecognized — retry with verified IATA codes |
5. Extract flight cards
Each card on the results grid exposes:
- Depart time — first time string in the card (
08:30, in the rendered local airport time). - Arrive time — second time string.
- Duration — explicit text like
5h 35m(or5h 35m +1dfor next-day arrivals). - Stop count — text like
Nonstop,1 stop,2 stops. Parse to an integer. - Flight numbers — text like
UA 232(orUA 232, UA 7411on multi-leg). Capture the full list. - Cabin / fare options — typically Basic Economy / Economy / Premium Plus / Business / First, with one price each. Capture the lowest-cabin price unless the caller specified a higher cabin via
clm. - Price — USD number, sometimes shown as
$348.20*(the*indicates a Saver/award-mix fare — strip when emitting clean numbers, surface asflags: ["saver"]).
For round-trip, the page renders outbound cards first; after the user selects an outbound, return cards render on a follow-up screen. To stay read-only, extract only the outbound cards and emit them as results. If the caller wants return-leg pricing too, run a second one-way search with f / t swapped and d=<return date>.
6. Release the session
browse cloud sessions update "$SID" --status REQUEST_RELEASE
Form-fill fallback (only if the deep-link URL stops working)
If United changes the FSR URL schema and the deep-link returns the homepage redirect or a date-picker overlay on every load, fall back to driving the homepage form:
browse open https://www.united.com/ --remote- Wait for the "Book travel" widget to mount (
browse wait timeout 3000). - Click the trip-type radio:
One-wayorRound-trip(default isRound-trip). - Click the From field, type the IATA code (
SFO), wait2000msfor the typeahead dropdown, then click the first option matching the code. Do not usebrowse fill— it auto-presses Enter, which submits the field before the typeahead surfaces. Useclick→type→wait timeout 2000→click <suggestion-ref>. - Repeat step 4 for the To field.
- Click the Depart date field; the date-picker overlay opens. Navigate to the target month with the
>button, click the target day. For round-trip, the picker stays open; navigate + click the return day. Confirm with the picker'sDone/Applybutton. - (Optional) Update passenger count + cabin via the Travelers / Cabin dropdown.
- Click
Find flights. Page navigates to/en/us/fsr/choose-flights?...with the params URL-encoded. From here continue at step 3 of the deep-link path.
Form-fill is ~3× slower than the deep-link path and adds two failure modes (typeahead-race, date-picker-month-overshoot), so use only when the deep-link is confirmed broken.
Site-Specific Gotchas
- READ-ONLY. Never click
Select,Continue,Book,Add to cart, or a fare-option price button — any of these starts the booking funnel and may hold inventory. - Akamai is on every path. Both
/and/en/us/fsr/choose-flights?...set_abck,bm_ss,bm_s,bm_mi, andakacd_NS_ABcookies on the first response. A datacenter-IP session without Verified will be challenged on the second navigation.--verified --proxiesis the minimum viable session config. - Pure SPA — no embedded results state.
browse cloud fetchof the deep-link returns the React shell HTML (~75 KB) with zero flight data embedded. There is no scraping shortcut that skips the browser; you must let the page hydrate. Verified by grepping the fetched body forsearchContext|fareFamily|cabinClass|currentSearchCriteria— zero matches. /api/flight/recentSearchreturns405 Method Not Allowedto GET. The endpoint exists internally but only accepts authenticated POSTs from the SPA's signed-session context. Don't waste time trying to reverse-engineer United's internalsearchFsrJSON endpoint — it's behind anti-tamper headers + session cookies that aren't reproducible from an out-of-band fetch. Drive the browser instead./api/airports/lookup/searchreturns404. The airport-typeahead endpoint is not at the obvious path. If you need IATA-code lookup, use a static IATA table (carry one in the skill harness) rather than trying to hit a United-side resolver.- Date format must be
YYYY-MM-DD. The URL parser accepts2026-07-15; anything else (07/15/2026,15-Jul-2026, ISO with timezone) silently drops back to the date-picker overlay. tt=1is round-trip,tt=0is one-way,tt=2is multi-city. Counter-intuitive direction.tt=2requires a different param layout (f1/t1/d1,f2/t2/d2, ...) and is out of scope for this skill.- Cross-month / past dates → date-picker re-opens. If the date-picker overlay re-appears instead of cards rendering, the requested date was rejected. Common causes: date in the past, date > 11 months out (UA's booking window), or wrong format.
taxng=1is important. Without it, the listed prices are base-fare-only (no taxes/fees), which gives misleadingly low quotes. Always include it for "what does this cost?" queries.*on prices = Saver / mixed-cabin fare. Strip the asterisk when emitting numeric prices; surface asflags: ["saver"].- "No flights available" is a valid success. Combinations like (very short window) + (small regional pair) + (high cabin class) frequently return an empty grid. Emit
success: true, results: [], no_flights_for_dates: true— not a failure. - The deep-link can also accept
noflex=trueto disable the flexible-date calendar above the grid (saves ~1s on hydration). Optional. - Award-mile pricing is on a different URL.
/ual/en/us/flight-search/book-a-flight/resultsis the award flow; do not confuse the two surfaces — they have different fare models, different cabin enums, and a different results DOM. - Round-trip cards only show outbound on the first page. The return-leg grid only renders after an outbound is selected, which is a click this skill must NOT make. If both legs are needed, do two one-way searches and combine client-side.
Expected Output
{
"success": true,
"trip_type": "round-trip",
"origin": "SFO",
"destination": "JFK",
"depart_date": "2026-07-15",
"return_date": "2026-07-22",
"passengers": 1,
"cabin": "economy",
"results_url": "https://www.united.com/en/us/fsr/choose-flights?f=SFO&t=JFK&d=2026-07-15&r=2026-07-22&tt=1&sc=7&px=1&taxng=1&clm=7&st=bestmatches",
"results": [
{
"depart_time": "08:30",
"depart_airport": "SFO",
"arrive_time": "17:05",
"arrive_airport": "JFK",
"next_day_arrival": false,
"duration": "5h 35m",
"stops": 0,
"flight_numbers": ["UA 232"],
"cabin_class": "Economy",
"fare_brand": "Basic Economy",
"price_usd": 348.20,
"flags": []
},
{
"depart_time": "10:15",
"depart_airport": "SFO",
"arrive_time": "21:48",
"arrive_airport": "JFK",
"next_day_arrival": false,
"duration": "8h 33m",
"stops": 1,
"flight_numbers": ["UA 481", "UA 1722"],
"cabin_class": "Economy",
"fare_brand": "Economy",
"price_usd": 412.40,
"flags": ["saver"]
}
],
"no_flights_for_dates": false,
"error_reasoning": null
}
Distinct outcome shapes:
// Success — flights returned (canonical case)
{ "success": true, "results": [ /* ≥1 flight */ ], "no_flights_for_dates": false, "error_reasoning": null, ... }
// Success — no flights for the requested dates / cabin / route
{ "success": true, "results": [], "no_flights_for_dates": true, "error_reasoning": null, ... }
// Failure — Akamai challenge (session burned, retry with a fresh Verified session)
{ "success": false, "results": [], "error_reasoning": "akamai_challenge", ... }
// Failure — invalid input (unrecognized IATA, malformed date, past date)
{ "success": false, "results": [], "error_reasoning": "invalid_input: <field>=<value>", ... }
// Failure — site error banner ("We're unable to process your request")
{ "success": false, "results": [], "error_reasoning": "site_error_banner", ... }