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>
15 KiB
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) |
| 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, equipmentGET /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 capacityStandbyList.FlightLoad.PremiumClassConfigured— first-class existsStandbyList.Passengers[]—DisplayName,Position,Seat,UpgradedToPCUpgradeList.FlightLoad.Authorized— first-class capacityUpgradeList.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, 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)
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).
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 + namePOST /offers/shop— returnsseatsAvailableCountper fare, needs SkyMiles auth + SPA session flowPOST /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;
flightSeatCountreturns 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
- Start with Alaska (easiest, 15 min to wire up).
- Add United for the standby/upgrade killer feature (needs Playwright worker).
- Layer in American for the third major US carrier.
- JSX as a bonus — only route pairs that JSX serves (private terminals).
- For Delta/JetBlue: show flight status only, note "seat data unavailable" unless you have a PNR.
- 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).