Python client for Happy Endpoint real-time data APIs: real estate listings and transactions, ecommerce and retail product data, and travel pricing. One key, one client, every data domain.
Beta. This is a 0.x release. The API surface may change before 1.0.
pip install happyendpointfrom happyendpoint import HappyEndpoint
he = HappyEndpoint() # reads RAPIDAPI_KEY
results = he.realestate.search("dubai marina", bedrooms="1", price_max=1_500_000)
print(f"{results.total} matching, median {results.median_price:,.0f}")
for prop in results[:3]:
print(prop.title)
print(f" {prop.price:,.0f} | {prop.area_sqm}sqm | {prop.price_per_sqm:,}/sqm")pip install happyendpointGet a key at rapidapi.com/user/happyendpoint. One key works across every API and each has a free tier, but you subscribe to each API separately.
he = HappyEndpoint() # from RAPIDAPI_KEY
he = HappyEndpoint(api_key="your_key") # explicitRequires Python 3.9 or newer.
Clients are grouped by what the data is about, not by which site it came from.
| Attribute | Covers |
|---|---|
he.realestate |
Property listings, transactions, agents, off-plan developments |
he.beauty |
Beauty and cosmetics product catalogues, pricing, reviews |
he.home |
Home furnishing and furniture catalogues across several countries |
he.travel |
Hotels, flights, and car rental pricing |
Domain aliases are the recommended way in. Source-specific attributes exist for anyone who needs a particular provider, but the aliases read better and stay stable if the underlying source for a domain changes.
Property search endpoints take a numeric location id, and passing a wrong one returns a different area rather than an error. Results look plausible and are quietly about the wrong place.
So this client takes names and resolves them, then reports what it matched:
results = he.realestate.search("jvc")
print(results.location.name, results.location.id)Ambiguous names resolve to the busiest match, which is almost always the community rather than a building sharing its name. To choose yourself:
for loc in he.realestate.find_locations("marina", limit=5):
print(f"{loc.name:<40} {loc.id:<8} {loc.listings:,} listings")
results = he.realestate.search(he.realestate.find_locations("marina")[2])Unresolvable names raise LocationNotFound rather than falling back to a guess.
Gross yield across several areas is not a single API call. This computes it:
for row in he.realestate.compare_yields(["jvc", "business bay", "downtown dubai"]):
print(f"{row.area:<34} gross {row.gross_yield_pct:>5}% net {row.net_yield_pct():>5}%")net_yield_pct() subtracts typical holding costs and takes overrides:
row.net_yield_pct(
service_charge_per_sqm=215,
area_sqm=90,
management_pct=5,
maintenance_pct=1,
vacancy_pct=5,
)Defaults are deliberately realistic rather than flattering. The gap between gross and net is usually 2 to 3 percentage points.
The distinction that matters most for analysis:
listings = he.realestate.search("dubai marina") # what sellers ASK
txns = he.realestate.transactions("dubai marina") # what buyers PAID
for t in txns[:3]:
print(f"{t.date} {t.amount:>12,.0f} {t.price_per_sqm:>8,.0f}/sqm {t.sale_type}")sale_type distinguishes a developer's first sale from an owner resale, which
matters when comparing new-build against existing stock.
Upstream endpoints disagree with each other about casing, nesting, and envelope shape. This client hides that behind consistent dataclasses:
prop.title # str, whichever shape the endpoint returned
prop.price_per_sqm # computed
prop.area_sqft # converted
prop.location # readable hierarchy path
prop.amenities # flattened out of nested groups
prop.is_annual_rent # rentals are quoted yearly, easy to misread
prop.raw # the untouched payload, for fields not modelled here| Method | Returns |
|---|---|
search(location, purpose, property_type, bedrooms, price_min, price_max, page) |
SearchResult |
get_property(property_id) |
Property, slow endpoint |
search_off_plan(location, price_max, max_pre_handover_payment) |
SearchResult |
iter_all(location, max_pages, delay, **kwargs) |
iterator of Property |
transactions(location, purpose, time_period, page) |
list[Transaction] |
find_agents(location, purpose) |
list[Agent] |
find_locations(query, limit) |
list[Location] |
resolve_location(query) |
Location |
rental_yield(location, bedrooms, property_type) |
YieldResult or None |
compare_yields(locations, bedrooms, property_type) |
list[YieldResult], ranked |
he.beauty.search("moisturizer") # note: `keyword`, not `query`, upstream
he.beauty.search_by_brand("gucci") # brand name, not an id
he.beauty.reviews(product_id)
he.beauty.availability(sku_id, latitude, longitude)
he.beauty.stores(latitude, longitude, radius=50)
he.home.search("desk", country_code="us")
he.home.search_filters("desk") # facets, for building filter UIs
he.home.countries()Home endpoints require both a country and a language code. language_code
defaults to "en"; omitting it upstream returns a validation error rather than
falling back, so the client always sends it.
Home furnishing data covers eight countries, and the same product is priced differently in each, so cross-market comparison is a couple of calls:
for country in ("us", "gb", "de", "se"):
print(country, he.home.search("bookcase", country_code=country))locations = he.travel.find_location("new york")
hotels = he.travel.search_hotels(
location_id="3000016152",
check_in="2026-03-05", # ISO in, converted internally
check_out="2026-03-07",
adults=2,
)Pass ISO dates. Hotel endpoints expect MM-DD-YYYY while car endpoints expect
YYYY-MM-DD; the client handles the difference.
from happyendpoint import (
HappyEndpointError, # base class
AuthenticationError, # 401, key missing or wrong
SubscriptionError, # 403, not subscribed or quota exhausted
RateLimitError, # 429 after retries
LocationNotFound, # no area matched
)SubscriptionError names the API and links its subscription page, because "403"
alone does not tell you which API you need.
Rate limits and timeouts retry with exponential backoff. Tune with
HappyEndpoint(max_retries=5, timeout=45).
page_two = he.realestate.search("jvc", page=2)
for prop in he.realestate.iter_all("jvc", max_pages=5, delay=0.5):
print(prop.title)iter_all sleeps between requests so you stay inside the free tier. Drop
max_pages to fetch everything.
No. Every API has a free tier, enough to explore and prototype.
You are subscribed to some APIs but not the one you called. The message names it
and links the page. HappyEndpoint.available_apis() lists all of them.
Annual. prop.is_annual_rent tells you, so you do not divide by twelve without
noticing.
Transactions. search() returns asking prices, which run higher than what
property actually sells for.
Yes, prop.raw, txn.raw, and agent.raw hold the original payload.
Yes, happyendpoint-js. There is also an MCP server for AI assistants, happyendpoint-mcp.
git clone https://github.com/happyendpointhq/happyendpoint-python
cd happyendpoint-python
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytestTests mock HTTP, so they run without a key.
Beta, 0.x. The public API may change before 1.0. Pin a version if you need stability:
happyendpoint==0.2.2
Happy Endpoint is an independent provider. This package is not affiliated with, endorsed by, sponsored by, or connected to any of the websites, platforms, retailers, or marketplaces whose data may be accessible through the underlying APIs.
All product names, brands, trademarks, and registered trademarks are the property of their respective owners. Any reference to them is descriptive only, to identify the subject matter of the data, and does not imply any association or endorsement.
Users are responsible for ensuring their use of any data complies with applicable laws and the terms of service of the relevant source.
Happy Endpoint builds and maintains real-time data APIs across real estate, ecommerce, retail, and travel.
- Catalogue: happyendpoint.com/library
- Datasets: happyendpoint.com/datasets
- Documentation: docs.happyendpoint.com
- Contact: happyendpointhq@gmail.com
MIT. See LICENSE.