Files
Flights/AIRLINE_INTEGRATION_GUIDE.md
T
Trey T 92a69cf16c Add Sun Country (SY) load integration
Sun Country runs Navitaire (same PSS as JSX) but exposes their public
availability search endpoint that returns BETTER load data than AA:
per-flight `capacity` AND `sold` (booked passenger count), so we can
compute exact load factor.

Implementation:
- AirlineLoadService.fetchSunCountryLoad: POSTs to
  syprod-api.suncountry.com/api/nsk/v4/availability/search/simple.
  Parses results→trips→journeysAvailableByMarket, matches by flight
  number, pulls capacity + sold + equipmentType from legInfo.
- Returns a single Economy CabinLoad with capacity/booked = sold.
  No standby program — SY is single-cabin Y.
- Auth: Azure APIM subscription key + a long-lived dotREZ JWT
  (both static, captured from suncountry.com network traffic, neither
  is a user session token).
- Anti-bot: Imperva WAF in front of syprod-api.suncountry.com is gated
  on User-Agent + Referer + Origin headers. applySunCountryBrowserHeaders
  mirrors the pattern we use for UA / AA. NO WebView needed.
- Explicit ⚠️ log when 403 Incapsula response detected, pointing at
  the header helper.

Test infrastructure:
- knownDailyFlights now carries a dayOffset (today vs tomorrow) per
  carrier — different upstreams have different snapshot windows:
  AM is T-1d..T+0 (today); SY's Navitaire only returns future flights
  (tomorrow); others default to tomorrow as a safer choice.
- Added test_SY_sunCountry with hubs MSP/LAS/MCO/DEN. Fallback is
  SY104 LAS-MSP tomorrow.

Docs:
- AIRLINE_INTEGRATION_GUIDE: SY status row + full section 5c covering
  endpoint, auth, headers, response shape, failure modes, and how to
  re-capture tokens when they rotate.

Reverse-engineering notes:
- SY app is Flutter (Dart AOT) — bridge smali is minimal. Strings
  extracted from libapp.so revealed isNonRevTrip/isStandby/
  inventoryControl keywords + the syprod-api hostname.
- Token endpoint is PUT (not POST). Returns {"data":null} — token is
  the existing Authorization JWT, not a session refresh.
- Confirmed working from plain curl with browser headers (no Imperva
  TLS-fingerprint gate beyond UA/Referer/Origin).

Test run 2026-05-26 (xcodebuild test):
   AA, AM, AS, B6, EK, KE, SY (capacity=186 sold=184 load=99%), UA
  ⏭️ XE
  8 passing, 1 skipped, 0 failures, 11s total.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 20:30:55 -05:00

21 KiB
Raw Blame History

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:

{
  "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:

{
  "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)

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)

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)

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, 12 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: <uuid>. 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=<IATA>
  &code=<4-digit, zero-padded>
  &departureDate=<YYYY-MM-DD>
  &operatingCarrier=AM
  &operatingFlightCode=<4-digit, zero-padded>

GET https://lw18yj0mhb.execute-api.us-east-1.amazonaws.com/rb/passengerlistupgrade
  ?<same params>

operatingFlightCode is validated against ^[0-9]{4}$ — zero-pad short flight numbers.

Response shape:

{
  "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: <dotREZ JWT — eyJhbGc...>
  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):

{
  "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)

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 2448 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).

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)

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.
  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).