Guía de integración de la API de Idealista — apidea
apidea es una API no oficial de terceros, sin afiliación, respaldo ni patrocinio de Idealista S.A.U.
Recetas prácticas con código listo para copiar: listar las ubicaciones de España, consultar los alquileres de una ciudad, descargar promociones de obra nueva y sus viviendas, filtrar resultados y extraer datos a escala nacional.
1. Primeros pasos
Si usas Python, empieza por el tutorial completo de búsqueda y exportación a CSV en
español o
inglés. Los tutoriales, esta guía y la
especificación OpenAPI son públicos y se pueden leer sin clave.
El sitemap recoge las páginas HTML públicas;
/robots.txt anuncia su ubicación. Ambos archivos son públicos y se guardan en
caché durante una hora.
Todas las peticiones pasan por RapidAPI. Necesitas dos cabeceras: la clave de tu suscripción y el 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'
Esta llamada devuelve las primeras cinco viviendas en venta de la provincia de Madrid.
En los ejemplos de esta guía usaremos esta función auxiliar de JavaScript con
fetch. El ejemplo lee la clave de process.env.RAPIDAPI_KEY, como
en Node.js o Bun:
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. Conceptos básicos
| Concepto | Qué significa |
|---|---|
locationIds | Identificador jerárquico de ubicación de Idealista.
Por ejemplo, 0-EU-ES-28 (provincia de Madrid) o
0-EU-ES-28-07-001-079 (Madrid ciudad). Se envía entre corchetes:
locationIds=[0-EU-ES-28] (codificado en la URL:
%5B0-EU-ES-28%5D). La sección 3 explica cómo obtenerlo. |
operation | sale (venta) o rent (alquiler). |
propertyType | homes, newDevelopments,
offices, premises (locales), garages,
storageRooms, lands, buildings.
Los terrenos y edificios solo admiten venta. |
propertyCode / adid | Identificador del anuncio.
La búsqueda lo llama propertyCode y los endpoints de detalle,
adid. Es el mismo número. |
country | es (por defecto), pt o it. |
| Paginación | numPage (empieza en 1) y maxItems
(máximo 40 por página). Las respuestas incluyen total,
totalPages y actualPage. |
Todos los endpoints de búsqueda devuelven esta estructura:
{
"total": 28774, // total matching listings
"totalPages": 720, // at the current maxItems
"actualPage": 1,
"itemsPerPage": 40,
"elementList": [ { "propertyCode": "110715434", "price": 1400000, ... } ]
}
3. Trabajar con ubicaciones
3.1 Todas las provincias de España
Un mapa estático de las 17 comunidades autónomas, Ceuta y Melilla, con sus provincias
y los locationId listos para usar. Cuesta una llamada y sirve como punto de
partida para una extracción de toda España:
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 De provincia a municipio, distrito y barrio
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, ... }, ...]
Las llamadas recursivas permiten recorrer el árbol de ubicaciones. Cada ubicación hija
incluye divisible: si es false, no tiene subdivisiones útiles y
puedes dejar de descender por esa rama.
total de la respuesta de ubicaciones hijas para contar anuncios.
Ese campo no es fiable y a menudo vale 0. Para obtener el número de anuncios
de una ubicación, consulta el endpoint de búsqueda correspondiente con
maxItems=1; consulta la sección 6.3.3 Buscar una ubicación por nombre, coordenadas o código corto
| Endpoint | Cuándo usarlo |
|---|---|
GET /v1/locations/autocomplete?query=malasaña | Dispones de texto
libre, como el de un buscador. Devuelve ubicaciones con locationId y puntos de
interés, como calles o estaciones de metro, que no lo tienen. Conserva los resultados
que sí incluyan el identificador. |
GET /v1/locations/by-coordinates?lat=40.42&lng=-3.70 | Dispones de coordenadas GPS y necesitas la jerarquía administrativa de ese punto (geocodificación inversa). |
POST /v1/locations/by-shorturis | Tienes un código corto de
Idealista, como [bd4] de una URL compartida, y necesitas su
locationId. |
const { locations } = await apiGet("/v1/locations/autocomplete", {
query: "chamberi", country: "es",
});
const usable = locations.filter(l => l.locationId); // drop POIs
4. Recetas de búsqueda
4.1 Todas las viviendas de alquiler en Madrid
Primero elige qué Madrid necesitas: la provincia (0-EU-ES-28, que incluye
los municipios de alrededor) o la ciudad (0-EU-ES-28-07-001-079).
Después recorre sus páginas:
# 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'
Para descargar los resultados, recorre las páginas hasta que numPage
alcance totalPages. Encontrarás la función reutilizable
paginate en la sección 6. Antes de ejecutar
el recorrido completo, revisa el límite explicado a continuación.
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 es un atajo de
/v1/search?operation=rent&propertyType=homes: ambos funcionan igual.
total.
Madrid ciudad suele superar los 2.000 alquileres activos. Una sola consulta solo devuelve
anuncios distintos entre sus primeros 2.000 resultados aproximadamente; después, las
páginas se repiten. Si la primera respuesta indica total > 2000, divide
la consulta entre las ubicaciones hijas (distritos y, después, barrios) y pagina cada una.
La sección 7 muestra el procedimiento.4.2 Viviendas de segunda mano en venta
propertyType=homes con operation=sale devuelve las viviendas
en venta, tanto de segunda mano como unidades de promociones de obra nueva. La API de
origen no tiene un interruptor para excluir la obra nueva en el servidor. Puedes filtrarla
en tu código con el campo newDevelopment de cada anuncio:
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 Promociones de obra nueva y sus viviendas
La obra nueva tiene dos niveles: la promoción (el anuncio comercial del conjunto) y sus unidades (las viviendas individuales que se venden).
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" });
}
En /v1/new-homes puedes usar finished=true para promociones
terminadas o newDevelopmentInProject=true para proyectos sobre plano.
El campo total de la respuesta de unidades indica cuántas viviendas tiene
la promoción actualmente en el mercado.
4.4 Oficinas, locales, garajes, trasteros, terrenos y edificios
Cada tipo de inmueble tiene un endpoint específico con la misma estructura de respuesta y paginación:
| Endpoint | Tipo de inmueble | Operación |
|---|---|---|
/v1/offices | Oficinas | sale | rent (obligatorio) |
/v1/premises | Locales comerciales | sale | rent (obligatorio) |
/v1/garages | Garajes y plazas de aparcamiento | sale | rent (obligatorio) |
/v1/storage-rooms | Trasteros | sale | rent (obligatorio) |
/v1/lands | Terrenos y parcelas | Siempre sale |
/v1/buildings | Edificios completos | Siempre 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. Filtrar resultados
Los endpoints de búsqueda aceptan filtros como parámetros de consulta. La API de origen
ignora los filtros reconocidos que no se aplican al contexto actual. Sin embargo, un
nombre de parámetro que el endpoint no declara devuelve
400 unknown_query_parameter: la respuesta enumera los nombres
accepted y sugiere el más parecido. Así, un error como
locationId se detecta inmediatamente.
Los endpoints específicos de cada tipo de inmueble aceptan menos parámetros que
/v1/search. Si compartes un generador de consultas entre varios tipos,
elimina los parámetros que no figuren en la lista accepted de cada endpoint,
o utiliza /v1/search.
Filtros habituales
| Filtro | Tipo | Ejemplo |
|---|---|---|
priceFrom / priceTo | Número (EUR; €/mes en alquiler) | priceTo=1500 |
sizeFrom / sizeTo | Número (m²) | sizeFrom=80 |
bedrooms | Lista CSV | bedrooms=2,3 (2 o 3 dormitorios) |
bathsFrom / bathsTo | Entero (la API de origen aplica valores de 1 a 3) | bathsFrom=2 |
order + sort | Valores enumerados | order=price&sort=asc · criterios: publicationDate, price, priceDown, size, distance, weigh |
minPublicationDate | Etiqueta o fecha ISO | minPublicationDate=W (última semana; también Y=48 h, M=mes, D=día) |
Características y equipamiento (booleanos)
withElevator, withTerrace, withParking,
withSwimmingPool, withGarden, withStorageRoom,
withAirConditioning, builtinWardrobes, exterior,
accessible, luxury, virtualTour, hasPlan,
bankOffer.
Filtros con varios valores (CSV)
| Filtro | Valores |
|---|---|
floor | ground, intermediate, top.
Para buhardillas, usa 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 (necesita reforma) |
Filtros específicos de cada operación
| Solo para | Filtro | Significado |
|---|---|---|
rent | petsAllowed=true | Admite mascotas |
longTermRental=true | Alquiler residencial de larga duración | |
seasonalRental=true | Alquiler de temporada o corta estancia | |
sale | occupationType=free,tenanted,bareOwnership,illegallyOccupied | Estado de ocupación (CSV; selecciona los valores que necesites) |
newDevelopments | finished / newDevelopmentInProject | Estado de construcción |
Combinar los filtros
Alquileres de dos o tres dormitorios en Madrid ciudad, hasta 1.800 €/mes, con ascensor, que admitan mascotas, de larga duración y publicados en la última semana, empezando por los más baratos:
# 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'
Viviendas en venta para reformar en las últimas plantas de la provincia de Barcelona, con al menos 60 m² y menos de 250.000 €:
// 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]
devuelve los bloques de filtros aplicables a ese contexto, con sus tipos de datos y
etiquetas. Es útil para crear una interfaz de búsqueda que se adapte al tipo de inmueble.6. Paginación y recuentos
Las páginas tienen un máximo de 40 anuncios. Esta función recorre los resultados de los ejemplos anteriores:
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 en la primera página y, si lo supera, repite la consulta
sobre las ubicaciones hijas (/v1/locations/{id}/children) hasta que cada
subconsulta quede por debajo del límite. La sección 7 automatiza
este reparto.Si solo necesitas saber cuántos anuncios coinciden, por ejemplo para un panel
o para decidir si divides la extracción, pide un solo resultado y lee
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. Extraer datos de zonas grandes
Esta sección convierte el límite de unos 2.000 resultados
en una estrategia completa de extracción. Pasado ese punto, las páginas de una misma
consulta repiten anuncios. Los valores de total y totalPages
siguen siendo correctos, pero no puedes descargar los 30.000 anuncios de una consulta
con un único bucle, por muchas páginas que solicites.
La estrategia que usamos en nuestra extracción nacional consiste en consultar las ubicaciones hijas cuando una búsqueda supera unos 2.000 resultados, y repetirlo recursivamente hasta que cada segmento quede por debajo del límite. Usamos un umbral de 1.800 para dejar margen. Después recorremos las páginas de cada segmento:
/**
* 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);
}
}
Consejos prácticos de la extracción diaria a escala nacional:
- Empieza con
/v1/locations/regionsy divide cada combinación de provincia y tipo de búsqueda (venta, alquiler u obra nueva). Guarda la lista de segmentos en caché: cambia lentamente. Antes de cada extracción, revisa eltotalde cada segmento y vuelve a dividir solo los que hayan superado el umbral. - Elimina duplicados por
propertyCode. Un anuncio cercano a los límites puede aparecer en dos segmentos; las consultas de varias ubicaciones también se solapan. - Añade reintentos con espera creciente (dos o tres intentos) y una
pequeña pausa entre páginas. Los fallos ocasionales
502/503pueden ser transitorios; consulta la sección 10. - Comprueba los anuncios que desaparecen: pueden haberse vendido,
alquilado o retirado. Confírmalo con el endpoint de detalle: un
410confirma la baja del anuncio, como explica la siguiente sección.
7.1 Script completo de extracción
El siguiente script reúne los pasos anteriores. Guárdalo como crawl.mjs
y ejecútalo con Node.js 18 o posterior, o Bun, sin dependencias adicionales.
La fase 1 muestra los segmentos que encuentra; la fase 2 los descarga, elimina
duplicados y escribe un anuncio JSON por línea. Si una ubicación no se puede subdividir
y aún supera el límite, ese segmento puede quedar incompleto.
# 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}`);
Para cubrir España, recorre las 52 provincias de /v1/locations/regions
(sección 3.1). Si solo necesitas el mapa de segmentos, termina
al finalizar la fase 1, antes de descargar los anuncios.
GET /locations-es-index.json y
GET /locations-it-index.json
son públicos, gratuitos y no requieren autenticación. Contienen una instantánea
precalculada de las ubicaciones de España e Italia con hasta unos 1.800 anuncios
para rent y sale, con su locationId, nombre y
recuento. Usarlos como punto de partida ahorra las llamadas de segmentación.generatedAt. Los recuentos cambian y un segmento puede
superar los 2.000 anuncios con el tiempo; en ese caso, su paginación deja de ser completa.
La estructura suele mantenerse durante unas semanas. Si el mapa es más antiguo o algún
segmento se acerca a 2.000 anuncios (compruébalo con una petición maxItems=1),
vuelve a ejecutar la fase 1 y usa un mapa actualizado.¿Cuántas llamadas a la API consume?
El script cuenta las peticiones y muestra el consumo antes de la descarga principal. Tras la fase 1, indica las llamadas usadas y una estimación para la fase 2, y espera tres segundos para que puedas cancelarlo. Para estimar el consumo:
- Fase 1 (segmentación): una llamada por cada ubicación visitada
(recuento con
maxItems=1) y otra por cada ubicación que deba dividirse (consultachildren). Una ubicación por debajo del umbral cuesta una llamada. - Fase 2 (descarga):
ceil(total / 40)llamadas por segmento, es decir, una por cada página de hasta 40 anuncios.
En una medición del mercado de alquiler de Soria, 97 anuncios sin necesidad de dividir,
se consumieron cuatro llamadas: una de recuento y tres de descarga.
La misma extracción forzada a bajar hasta barrios consumió 84 llamadas para los mismos
97 anuncios. Por eso conviene dividir solo cuando total supera unos
1.800 resultados. Como estimación, una extracción completa consume unas
total_listings / 40 llamadas más el coste de segmentación, normalmente
un 5–15 % adicional en zonas densas.
En una ejecución sobre todo el mercado español de alquiler, partiendo
de las 52 provincias de /v1/locations/regions, obtuvimos 417 segmentos con
unos 87.000 anuncios. Todos quedaron por debajo de 2.000 y el mayor rondó los 1.700.
La segmentación consumió 491 llamadas; descargar los anuncios añadiría
unas 2.400, para un total de aproximadamente 2.900 llamadas.
Son mediciones de referencia, no recuentos actuales del mercado.
8. Detalle, bajas y estadísticas de anuncios
Detalle completo de un anuncio
Los resultados de búsqueda son resúmenes. El endpoint de detalle añade la descripción
completa, la galería de fotos (quality=high, valor por defecto, hasta
1500 px), el certificado energético, las características y la ubicación precisa:
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() };
}
Estadísticas de rendimiento del anuncio
{
"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" }
}
Son las cifras de la ficha del anuncio, útiles para analizar la demanda y el interés
que genera. Se guardan en caché durante 30 minutos; envía X-No-Cache: true
para pedir una actualización.
9. Caché y actualización
| Familia de endpoints | Duración de la caché |
|---|---|
Búsquedas (/v1/search, tipos de inmueble y unidades de promociones) | 5 minutos |
| Detalle y estadísticas | 30 minutos |
| Ubicaciones (hijas, autocompletado, coordenadas, códigos cortos) y filtros | 7 días |
/v1/locations/regions | 30 días (estático) |
Una petición idéntica dentro del periodo de caché devuelve la copia guardada.
Si necesitas datos actualizados, envía X-No-Cache: true o la cabecera
estándar Cache-Control: no-cache. La petición omite la lectura de la caché
y actualiza la copia para las siguientes consultas.
10. Referencia de errores
| Estado | Significado | Qué hacer |
|---|---|---|
400 | Parámetro ausente o inválido; el cuerpo indica cuál | Corrige la petición. Los valores CSV inválidos devuelven la lista de valores admitidos. |
400 unknown_query_parameter | El endpoint no declara ese parámetro de consulta (protección frente a erratas en /v1/*) | Corrige el nombre con la lista accepted y la sugerencia de message antes de volver a intentarlo. |
401 | Clave de RapidAPI incorrecta o ausente | Revisa x-rapidapi-key y tu suscripción. |
410 | Anuncio desactivado (solo endpoints de detalle) | Registra la baja; la última instantánea está en detail.ad. |
502 | Error transitorio de la API de origen | Reintenta con espera creciente, un máximo de dos o tres intentos. |
503 | Servicio de origen temporalmente no disponible; circuito de protección abierto | Espera un minuto antes de continuar. |
// 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;
}
Referencias para herramientas: la especificación OpenAPI 3.1 en
/openapi.json, el contrato para asistentes de
programación en /llms.txt y los mapas de segmentos
nacionales en /locations-es-index.json y
/locations-it-index.json.
Si tienes dudas o necesitas un caso de uso que no aparece aquí, contacta desde la
ficha de RapidAPI
o escribe a contacto@apidea.es.