How to Parse the Data from RealtyAPI.io

You've got the first RealtyAPI response open in one tab, your app model in another, and a sinking feeling that the payload isn't going to fit the neat little row you planned for. The object tree is deeper than expected, fields show up in different places depending on the source, and half the work is deciding what to keep, what to flatten, and what to ignore. That's the job when you parse the data from a real estate API.
RealtyAPI's docs make the surface area look simple at first, but the actual integration work starts when you turn nested responses into records your app can trust. If you're also reviewing the platform's introduction docs, the key mental shift is this, parsing isn't just string handling, it's schema design with messy inputs. Good API documentation helps here too, especially a developer-friendly API docs guide when you're trying to make internal docs match the shape your parser expects.
What Parsing RealtyAPI Responses Entails
The first surprise is that a listing response is rarely a flat object you can dump straight into storage. You're usually dealing with a nested tree of details, availability windows, host profile data, amenities, and reviews, and each source can place those pieces differently. In practice, parsing means turning that heterogeneous response into one predictable application shape, not just decoding JSON.
Think in pipeline stages, not one-off transforms
The cleanest mental model is the same one used in parsing work more broadly, receive, tokenize, extract, validate, then output. That sequence keeps the work readable when the payload gets ugly, because each step has one job. A parser has to be idempotent, schema-aware, and tolerant of missing fields, since real estate data is messy by nature and the source platforms don't all expose the same fields in the same way.
That matters even more when you're normalizing public listings from Redfin, Realtor, Airbnb, Zoopla, Bayut, Apartments.com, and Idealista into one shape your app can use. You're not just parsing a response, you're reconciling different public catalogs into a consistent record for search, analytics, or automation. The practical payoff is that the same parser can sit behind different ingest paths without changing your downstream model. The RealtyAPI documentation overview provides the broader product context behind the response shape.

