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

ConceptoQué significa
locationIdsIdentificador 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.
operationsale (venta) o rent (alquiler).
propertyTypehomes, newDevelopments, offices, premises (locales), garages, storageRooms, lands, buildings. Los terrenos y edificios solo admiten venta.
propertyCode / adidIdentificador del anuncio. La búsqueda lo llama propertyCode y los endpoints de detalle, adid. Es el mismo número.
countryes (por defecto), pt o it.
PaginaciónnumPage (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

GET/v1/locations/regions

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

GET/v1/locations/{locationId}/children
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.

⚠ No uses 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

EndpointCuándo usarlo
GET /v1/locations/autocomplete?query=malasañaDispones 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.70Dispones de coordenadas GPS y necesitas la jerarquía administrativa de ese punto (geocodificación inversa).
POST /v1/locations/by-shorturisTienes 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

GET/v1/rent-homes

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.

⚠ Comprueba primero 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

GET/v1/search?operation=sale&propertyType=homes

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/new-homes
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:

EndpointTipo de inmuebleOperación
/v1/officesOficinassale | rent (obligatorio)
/v1/premisesLocales comercialessale | rent (obligatorio)
/v1/garagesGarajes y plazas de aparcamientosale | rent (obligatorio)
/v1/storage-roomsTrasterossale | rent (obligatorio)
/v1/landsTerrenos y parcelasSiempre sale
/v1/buildingsEdificios completosSiempre 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

FiltroTipoEjemplo
priceFrom / priceToNúmero (EUR; €/mes en alquiler)priceTo=1500
sizeFrom / sizeToNúmero (m²)sizeFrom=80
bedroomsLista CSVbedrooms=2,3 (2 o 3 dormitorios)
bathsFrom / bathsToEntero (la API de origen aplica valores de 1 a 3)bathsFrom=2
order + sortValores enumeradosorder=price&sort=asc · criterios: publicationDate, price, priceDown, size, distance, weigh
minPublicationDateEtiqueta o fecha ISOminPublicationDate=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)

FiltroValores
floorground, intermediate, top. Para buhardillas, usa homeSubtype=atticStudio.
energyCertificatehigh (A–B), medium (C–D), low (E–G)
homeSubtypeapartment, loft, atticStudio, casaBaja, cortijo, stoneHouse, villaLabel, independentHouse, terracedHouse, semidetachedHouse
preservationgood, renew (necesita reforma)

Filtros específicos de cada operación

Solo paraFiltroSignificado
rentpetsAllowed=trueAdmite mascotas
longTermRental=trueAlquiler residencial de larga duración
seasonalRental=trueAlquiler de temporada o corta estancia
saleoccupationType=free,tenanted,bareOwnership,illegallyOccupiedEstado de ocupación (CSV; selecciona los valores que necesites)
newDevelopmentsfinished / newDevelopmentInProjectEstado 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",
});
💡 Descubre los filtros desde tu código. 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
  }
}
⚠ El límite aproximado de 2.000 resultados. Una consulta solo devuelve anuncios distintos entre sus primeros 2.000 resultados aproximadamente (unas 50 páginas de 40). A partir de ahí las páginas empiezan a repetir anuncios. Considera 2.000 como el máximo práctico por consulta: comprueba 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:

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.

💡 Puedes usar nuestros mapas de segmentos para omitir la fase 1. 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.
⚠ El mapa es una instantánea. Comprueba su campo 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:

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

GET/v1/property/{propertyCode}

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() };
}
💡 El estado 410 permite confirmar una baja. Si mantienes una base de datos, consulta el detalle de los anuncios que desaparezcan de las búsquedas. La respuesta indica que el anuncio se ha desactivado y aporta la fecha, como hace nuestro proceso de producción para confirmar las retiradas.

Estadísticas de rendimiento del anuncio

GET/v1/property/{propertyCode}/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" }
}

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 endpointsDuración de la caché
Búsquedas (/v1/search, tipos de inmueble y unidades de promociones)5 minutos
Detalle y estadísticas30 minutos
Ubicaciones (hijas, autocompletado, coordenadas, códigos cortos) y filtros7 días
/v1/locations/regions30 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

EstadoSignificadoQué hacer
400Parámetro ausente o inválido; el cuerpo indica cuálCorrige la petición. Los valores CSV inválidos devuelven la lista de valores admitidos.
400 unknown_query_parameterEl 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.
401Clave de RapidAPI incorrecta o ausenteRevisa x-rapidapi-key y tu suscripción.
410Anuncio desactivado (solo endpoints de detalle)Registra la baja; la última instantánea está en detail.ad.
502Error transitorio de la API de origenReintenta con espera creciente, un máximo de dos o tres intentos.
503Servicio de origen temporalmente no disponible; circuito de protección abiertoEspera 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.