Real Estate Listings API
Integration cookbook — practical, copy-pasteable recipes for the most common tasks: listing every location in Spain, pulling all rentals in a city, downloading entire new-development inventories, filtering, and crawling at national scale.
1. Getting started
All requests go through RapidAPI. You need two headers: your subscription key and the host.
curl --request GET \
--url 'https://idealista-real-estate.p.rapidapi.com/v1/search?operation=sale&propertyType=homes&locationIds=%5B0-EU-ES-28%5D&maxItems=5' \
--header 'x-rapidapi-key: YOUR_RAPIDAPI_KEY' \
--header 'x-rapidapi-host: idealista-real-estate.p.rapidapi.com'
That one call returns the first 5 homes for sale in the province of Madrid. Throughout this
guide we will use this tiny JavaScript helper (Node 18+, Bun, Deno or the browser — anything
with fetch):
const BASE = "https://idealista-real-estate.p.rapidapi.com";
const HEADERS = {
"x-rapidapi-key": process.env.RAPIDAPI_KEY,
"x-rapidapi-host": "idealista-real-estate.p.rapidapi.com",
accept: "application/json",
};
async function apiGet(path, params = {}) {
const qs = new URLSearchParams();
for (const [k, v] of Object.entries(params)) {
if (v !== undefined && v !== null && v !== "") qs.set(k, String(v));
}
const url = `${BASE}${path}${qs.size ? `?${qs}` : ""}`;
const res = await fetch(url, { headers: HEADERS });
if (!res.ok) throw new Error(`HTTP ${res.status} ${url} :: ${await res.text()}`);
return res.json();
}
2. Core concepts
| Concept | What it is |
|---|---|
locationIds | Idealista's hierarchical location tag, e.g. 0-EU-ES-28
(province of Madrid) or 0-EU-ES-28-07-001-079 (city of Madrid). Passed in brackets:
locationIds=[0-EU-ES-28] (URL-encoded: %5B0-EU-ES-28%5D). See
section 3 for how to obtain them. |
operation | sale or rent. |
propertyType | homes, newDevelopments, offices,
premises (retail), garages, storageRooms, lands,
buildings. Land and buildings are sale-only. |
propertyCode / adid | The listing ID. Search results call it
propertyCode; the detail endpoints call it adid. Same number. |
country | es (default), pt, it. |
| Pagination | numPage (1-based) + maxItems (max 40
per page). Responses include total, totalPages, actualPage. |
Every search-style endpoint returns the same envelope:
{
"total": 28774, // total matching listings
"totalPages": 720, // at the current maxItems
"actualPage": 1,
"itemsPerPage": 40,
"elementList": [ { "propertyCode": "110715434", "price": 1400000, ... } ]
}
3. Working with locations
3.1 Every province of Spain, zero guesswork
A static map of the 17 autonomous communities plus Ceuta and Melilla, each with its provinces
and their ready-to-use locationId. It never changes, costs one call, and is the natural
seed for any Spain-wide crawl:
const { regions } = await apiGet("/v1/locations/regions");
const provinces = regions.flatMap(r => r.provinces);
// 52 entries: { locationId: "0-EU-ES-08", name: "Barcelona" }, ...
3.2 Drilling down: province → municipality → district → neighbourhood
const { locations } = await apiGet("/v1/locations/0-EU-ES-28/children", {
country: "es", locale: "es",
});
// [{ locationId: "0-EU-ES-28-07-001-079", name: "Madrid, Madrid", divisible: true, ... }, ...]
Calling it recursively gives you the complete location tree. Each child includes a
divisible flag — when it is false the node has no further useful
subdivisions, so you can stop descending.
total in children responses.
The total field returned by the children endpoint is not reliable (often
0). When you need the real listing count for a location, ask the relevant search
endpoint with maxItems=1 — see section 6.3.3 Finding a location by name, coordinates or short code
| Endpoint | Use when |
|---|---|
GET /v1/locations/autocomplete?query=malasaña | You have free text
(a search box). Returns official locations (with locationId) mixed with POIs —
streets, metro stations — that have no locationId; keep only entries that have one. |
GET /v1/locations/by-coordinates?lat=40.42&lng=-3.70 | You have GPS coordinates and want the administrative chain around them (reverse geocoding). |
POST /v1/locations/by-shorturis | You have an Idealista short code like
[bd4] (from a shared URL) and need the real locationId. |
const { locations } = await apiGet("/v1/locations/autocomplete", {
query: "chamberi", country: "es",
});
const usable = locations.filter(l => l.locationId); // drop POIs
4. Search recipes
4.1 All rental homes in Madrid
First decide which "Madrid" you mean — the province (0-EU-ES-28, includes every
town around the capital) or the city (0-EU-ES-28-07-001-079). Then page through:
# First page of rentals in Madrid city
curl --request GET \
--url 'https://idealista-real-estate.p.rapidapi.com/v1/rent-homes?locationIds=%5B0-EU-ES-28-07-001-079%5D&maxItems=40&numPage=1&country=es&locale=es' \
--header 'x-rapidapi-key: YOUR_RAPIDAPI_KEY' \
--header 'x-rapidapi-host: idealista-real-estate.p.rapidapi.com'
To download all of them, loop until numPage reaches
totalPages (a reusable paginate helper is in
section 6):
const rentals = [];
for await (const item of paginate("/v1/rent-homes", {
locationIds: "[0-EU-ES-28-07-001-079]",
country: "es",
})) {
rentals.push(item);
}
console.log(`${rentals.length} rental homes in Madrid city`);
/v1/rent-homes is a convenience preset of
/v1/search?operation=rent&propertyType=homes — both work identically.
total first. Madrid city usually
has well over 2,000 active rentals, and a single query only yields distinct results up to roughly
the first ~2,000 — beyond that, pages repeat. If the first response reports
total > 2000, don't paginate to the end: split the query into the location's
children (districts, then neighbourhoods) and paginate each one. The exact pattern is in
section 7.4.2 Second-hand (resale) homes for sale
propertyType=homes with operation=sale returns every
home for sale — resale properties plus individual units that belong to new developments. The
upstream API has no server-side "exclude new builds" switch, so filter on the client using the
newDevelopment flag each item carries:
const resale = [];
for await (const item of paginate("/v1/search", {
operation: "sale",
propertyType: "homes",
locationIds: "[0-EU-ES-22]", // province of Huesca
country: "es",
})) {
if (!item.newDevelopment) resale.push(item); // keep second-hand only
}
4.3 New developments — and every unit inside them
New-build inventory has two levels: the promotion (the development itself, one marketing listing) and its units (the individual homes you can actually buy).
GET/v1/promotion/{promotionId}/units
GET/v1/promotion/{promotionId}
// 1) All developments in Cantabria
for await (const promo of paginate("/v1/new-homes", {
locationIds: "[0-EU-ES-39]",
country: "es",
})) {
// 2) Every unit inside this development
for await (const unit of paginate(`/v1/promotion/${promo.propertyCode}/units`, {
operation: "sale",
country: "es",
})) {
// unit is a full listing: price, size, rooms, floor plan, ...
}
// 3) Optional: rich detail of the development itself
// const detail = await apiGet(`/v1/promotion/${promo.propertyCode}`, { country: "es" });
}
Useful extras on /v1/new-homes: finished=true (completed
developments only) or newDevelopmentInProject=true (off-plan only). The
total field of the units response tells you how many units the development
currently has on the market.
4.4 Offices, retail, garages, storage rooms, land, buildings
Each vertical has a shortcut endpoint with the same envelope and pagination:
| Endpoint | Vertical | Operation |
|---|---|---|
/v1/offices | Offices | sale | rent (required) |
/v1/premises | Retail / commercial premises | sale | rent (required) |
/v1/garages | Garages / parking spots | sale | rent (required) |
/v1/storage-rooms | Storage rooms | sale | rent (required) |
/v1/lands | Land / plots | always sale |
/v1/buildings | Entire buildings | always sale |
// Garages for rent near Atocha
const garages = await apiGet("/v1/garages", {
operation: "rent",
locationIds: "[0-EU-ES-28-07-001-079]",
priceTo: 150,
});
5. Filtering results
All search endpoints accept filters as plain query parameters. Unknown or non-applicable filters are silently ignored (never an error), so you can share one query-builder across verticals.
The everyday filters
| Filter | Type | Example |
|---|---|---|
priceFrom / priceTo | number (EUR; €/month for rent) | priceTo=1500 |
sizeFrom / sizeTo | number (m²) | sizeFrom=80 |
bedrooms | CSV list | bedrooms=2,3 (2 or 3 bedrooms) |
bathsFrom / bathsTo | int (upstream honours 1–3) | bathsFrom=2 |
order + sort | enum | order=price&sort=asc · orders: publicationDate, price, priceDown, size, distance, weigh |
minPublicationDate | tag or ISO date | minPublicationDate=W (last week; also Y=48h, M=month, D=day) |
Amenities (booleans)
withElevator, withTerrace, withParking,
withSwimmingPool, withGarden, withStorageRoom,
withAirConditioning, builtinWardrobes, exterior,
accessible, luxury, virtualTour, hasPlan,
bankOffer.
Multi-value filters (CSV)
| Filter | Values |
|---|---|
floor | ground, intermediate, top — for penthouses/attics use homeSubtype=atticStudio |
energyCertificate | high (A–B), medium (C–D), low (E–G) |
homeSubtype | apartment, loft, atticStudio, casaBaja, cortijo, stoneHouse, villaLabel, independentHouse, terracedHouse, semidetachedHouse |
preservation | good, renew (needs renovation — great for finding fixer-uppers) |
Operation-specific filters
| Only for | Filter | Meaning |
|---|---|---|
rent | petsAllowed=true | Pets accepted |
longTermRental=true | Long-term residential lease | |
seasonalRental=true | Seasonal / short-stay lease | |
sale | occupationType=free,tenanted,bareOwnership,illegallyOccupied | Occupancy status (CSV, pick any subset) |
newDevelopments | finished / newDevelopmentInProject | Construction status |
Putting it together
# 2–3 bedroom rentals in Madrid city, ≤ €1,800/month, with elevator,
# pets allowed, long-term, published in the last week, cheapest first
curl --request GET \
--url 'https://idealista-real-estate.p.rapidapi.com/v1/rent-homes?locationIds=%5B0-EU-ES-28-07-001-079%5D&priceTo=1800&bedrooms=2%2C3&withElevator=true&petsAllowed=true&longTermRental=true&minPublicationDate=W&order=price&sort=asc&maxItems=40' \
--header 'x-rapidapi-key: YOUR_RAPIDAPI_KEY' \
--header 'x-rapidapi-host: idealista-real-estate.p.rapidapi.com'
// Top-floor fixer-uppers for sale in Barcelona province, 60 m²+, under €250k
const deals = await apiGet("/v1/search", {
operation: "sale",
propertyType: "homes",
locationIds: "[0-EU-ES-08]",
priceTo: 250000,
sizeFrom: 60,
preservation: "renew",
floor: "top",
order: "publicationDate",
sort: "desc",
});
GET /v1/search/filters?operation=rent&propertyType=homes&locationIds=[0-EU-ES-28]
returns the filter blocks that apply to that exact context (with data types and UI labels) —
ideal if you are building a search UI and want it to adapt per vertical.6. Pagination & counting
Pages are capped at 40 items. The loop that powers everything above:
async function* paginate(path, params) {
let page = 1;
while (true) {
const resp = await apiGet(path, { ...params, numPage: page, maxItems: 40 });
yield* resp.elementList;
if (resp.elementList.length === 0 || page >= resp.totalPages) break;
page++;
await new Promise(r => setTimeout(r, 25)); // small politeness delay
}
}
total on the first page, and if it's above ~2,000, don't
keep paginating — re-issue the query against the location's children
(/v1/locations/{id}/children) until each sub-query is under the limit. That is
exactly what the recursive splitter in section 7 automates.When you only need how many listings match — market sizing, dashboards, deciding
whether to split a crawl — don't download pages. Ask for a single item and read
total:
async function fetchTotal(path, params) {
const resp = await apiGet(path, { ...params, maxItems: 1, numPage: 1 });
return resp.total; // e.g. 28774 homes for sale in Madrid
}
7. Crawling large areas reliably
This section turns the ~2,000-result rule into a complete crawling
strategy. To recap: past roughly the first ~2,000 results of a single query, additional pages
start repeating listings instead of yielding new ones. The total and
totalPages values are accurate — but you can't exhaustively drain a 30,000-listing
query with one loop, no matter how many pages you request.
The fix is the strategy our national production crawler uses: whenever a query reports more than ~2,000 results, retry against the location's children instead — recursively, until every segment is under the limit (we use 1,800 as the threshold, leaving a safety margin below 2,000), then paginate each segment fully:
/**
* Returns "leaf" locations, each with ≤ threshold listings — safe to
* paginate exhaustively. One cheap maxItems=1 call per node visited.
*/
async function splitIntoSegments(rootLocationId, searchPath, searchParams, threshold = 1800) {
const leafs = [];
const stack = [{ locationId: rootLocationId, divisible: true }];
while (stack.length > 0) {
const node = stack.pop();
const total = await fetchTotal(searchPath, {
...searchParams, locationIds: `[${node.locationId}]`,
});
if (total <= threshold) { // small enough → crawl directly
if (total > 0) leafs.push({ locationId: node.locationId, total });
continue;
}
if (!node.divisible) { // can't split further → accept as-is
leafs.push({ locationId: node.locationId, total });
continue;
}
const { locations } = await apiGet( // too big → recurse into children
`/v1/locations/${node.locationId}/children`, { country: "es" });
if (locations.length === 0) { leafs.push({ locationId: node.locationId, total }); continue; }
for (const child of locations) {
stack.push({ locationId: child.locationId, divisible: child.divisible });
}
}
return leafs;
}
// Full example: every rental home in the province of Madrid
const segments = await splitIntoSegments("0-EU-ES-28", "/v1/rent-homes", { country: "es" });
const all = [];
for (const seg of segments) {
for await (const item of paginate("/v1/rent-homes", {
locationIds: `[${seg.locationId}]`, country: "es",
})) {
all.push(item);
}
}
Practical advice from running this daily at national scale:
- Seed with
/v1/locations/regionsand run the splitter once per province × vertical (sale homes, rent homes, new developments). Cache the resulting segment list — it changes slowly. Re-check each segment'stotalbefore a crawl and only re-split the ones that outgrew the threshold. - Deduplicate by
propertyCode. Listings near boundaries can appear in two segments, and multi-location queries overlap. - Add retries with backoff (2–3 attempts) around every call, and keep a small
delay between pages. Occasional
502/503responses are transient — see section 10. - Track disappearances: a listing you saw yesterday but not today has likely
been sold, rented or withdrawn. Confirm with the detail endpoint — a
410is a positive deactivation signal (next section).
7.1 Copy-paste: a complete, runnable crawler
Everything above in one self-contained script. Save it as crawl.mjs and run it
with Node 18+ (or Bun) — no dependencies. Phase 1 prints every segment it discovers (each one
safely under the ~2,000 limit); phase 2 downloads them all, deduplicates, and writes one JSON
listing per line:
# All rentals in the province of Madrid:
RAPIDAPI_KEY=your_key node crawl.mjs 0-EU-ES-28 rent
# Second-hand + new-build units for sale in Barcelona province:
RAPIDAPI_KEY=your_key node crawl.mjs 0-EU-ES-08 sale
# New developments in Valencia province:
RAPIDAPI_KEY=your_key node crawl.mjs 0-EU-ES-46 new
// crawl.mjs — download every listing under a location, respecting the
// ~2,000-results-per-query limit by recursively splitting into child locations.
//
// Usage: RAPIDAPI_KEY=xxx node crawl.mjs [rootLocationId] [rent|sale|new]
// Output: segment list on stdout + listings-<root>-<vertical>.ndjson
import { createWriteStream } from "node:fs";
const [root = "0-EU-ES-28", vertical = "rent"] = process.argv.slice(2);
const BASE = "https://idealista-real-estate.p.rapidapi.com";
const HEADERS = {
"x-rapidapi-key": process.env.RAPIDAPI_KEY,
"x-rapidapi-host": "idealista-real-estate.p.rapidapi.com",
accept: "application/json",
};
const VERTICALS = {
rent: { path: "/v1/rent-homes", params: {} },
sale: { path: "/v1/search", params: { operation: "sale", propertyType: "homes" } },
new: { path: "/v1/new-homes", params: {} },
};
const { path: SEARCH_PATH, params: SEARCH_PARAMS } = VERTICALS[vertical];
const THRESHOLD = 1800; // stay safely under the ~2,000 hard limit
const DELAY_MS = 25; // politeness delay between calls
let apiCalls = 0; // every HTTP request = one billed API call
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function apiGet(path, params = {}, attempts = 3) {
const qs = new URLSearchParams();
for (const [k, v] of Object.entries(params)) {
if (v !== undefined && v !== null && v !== "") qs.set(k, String(v));
}
const url = `${BASE}${path}${qs.size ? `?${qs}` : ""}`;
let lastErr;
for (let i = 1; i <= attempts; i++) {
try {
apiCalls++;
const res = await fetch(url, { headers: HEADERS });
if (!res.ok) throw new Error(`HTTP ${res.status} :: ${(await res.text()).slice(0, 200)}`);
await sleep(DELAY_MS);
return res.json();
} catch (err) {
lastErr = err;
if (i < attempts) await sleep(250 * i * i); // backoff: 250 ms, 1 s
}
}
throw lastErr;
}
const fetchTotal = async (locationId) =>
(await apiGet(SEARCH_PATH, {
...SEARCH_PARAMS, locationIds: `[${locationId}]`, maxItems: 1,
})).total;
// ── Phase 1: split into segments of ≤ THRESHOLD results ──────────────────
const segments = [];
const stack = [{ locationId: root, name: root, divisible: true }];
while (stack.length > 0) {
const node = stack.pop();
const total = await fetchTotal(node.locationId);
if (total <= THRESHOLD || !node.divisible) {
if (total > 0) {
segments.push({ ...node, total });
console.log(`✓ segment ${node.locationId} (${node.name}) — ${total} listings`);
}
continue;
}
console.log(`… splitting ${node.locationId} (${node.name}) — ${total} > ${THRESHOLD}`);
const { locations } = await apiGet(
`/v1/locations/${node.locationId}/children`, { country: "es" });
if (locations.length === 0) { segments.push({ ...node, total }); continue; }
for (const c of locations) {
stack.push({ locationId: c.locationId, name: c.name, divisible: c.divisible });
}
}
const expected = segments.reduce((s, x) => s + x.total, 0);
const phase2Calls = segments.reduce((s, x) => s + Math.ceil(x.total / 40), 0);
console.log(`\n${segments.length} segments, ~${expected} listings expected`);
console.log(`API calls spent so far: ${apiCalls}. ` +
`Phase 2 will spend ~${phase2Calls} more. Ctrl-C now to abort.\n`);
await sleep(3000); // grace period to abort before spending phase-2 credits
// ── Phase 2: download every segment, dedup by propertyCode ───────────────
const outFile = `listings-${root}-${vertical}.ndjson`;
const out = createWriteStream(outFile);
const seen = new Set();
for (const seg of segments) {
let page = 1;
while (true) {
const resp = await apiGet(SEARCH_PATH, {
...SEARCH_PARAMS, locationIds: `[${seg.locationId}]`,
maxItems: 40, numPage: page,
});
for (const item of resp.elementList) {
if (seen.has(item.propertyCode)) continue; // overlap at segment borders
seen.add(item.propertyCode);
out.write(JSON.stringify(item) + "\n");
}
if (resp.elementList.length === 0 || page >= resp.totalPages) break;
page++;
}
console.log(`↓ ${seg.locationId} done — ${seen.size} unique listings so far`);
}
out.end();
console.log(`\nDone: ${seen.size} unique listings → ${outFile}`);
console.log(`Total API calls spent: ${apiCalls}`);
To cover all of Spain, wrap it in a loop over the 52 provinces from
/v1/locations/regions (section 3.1). And if you only want
the segment map — every location under the 2,000 limit, without downloading anything — just stop
after phase 1.
GET /locations-es-index.json and
GET /locations-it-index.json (free, no auth,
not billed) are pre-computed snapshots of exactly what phase 1 produces: every location in Spain
and Italy with ≤ ~1,800 listings, for both rent and sale, with its
locationId, name and listing count. Start from one and you save the entire
segmentation cost.generatedAt field before relying on it. Listing counts drift daily, and a segment
that was comfortably under the limit can grow past ~2,000 over time — at which point paginating
it stops being exhaustive. As a rule of thumb the segment structure stays valid for a few weeks.
If the snapshot is older than that, or you see any segment whose live total (one
maxItems=1 call) approaches 2,000, don't trust it — re-run phase 1 of the script
above yourself and work from your own fresh map.How many API calls will it spend?
The script counts every billed request and tells you before spending the bulk of them: after phase 1 it prints the exact number of calls used so far plus the estimate for phase 2, then waits 3 seconds so you can abort. The arithmetic, if you want to budget upfront:
- Phase 1 (segmentation): 1 call per location node visited
(
maxItems=1count), plus 1 call per node that needs splitting (itschildrenlookup). A location that fits under the threshold costs exactly 1 call. - Phase 2 (download):
ceil(total / 40)calls per segment — i.e. one call per 40 listings.
Measured examples against the live API: the entire rental market of a small province
(Soria, 97 listings, no splitting needed) costs 4 calls — 1 for the count,
3 to download. The same crawl forced to split down to neighbourhood level spent 84 calls for
the same 97 listings, which is why the threshold exists: only split when
total exceeds ~1,800 — never pre-emptively. As a rule of thumb, a full
crawl costs about total_listings / 40 calls plus a small segmentation overhead
(typically 5–15% for dense areas).
For a sense of scale, we ran the segmenter over the entire Spanish rental
market (all 52 provinces seeded from /v1/locations/regions): it produced
417 segments covering ~87,000 listings — every segment under the 2,000 limit, the largest at
~1,700 — using 491 calls for the full segmentation map. Downloading everything
would add ~2,400 more, so a complete national rent crawl runs on the order of
~2,900 API calls.
8. Property details, deactivations & stats
Full detail of a listing
Search results are summaries. The detail endpoint adds the full description, complete
photo gallery (quality=high → up to 1500 px, the default), energy certificate,
full characteristics and precise location:
async function getDetail(propertyCode) {
const res = await fetch(`${BASE}/v1/property/${propertyCode}?country=es&locale=es`,
{ headers: HEADERS });
if (res.status === 410) {
const body = await res.json();
// Listing was deactivated. body.detail.ad keeps its last known snapshot:
// { adid, operation, propertyType, locationId, suggestedTitle, lastDeactivationDate }
return { status: "deactivated", ad: body.detail.ad };
}
if (!res.ok) return { status: "error", httpStatus: res.status };
return { status: "active", data: await res.json() };
}
Listing performance stats
{
"views": { "value": 67599, "text": "67.599 visitas" },
"favorites": { "value": 1101, "text": "1.101 veces guardado como favorito" },
"contactMails": { "value": 254, "text": "254 contactos por email" },
"sentToFriend": { "value": 20, "text": "20 envíos a amigos" }
}
Real numbers from the listing page — useful for demand analysis and lead scoring.
Cached 30 minutes; send X-No-Cache: true for live values.
9. Caching & freshness
| Endpoint family | Cache TTL |
|---|---|
Search (/v1/search, verticals, promotion units) | 5 minutes |
| Detail & stats | 30 minutes |
| Locations (children, autocomplete, coordinates, short URIs, filters) | 7 days |
/v1/locations/regions | 30 days (static) |
Repeating an identical request within the TTL is served from cache — fast and consistent.
When you genuinely need fresh data (e.g. a live dashboard), send the header
X-No-Cache: true (or standard Cache-Control: no-cache): it skips the
cache read but still refreshes the cached copy for other consumers.
10. Error reference
| Status | Meaning | What to do |
|---|---|---|
400 | Missing/invalid parameter (body says which) | Fix the request. Invalid CSV values return the list of valid ones. |
401 | Bad or missing RapidAPI key | Check x-rapidapi-key and your subscription. |
410 | Listing deactivated (detail endpoints only) | Not an error — the ad is gone; last snapshot in detail.ad. |
502 | Transient upstream error | Retry with backoff (2–3 attempts). |
503 | Upstream temporarily unavailable (circuit breaker) | Back off for a minute, then resume. |
// Retry wrapper used in production
async function apiGetRetry(path, params, attempts = 3) {
let lastErr;
for (let i = 1; i <= attempts; i++) {
try { return await apiGet(path, params); }
catch (err) {
lastErr = err;
if (i < attempts) await new Promise(r => setTimeout(r, 250 * i * i));
}
}
throw lastErr;
}
Machine-readable references: the OpenAPI 3.1 specification at
/openapi.json, an LLM-oriented endpoint reference at
/llms.txt, and the pre-computed national segment maps at
/locations-es-index.json and
/locations-it-index.json. Questions or a use case
not covered here? Reach out through the
RapidAPI listing or at
contacto@apidea.es — we read everything.