Why surface choice changes the parser
RealtyAPI exposes REST, GraphQL, and webhooks, and that changes how much work your parser has to do. REST usually gives you the widest payload, GraphQL lets you ask for the exact nested fields you need, and webhooks give you event-shaped updates that are better for live change detection than broad backfills. The parsing strategy follows the surface, not the other way around.
Practical rule: treat the API surface as part of your schema. If the surface changes, your parser contract changes too.
If you're building around one of the common real estate workflows, the goal isn't perfect parsing on day one. The goal is a parser that can survive missing amenities, partial availability, and source-specific quirks without breaking the row you store. A developer-friendly API docs guide helps when you need internal docs to stay aligned with the fields your parser expects.
The Three Surfaces You Can Parse From
A parser gets a different job depending on where the payload comes from. REST gives you broad records for search and detail fetches, GraphQL lets you request a narrower shape, and webhooks send change events that need to be filtered before they become usable rows. If you choose the wrong surface for the job, you end up writing cleanup code you did not need.
REST for full listings and search result sets
REST is the straight path when you need a canonical listing response or a search result set you can store as-is after normalization. That is usually the case for discovery flows, record enrichment, and backfills, where you want a predictable payload and fewer surprises in the parser. The RealtyAPI Zillow endpoint examples show the common lookup patterns, including detail fetches and search queries, so you can see how the same record behaves across requests.
A simple REST pull might look like this:
GET /api/zillow/listing?id=LISTING_ID
GET /api/zillow/search?location=Barcelona
GET /api/zillow/search?lat=41.3851&lng=2.1734
GET /api/zillow/search?place_id=PLACE_ID
GET /api/zillow/search?url=https%3A%2F%2Fexample.com%2Flisting
GraphQL for shaped payloads
GraphQL changes the parser's workload because you ask for the fields you plan to use, including availability, reviews, host, amenities, and accessibility. That reduces the amount of post-processing in many cases, since you are not stripping out branches that never arrived in the response. The parser still has to normalize names and handle nulls, but the payload is usually narrower and easier to map into a stable record.
query Listing($id: ID!) {
listing(id: $id) {
id
price
host { name rating }
amenities
accessibility
availability { start end }
reviews { rating text }
}
}
Webhooks for change detection
Webhooks fit the cases where the app cares about live listing edits, availability shifts, or market movement as it happens. The payload arrives in an event envelope, so the parser has to read the event metadata first, then inspect the changed record. That is a different shape from a search response, because the main question is whether the event should trigger a re-parse, a reindex, or a single-field update.
POST /webhooks/listing-updated
{
"event": "listing.updated",
"listing": {
"id": "LISTING_ID",
"price": 2400,
"availability": [...]
}
}
| Surface | Best For | Parsing Concern |
|---|---|---|
| REST | Search and detail ingestion | Larger payloads, more field normalization |
| GraphQL | Minimal payloads with exact fields | Null handling, schema alignment |
| Webhooks | Live updates and listing changes | Event envelopes, idempotency, replay safety |
Parsing Listings in Python and JavaScript
A parser only becomes useful when it turns a listing response into a stable record that the rest of the app can trust. In practice that means flattening nested availability, pulling host data out of the main listing object, handling optional arrays, and keeping the output consistent whether the source sent weekly pricing, monthly pricing, or a regular nightly rate. It also means stripping HTML from descriptions before that text reaches search or email templates, which is why a clean HTML to plain text guide helps when the source content is not already sanitized.
The same normalization job looks a little different across REST, GraphQL, and webhooks. REST usually gives you a broader payload, GraphQL lets you ask for only the fields you want, and webhooks hand you a change event that may only contain part of the record. The record shape you build should stay consistent across all three, even if the incoming surface does not.
You can test these normalization strategies directly in the RealtyAPI API playground and compare the output side by side before wiring the parser into your app.
Python normalization with a small parser class
import html
import re
from datetime import datetime
import requests
def strip_html(value):
if not value:
return ""
text = re.sub(r"<[^>]+>", " ", value)
return re.sub(r"\s+", " ", html.unescape(text)).strip()
class ListingParser:
def normalize(self, payload):
listing = payload.get("listing", payload)
price = listing.get("price")
nightly = listing.get("nightly_price")
if nightly is None:
weekly = listing.get("weekly_price")
monthly = listing.get("monthly_price")
if weekly is not None:
nightly = round(float(weekly) / 7, 2)
elif monthly is not None:
nightly = round(float(monthly) / 30, 2)
availability = []
for window in listing.get("availability", []):
availability.append({
"start": self._parse_date(window.get("start")),
"end": self._parse_date(window.get("end"))
})
host = listing.get("host", {}) or {}
amenities = listing.get("amenities") or []
accessibility = listing.get("accessibility") or []
return {
"source_id": listing.get("id"),
"title": listing.get("title", ""),
"price": price,
"nightly_price": nightly,
"currency": listing.get("currency", "USD"),
"location": listing.get("location", {}) or {},
"description": strip_html(listing.get("description")),
"availability": availability,
"host": {
"name": host.get("name", ""),
"rating": host.get("rating")
},
"amenities": amenities,
"accessibility": accessibility,
"source": listing.get("source", "realtyapi"),
"fetched_at": payload.get("fetched_at", datetime.utcnow().isoformat() + "Z")
}
def _parse_date(self, value):
if not value:
return None
return datetime.fromisoformat(value.replace("Z", "+00:00")).date().isoformat()
response = requests.get("https://api.example.com/listing")
parser = ListingParser()
record = parser.normalize(response.json())
print(record)
The main rules stay the same across most integrations. Use defensive defaults, normalize snake_case and camelCase into one shape, parse dates as ISO 8601, and keep missing arrays from crashing the transform. If the source sometimes returns a partial object, the parser should still emit the same keys so downstream code does not have to special-case every response.
JavaScript normalization with fetch
function stripHtml(value = "") {
return value.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
}
function parseDate(value) {
if (!value) return null;
return new Date(value).toISOString().slice(0, 10);
}
function normalizeListing(payload) {
const listing = payload.listing ?? payload;
let nightlyPrice = listing.nightly_price ?? null;
if (nightlyPrice == null) {
if (listing.weekly_price != null) nightlyPrice = Number(listing.weekly_price) / 7;
else if (listing.monthly_price != null) nightlyPrice = Number(listing.monthly_price) / 30;
}
const availability = Array.isArray(listing.availability)
? listing.availability.map(window => ({
start: parseDate(window.start),
end: parseDate(window.end)
}))
: [];
const host = listing.host ?? {};
const amenities = Array.isArray(listing.amenities) ? listing.amenities : [];
const accessibility = Array.isArray(listing.accessibility) ? listing.accessibility : [];
return {
sourceId: listing.id ?? "",
title: listing.title ?? "",
price: listing.price ?? null,
nightlyPrice,
currency: listing.currency ?? "USD",
location: listing.location ?? {},
description: stripHtml(listing.description ?? ""),
availability,
host: {
name: host.name ?? "",
rating: host.rating ?? null
},
amenities,
accessibility,
source: listing.source ?? "realtyapi",
fetchedAt: payload.fetched_at ?? new Date().toISOString()
};
}
const res = await fetch("https://api.example.com/listing");
const data = await res.json();
const record = normalizeListing(data);
console.log(record);
The trade-off in JavaScript is usually between simplicity and date handling. new Date() is easy to reach for, but it will happily accept inputs you may not want to trust, so a parser that touches real production feeds should keep date parsing tight and predictable. If the payload might already be normalized upstream, keep this layer thin. If not, you absorb the messy source quirks once and keep them out of the rest of the app.
A complete normalized listing record should usually contain source ID, title, price, nightly price, currency, location, description, availability, host, amenities, accessibility, source, and fetched_at. If one of those fields is missing in the source, keep the key in the normalized output so downstream code stays predictable, and use empty strings, nulls, or empty arrays where the data really is absent.
Handling Pagination, Rate Limits, and Retries
Parsing at scale fails in boring ways before it fails in dramatic ones. The common issues are pagination that stops too early, rate limits that trigger noisy retries, and partial parse failures that get stored as if they were complete records. The fix is to treat pagination metadata, quota headers, and retry behavior as part of the parser, not as separate concerns.
Walk pages until the API tells you to stop
Some endpoints return a page number, some return a cursor, and others return a next token. The implementation should follow the metadata that comes back from the endpoint, not a guess baked into the client. That's the difference between a parser that drops records and one that can backfill a catalog reliably.
A Python loop can walk cursor-based pages like this:
import time
import requests
def fetch_all(base_url, params=None):
params = params or {}
cursor = None
results = []
while True:
query = dict(params)
if cursor:
query["next_cursor"] = cursor
response = requests.get(base_url, params=query)
payload = response.json()
batch = payload.get("results", [])
if not batch:
break
results.extend(batch)
cursor = payload.get("next_cursor")
if not cursor:
break
time.sleep(1)
return results
A JavaScript async iterator keeps the same logic readable:
async function* fetchPages(url, params = {}) {
let cursor = null;
while (true) {
const nextParams = new URLSearchParams(params);
if (cursor) nextParams.set("next_cursor", cursor);
const res = await fetch(`${url}?${nextParams.toString()}`);
const payload = await res.json();
const results = payload.results ?? [];
if (!results.length) return;
yield results;
cursor = payload.next_cursor ?? null;
if (!cursor) return;
}
}
Parse headers before you hit the wall
Rate-limit handling works best when the client reads the remaining-quota and reset-at style headers before it retries. That lets the parser throttle itself instead of hammering the upstream API until the queue backs up. RealtyAPI's docs also call out intelligent retries with exponential backoff, but the client still needs its own guard around parse failures so a bad batch can be replayed cleanly, not partially committed.
Practical rule: retry the parse step as well as the network call. A successful fetch with a failed transform is still a failed ingest.
For retry behavior, watch the 429 and 5xx classes and back off with jitter. That's also where a proxy layer from Stella Proxies' scraping best practices can help in scraping-heavy stacks, especially when you're separating fetch pressure from parse and storage work. The rate limits documentation is worth keeping open next to your client code so the throttle behavior matches the API's expectations.
A Search Store Index Recipe You Can Adapt
A solid ingest path starts with search, not storage. Pull listings by city or coordinates, normalize each result into the same record shape, and validate required fields before anything touches your primary database. If a row is missing price, location, source, or fetched_at, send it to a dead-letter queue instead of pretending it's ready.
A minimal pipeline that won't paint you into a corner
import sqlite3
from datetime import datetime
conn = sqlite3.connect("listings.db")
cur = conn.cursor()
cur.execute("""
CREATE TABLE IF NOT EXISTS listings (
source_id TEXT PRIMARY KEY,
title TEXT,
price REAL,
location ტექსტ,
source TEXT,
fetched_at TEXT,
raw_json TEXT
)
""")
def is_valid(record):
required = [record.get("price"), record.get("location"), record.get("source"), record.get("fetched_at")]
return all(value is not None and value != "" for value in required)
for record in normalized_records:
if not is_valid(record):
dead_letter_queue.append(record)
continue
cur.execute(
"INSERT OR REPLACE INTO listings VALUES (?, ?, ?, ?, ?, ?, ?)",
(
record["source_id"],
record["title"],
record["price"],
str(record["location"]),
record["source"],
record["fetched_at"],
record["raw_json"]
)
)
conn.commit()
From there, push the normalized rows into Meilisearch or Elasticsearch so search can hit the flat fields directly. Keep the raw response beside the normalized record, because schema drift is normal and re-parsing is much easier when the original payload is still available. Parse in chunks, dedupe by source-listing ID, and don't index a record until the validation gate passes.

Key Takeaways and Parsing FAQ
The rules that hold up in production are pretty consistent. Validate before you persist, normalize across sources into one shape, parse pagination metadata instead of assuming the response is complete, and never trust a single field without a fallback. A good parser is boring in the best way, because the ugly payloads don't get to decide your storage schema.
Should you parse once and cache, or re-parse on every request? Parse once for storage and search, then re-parse only when the schema changes or the raw source needs a correction. What if a field exists in one source but not another? Keep the key in the normalized record and use a null or empty default, so downstream code doesn't branch on source type. What should you log when parsing fails? Log the source ID, raw payload reference, validation error, and parse stage, because that's what helps you replay the failure cleanly.
GraphQL can shrink the normalization layer, but it doesn't remove it. You still need a consistent internal record shape, especially when different clients query different field subsets. RealtyAPI sources public listings, so the parser doesn't need to strip PII from private user data, but it should still log and audit source usage so you can trace where every record came from.
RealtyAPI.io gives you a unified real estate data layer with REST, GraphQL, and webhooks, so you can pull listings, normalize them, and keep the pipeline aligned with the way your stack works. If you're building search, analytics, or live listing monitoring, visit RealtyAPI.io and wire your parser to a response shape that's meant to be consumed.