6005146e75
- 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>
109 lines
4.6 KiB
Markdown
109 lines
4.6 KiB
Markdown
# StaffTraveler API captures
|
|
|
|
Real request/response pairs captured from the iOS app `com.stafftraveler.webview` v3.12.0 (build 1880000346) against `api.stafftraveler.com` on 2026-04-22. All captures are by user `3NNPesQMiMRNYnPmuQzh6w2YKyh1` (`stafftraveler@treymail.com`, WN/Southwest, airline ST code `st_SWA`).
|
|
|
|
## Auth
|
|
|
|
All command-API calls are authenticated with:
|
|
```
|
|
Authorization: Bearer <Firebase ID token>
|
|
```
|
|
Token issued by `securetoken.google.com/stafftraveler-prod`, `aud=stafftraveler-prod`, ~1h TTL. The captured tokens are already expired — do not try to reuse them. To get a new one, sign in via Firebase Auth REST against API key `AIzaSyC2zG6ArnguzzdWsLYV1qjQznma0zl1Q0s` (needs `X-Android-Package` + `X-Android-Cert` headers or iOS SDK).
|
|
|
|
Only standard headers are sent. No App Check. No SSL pinning. No device/client signing beyond the ID token.
|
|
|
|
## Endpoint path pattern
|
|
|
|
```
|
|
POST https://api.stafftraveler.com/v1/commands/user/<commandName>
|
|
```
|
|
|
|
Note the `user/` segment — the `api_commands_url` Remote Config value is `https://api.stafftraveler.com/v1/commands/user`, and the command name is appended with `/`. I initially documented this as `/v1/commands/<cmd>` which is wrong.
|
|
|
|
## Files
|
|
|
|
### `searchFlightsByRoute_DFW-LAS_*.json`
|
|
DFW→LAS direct-only search for 2026-04-22.
|
|
|
|
Request body:
|
|
```json
|
|
{
|
|
"originIata": "DFW",
|
|
"destinationIata": "LAS",
|
|
"dates": ["2026-04-22"],
|
|
"maxConnections": 0,
|
|
"allowNearbyDepartures": false,
|
|
"allowNearbyArrivals": false,
|
|
"payloadType": "passenger",
|
|
"includeMultipleCarriers": false
|
|
}
|
|
```
|
|
|
|
Response: `{payload: {directFlights: [...], connectingFlights: [...], numberOfDiscardedFlights, messages, settingsFilteredCount}}`.
|
|
|
|
Each flight carries three IDs (`id`, `id_v2`, `id_v3`), airline info, local/UTC times, equipment, and a `seatsByClass` **aircraft configuration** (NOT current availability — that's a different read path entirely).
|
|
|
|
Corrections vs the api_doc's initial guesses:
|
|
- `stops` → actually `maxConnections`
|
|
- `payloadType: "route"` → actually `"passenger"`
|
|
- No `origin`/`destination`/`date` fields — only the `*Iata` variants + `dates` array.
|
|
|
|
### `createLoadsRequests_AA2178_*.json`
|
|
Request loads for AA2178 DFW→LAS 2026-04-22.
|
|
|
|
**Request body** is a bare JSON array of flight objects (no outer wrapper). Each flight is the complete flight object from the search response with one added field: `isPriorityRequest: bool`.
|
|
|
|
**Response shape:**
|
|
```json
|
|
{
|
|
"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": { /* full flight obj */ }
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### Dedup behavior (important)
|
|
|
|
On this capture, `numberOfRequests: 0` and `isDuplicateForUser: true` — the server recognized this user had already requested this flight and **did NOT charge a credit**. Replaying the same `createLoadsRequests` payload is safe (idempotent).
|
|
|
|
This means:
|
|
- `numberOfRequests` is the count of *newly created* requests in this call.
|
|
- `isDuplicateForUser` tells you whether the flight already had an open request for this user.
|
|
- **No credit is spent on duplicates.** Useful for polling the current request state without cost.
|
|
|
|
### `hasRecentLoads: false` — where loads actually come from
|
|
|
|
`hasRecentLoads` is the flag the app uses to decide whether load data is available for display. It's `false` here because nobody has submitted loads for AA2178 yet. The actual load data (when `hasRecentLoads: true`) does NOT come through the commands API — it arrives via a Firestore realtime listener on a doc that the user's account has permission to read.
|
|
|
|
To find the exact Firestore path when a flight has loads, one more capture is needed: tap a flight that already has loads populated, and grab the `firestore.googleapis.com/.../Listen/channel` traffic. That will show the watched doc path.
|
|
|
|
## Known error shapes
|
|
|
|
Not captured yet:
|
|
- What happens when you send an invalid flight ID
|
|
- What `createLoadsRequests` returns when credits are insufficient (`hasSufficientCredits: false`)
|
|
- What `numberOfRequests > 0` responses look like (fresh request, credit actually spent)
|
|
|
|
## Replay safety
|
|
|
|
To replay any of these:
|
|
1. Grab a fresh token from the app or by re-signing-in.
|
|
2. Swap it into the curl.
|
|
3. For `createLoadsRequests`, same payload = dedup-safe. Different payload = may charge credits.
|