Files
Flights/AIRLINE_INTEGRATION_GUIDE.md
T
Trey T 398862e88b Add Aeromexico (AM) load integration
AM exposes a public Sabre GetPassengerListRQ proxy via AWS API Gateway —
no auth, no API key — used by the consumer app's flight-status widget.
The endpoint returns per-cabin authorized/available plus full standby +
upgrade passenger lists with isStaff flag, numeric priority, fare class,
position movement, and PII (matching what we get from AA but with
better cabin capacity data).

Implementation:
- AirlineLoadService.fetchAeromexicoLoad: parallel GETs against
  /rb/passengerliststandby and /rb/passengerlistupgrade, merging
  cabin info + per-list passengers into a single FlightLoad. Headers
  channel=web / flow=CHECKIN extracted from the AM APK Constant.smali.
  Cabin codes Y/C/P/F mapped to readable names (Economy / Clase Premier /
  Premier One / First).
- 4-digit zero-padding of the operating flight code (server validates
  ^[0-9]{4}$).
- "NONE LISTED" warning treated as nil (snapshot outside T-1d/T+2d
  window or no pax yet); explicit log so future failures are
  diagnosable.

Test infrastructure:
- Added test_AM_aeromexico using MEX/GDL/MTY/CUN hubs.
- Cascading fallback in runAirlineLoadTest: try the route-explorer
  discovered flight first; if it returns nil (typical for AM Connect
  regionals that aren't in Sabre), fall back to the known-daily flight
  (AM0058 MEX-MTY). Pattern useful for any future carrier whose
  regional ops don't show up in the load system.
- knownDailyFlights extended with AM0058 MEX-MTY.

Docs:
- AIRLINE_INTEGRATION_GUIDE: AM status row + full section 5b with
  endpoint params, response shape, snapshot window timing, failure
  modes, cabin code mapping, regional carrier caveat.

Test run 2026-05-26:
   AA, AM (cabins=1 upgrade=1), AS, B6, EK, KE, UA  ⏭️ XE
  7 passing, 1 skipped, 0 failures, 12s total.

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

462 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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. |
| ~~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, 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:**
```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.
---
## 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 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).**
```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).