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

ConceptWhat it is
locationIdsIdealista'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.
operationsale or rent.
propertyTypehomes, newDevelopments, offices, premises (retail), garages, storageRooms, lands, buildings. Land and buildings are sale-only.
propertyCode / adidThe listing ID. Search results call it propertyCode; the detail endpoints call it adid. Same number.
countryes (default), pt, it.
PaginationnumPage (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

GET/v1/locations/regions

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

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, ... }, ...]

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.

⚠ Don't trust 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

EndpointUse when
GET /v1/locations/autocomplete?query=malasañaYou 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.70You have GPS coordinates and want the administrative chain around them (reverse geocoding).
POST /v1/locations/by-shorturisYou 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

GET/v1/rent-homes

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.

⚠ Check 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

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

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/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" });
}

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:

EndpointVerticalOperation
/v1/officesOfficessale | rent (required)
/v1/premisesRetail / commercial premisessale | rent (required)
/v1/garagesGarages / parking spotssale | rent (required)
/v1/storage-roomsStorage roomssale | rent (required)
/v1/landsLand / plotsalways sale
/v1/buildingsEntire buildingsalways 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

FilterTypeExample
priceFrom / priceTonumber (EUR; €/month for rent)priceTo=1500
sizeFrom / sizeTonumber (m²)sizeFrom=80
bedroomsCSV listbedrooms=2,3 (2 or 3 bedrooms)
bathsFrom / bathsToint (upstream honours 1–3)bathsFrom=2
order + sortenumorder=price&sort=asc · orders: publicationDate, price, priceDown, size, distance, weigh
minPublicationDatetag or ISO dateminPublicationDate=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)

FilterValues
floorground, intermediate, top — for penthouses/attics use homeSubtype=atticStudio
energyCertificatehigh (A–B), medium (C–D), low (E–G)
homeSubtypeapartment, loft, atticStudio, casaBaja, cortijo, stoneHouse, villaLabel, independentHouse, terracedHouse, semidetachedHouse
preservationgood, renew (needs renovation — great for finding fixer-uppers)

Operation-specific filters

Only forFilterMeaning
rentpetsAllowed=truePets accepted
longTermRental=trueLong-term residential lease
seasonalRental=trueSeasonal / short-stay lease
saleoccupationType=free,tenanted,bareOwnership,illegallyOccupiedOccupancy status (CSV, pick any subset)
newDevelopmentsfinished / newDevelopmentInProjectConstruction 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",
});
💡 Discover filters programmatically. 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
  }
}
⚠ The ~2,000-result rule. A single query only returns distinct listings for roughly its first ~2,000 results (~50 pages at 40 items); past that point pages start repeating listings you already have. So treat 2,000 as the practical maximum per query: check 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:

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.

💡 Skip phase 1 entirely: we publish the segment map. 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.
⚠ It is a snapshot, not a live feed. Check its 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:

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

GET/v1/property/{propertyCode}

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() };
}
💡 The 410 is a feature. If you maintain a database of listings, calling the detail endpoint for items that vanished from search gives you a reliable "this ad was deactivated" signal, with a timestamp — exactly how our production pipeline confirms delistings instead of guessing.

Listing performance stats

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" }
}

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 familyCache TTL
Search (/v1/search, verticals, promotion units)5 minutes
Detail & stats30 minutes
Locations (children, autocomplete, coordinates, short URIs, filters)7 days
/v1/locations/regions30 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

StatusMeaningWhat to do
400Missing/invalid parameter (body says which)Fix the request. Invalid CSV values return the list of valid ones.
401Bad or missing RapidAPI keyCheck x-rapidapi-key and your subscription.
410Listing deactivated (detail endpoints only)Not an error — the ad is gone; last snapshot in detail.ad.
502Transient upstream errorRetry with backoff (2–3 attempts).
503Upstream 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.