Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,8 @@ htmlcov/

# examples/price_drop_watcher.py cache (written to CWD)
.swoop-watch-cache.json

# Local reverse-engineering captures
references/google-flights/explore/
scripts/capture_explore.py
scripts/capture_explore_browser.mjs
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ for option in results.results[:3]:
> [!NOTE]
> swoop is not affiliated with Google. It calls undocumented RPC endpoints that can change without notice.

swoop calls Google Flights' internal `GetShoppingResults` and `GetBookingResults` RPC endpoints, the same ones the web app uses when you search for flights. Requests use TLS fingerprint impersonation via [primp](https://github.com/deedy5/primp) to match a real browser session. Responses are deeply nested lists (matching an internal protobuf schema) decoded into typed Python dataclasses.
swoop calls Google Flights' internal `GetShoppingResults`, `GetBookingResults`, and `GetExploreDestinations` RPC endpoints, the same ones the web app uses when you search for flights. Requests use TLS fingerprint impersonation via [primp](https://github.com/deedy5/primp) to match a real browser session. Responses are deeply nested lists (matching an internal protobuf schema) decoded into typed Python dataclasses.

[Perch](https://perchtravel.com) uses swoop in production to monitor booked flights for price drops, saving users an average of $247 per trip.

Expand Down Expand Up @@ -60,6 +60,9 @@ swoop price JFK LAX --depart 2026-06-15 DL2300
# Show copy/paste price commands for displayed rows
swoop search JFK LAX 2026-06-15 --show-price-commands

# Flexible destination ideas
swoop explore JFK

# Script-stable pricing via selector
SELECTOR=$(swoop search JFK LAX 2026-06-15 -o json -q | jq -r '.results[0].selector')
swoop price --selector "$SELECTOR"
Expand Down Expand Up @@ -240,6 +243,16 @@ results = search(
)
```

### Explore destinations

```python
from swoop import explore

result = explore("JFK", cabin="economy")
for destination in result.destinations[:10]:
print(destination.name, destination.airport_code, destination.departure_date)
```

### Booking details (fare options)

```python
Expand Down Expand Up @@ -435,6 +448,10 @@ Look up the current bookable fare for a specific flight. Optimized for the "what

Returns `PriceResult | None`. `PriceResult` has `price`, `fare_brand`, `is_basic_economy`, `booking_options`, `itinerary`, `resolved_legs`, `rpc_calls`.

### `explore(origin, **kwargs)`

Discover flexible destination ideas from the Google Flights Explore endpoint. Returns `ExploreResult` with destination names, coordinates, optional airport codes, suggested dates, images, distance text, and approximate travel duration.

### `get_booking_results(itinerary_or_token, **kwargs)`

Get fare options for a specific itinerary. Pass an `Itinerary` object directly, or a booking token string with explicit `origin`, `destination`, `date`, and `selected_legs`. Returns `list[BookingOption]` with `price`, `brand_label`, `brand_code`, `is_basic`, `fare_family`, `rebookability_signal`, plus seller fields `seller_name`, `seller_code`, `booking_url`, `logo_url`, and `is_airline_direct` for routing users to the actual booking page.
Expand All @@ -453,6 +470,8 @@ Set the default proxy URL for all subsequent requests. Pass `None` to clear.
- **`ResolvedLeg`** — `flight_summary: str`, `origin: str`, `destination: str`, `date: str`, `itinerary: Itinerary | None`, `selection: str`
- **`SelectedLeg`** — `flight_number: str`, `origin: str`, `destination: str`, `date: str`
- **`SearchLeg`** — `date: str`, `from_airport: str`, `to_airport: str`, `max_stops: int | None`, `airlines: list[str] | None`
- **`ExploreResult`** — `destinations: list[ExploreDestination]`, origin metadata
- **`ExploreDestination`** — `place_id`, `name`, `country`, coordinates, `airport_code: str | None`, suggested dates, images, distance, duration
- **`SearchResult`** — `results: list[TripOption]`, `price_range: PriceRange | None`, `is_complete: bool`, `currency: str | None`
- **`TripOption`** — `selector: str`, `price: int | None`, `currency: str | None`, `legs: list[TripLeg]`
- **`TripLeg`** — `origin: str`, `destination: str`, `date: str`, `itinerary: Itinerary | None`
Expand Down
45 changes: 44 additions & 1 deletion swoop/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
from ._regions import Region
from .exceptions import SwoopError, SwoopHTTPError, SwoopParseError, SwoopRateLimitError
from .builders import CabinClass, SearchLeg
from .models import Deal, DealsDiff, DealsResult, Passengers, PriceChange, PriceResult, ResolvedLeg, SearchResult, SelectedLeg, TransportConfig, TripLeg, TripOption
from .models import Deal, DealsDiff, DealsResult, ExploreDestination, ExploreResult, Passengers, PriceChange, PriceResult, ResolvedLeg, SearchResult, SelectedLeg, TransportConfig, TripLeg, TripOption
from .rpc import (
SORT_ARRIVAL_TIME,
SORT_CHEAPEST,
Expand Down Expand Up @@ -903,6 +903,46 @@ def deals(
return result


# ---------------------------------------------------------------------------
# explore() — discover flexible destination ideas from an origin airport.
# ---------------------------------------------------------------------------


def explore(
origin: str,
*,
cabin: CabinClass = "economy",
max_stops: Optional[int] = None,
passengers: Passengers = Passengers(),
transport: TransportConfig = TransportConfig(),
) -> ExploreResult:
"""Discover flexible destination ideas from Google Flights Explore.

Args:
origin: Origin airport IATA code (e.g. ``"JFK"``).
cabin: Cabin class (default ``"economy"``).
max_stops: Maximum stops. ``None`` = any, ``0`` = nonstop.
passengers: Passenger counts (default ``Passengers()``).
transport: HTTP transport configuration (default ``TransportConfig()``).

Returns:
An :class:`ExploreResult` containing destination recommendations.
"""
validate_iata_code(origin, "origin")
validate_cabin(cabin)
validate_adults(passengers.adults)

from ._explore import fetch_explore

return fetch_explore(
origin,
cabin=cabin,
max_stops=max_stops,
passengers=passengers,
transport=transport,
)


__all__ = [
# Functions
"search",
Expand All @@ -915,6 +955,7 @@ def deals(
"price_deal",
"watch_deals",
"diff_deals",
"explore",
"get_booking_results",
"search_raw",
"set_country",
Expand All @@ -928,6 +969,8 @@ def deals(
"DealsResult",
"PriceChange",
"Region",
"ExploreDestination",
"ExploreResult",
"Passengers",
"TransportConfig",
"PriceResult",
Expand Down
Loading