4a939340a2
Spirit ceased operations, so the fetchSpiritStatus path and all NK references are dead code. Pulled out: - AirlineLoadService: drop `case "NK"` from the router, delete fetchSpiritStatus (the GetFlightInfoBI POST that was returning 403 even after our APIM key was accepted). - FlightLoadDetailView: drop the `schedule.airline.iata == "NK"` branch and the spiritUnavailableView placeholder. - FlightLoad model: update the airlineCode comment. - AirlineLoadIntegrationTests: remove test_NK_spirit and drop "NK" from statusOnlyAirlines / knownDailyFlights fallback table. - AIRLINE_INTEGRATION_GUIDE.md: tombstone the Spirit section and remove it from the cheat-sheets and recommendations. Test suite now: 6 airlines passing (AA, AS, B6, EK, KE, UA), 1 skipped (XE — WKWebView host required), 0 failures, runs in ~10s. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
396 lines
15 KiB
Markdown
396 lines
15 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) |
|
||
| ~~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.
|
||
|
||
---
|
||
|
||
## 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).
|