The Question I Was Trying to Answer
I have Hyatt points. I'm considering hotels in Brazil. Is it worth using the points, or should I just pay cash?
That question is easy to answer for one hotel on one date — open the Hyatt site, check both rates, divide. But if you want to compare multiple properties across a date range, or check periodically as rates change, the manual version gets tedious fast.
CPP — cents per point — is the standard unit. You divide the cash rate by the number of points required and multiply by 100. If you'd pay $230 for a room that costs 9,000 points, that's 2.56 cpp. The threshold I use: below 2.0 cpp, pay cash. Above 2.0, consider using points.
I built a scraper that pulls live cash and award rates for a list of properties and prints a CPP table.
Why Cookie Injection Instead of Logging In
The Hyatt site has reCAPTCHA on the login page. Automating through it is slow and unreliable.
There's a better way: I already have a valid Hyatt session in my Chrome browser. browser_cookie3 can read that session directly from Chrome's cookie database and inject it into a Selenium-controlled browser. The automation browser inherits the authenticated session — no login page, no CAPTCHA.
The one friction: Chrome on macOS stores cookie values encrypted using AES-128-CBC, with the decryption key stored in macOS Keychain under "Chrome Safe Storage." When browser_cookie3 reads your Hyatt cookies, it triggers a system Keychain access prompt. This is expected behavior — it's Chrome's own security design, not a bug in the script.
import browser_cookie3
cookies = browser_cookie3.chrome(domain_name='.hyatt.com')
After extracting the cookies, the script injects them into a Selenium Chrome session before navigating to any Hyatt page. The browser lands directly on the authenticated rates page.
The Rates Page Structure
For each property, the URL pattern is:
https://www.hyatt.com/shop/rooms/{SHOP_CODE}?checkinDate=YYYY-MM-DD&checkoutDate=YYYY-MM-DD&adults=1&rooms=1
The page loads with cash rates visible. Award rates appear after clicking the "Use Points" toggle (input[data-js='usePoints']).
Key selectors:
| Element | Selector |
|---|---|
| Cash rate | div[data-locator='cash-rate'] span.rate-values |
| Public rate | div[data-locator='public-rate'] span.rate-values |
| Points rate | div[data-locator='points-rate'] |
| Use Points toggle | input[data-js='usePoints'] |
| Award category | div[data-locator='award-category-label'] |
The script clicks the toggle, waits for the points rate to appear, scrapes both values, and calculates CPP. Each property takes about 20–25 seconds due to page load times.
The Properties I Was Checking
Three properties in Brazil, all for August 10–12, 2026:
| Property | Award tier | Pts/night |
|---|---|---|
| A Category 3 property in Rio de Janeiro | Category 3 | 9,000 — standard rooms; 14,000 — club floor |
| A Category 5 property in São Paulo | Category 5 | ~17,000 (est.) |
| A WoH affiliate property in Leblon | Mr & Mrs Smith / WoH affiliate | Variable — not standard Hyatt award chart |
The Rio property returned real data on the first confirmed working run. São Paulo and the Leblon affiliate were not successfully scraped — a threading bug in the multi-worker path caused cookie injection to fail silently (details in the Open Bug section below). The Rio numbers are what drove the decision.
Results: Category 3 Rio Property, August 2026
The scraper confirmed 8 room types across two award tiers. Exchange rate used: USD/BRL ~5.35.
| Room type | Cash/night (BRL) | Cash/night (USD) | Pts/night | CPP |
|---|---|---|---|---|
| 2 Twin Beds | R$1,231 | $230 | 9,000 | 2.56 |
| 1 King Bed | R$1,231 | $230 | 9,000 | 2.56 |
| 2 Twin Beds — Ocean View | R$1,323 | $247 | 9,000 | 2.75 |
| 1 King Bed Oceanfront | R$1,469 | $275 | 9,000 | 3.05 |
| 2 Twin Beds Oceanfront | R$1,469 | $275 | 9,000 | 3.05 |
| 1 King Bed With Club Access | R$1,966 | $367 | 14,000 | 2.62 |
Every room type came in above the 2.0 cpp threshold. The oceanfront rooms at 3.05 cpp crossed the "strong redemption" line I use as a secondary target.
The most interesting data point: all standard and ocean-view rooms share the same 9,000-point cost regardless of view. The only variable is the cash rate. An oceanfront room at R$1,469 is 19% more expensive in cash but costs the same number of points — that's where using points pulls ahead the most.
The Club floor rooms (14,000 pts) come in at 2.62 cpp. That's above threshold, but not enough of a premium over the 9,000-point rooms to justify the extra points spend. The standard 1 King Bed at 2.56 cpp is the straightforward value play; the oceanfront at 3.05 cpp is the better one if availability holds.
Running It
cd ~/projects/hyatt-cpp
source venv/bin/activate
# Check August 2026 dates
python hyatt_cpp.py --checkin 2026-08-10 --checkout 2026-08-12
# Test with a single property first
python hyatt_cpp.py --checkin 2026-08-10 --checkout 2026-08-12 --workers 1 --limit 1
The --workers flag controls parallelism. Multiple workers speed things up but each one opens a separate Chrome window with its own injected session. If Chrome has the Cookies SQLite database locked (because Chrome itself is open), cookie injection fails silently and you get empty rate cards. Close Chrome before running the script.
The Open Bug
There's a known issue with the multi-threaded version: cookie injection works perfectly in a standalone inline script but fails silently when called through hyatt_login.make_driver() in the threaded context. Single-worker mode (--workers 1) works reliably. The multi-worker threading is still being debugged.
Diagnostic to run first if you're seeing empty rate cards:
python - <<'EOF'
from hyatt_login import get_chrome_cookies, make_driver
from selenium.webdriver.common.by import By
import time
cookies = get_chrome_cookies()
print(f"Cookies extracted: {len(cookies)}")
driver = make_driver()
hyatt_cookies = [c for c in driver.get_cookies() if "hyatt" in c.get("domain", "")]
print(f"Hyatt cookies in browser: {len(hyatt_cookies)}")
driver.get("https://www.hyatt.com/shop/rooms/[SHOP_CODE]?checkinDate=2026-08-10&checkoutDate=2026-08-12&adults=1&rooms=1")
time.sleep(22)
cards = driver.find_elements(By.CSS_SELECTOR, "div.room-rate-card-wrapper")
print(f"Room cards found: {len(cards)}") # Should be 8
driver.quit()
EOF
If Room cards found: 0, Chrome is likely locking the Cookies database. Close Chrome and retry.
What Comes Next
The scraper is a sourcing tool. Once it finds a deal above the threshold, the next step is automating the booking — a hyatt_book.py that imports from the same hyatt_login.py auth module and clicks through the booking flow.
That's on the roadmap. For now, the CPP table gets generated in a few minutes instead of an hour, and I know which properties are worth redeeming points at before I have to go through the full booking flow.