Southwest Airlines Flight Search
Purpose
Search southwest.com for available flights between two airports on given dates and return matching itineraries — flight numbers, routing, segments, durations, stop counts, connection airports, layovers, on-time-performance (when shown), and a four-bucket fares object (wanna_get_away / wanna_get_away_plus / anytime / business_select) priced in both USD and Rapid Rewards points + tax. Supports round-trip and one-way (multi-city is modeled as a sequence of one-ways — Southwest does not sell true multi-city), Dollars or Points fare mode, full passenger mix (adult / child / lap-infant / senior), promo code, time-of-day / stops / sort filters, and the Low Fare Calendar month-grid sweep. Read-only — never click Continue, Select, Book, or any seat-selection control. Southwest deliberately refuses syndication to third-party search engines (Kayak, Google Flights, Expedia), so southwest.com is the only source of truth for Southwest fares.
When to Use
- Compare Southwest's four fare tiers for a specific O-D pair on specific dates.
- Sweep a month-grid for the cheapest-day fare (Low Fare Calendar).
- Price the same itinerary in dollars vs. Rapid Rewards points to inform a points-vs-cash decision.
- Confirm whether a Southwest route exists at all (Southwest's network is heavily point-to-point — many city pairs require connections, and some "expected" pairs like JFK don't exist because Southwest doesn't serve JFK).
- Any flow that needs Southwest fares — they cannot be obtained from any OTA, meta-search, or GDS aggregator.
Workflow
The only reliable surface is the scripted browser flow with the /air/booking/select.html?… deep-link URL. The internal JSON endpoint /api/air-booking/v1/air-booking/page/air/booking/shopping (and all sibling /api/air-booking/v1/* and /api/content/v1/* routes) returns Akamai 403 Access Denied on every cookieless HTTP call — verified across direct fetches and a residential-proxy fetch (Reference #18.1071ca17). Don't waste cycles trying to hit those endpoints; lead with the deep-link + scripted browsing path described below.
1. Verified + residential-proxy + verified session (mandatory)
SID=$(browse cloud sessions create --keep-alive --verified --proxies | jq -r .id)
export BROWSE_SESSION="$SID"
All three flags (--keep-alive, --verified, --proxies) are mandatory. Southwest is fronted by Akamai Bot Manager (bazadebezolkohpepadr token, /akam/13/8333552 sensor, ak_bmsc + bm_mi + bm_sz cookies). A bare or proxy-less session gets either the generic "There was a problem" error page or an outright Akamai-Access-Denied HTML.
2. Skip the form — go directly to the results page via deep-link URL
Southwest accepts the entire search payload as URL query parameters on /air/booking/select.html, which 301-redirects to /air/booking/select-depart.html and triggers the same React shopping flow the form would. This saves ~6 turns of form filling, autocomplete waiting, and date-picker clicking per search.
https://www.southwest.com/air/booking/select.html
?originationAirportCode=DAL
&destinationAirportCode=LAS
&departureDate=2026-06-15
&returnDate=2026-06-18
&tripType=roundtrip // or "oneway" (no &returnDate)
&adultPassengersCount=1 // 1-8
&seniorPassengersCount=0 // 65+; counts toward total (max 8)
&passengerType=ADULT // primary passenger pricing class
&fareType=USD // "USD" (dollars) or "POINTS"
&promoCode= // optional, leave empty for none
&int=HOMEQBOMAIR // internal tracking; safe to omit
Open it:
browse open --remote "https://www.southwest.com/air/booking/select.html?originationAirportCode=DAL&destinationAirportCode=LAS&departureDate=2026-06-15&returnDate=2026-06-18&tripType=roundtrip&adultPassengersCount=1&fareType=USD&passengerType=ADULT"
browse wait --remote load
browse wait --remote timeout 4000 # fare grid hydrates asynchronously
URL-param contract (verified from 301-redirect echo behavior and bootstrap config):
| Param | Values | Notes |
|---|---|---|
originationAirportCode / destinationAirportCode | 3-letter IATA | Must be a Southwest-served airport (see §3). |
departureDate / returnDate | YYYY-MM-DD | Local-date at the origin airport. Omit returnDate for tripType=oneway. |
tripType | roundtrip | oneway | Southwest does not offer true multi-city — model as a sequence of one-ways. |
adultPassengersCount | 1 … 8 | |
seniorPassengersCount | 0 … 7 | 65+; reduces the adult bucket. Combined adult + senior ≤ 8. |
passengerType | ADULT | The lookup pricing class. Children (2–11) and lap-infants are added on the in-page passenger drawer — they are NOT URL-deeplinkable; if you need them, fall through to the form-fill path in §6. |
fareType | USD | POINTS | URL form sends USD/POINTS; the page's own internal state object may show DOLLARS/POINTS — don't conflate. |
promoCode | string | Optional. |
3. Resolve airport codes (when input is a city name, not IATA)
The full Southwest airport list (122 stations, including Caribbean / Mexico / Central America) is published as a base64-encoded JS bundle at:
https://www.southwest.com/swa-ui/bootstrap/air-booking-v2/1/data.js
Fetch it with browse cloud fetch <url> --proxies, base64-decode the response body, and grep for "emailDisplayName":"…","cityServed":"…","stationName":"…","id":"XYZ" records. This is faster, cheaper, and more reliable than driving Southwest's airport autocomplete UI for the lookup. Cache the decoded list locally — the bundle is ~650 KB after decode.
Example records:
"emailDisplayName":"Dallas (Love)","cityServed":"Dallas","stationName":"Dallas (Love Field)","id":"DAL"
"emailDisplayName":"Chicago (Midway)","cityServed":"Chicago","stationName":"Chicago (Midway)","id":"MDW"
"emailDisplayName":"Chicago (O'Hare-Terminal 5)","cityServed":"Chicago","stationName":"Chicago (O'Hare-Terminal 5)","id":"ORD"
Multi-airport cities surface multiple records — use cityServed for "any Dallas airport" and pick DAL (Love) vs. DFW (Southwest does NOT serve DFW; only DAL).
4. Wait for the fare grid to hydrate
The HTML shell that comes back from /air/booking/select-depart.html is ~5 KB of <div id="root"> + script tags — the fare cells are rendered client-side after the React app calls /api/air-booking/v1/air-booking/page/air/booking/shopping from within the page context (where Akamai accepts the session). The grid takes 2–5 s wall-time to render after load fires.
Wait signal: at least one outbound row contains the text "Wanna Get Away" in a button. Don't snapshot before that — early snapshots get a skeleton-loader DOM.
# Snapshot once the price cells render
browse snapshot --remote > /tmp/snap-outbound.txt
5. Extract per-flight data from the snapshot
Each outbound itinerary card on /air/booking/select-depart.html exposes:
- Flight number(s) — 4-digit Southwest numerics (e.g.,
1234,2789); a connecting itinerary shows multiple flight numbers joined by/(e.g.,1234 / 5678). - Routing — origin → connection(s) → destination IATAs, visible as "DAL → LAS" or "DAL → HOU → LAS".
- Depart / Arrive times — local times per segment, displayed as
6:00 AMetc. Combine with the URL'sdepartureDateto build local ISO datetimes. - Aircraft type — when surfaced, appears as "Boeing 737-700", "Boeing 737-800", or "Boeing 737 MAX 8" in the segment detail row. Southwest operates an all-737 fleet.
- Total duration — formatted
Xh Ym. - Stop count + connection airport(s) — "Nonstop", "1 stop in HOU", "2 stops in HOU, MCO".
- Layover duration — shown per connection as
Xh Ym layover in <CITY>. - On-time-performance % — when shown (Southwest surfaces this inconsistently, mostly on mainline routes), labeled "On-time performance: 78%".
- Four-bucket fares —
Wanna Get Away/Wanna Get Away Plus/Anytime/Business Select. In Points mode the cell shows points ++ $5.60cash for taxes/fees. Sold-out buckets render as "Unavailable" or are missing entirely — flag assold_out: true.
Internal fare-class enum (from the bootstrap config — useful when parsing DOM attributes): WANNA_GET_AWAY_FARE, WANNA_GET_AWAY_PLUS_FARE, ANYTIME_FARE, BUSINESS_SELECT_FARE.
For round-trips, the page renders outbound first; after extraction, scroll/click the "Return" tab (read-only — selecting a tab is not a booking action) and re-snapshot to extract the return leg using the same shape.
6. Form-fallback path (only when URL deep-link is insufficient)
The deep-link URL covers adults + seniors, dates, O-D, fare type, promo. It does not cover:
- Children (ages 2–11) — must be added on the in-page passenger drawer.
- Lap infants (< 2) — same.
- Multi-city sequences — drive two one-way searches in sequence.
When the search requires children or lap-infants, open https://www.southwest.com/air/booking/ (the form, not the deep-link), and drive the form:
- Click
radio: Round Triporradio: One Way. - Click
combobox: Depart(the origin field), type the 3-letter IATA, wait 1500 ms for the autocomplete dropdown, click the matchingoption: <City>, <ST> - <IATA>. - Repeat for
combobox: Return. - Click the depart-date input → date picker → click the target date cell. Repeat for return date.
- Click
button: Passengers→ adjust adult / child / lap-infant counts via the +/- steppers → clickbutton: Confirm. Senior toggles also live here. - Click
button: Searchand continue from §4.
browse fill on the airport combobox will auto-press Enter and submit before the dropdown surfaces — use browse click then browse type separately, then browse wait timeout 1500 before clicking the option.
7. Low Fare Calendar (month-grid sweep)
The Low Fare Calendar shows the cheapest fare across a 30-day window. Deep-link:
https://www.southwest.com/air/low-fare-calendar/
?originationAirportCode=DAL
&destinationAirportCode=LAS
&tripType=roundtrip
&adultPassengersCount=1
&fareType=USD
&passengerType=ADULT
(No departureDate / returnDate — the calendar picks its own anchor month.) Each day-cell renders the lowest available fare for an outbound departing that date (round-trip pricing assumes a 3-night return; one-way mode shows one-way price). Capture per-day cheapest fare and the matching day-cell data- attribute or aria-label. Calendar hydration is the same — wait for the first price cell to render before snapshotting.
/air/low-fare-calendar.html (note the .html suffix) returns 404 — only the trailing-slash form works.
8. Filters (results page only)
Once on /air/booking/select-depart.html, the results page exposes a filter drawer (button: Filter):
- Stops — Nonstop / 1 stop / 2+ stops.
- Departure time window — Early morning (12am–5am), Morning (5am–noon), Afternoon (noon–6pm), Evening (6pm–midnight).
- Sort — Departure / Arrival / Duration / Price (per fare bucket).
Apply via UI clicks; there are no URL-param forms of these filters that survive a 301 redirect. Re-snapshot after each filter click.
9. Release the session
browse cloud sessions update "$SID" --status REQUEST_RELEASE
Site-Specific Gotchas
- READ-ONLY. Never click
button: Continue, the per-fare-cellbutton: Select, or any seat-map control. Selecting a fare advances to a passenger-info page and starts a booking flow. Stop at the results / calendar grid. - Akamai is the protection layer. Bot-manager fingerprint script at
/akam/13/8333552, sensor tokenbazadebezolkohpepadr, edge cookiesak_bmsc+bm_mi+bm_sz. Verified Akamai routing viaAkamai-Request-Bcresponse header.--verified --proxiesis non-negotiable. - The internal JSON shopping API is confirmed-blocked from cookieless callers.
/api/air-booking/v1/air-booking/page/air/booking/shopping,/api/air-booking/v1/air-booking/page/air/booking/price,/api/air-booking/v1/air-booking/page/air/booking/purchase-secure,/api/air-booking/v1/air-booking/page/air/booking/confirmation-secure, and/api/content/v1/*all return Akamai 403 (Reference #18.1071ca17.…) when called directly withbrowse cloud fetch --proxies— even with a fresh residential-proxy IP and no other anti-bot diff. Don't try to replay them out-of-band; the page-context fetch the React app makes is the only way Akamai accepts them. Don't waste time on direct API calls. /v2/path isDisallow:inrobots.txt. Southwest's robots.txt forbids spidering/v2/*(which is where the React static bundles live:/v2/air/booking/static/9.0.1/...). This is a politeness signal — the booking flow is on/air/booking/*which is allowed.select.html→select-depart.html301 redirect is part of the contract. The form-submit endpoint is/air/booking/select.html; that URL 301s to/air/booking/select-depart.htmlcarrying all query params unchanged. Build your deep-link againstselect.htmland let the redirect resolve — both URLs work, but constructing againstselect.htmlmatches what the form does.fareTypevalue mismatch. The URL param value isUSD(orPOINTS); the page's internal state object usesDOLLARS/POINTS. Don't sendfareType=DOLLARSin the URL — verified the form sendsUSD.- Children and lap-infants are NOT URL-deeplinkable. Only
adultPassengersCountandseniorPassengersCountare read from the URL; children and lap-infants must be added in the in-page passenger drawer. If you need them, take the form-fill path (§6). - Southwest does NOT serve DFW or JFK. Common pre-flight check: confirm both origin and destination are in the 122-airport list at §3. Don't fabricate a route — the SPA renders "We couldn't find any flights" silently if the route doesn't exist.
- All-737 fleet. Aircraft type is always
Boeing 737-700,Boeing 737-800, orBoeing 737 MAX 8when shown. Treat any other value as a parse error. - The fare grid hydrates 2–5 s after
load. Snapshot too early and you get the skeleton DOM with no price cells. Wait until at least one row contains "Wanna Get Away" text before extracting. - "There was a problem" generic error page. When Akamai bot-detection trips mid-session or the hydration XHR fails, Southwest renders a generic error card. Capture the page text, kill the session, re-create with fresh Verified flags, and retry once. If it recurs, return
success: false, reason: "site_error"rather than re-attempting indefinitely. - Geolocation is Akamai-inferred from the proxy IP. The HTML shell includes
swa.geolocation = "georegion=…,country_code=US,region_code=NY,…". This doesn't affect fare results but may cause US dollar/points denomination defaults. For non-US searches, no URL param is needed — Southwest only sells in USD/Rapid Rewards. - The bootstrap data bundle is base64-encoded.
https://www.southwest.com/swa-ui/bootstrap/air-booking-v2/1/data.jsreturns a base64 string that decodes to a ~650 KB JS module containing the airport list, fare-class enums, family-trip destination lists, and other UI metadata. The accompanying…/content/en.jsis similarly base64-encoded but contains CMS strings (homepage copy), not fare/airport data — onlydata.jsis useful for the airport-resolution shortcut in §3. - Sold-out buckets ≠ missing fare class. A flight that shows three of four prices with the fourth replaced by "Unavailable" is sold out on that bucket. A flight that shows fewer than four buckets total may be on a fare-class-restricted route (e.g., some short-haul routes don't sell Business Select). Distinguish in output:
sold_out: truevsnot_offered: true. - Senior fares (
seniorPassengersCount) are a separate pricing class but render in the same four-bucket grid; you can verify by re-running the same itinerary withseniorPassengersCount=1and confirming a different price (typically only Anytime and Wanna Get Away change). - No-change-fees + two-free-bags are static perks. Surface them as static metadata in the response (
perks: ["Two free checked bags", "No change fees"]), not as a per-flight field — they apply to all Southwest itineraries equally and the bootstrap config confirmsfareTypes.WGA.features.NO_CHANGE_FEE = truefor every fare bucket. - The Low Fare Calendar URL is
/air/low-fare-calendar/(trailing slash, no.html)./air/low-fare-calendar.htmlreturns 404. Easy footgun. - Filter state is not URL-encoded. Stops, time-of-day, and sort filters live in client-side React state only — they do not survive a page reload. Apply filters via UI clicks after the results render; don't try to URL-encode them.
Expected Output
{
"success": true,
"trip_type": "round_trip",
"origin": "DAL",
"destination": "LAS",
"depart_date": "2026-06-15",
"return_date": "2026-06-18",
"pay_with": "dollars",
"passengers": {
"adults": 1,
"seniors": 0,
"children": 0,
"lap_infants": 0
},
"promo_code": null,
"outbound_flights": [
{
"flight_numbers": ["1234"],
"routing": ["DAL", "LAS"],
"segments": [
{
"flight_number": "1234",
"origin": "DAL",
"destination": "LAS",
"depart_local": "2026-06-15T06:00:00",
"arrive_local": "2026-06-15T07:25:00",
"aircraft": "Boeing 737-800"
}
],
"duration_minutes": 205,
"stops": 0,
"connections": [],
"on_time_performance_pct": 78,
"fares": {
"wanna_get_away": {"price_usd": 79, "points": 4567, "points_plus_dollars": 5.60, "sold_out": false},
"wanna_get_away_plus": {"price_usd": 109, "points": 6800, "points_plus_dollars": 5.60, "sold_out": false},
"anytime": {"price_usd": 280, "points": 17500, "points_plus_dollars": 5.60, "sold_out": false},
"business_select": {"price_usd": 330, "points": 20700, "points_plus_dollars": 5.60, "sold_out": false}
}
},
{
"flight_numbers": ["2789", "3411"],
"routing": ["DAL", "HOU", "LAS"],
"segments": [
{"flight_number": "2789", "origin": "DAL", "destination": "HOU", "depart_local": "2026-06-15T08:15:00", "arrive_local": "2026-06-15T09:20:00", "aircraft": "Boeing 737 MAX 8"},
{"flight_number": "3411", "origin": "HOU", "destination": "LAS", "depart_local": "2026-06-15T10:55:00", "arrive_local": "2026-06-15T12:10:00", "aircraft": "Boeing 737-700"}
],
"duration_minutes": 295,
"stops": 1,
"connections": [{"airport": "HOU", "layover_minutes": 95}],
"on_time_performance_pct": null,
"fares": {
"wanna_get_away": {"price_usd": 119, "points": 7200, "points_plus_dollars": 5.60, "sold_out": false},
"wanna_get_away_plus": {"price_usd": 149, "points": 9400, "points_plus_dollars": 5.60, "sold_out": false},
"anytime": {"price_usd": 350, "points": 21900, "points_plus_dollars": 5.60, "sold_out": true},
"business_select": {"price_usd": null,"points": null, "points_plus_dollars": null, "not_offered": true}
}
}
],
"return_flights": [
{
"flight_numbers": ["4501"],
"routing": ["LAS", "DAL"],
"segments": [
{"flight_number": "4501", "origin": "LAS", "destination": "DAL", "depart_local": "2026-06-18T17:30:00", "arrive_local": "2026-06-18T22:35:00", "aircraft": "Boeing 737-800"}
],
"duration_minutes": 185,
"stops": 0,
"connections": [],
"on_time_performance_pct": 82,
"fares": {
"wanna_get_away": {"price_usd": 89, "points": 5300, "points_plus_dollars": 5.60, "sold_out": false},
"wanna_get_away_plus": {"price_usd": 119, "points": 7400, "points_plus_dollars": 5.60, "sold_out": false},
"anytime": {"price_usd": 295, "points": 18400, "points_plus_dollars": 5.60, "sold_out": false},
"business_select": {"price_usd": 345, "points": 21600, "points_plus_dollars": 5.60, "sold_out": false}
}
}
],
"perks": ["Two free checked bags", "No change fees"],
"error_reasoning": null
}
Distinct outcome shapes:
// Site-level error / Akamai wall
{ "success": false, "reason": "site_error", "error_reasoning": "Generic 'There was a problem' page rendered after hydration. Recreate session with fresh Verified flags and retry once.", ... }
// Anti-bot block (page-load 403 Access Denied)
{ "success": false, "reason": "anti_bot_block", "error_reasoning": "Akamai Access Denied at page load. Reference #18.xxxxxxxx.…", ... }
// Route not served by Southwest
{ "success": false, "reason": "route_not_served", "error_reasoning": "Southwest does not operate between <ORIG> and <DEST>. Both airports must appear in the 122-station network list.", ... }
// No availability on the requested date (route exists but zero flights)
{ "success": true, "outbound_flights": [], "return_flights": [], "no_availability": true }
// Low Fare Calendar month-grid sweep
{
"success": true,
"mode": "low_fare_calendar",
"origin": "DAL", "destination": "LAS", "trip_type": "round_trip",
"calendar": [
{"date": "2026-06-01", "cheapest_usd": 59, "fare_class": "wanna_get_away"},
{"date": "2026-06-02", "cheapest_usd": 69, "fare_class": "wanna_get_away"},
{"date": "2026-06-03", "cheapest_usd": null, "sold_out": true}
],
"perks": ["Two free checked bags", "No change fees"]
}