- Published on
Real Estate API Integration Guide for Developers
Real Estate API Integration Guide for Developers
If you're building a property search platform, a valuation tool, or a market analysis dashboard, you need a real estate data API. The concepts are the same across providers: authenticate, query listings, paginate through results, and react to new data. This guide walks through each step with working code examples using the Stream.estate API.
Authentication
Most real estate APIs use API key authentication. You pass a key in a request header, and the server uses it to identify your account and enforce rate limits. Here's the basic pattern:
curl -X GET "https://api.stream.estate/documents/properties?city=Lyon" \
-H "X-API-KEY: your-api-key"
Keep your API key out of client-side code. Store it in an environment variable or a secrets manager. If your key leaks, someone else runs up your bill.
Some providers use OAuth 2.0 instead, particularly when user-level permissions matter (think multi-tenant platforms where each user has their own data access scope). But in the real estate data space, API keys are the norm.
Querying Property Listings
Property search is the core operation. You send a GET request with filters and receive matching listings. The more specific your filters, the fewer results you pay for and the less post-processing you need.
Basic search — apartments for sale in Paris:
curl "https://api.stream.estate/documents/properties?transactionType=sale&propertyType=apartment&city=Paris&itemsPerPage=10" \
-H "X-API-KEY: your-api-key"
Filtered search — 2+ bedroom houses under 300K in the Rhone department:
curl "https://api.stream.estate/documents/properties?transactionType=sale&propertyType=house&departmentCode=69&advertPriceMax=300000&roomsMin=2" \
-H "X-API-KEY: your-api-key"
Python search function — wrapping the API in a reusable function:
import requests
API_KEY = "your-api-key"
BASE_URL = "https://api.stream.estate"
def search_properties(city, transaction_type="sale", property_type=None, max_price=None):
params = {
"city": city,
"transactionType": transaction_type,
"itemsPerPage": 30,
}
if property_type:
params["propertyType"] = property_type
if max_price:
params["advertPriceMax"] = max_price
response = requests.get(
f"{BASE_URL}/documents/properties",
headers={"X-API-KEY": API_KEY},
params=params,
)
response.raise_for_status()
data = response.json()
return data["hydra:member"], data["hydra:totalItems"]
listings, total = search_properties("Bordeaux", max_price=400000)
print(f"Found {total} properties")
The same logic works in any language. Here's a JavaScript equivalent using fetch:
const API_KEY = 'your-api-key'
const BASE_URL = 'https://api.stream.estate'
async function searchProperties(city, { transactionType = 'sale', propertyType, maxPrice } = {}) {
const params = new URLSearchParams({
city,
transactionType,
itemsPerPage: '30',
})
if (propertyType) params.set('propertyType', propertyType)
if (maxPrice) params.set('advertPriceMax', maxPrice)
const response = await fetch(`${BASE_URL}/documents/properties?${params}`, {
headers: { 'X-API-KEY': API_KEY },
})
if (!response.ok) throw new Error(`API error: ${response.status}`)
const data = await response.json()
return { listings: data['hydra:member'], total: data['hydra:totalItems'] }
}
const { listings, total } = await searchProperties('Lyon', {
propertyType: 'apartment',
maxPrice: 250000,
})
console.log(`Found ${total} properties`)
Understanding the Data Model
One thing that catches developers off guard: the same apartment can appear on five different listing portals. If you aggregate raw listings, your results are full of duplicates.
Stream.estate handles this with a two-level data model:
- A Property represents a single physical property. One apartment = one Property record, regardless of how many portals list it.
- Each Property has one or more Adverts — one per source portal (LeBonCoin, SeLoger, Bien'ici, etc.). Each Advert has its own price, title, description, and source URL.
This deduplication matters. Without it, a search for apartments in Paris returns the same listing three times with slightly different prices. With it, you get one result per actual property and can compare prices across sources.
Here's what a property response looks like in practice:
{
"uuid": "abc-123",
"propertyType": "apartment",
"transactionType": "sale",
"rooms": 3,
"surface": 65,
"city": "Paris",
"postalCode": "75011",
"latitude": 48.8566,
"longitude": 2.3522,
"adverts": [
{
"source": "leboncoin",
"price": 420000,
"title": "3 pièces lumineux",
"url": "https://..."
},
{
"source": "seloger",
"price": 425000,
"title": "Appartement 3P 65m²",
"url": "https://..."
}
]
}
Notice the price difference between the two adverts. Same apartment, different asking prices on different portals. Your application can display the lowest price, the average, or let the user compare — the API gives you the raw data to work with.
To fetch the full details of a single property once you have its UUID:
curl "https://api.stream.estate/documents/properties/abc-123" \
-H "X-API-KEY: your-api-key"
Pagination
Property searches can match thousands of results. The API paginates them with two parameters:
itemsPerPage— how many results per page (max 30, default 30)page— which page to fetch (starting at 1)
The response format is JSON-LD with Hydra pagination metadata, so you get hydra:totalItems telling you the total count and hydra:member containing the current page of results.
One constraint to know: the underlying search engine limits deep pagination to roughly 10,000 results (page 333 at 30 items per page). If your query matches more than that, you can't paginate past it. For large datasets, iterate by date range or geographic subdivision instead.
Here's a Python function that fetches all pages:
def get_all_properties(city, **filters):
all_properties = []
page = 1
while True:
params = {"city": city, "page": page, "itemsPerPage": 30, **filters}
resp = requests.get(
f"{BASE_URL}/documents/properties",
headers={"X-API-KEY": API_KEY},
params=params,
)
data = resp.json()
items = data["hydra:member"]
if not items:
break
all_properties.extend(items)
page += 1
return all_properties
Billing tip: Stream.estate charges per property returned, not per API request. Setting itemsPerPage=0 returns only the count (hydra:totalItems) without any property data — and costs nothing. Use this to check how many results a query matches before pulling actual data:
curl "https://api.stream.estate/documents/properties?city=Paris&transactionType=sale&itemsPerPage=0" \
-H "X-API-KEY: your-api-key"
This is useful for analytics dashboards, search result counts, and validating filters before committing to a full data pull.
Market Data Endpoints
Beyond individual listings, you often need aggregate market data. Stream.estate provides a price-per-meter endpoint that returns average prices for a given city and transaction type:
curl "https://api.stream.estate/indicators/price-per-meter?city=Paris&transactionType=sale" \
-H "X-API-KEY: your-api-key"
For building valuation comparables — finding properties similar to a known one — use the similar properties endpoint:
curl "https://api.stream.estate/documents/properties/abc-123/similar" \
-H "X-API-KEY: your-api-key"
This returns properties with matching characteristics (location, size, type) and is useful for automated valuation models where you need comps without writing your own similarity algorithm. For a full walkthrough of building a valuation tool with these endpoints, see our property valuation tutorial.
Webhooks for Real-Time Updates
Polling an API every few minutes to check for new listings is wasteful. Most requests return no new data, and you're burning API calls (and budget) on empty responses.
Webhooks invert the flow. You tell the API what you're looking for, and it sends a POST request to your server when a matching property appears. Two steps:
First, save a search that defines your criteria:
curl -X POST "https://api.stream.estate/documents/searches" \
-H "X-API-KEY: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"transactionType": "sale",
"propertyType": "apartment",
"city": "Lyon",
"advertPriceMax": 300000
}'
Then, create a webhook that references that saved search:
curl -X POST "https://api.stream.estate/documents/webhooks" \
-H "X-API-KEY: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/new-listing",
"search": "/documents/searches/xyz-789"
}'
When a new property matches your saved search criteria, the API sends a POST to your webhook URL with the property data. Your server processes it — send a notification, update a database, trigger a workflow. No polling required.
Make sure your webhook endpoint responds with a 200 status quickly. If it times out or returns errors repeatedly, the API will stop sending notifications.
Production Best Practices
A few things that save you time and money once you're past the prototype stage:
Cache aggressively. Property details don't change every minute. Cache property responses for at least an hour. If you're displaying search results, cache those for a few minutes. This reduces API calls and speeds up your application.
Use itemsPerPage=0 first. Before pulling full data, check the result count. If a query matches 50,000 properties, you probably want to add more filters rather than paginating through all of them at full cost.
Handle rate limits. Implement exponential backoff when you get a 429 response. Start with a 1-second delay, double it on each retry, cap at 30 seconds. Don't hammer the API with retries — you'll just extend your lockout.
import time
def api_request_with_retry(url, headers, params, max_retries=5):
delay = 1
for attempt in range(max_retries):
resp = requests.get(url, headers=headers, params=params)
if resp.status_code == 429:
time.sleep(delay)
delay *= 2
continue
resp.raise_for_status()
return resp.json()
raise Exception("Max retries exceeded")
Store property UUIDs. Track which properties you've already processed. This prevents duplicate work when paginating through results across multiple runs and lets you detect when properties are delisted (they disappear from search results but you still have their UUID).
Monitor your usage. Since billing is per property returned, an unoptimized query can get expensive fast. Log how many properties each query returns. Set up alerts if your daily usage spikes unexpectedly. For a detailed breakdown of how different providers charge, see our pricing comparison.
Use specific filters. The more filters you apply, the fewer results you get, and the less you pay. Don't fetch all properties in a city when you only need 2-bedroom apartments under a certain price.
Next Steps
This guide covers the core integration patterns. The specifics vary between providers, but the architecture — authenticate, query, paginate, subscribe to updates — is the same everywhere.
For a comparison of available APIs, see Top 10 Real Estate APIs in 2026. If you're working specifically with French market data, read our French Property Data API guide.