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