92a69cf16c
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>
534 lines
21 KiB
Markdown
534 lines
21 KiB
Markdown
# 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: <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:**
|
||
```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: <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):**
|
||
```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).
|