Files
Flights/api_docs/stafftraveler_api.md
T
Trey t 6005146e75 Airline integration work: AirlineLoadService updates, docs, JSX scripts
- AirlineLoadService: pass airport DB for timezone-aware date strings,
  add browser-shaped headers for United, expand JetBlue/Alaska/Emirates
  signatures to take origin, log/parse fixes for Korean Air.
- FlightsApp: build AirlineLoadService with the airport DB and inject it.
- JSX: continued WebView-based fetcher work plus updated JSX_NOTES.
- Docs: add AIRLINE_INTEGRATION_GUIDE.md, drop the old AIRLINE_API_SPEC.md,
  add api_docs/ (StaffTraveler reverse-engineering captures + findings).
- Scripts: jsx_cdp_probe, jsx_live_monitor, jsx_swift_smoke for JSX
  protocol exploration.
- .gitignore: exclude airlines/ (local-only APK/IPA reverse-engineering).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 23:21:30 -05:00

16 KiB

StaffTraveler API

Extracted from com.stafftraveler.webview v3.12.0 (build 1880000346). Source artifacts:

  • apps/com.stafftraveler.webview_3.12.0-1880000346_apkcombo.com.xapk
  • /Users/m4mini/Desktop/code/airlines/extracted/stafftraveler/
  • Hermes disassembly: extracted/stafftraveler/hermes_out/bundle.hasm

Architecture

React Native + Hermes bytecode. All business logic lives in assets/index.android.bundle. Native Android is a thin RN shell (MainActivity, MainApplication).

