# Airline API Integration Guide Drop-in reference for integrating flight load / seat availability data from 11 airlines. Each section tells you **what works today, how to call it, what you get back, and what's blocked**. Verified 2026-04-12, with regression-test runs 2026-05-26. ## Quick status (run `xcodebuild test -scheme Flights` to re-verify) | Carrier | Status | Notes | |---|---|---| | AA | ✅ Working | UA version gate — bump `aaAppVersion` in `AirlineLoadService.swift` when AA rejects with "Please update your version" | | UA | ✅ Working | Anonymous token, 30min TTL | | AS | ✅ Working | Static APIM key | | B6 | ✅ Status-only | Confirms flight exists; no load data without check-in session | | EK | ✅ Status-only | Confirms flight exists; load data requires PNR | | KE | ✅ Working | Returns seat count only (no capacity) | | AM | ✅ Working | Public AWS gateway Sabre proxy. Returns per-cabin `authorized`+`available` + full standby/upgrade passenger lists with `isStaff` flag and priority. Snapshot window: T-1d to T+2d. | | SY | ✅ Working | Navitaire availability search returns **`capacity` + `sold` per flight** (true load factor, better than AA). Imperva WAF gated on browser-shaped headers. No standby list (SY is single-class). | | ~~NK~~ | Removed | Spirit Airlines ceased operations (merged into Frontier). Removed from `AirlineLoadService` and tests. | | XE | Manual only | WKWebView path; unit tests can't exercise it | --- ## 1. United Airlines — WORKING (best US data) **What you get:** Per-cabin capacity, booked, available, revenue standby, space-available, upgrade/standby passenger lists with names. **Auth:** Anonymous token (~30 min lifetime), Playwright required (TLS fingerprinting blocks curl). **Flow:** ``` GET https://www.united.com/api/auth/anonymous-token → { data: { token: { hash: "DAAAA..." } } } GET https://www.united.com/api/flightstatus/upgradeListExtended ?flightNumber=2238&flightDate=2026-04-08&fromAirportCode=EWR Headers: x-authorization-api: bearer {token.hash} Accept: application/json ``` **Response shape:** ```json { "pbts": [ { "cabin": "Front", "capacity": 50, "booked": 50, "revenueStandby": 0, "sa": 5, "waitList": 0 }, { "cabin": "Middle", "capacity": 24, "booked": 16 }, { "cabin": "Rear", "capacity": 202, "booked": 164, "revenueStandby": 2, "sa": 4 } ], "front": { "cleared": [...], "standby": [...] } } ``` **Derived:** `availableSeats = capacity - booked`, `loadFactor = booked / capacity`. **Cabin mapping:** `Front` = Polaris/First, `Middle` = Premium Plus, `Rear` = Economy. **Other useful endpoints:** - `GET /api/flightstatus/status/{num}/{date}/{origin}/{dest}?carrierCode=UA` — gates, times, equipment - `GET /api/flightstatus/seatmap/{num}/{date}/{origin}/{dest}?carrierCode=UA` — seat map --- ## 2. American Airlines — WORKING **What you get:** Waitlist per class (First, Standby), seats available per class with semantic color, passenger names + order. **Auth:** None — but mobile User-Agent is mandatory. Direct curl blocked by TLS fingerprinting; use Playwright. **Required headers (on browser context):** ``` User-Agent: Android/2025.31 Pixel 7|14|1080|2400|1.0|AmericanAirlines x-clientid: MOBILE Device-ID: {any-uuid} Accept: application/json ``` **The UA format is strict.** Any deviation returns `{"error":["Invalid user-agent header"]}` from Akamai. **Flow:** ``` # Step 1 — find the flight GET https://cdn.flyaa.aa.com/apiv2/mobile-flifo/flightSchedules/v1.0 ?origin=DFW&destination=IAH&departureDay=9&departureMonth=4 &searchType=schedule&noOfFlightsToDisplay=20 # Step 2 — get waitlist + seats GET https://cdn.flyaa.aa.com/api/mobile/loyalty/waitlist/v1.2 ?carrierCode=AA&flightNumber={num}&departureDate={YYYY-MM-DD} &originAirportCode={origin}&destinationAirportCode={dest} Headers: x-referrer: fs ``` **Response shape:** ```json { "waitList": [ { "listName": "First", "seatsAvailableValue": 1, "seatsAvailableSemanticColor": "failure", "passengers": [ { "order": 1, "displayName": "BRI, K", "cleared": false, "seat": null } ] }, { "listName": "Standby", "seatsAvailableValue": 45, "seatsAvailableSemanticColor": "success", "passengers": [...] } ] } ``` `seatsAvailableSemanticColor`: `success` (many), `warning` (few), `failure` (≤1). --- ## 3. Alaska Airlines — WORKING (easiest integration) **What you get:** Seat map with per-seat status + `AvailableSeats` per cabin, **full standby + upgrade waitlists with passenger names**, capacity, cabin configuration. **Auth:** Static APIM key (decrypted from APK). **Plain curl works — no Playwright needed.** **Key:** `de1d0ff837444468a5ea868945aab738` **Header:** `Ocp-Apim-Subscription-Key: de1d0ff837444468a5ea868945aab738` ### Seat map (per-seat availability) ```bash curl "https://apis.alaskaair.com/1/guestservices/customermobile/viewseatmap/seatmap\ ?flightnumber=308&departureairport=SEA&arrivalairport=PSP&departuredate=2026-04-13" \ -H "Ocp-Apim-Subscription-Key: de1d0ff837444468a5ea868945aab738" ``` Per-seat status: `OCCD` (occupied), `OPEN`, `PCLA` (premium class), `PREM` (premium). Returns `AvailableSeats` per cabin section. ### Standby + upgrade waitlist (the prize — **no PNR needed**) ```bash curl -X POST "https://apis.alaskaair.com/1/guestservices/customermobile/seats/waitlist" \ -H "Ocp-Apim-Subscription-Key: de1d0ff837444468a5ea868945aab738" \ -H "Content-Type: application/json" \ -d '{ "marketedByAirlineCode":"AS", "departureAirportCode":"SEA", "departureLocalDate":"2026-04-12", "flightNumber":"308", "confirmationCode": null }' ``` **Response includes:** - `StandbyList.FlightLoad.Authorized` — cabin capacity - `StandbyList.FlightLoad.PremiumClassConfigured` — first-class exists - `StandbyList.Passengers[]` — `DisplayName`, `Position`, `Seat`, `UpgradedToPC` - `UpgradeList.FlightLoad.Authorized` — first-class capacity - `UpgradeList.Passengers[]` — upgrade waitlist **Key insight:** `confirmationCode: null` is accepted. Works for past, current, and future flights (tested 2+ weeks out). ### Flight status ``` GET /1/guestservices/customermobile/flights/status/AS/{num}/{YYYY-MM-DD} ``` --- ## 4. JSX — WORKING (per-flight seat counts) **What you get:** Per-flight `availableCount` per fare class, full schedule, route network. **Auth:** Anonymous JWT (15 min idle), Playwright required for the initial token call (Akamai). **Carrier code:** X2 (ICAO: XSR). ### Step 1 — get token (Playwright) ```javascript await page.goto('https://www.jsx.com'); const { data } = await page.evaluate(async () => { const r = await fetch('https://api.jsx.com/api/nsk/v2/token', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify({ applicationName: 'IBE', credentials: { channelType: 'DigitalWeb' } }) }); return r.json(); }); const token = data.token; // raw JWT, no "Bearer " prefix ``` ### Step 2 — per-flight availability (GraphQL) ``` POST https://api.jsx.com/api/v2/graph/searchAvailability Headers: authorization: {token} Content-Type: application/json Body: { "cachedResults": false, "query": "{ availabilityv5(request: { criteria: [{ dates: { beginDate: \"2026-04-15\", endDate: \"2026-04-15\" }, stations: { originStationCodes: [\"BUR\"], destinationStationCodes: [\"LAS\"] } }], passengers: { types: [{ count: 1, type: \"ADT\" }] }, codes: { currencyCode: \"USD\" } }) { results { trips { date journeysAvailableByMarket { key value { journeyKey stops designator { arrival departure destination origin } segments { identifier { identifier carrierCode } } fares { details { availableCount classOfService productClass passengerFares { fareAmount } } } } } } } } }" } ``` Returns per-flight seat counts per fare class (sample: BUR→LAS 6 flights, 1–2 seats each). ### Step 3 — low fare calendar (REST, simpler) ``` GET https://api.jsx.com/api/nsk/v1/availability/lowfare/estimate ?Origin=BUR&Destination=LAS&StartDate=2026-04-15&EndDate=2026-04-18 &IncludeTaxesAndFees=true&PassengerCount=1&CurrencyCode=USD Headers: authorization: {token} ``` ### Route network (GraphQL) `POST /api/v2/graph/primaryResources` — returns all markets + station coords. Script ready at `scripts/jsx_availability.js`. --- ## 5. ~~Spirit Airlines~~ — DEFUNCT Spirit ceased operations and merged into Frontier. Removed from the codebase entirely. Section retained as a placeholder so the numbering below doesn't shift. --- ## 5b. Aeromexico — WORKING (richer than AA in some ways) **What you get:** per-cabin `authorized` (capacity) + `available` (open seats), full standby + upgrade passenger lists with `isStaff` flag, numeric priority, fare class, booking class, ascendsToClass, original/new position, check-in / board status, PII (firstName, lastName, reservationCode/PNR). **Auth:** None. Public AWS API Gateway. Headers required: `channel: web`, `flow: CHECKIN`, `x-transaction-id: `. Values extracted from `com.aeromexico.aeromexico.amwidgets.utils.Constant` in the APK. **Flow:** ``` GET https://lw18yj0mhb.execute-api.us-east-1.amazonaws.com/rb/passengerliststandby ?departureAirport= &code=<4-digit, zero-padded> &departureDate= &operatingCarrier=AM &operatingFlightCode=<4-digit, zero-padded> GET https://lw18yj0mhb.execute-api.us-east-1.amazonaws.com/rb/passengerlistupgrade ? ``` `operatingFlightCode` is validated against `^[0-9]{4}$` — zero-pad short flight numbers. **Response shape:** ```json { "itineraryInfo": {"airline":"AM","flight":"0058","origin":"MEX","destination":"MTY","aircraftType":"789"}, "cabinInfoList": [{"cabin":"Y","authorized":238,"available":0}], "totalListed": 1, "passengers": [{ "isStaff": true, "rawPriorityCode": "SAE", "priorityCode": {"id":"SAE","priority":21}, "status": "STB", "bookingClass": "H", "ascendsToClass": "Y", "firstName": "RAMSITO", "lastName": "UNO", "reservationCode": "OBLWDT", "passengerId": "0A6612610001", "seat": null, "originalPosition": 2, "newPosition": 1, "checkInStatus": false, "boardStatus": false, "boardingPassFlag": false }] } ``` **Snapshot window** (empirical, AM0058 MEX-MTY): - T-3 days and earlier → `NONE LISTED` (data purged) - **T-1 day → T+0** → snapshot live, `passengers[]` populates when listed - T+1, T+2 → `NONE LISTED` (flight known but no snapshot) - T+3 and beyond → `FLIGHT NOT INITIALIZED` **Failure modes** to watch for in the response body: - `NONE LISTED` → params valid, no passengers / no snapshot yet - `FLIGHT NOT INITIALIZED - INVALID DATE OR CITY` → flight number doesn't match a real AM operation on that date+airport, OR snapshot window not open - The `code` query param is ignored — only `operatingCarrier` + `operatingFlightCode` + `departureAirport` + `departureDate` are discriminating **Cabin codes:** `Y` = Economy, `C` = Clase Premier (business), `P` = Premier One (long-haul biz/first), `F` = First. Mapped in `aeromexicoCabinName(code:)`. **AM Connect / regional flights** (e.g. AM1460 MEX-QRO) often return `FLIGHT NOT INITIALIZED` — they're not in AM's Sabre system. The integration falls back to a known-daily mainline flight (AM0058 MEX-MTY) when route-explorer surfaces a regional that the load endpoint doesn't recognise. --- ## 5c. Sun Country — WORKING (true load factor) **What you get:** Per-flight `capacity`, **`sold` (booked passenger count)**, equipment type, and per-fare-class `availableCount`. Direct load factor calculation (sold/capacity). No standby list — SY is single-cabin Y, no upgrade program. **Auth:** Azure APIM subscription key + a long-lived dotREZ JWT. Both static, both extracted from suncountry.com network traffic. No user session or login required. **Anti-bot:** Imperva WAF in front of `syprod-api.suncountry.com`. Gated on `User-Agent` + `Referer: https://www.suncountry.com/` + `Origin: https://www.suncountry.com` headers. Bare curl returns 403 with an Incapsula page; full browser-shaped headers pass cleanly. No WebView needed. **Flow:** ``` POST https://syprod-api.suncountry.com/api/nsk/v4/availability/search/simple Headers: Ocp-Apim-Subscription-Key: bc7f707786c44a56859c396102f6cd21 Authorization: User-Agent: Mozilla/5.0 (Macintosh; ...) Chrome/145 Safari/537.36 Referer: https://www.suncountry.com/ Origin: https://www.suncountry.com Content-Type: application/json Body: { "Origin": "MSP", "Destination": "LAX", "BeginDate": "2026-06-15", "EndDate": "2026-06-15", "Passengers": { "Types": [{"Type":"ADT","Count":1}] }, "Currency": "USD" } ``` **Response shape (truncated to the load-relevant bits):** ```json { "data": { "results": [{ "trips": [{ "journeysAvailableByMarket": { "MSP|LAX": [{ "designator": {"origin":"MSP","destination":"LAX","departure":"...","arrival":"..."}, "segments": [{ "identifier": {"identifier":"421","carrierCode":"SY"}, "legs": [{ "legInfo": { "capacity": 186, // total seats "adjustedCapacity": 186, "lid": 186, "sold": 106, // booked passenger count "equipmentType": "78T", "departureTimeUtc": "...", "arrivalTimeUtc": "..." } }] }], "fares": [{ "details": [{ "availableCount": 4 }] }] }] } }]}] } } ``` **Why this is better than AA:** AA returns "seatsAvailable" per cabin without telling you capacity. SY gives both, so load factor = sold/capacity is exact (~57% above for SY421 MSP-LAX). **Failure modes:** - HTTP 403 with Incapsula HTML → User-Agent / Referer / Origin headers dropped - HTTP 200 with empty `journeysAvailableByMarket` → flight already departed (Navitaire only returns future flights) or no SY service on that route/date - HTTP 401 → APIM key or JWT no longer valid; re-capture from www.suncountry.com network traffic **Re-capturing tokens:** Open suncountry.com in a browser DevTools network tab, find the `PUT /api/nsk/v1/token` request, copy the `Ocp-Apim-Subscription-Key` and `Authorization` header values. Update `sunCountryAPIMKey` and `sunCountryJWT` constants in `AirlineLoadService.swift`. --- ## 6. JetBlue — PARTIAL (status yes, loads need PNR) **What you get without PNR:** Flight status, full route database (12MB of origin/dest pairs, Mint/seasonal flags). **What you get with a PNR:** Per-cabin capacity, confirmed pax, authorized seats, standby/waitlist counts, passenger names. **Auth:** Static API key. **Keys:** - Main: `49fc015f1ba44abf892d2b8961612378` - Seat map: `a5ee654e981b4577a58264fed9b1669c` - MYB/PNR: `45804e33f26b44d1b144090af2788abf` ### Flight status (no PNR) ```bash curl "https://az-api.jetblue.com/flight-status/get-by-number?number=524&date=2026-04-08" \ -H "apikey: 49fc015f1ba44abf892d2b8961612378" ``` ### Route database (no PNR) ``` GET https://azrest.jetblue.com/od/od-service/routes ``` ### Priority list / loads (PNR required) ``` POST https://jbrest.jetblue.com/lookup/itinerary Body: { "fName":"JOHN", "lName":"DOE", "from":"LAX", "pnr":"ABC123", "channelID":"M" } → returns jbSessionId POST https://jbrest.jetblue.com/prioritylist/getPriorityList?jbSessionId={id} → numberOfCapacityJ/Y, numberOfAvailableSeatsJ/Y, numberOfAuthorizedSeatsJ/Y, numberOfStandbyPassengers, numberOfWaitListedPassengers, priorityListPassengers[] ``` **No public load path exists without a real PNR.** --- ## 7. Korean Air — PARTIAL **What you get:** Flight status, route availability. `flightSeatCount` endpoint exists but returns 0 for far-out dates (works best within 24–48 hrs of departure). **Auth:** None. `channel` header required (`app` for flight search, `pc` for seat count). ``` POST https://www.koreanair.com/api/fs/scheduleFlightSearch/flight/status/app Headers: channel: app Body: {"departureDate":"20260408","flightNumber":"017","searchOption":"FLTNUM", "departureLocationCode":"","arrivalLocationCode":""} POST https://www.koreanair.com/api/et/ibeSupport/flightSeatCount Headers: channel: pc Body: {"carrierCode":"KE","flightNumber":"017","departureAirport":"ICN", "arrivalAirport":"LAX","departureDate":"20260409"} ``` --- ## 8. Emirates — PARTIAL (zero-auth status only) **What you get:** Flight status with gates, times, equipment — zero auth, zero headers. **Staff load tables exist but are staff-travel only (PNR + last name required).** ```bash curl "https://www.emirates.com/service/flight-status?departureDate=2026-04-08&flight=221" ``` --- ## 9. Delta — BLOCKED (status only, no public load path) **What you get:** Rich flight status (gates, times, equipment, amenities per cabin). **Zero seat counts anywhere public.** **Auth:** Mobile User-Agent only for status. Shop/standby data requires SkyMiles auth + Akamai BMP sensor (blocked from scripts). **Mobile UA:** `FlyDelta/24.10.1 (Pixel 7; Android 14; Build/UQ1A.240205.004)` ```bash curl -X POST "https://mobile-api.delta.com/flight-status-mobile/details" \ -H "Content-Type: application/json" \ -H "User-Agent: FlyDelta/24.10.1 (Pixel 7; Android 14; Build/UQ1A.240205.004)" \ -d '{"airlineCode":"DL","flightNumber":"996","flightOriginDate":"2026-04-12"}' ``` **Load data endpoints that exist but are blocked:** - `POST /mytrips/getUpgradeAndStandby` — needs PNR + name - `POST /offers/shop` — returns `seatsAvailableCount` per fare, needs SkyMiles auth + SPA session flow - `POST /mwsb/service/shop` — same data, same auth requirement The SkyMiles login alone is not enough — the SPA sets session state that the backend validates. Direct API calls fail even with valid auth. --- ## 10. British Airways — BLOCKED (no public load data) Flight availability search exists (`/sc4/baflt-paa/rs/v1/flightavailability/search`) but returns bookable fares, not load factors. Seat availability is SOAP and needs a booking reference. **Public OAuth:** `POST https://oauth.baplc.com/grant` with `client_id=baflt` **Legacy SOAP:** `Authorization: Basic cHVibGljOnB1YmxpYw==` (public:public) No standby/waitlist endpoint found. --- ## 11. Qantas — BLOCKED Seatmap endpoint returns `isSeatAvailable`/`isSeatOccupied` per seat, but requires a valid boarding pass (productId + surname). All upgrade endpoints require auth. Akamai BMP with native sensor SDK makes automation impractical. --- ## 12. Lufthansa — BLOCKED (developer API has maps, not occupancy) Main API behind Cloudflare WAF (403 from curl). Official developer API at `api.lufthansa.com/v1/` has seat map **layouts** but not occupancy. Seat recommendation API needs PNR. Register: https://developer.lufthansa.com/member/register (free, 6 req/sec, 1000/hr). Same backend powers Lufthansa, SWISS, Austrian, Brussels. --- ## Anti-bot & auth cheat sheet | Airline | Bypass | Effort | |------------|--------------------|---------| | Alaska | APIM key header | Lowest (curl works) | | Emirates | none | Lowest (curl works) | | JetBlue | apikey header | Low (curl works) | | Korean Air | `channel` header | Low (Playwright or curl) | | JSX | Playwright → JWT | Medium | | United | Playwright → token | Medium | | American | Playwright + mobile UA | Medium | | Delta | mobile UA for status; shop blocked | Low/High | | BA / Qantas / Lufthansa | — | N/A (no public load data) | --- # Summary — what to build against ## Tier 1: Plug-and-play (integrate today) | Airline | Data quality | Call pattern | |---------|--------------|--------------| | **Alaska** | ★★★★★ seat map + full standby/upgrade lists w/ names, no PNR | Plain `curl` with APIM key | | **United** | ★★★★★ per-cabin loads + cleared upgrades + standby list | Playwright token + API fetch | | **American** | ★★★★ waitlist + seats per class w/ pax names | Playwright w/ mobile UA | | **JSX** | ★★★★ per-flight seat counts per fare class | Playwright JWT + GraphQL | These four are the core of any flight-load product. Alaska is the easiest to integrate (pure HTTP), United returns the richest data, American is close behind, JSX is the only public source for per-flight counts on a Navitaire-hosted carrier. ## Tier 2: Status only (useful, but no seat data) - **Emirates** — status, zero auth - **Korean Air** — status; `flightSeatCount` returns 0 far out - **JetBlue** — status + route DB; loads need PNR - **Delta** — rich status, no seat counts anywhere public ## Tier 3: Blocked / not useful - **BA, Qantas, Lufthansa** — no public load data. Qantas/BA need booking ref; Lufthansa dev API is layouts only. ## Recommended product shape 1. Start with Alaska (easiest, 15 min to wire up). 2. Add United for the standby/upgrade killer feature (needs Playwright worker). 3. Layer in American for the third major US carrier. 4. JSX as a bonus — only route pairs that JSX serves (private terminals). 5. For Delta/JetBlue: show flight status only, note "seat data unavailable" unless you have a PNR. 6. Use Emirates/Korean Air for status on international routes. ## Shared integration notes - **Cache aggressively** — all four Tier 1 sources return stable data per flight-date; a 60-second cache dramatically cuts load. - **Token management** — United (30 min) and JSX (15 min idle) need refresh logic. - **Playwright workers** — run one persistent browser context per airline; reuse across requests. - **Alaska is the exception** — no browser, no token, just HTTP. Full endpoint-by-endpoint reference: `airlines_request.md` (1692 lines, same directory).