# American Airlines — booking checkout (aa.com) End-to-end guest checkout for a one-way revenue fare, through to the credit-card entry form. No antibot / CAPTCHA encountered on a clean, stock-Chrome CDP connection. ## URL map (in order) | Step | URL | Title | | --- | --- | --- | | Home | `https://www.aa.com/` → redirects to `/homePage.do` | American Airlines … | | Search results (**deep link**) | `https://www.aa.com/booking/search/find-flights?locale=en_US&fareType=Lowest&pax=1&adult=1&type=OneWay&searchType=Revenue&cabin=&carriers=ALL&travelType=personal&slices=` | Choose flights | | After fare select | `https://www.aa.com/booking/choose-flights/1?sid=…` | same | | Trip summary | `https://www.aa.com/booking/your-trip-summary?sid=…` | Your trip summary | | Passenger details (separate Angular app) | `https://www.aa.com/airfare-sales/ui/passenger-ui/?search-journey-id=…&cid=…&sid=…` | Passengers | | Seat map | `https://www.aa.com/booking/passengers/deeplink/airfare-booking/?journey-state-id=&shopping-cart=` | Choose your seat | | Trip extras | `https://www.aa.com/ancillaries/offers/storefront/2/` | Trip extras | | Checkout / payment | `https://www.aa.com/ecommerce/checkout-app/cart/` | American Airlines Checkout | `` payload (URL-encoded): ```json [{"orig":"DFW","origNearby":false,"dest":"AUS","destNearby":false,"date":"2026-05-06"}] ``` The deep link skips the home-page React form entirely. The in-page search form is React-controlled; plain `input.value = …` does not propagate to the controlled state, so prefer the deep link. ## Stable selectors & handles ### Search results (`/booking/choose-flights/1`) - `button#flight--product-group-` — top-level cabin card (e.g. `flight-0-product-group-MAIN`). **Clicking this expands the fare tray in place**, it does not navigate. Use `.click()` via `js(...)`; the coordinate click has a tendency to scroll past the target. - `button#slice--MAIN-basic-economy` / `slice--MAIN-coach` / `-coach-plus` / `-coach-select` / `-first` — the real "Select this fare" buttons inside the expanded tray. Visible only after the product-group button is clicked. - `button#carousel-` — date carousel navigation. ### Basic-Economy upgrade-upsell modal After clicking `slice-0-MAIN-basic-economy`, an upsell dialog appears. The decline button is: - `button#btn-no-upgrade` — text is **"Accept restrictions"** (not "No, thanks"). Click this to proceed with Basic Economy. ### Trip summary - `button#login-continue-btn` — "Log in and continue" - `button#continue-as-guest-btn` — "Continue as guest" (use this) ### Passenger details (`/airfare-sales/ui/passenger-ui/`) This is a **separate Angular app** using custom elements (``, ``, ``, etc.) with **open shadow roots**. Inputs are addressed by `formcontrolname` on the host element; the real `` / `` in shadow root | | `formMonth`, `formDay`, `formYear` | — | `` — values are single letters: `M`, `F`, `U`, `X` (not `MALE`) | | `country` | `residencyCountry` | `` (`NY`) — repopulates *after* `country` is set, so set state **after** country | | `loyaltyProgram`, `loyaltyNumber` | same | optional | | `documentNumber`, `documentCountry` | — | optional | Modal buttons (both ``, no stable id — filter by inner text): - "Cancel" - "Save" — commits the passenger and closes the modal. Contact form (appears on the main passenger page after at least one passenger is saved, NOT inside the modal): | host id | notes | | --- | --- | | `email`, `confirmationEmail` | text | | `phoneType` | values: `CEL` (Mobile), `HOME`, `BUSINESS` | | `countryCode` | phone country code, defaults to `US` | | `phoneNumber` | `tel-national` | | `tripPurposeType` | `BUSINESS` or `LEISURE` (required) | Main Continue: `` with `className` containing `save-button` and innerText `Continue`. There is a second Continue ("Log in and continue") on the page — filter it out. ### Seat map - `a#continueWithoutSeatsLink` — "Skip seats for all flights" (use this to skip) - `button#nextFlightButton` — "Continue" (only if seats are selected) ### Trip extras (ancillaries) - Single `` with text "Continue" — just click through. ### Checkout / payment (`/ecommerce/checkout-app/cart/`) The checkout page is **plain HTML, no shadow DOM, no iframes for card fields**. Fields are directly addressable by id. Sections are progressively disclosed. 1. Trip insurance section — two radios with `name="allianz-insurance-selections"`: - `value="purchase"` / `value="decline"` — pick `decline`, then click `button#trip-insurance-continue-button`. 2. Payment method radios, `name="paymentMethod"`: - `value="CREDIT_CARD"`, `AFFIRM`, `APPLE_PAY`, `GOOGLE_PAY`, `PAYPAL`, `HOLD`. - Selecting `CREDIT_CARD` expands the card form inline. 3. Credit card fields (all plain `` / ``). Target by the specific id to avoid collisions. - **Viewport emulation resets across `Emulation.setDeviceMetricsOverride` boundaries** after navigation. Re-apply `setDeviceMetricsOverride` if the later page's `page_info().w` jumps back up. - **Tab title is prefixed with the harness's `🟢 ` marker** on each real page, so `page_info().title` will start with that emoji — don't treat it as site content. ## Waits - `wait_for_load(timeout=20)` plus a `time.sleep(3-5)` after every page transition. The Angular apps (passenger-ui, ancillaries, ecommerce) hydrate lazily and `load` fires before the forms mount. - After `document.getElementById('slice-0-MAIN-basic-economy').click()`, wait 2-3s for the upsell `` to mount before trying to query `#btn-no-upgrade`. - After selecting a payment-method radio, wait ~3s for the credit-card subsection to expand (the `cvvInput` only appears after the first couple of fields hydrate). ## Antibot posture (observed) - No Akamai `_abck` / `bm_sz` challenge on the booking path with a vanilla user's Chrome. - No PerimeterX. No interstitial. No CAPTCHA on search, fare-select, passenger, or checkout. - Payment page is a first-party form — no Stripe/Braintree/Spreedly iframe on the CC fields at this stage. (Tokenization presumably happens on Pay-now submit; we did not submit.)