Stack

  • Firebase Auth — user accounts, ID token issuance.
  • Firebase Remote Config — runtime-fetched config (API base URL, Typesense host/key, feature flags, etc).
  • Firebase Firestore — realtime store for flights, load requests, load reports, tips, user state.
  • Firebase App Check — registered (FirebaseAppCheckKtxRegistrar) and Play Integrity is bundled (com.google.android.play.core.integrity) — but the /v1/commands/* fetch path does NOT attach X-Firebase-AppCheck (see createFirebaseCommand disassembly at bundle.hasm offset 0x017ed673). App Check enforcement on the HTTP API is not observed. Firestore rules may still consult App Check.
  • Typesense (enhk2ji1vu6csxzrp-1.a1.typesense.net) — flight search index. The API key is delivered via Remote Config, not hardcoded.
  • Custom "commands" HTTP APIPOST https://api.stafftraveler.com/v1/commands/<commandName> — all mutating/reading operations go through this, auth'd with a Firebase ID token.

Firebase Project Config (from res/values/strings.xml)

Key Value
Project ID stafftraveler-prod
Android App ID 1:628258099825:android:5fb976ba6ad1bb05
Sender ID 628258099825
Android API Key AIzaSyC2zG6ArnguzzdWsLYV1qjQznma0zl1Q0s
Maps API Key AIzaSyD2yg_WuGtrAC_fyyEwvAQgycMoP3Xf9cM
Storage Bucket stafftraveler-prod.appspot.com
Realtime DB https://stafftraveler-prod.firebaseio.com
Firestore (default, project stafftraveler-prod)

These are all public identifiers — Firebase security comes from rules + App Check, not from hiding them.

Remote Config Keys

Fetched at startup; controls every external URL. Full shape (from bundle.hasm at line 140129):

Key Purpose
api_commands_url Base for the commands API (production: https://api.stafftraveler.com/v1/commands)
search_typesense_hostname Typesense host
search_typesense_api_key Typesense search-only key
image_cdn_hostname Aircraft/airline image CDN
airline_logos_hostname Airline logo CDN
google_places_api_key, _details_url, _search_url, _photo_url Places integration
weather_service_url (api.weatherapi.com), weather_service_api_key Weather
location_service_url (pro.ip-api.com), location_service_api_key Geo-IP
google_web_client_id Google Sign-In
loads_jackpot_enabled_airline_ids, onboarding_invite_reward, notice_*, promotion_* Feature flags / content

To get the real values, call Firebase Remote Config REST with the project config above:

POST https://firebaseremoteconfig.googleapis.com/v1/projects/stafftraveler-prod/namespaces/firebase:fetch?key=AIzaSyC2zG6ArnguzzdWsLYV1qjQznma0zl1Q0s

Auth

Standard Firebase Auth (email/password, Google, Apple, Facebook providers present in bundle strings). No custom auth flow.

To authenticate:

  1. Sign in via Firebase Auth REST with the stafftraveler-prod API key above, or via the Firebase iOS SDK configured for the same project.
  2. getIdToken() produces a JWT with ~1h TTL.
  3. Use the token as Authorization: Bearer <idToken> on every commands-API call.

App logic in bundle.hasm (createFirebaseCommand generator, line 1827439) checks expirationTime and calls getIdToken() right before sending, so token refresh is transparent.

Commands API

Base: https://api.stafftraveler.com/v1/commands/user (from Remote Config api_commands_url — confirmed from captured traffic, 2026-04-22) Auth: Authorization: Bearer <firebase-id-token> Method: POST Headers: Content-Type: application/json, x-stafftraveler-client: mobile Body: JSON — shape is command-specific Response: 200 OK with {payload: {...}} on success. Business errors raise isError/isNetworkError flags in the bundle.

Fetch options object literally constructed in the bundle (offset 0x017ed673):

{ method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + idToken }, body: JSON.stringify(payload) }

Full endpoint pattern: POST https://api.stafftraveler.com/v1/commands/user/<commandName>

Confirmed Commands (built via createFirebaseCommand(name))

Endpoint = POST {api_commands_url}/<command>

Command Purpose
searchFlightsByRoute Flight search by origin+destination+date
searchFlightsByCode Flight search by flight number + date
createLoadsRequests Submit one or more load requests for selected flights (spends credits)
deleteLoadsRequest Cancel a pending request
reopenLoadsRequest Reopen an expired/cancelled request
upgradeLoadsRequest Promote a request to Priority (uses extra credits)
requestLockForLoadsRequest / unlockLoadsRequest Cooperative lock: while you're typing a load answer, others can't also submit
submitLoadsReport Answer someone else's request (the reverse direction — you earn credits)
reviseLoadsReport Edit your answer
addComment / removeComment / reportComment Flight comments
addAirlineDetails Provide airline employee credentials for verification
addCreditsForPurchase IAP receipt validation → credits
setAirlineStCodes Set user's airline ("ST code" = StaffTraveler internal airline code)

Command names are stored in the bundle as plain string identifiers; the full inventory is in /tmp/st_fn_names.txt (extracted function names).

searchFlightsByRoute — confirmed shape (captured 2026-04-22)

Request body:

{
  "originIata": "DFW",
  "destinationIata": "LAS",
  "dates": ["2026-04-22"],
  "maxConnections": 0,
  "allowNearbyDepartures": false,
  "allowNearbyArrivals": false,
  "payloadType": "passenger",
  "includeMultipleCarriers": false
}

Field corrections from the bytecode guess: the key I thought was stops is actually maxConnections; payloadType takes the literal value "passenger" (not "route"); dates is an array of YYYY-MM-DD strings.

Response:

{
  "payload": {
    "directFlights": [ /* array of flight objects */ ],
    "connectingFlights": [],
    "numberOfDiscardedFlights": 0,
    "settingsFilteredCount": 0,
    "messages": []
  }
}

Each flight object has three IDs (id, id_v2, id_v3 — the id_v3 is human-readable like AA_2998_DFW_2026_04_22), airline info (airlineId, airlineStCode, flightCode, flightNumber), local/UTC times, equipment (flightEquipmentId, flightEquipmentIata), durationMinutes, isCargoFlight, seatsTotal, and seatsByClass: {first, business, premiumEconomy, economy}.

Important: seatsByClass is the aircraft cabin configuration, not current availability. Current load data comes from Firestore (see below), not this response.

See api_docs/stafftraveler_captures/searchFlightsByRoute_DFW-LAS_*.json for the full live capture.

searchFlightsByCode — payload shape

From caller at bundle.hasm:1948510 (Function #38848):

{
  "flightCode": "UA123",
  "dates": ["<ISO date>", ...]
}

Returns flights matching the flight code on each listed date.

createLoadsRequests — confirmed shape (captured 2026-04-22)

Request body is a bare JSON array of flight objects (no outer wrapper). Each element is the full flight object as returned by searchFlightsByRoute, with one added field: isPriorityRequest: bool.

[
  {
    "id": "a23d6f8379f5109eddeb5bbf78bdccc1",
    "id_v2": "0d9d0702b203d224ace3e6eb94531dde",
    "id_v3": "AA_2178_DFW_2026_04_22",
    "airlineId": "AA",
    "flightNumber": 2178,
    "flightCode": "AA2178",
    "departureAirportId": "DFW",
    "arrivalAirportId": "LAS",
    "departureTimeLocal": "2026-04-22T16:45:00-05:00",
    "departureTimeUtc": "2026-04-22T21:45:00Z",
    "arrivalTimeLocal": "2026-04-22T17:46:00-07:00",
    "arrivalTimeUtc": "2026-04-23T00:46:00Z",
    "flightEquipmentId": "32Q",
    "durationMinutes": 181,
    "isCargoFlight": false,
    "airlineStCode": "st_AAL",
    "departureAirportIata": "DFW",
    "arrivalAirportIata": "LAS",
    "flightEquipmentIata": "32Q",
    "seatsTotal": 196,
    "seatsByClass": {"first": 0, "business": 20, "premiumEconomy": 0, "economy": 176},
    "isPriorityRequest": false
  }
]

Response:

{
  "payload": {
    "numberOfRequests": 0,
    "numberOfRequestsPaidFor": 0,
    "numberOfRequestsPaidForPriority": 0,
    "hasSufficientCredits": true,
    "verifiedRequests": [
      {
        "flightId": "AA_2178_DFW_2026_04_22",
        "isExisting": true,
        "isDuplicateForUser": true,
        "isRequestUpdateForUser": false,
        "hasDeparted": false,
        "isCancelled": false,
        "isPriorityRequest": false,
        "hasRecentLoads": false,
        "scheduledFlight": { /* echoed full flight obj */ }
      }
    ]
  }
}

Idempotent per (user, flight): on duplicate submission, numberOfRequests: 0, isDuplicateForUser: true, no credit charged. This makes the command safe to replay as a "what's the current state of my request for flight X" probe — you just pay for the first request on any given flight.

hasRecentLoads is the flag the app uses to decide whether load data is already available. When true, the Firestore listener is expected to deliver load-report documents; when false, there's nothing to show until someone in the community answers.

See api_docs/stafftraveler_captures/createLoadsRequests_AA2178_*.json for the full live capture.

Firestore Collections (realtime)

Once a request is created, load results stream back via Firestore listeners. Listener constructors in the bundle:

Listener What it subscribes to
TrackedFlightsStoreListener (bundle.hasm:253969) The user's currently-tracked flights (including ones with pending requests)
OpenRequestsStoreListener (bundle.hasm:443724) The user's open requests, filtered by useUserAirlineStCodes()
ConnectingFlightsStoreListener Connecting-flight variants
PinnedFlightsStoreListener User-pinned flights
PriorityRequestCounterStoreListener Priority request quota counter
OpenRequestsCountListener Open request count for UI badge
DerivedLoadsReportsStoreListener Reports on flights the user is answering
CreditsListener / UserSessionsListener / UserStoreListener Account state
RemoteConfigListener Remote Config changes

Exact collection paths are not plain strings in the bundle — they're built from user state (e.g. users/{uid}/flightRequests/{id} or users/{uid}/trackedFlights/{id}). To get them, run Firestore in debug (iOS SDK logs every collection path) or capture with Firebase Local Emulator proxy.

Typesense (search infrastructure)

Not directly hit by the client for flight search — searchFlightsByRoute goes through the commands API, which internally queries Typesense. However, the bundle also contains a typesense-instantsearch-adapter, and strings like searchTypesenseAndAdapt, searchTypesenseForFacetValuesAndAdapt, x-typesense-api-key (bundle.hasm:1995183) suggest direct Typesense queries are used for some facets (probably airline filtering, autocomplete on airports/airlines).

Host: enhk2ji1vu6csxzrp-1.a1.typesense.net Key: from Remote Config search_typesense_api_key (search-only, scoped, safe to embed).

Integration plan for the iOS Flights app

The cleanest path — do not build a parallel HTTP client, just use Firebase directly:

  1. Embed the stafftraveler-prod Firebase config in a second FirebaseApp instance (iOS supports multiple named apps). Use the API key + app ID + sender ID from above. Name it something like StaffTravelerFirebaseApp so it doesn't collide with Flights' own Firebase project.
  2. Sign in with the user's StaffTraveler credentials using Auth.auth(app: stApp).signIn(withEmail:password:). Store the refresh token in Keychain.
  3. Fetch Remote Config once on first use to get api_commands_url, search_typesense_api_key, etc.
  4. Search a routePOST https://api.stafftraveler.com/v1/commands/searchFlightsByRoute with the payload shape documented above and Authorization: Bearer <idToken>.
  5. Request loadsPOST .../createLoadsRequests with the flight IDs from step 4.
  6. Listen for answersFirestore.firestore(app: stApp).collection("users/\(uid)/flightRequests").document(requestId).addSnapshotListener { ... } (exact path TBD — confirm via traffic capture).

Firestore is the answer — schema captured (2026-04-22)

Loads and all real-time state live in Firestore, queried via the Listen/channel long-poll transport. Mobile iOS uses the gRPC variant (hard to mitm); the app.stafftraveler.com web client uses plain HTTP long-poll, which is fully inspectable. Playwright capture + URL-decoding gave us every addTarget body.

Full schema and query recipes: see api_docs/stafftraveler_captures/firestore_schema.md and the raw capture at api_docs/stafftraveler_captures/firestore_listen_targets_and_queries.txt.

The one query that renders loads:

collection: derivedLoadsReports
where:      flightId == "{id_v3}"   (e.g. "AA_2178_DFW_2026_04_22")
orderBy:    createdAt DESC

Returns docs containing openSeats.{first,business,premiumEconomy,eco}.count, staffListing.count, creatorName, createdAt, seatsAvailabilityScore, etc. Example at api_docs/stafftraveler_captures/derivedLoadsReports_AA2178_response.json.

Required REST headers (earlier runQuery attempts were 403 because the API key wasn't attached):

Authorization: Bearer <firebase id token>
X-Firebase-GMPID: 1:628258099825:web:35b20eaab4d441894041d0
?key=AIzaSyD82Hiqx-jAbHF8faMGSveqLw8CdX8a-Uc    (as URL query, web key)

Remaining nuances

  1. Account eligibilityaddAirlineDetails still required for the account to participate. Your existing account is verified (WN / st_SWA), so no blocker.
  2. Credits / access modelcreateLoadsRequests charges for new requests only. Idempotent per (user, flight)isDuplicateForUser: true returns current state with no charge. Firestore derivedLoadsReports reads are gated by an active trackedFlights/{flightId} doc, which is only created by createLoadsRequests. So you must spend 1 credit per new flight to unlock load reads for it; after that, reads are free and unlimited. You cannot peek at other flights' loads without requesting.
  3. App Check — not enforced on commands API or Firestore for this project (confirmed: web client makes all these requests with just ID token + API key).

Integration for iOS Flights app (final)

  • Secondary FirebaseApp configured against stafftraveler-prod with the web API key + GMPID above.
  • signInWithEmailAndPassword → persist refresh token.
  • Firestore.firestore(app: stApp).collection("derivedLoadsReports").whereField("flightId", isEqualTo: flightIdV3).order(by: "createdAt", descending: true).addSnapshotListener { ... } — realtime loads for any flight you know the id_v3 of.
  • Plus trackedFlights, airlinesBySt, airportsByIata, flightEquipment, conversations/{flightId} subscriptions — full recipes in firestore_schema.md.
  • Writes (request loads, submit report) keep using POST https://api.stafftraveler.com/v1/commands/user/<cmd>.