# Metrocuadrado Search API Documentation

Scrappa's `GET /api/metrocuadrado/search` endpoint returns Colombian property listings from metrocuadrado.com as structured JSON, for sale (`business_type=venta`), rent (`business_type=arriendo`) and new developments (`status=nuevo`). One request performs one lookup of the source and returns up to 48 listing cards plus the true result total.

## Filters

`business_type` is `venta` or `arriendo`. `property_type` takes the source's numeric codes — `1` apartment, `2` house, `3` office, `4,15` plot, `5` consulting room, `6` retail, `7` farm, `8` storage, `9` apartment building, `10` office building, `14` studio apartment — comma-joined for several. A plot is the composite code `4,15`: the bare code `4` matches 196352 listings and returns none, so this endpoint rejects it with a 422 rather than returning an empty page. `city`, `zone` and `neighborhood` are bare tokens from the source's own location vocabulary, never URL fragments; `GET /api/metrocuadrado/locations?mode=fanout&q=<city>` lists them. `bedrooms`, `bathrooms` and `stratum` are comma-joined numbers — `bedrooms=0` is a real zero-bedroom filter, not "any". `area_min`/`area_max` and `price_min`/`price_max` are inclusive bounds. `keyword`, `status` (`usado`, `nuevo`, or both comma-joined) and `company_id` narrow further.

Coordinates form a radius filter and must be sent together: `latitude`, `longitude` and `distance`. Under a radius, every returned listing is geo-referenced. Search result cards do not carry coordinates — read them from `GET /api/metrocuadrado/property`.

## Two card shapes

By default you get the compact listing card, which is the better-filtered surface. Add `rich=true` for the source's full web card: dozens of typed fields per listing. The two are alternatives — each request performs one lookup — not a pipeline, so pick one. The rich route returns exactly 50 per page and supports the price sorts only.

Two filters exist only on the default route, and combining either with `rich=true` is rejected with a 422 rather than quietly downgraded: `ids` (the source answers the rich route's `id` parameter with an empty page for every batch size except exactly 50) and the radius (`latitude`, `longitude`, `distance` — the rich route has no radius parameter, so it would return an unfiltered city-wide result that reports itself as radius-filtered). Both stay available on the default route.

`sort` accepts `price_asc`, `price_desc`, `date_newest`, `bedrooms` and `area`; each is translated to the sort field the chosen route actually honours.

## Enumerating past 10,000 listings

The source answers at most the first 10000 rows of a cell, and does so silently: a page past that point looks like a complete answer. When the result total exceeds 10000 the response sets `exceeds_offset_ceiling: true` and returns `total_hits` and `returned` so the gap is visible, so you always know whether a page is the whole cell. A request that starts past the ceiling returns an explicit empty result with `error.code = offset_ceiling` and is not charged.

To read a cell in full, split it into sub-areas and run one billed request per sub-area. `neighborhood` is a filter for targeting an area you already hold a slug for — from your own data, or from a listing link — and not a partition you can build one from.

**Subdivide by radius, not by neighbourhood.** The source's own neighbourhood vocabulary cannot enumerate: it returns at most 100 rows, hard-capped, and those rows are an alphabetical slice of a global list rather than one city's neighbourhoods. Summing Bogotá's zones covers 56.1 % of the city's listings, so a neighbourhood sweep silently misses the rest. Subdivide by radius instead — pass `latitude`, `longitude` and `distance` and page the sub-areas. Inside a radius every returned listing is geo-referenced (measured: 100 % at 2, 3, 5 and 8 km), so a radius grid cannot silently drop rows it can see. Start the grid at **3 km or tighter**: at 5 km the Bogotá apartment cell already returns 10,640 rows, past the ceiling again, so the grid has to recurse rather than stop at one ring. It is still not an exhaustive sweep: listings carrying no coordinates are unreachable by radius, and that is roughly 2 % of sale inventory and 6 % of rent inventory. **No partition of this source is provably exhaustive** — pick the method that covers the most, and reconcile against the reported total rather than trusting any sweep to be complete.

`neighborhood` itself is a working filter and is still exposed — use it to target a specific area, not to build a partition. Totals drift: this platform's measured inventory moved by two rows in a day, so treat any total as a current reading rather than a constant.

## Hydrating known listing ids

`ids` takes up to 50 comma-joined listing ids (format `20622-M6020965`, or `17004-C0001-10` for a project unit) and returns them in one lookup. It is a search mode you ask for explicitly; no other endpoint calls it behind your back. It applies to the default route only — `rich=true` with `ids` is rejected. An id batch is one fixed window upstream: no offset is sent, the batch size is the route's page size, and the source drops every filter sent alongside the ids. So `ids` stands alone — sending any other parameter with it, `page` and `per_page` included, is rejected with a 422 rather than answering with the listing's unfiltered city and status under a query that says otherwise. The list must hold at least one id: `?ids=` and `?ids=,,,` are rejected with a 422 rather than answered as an ordinary unfiltered search, because `implode(',', $ids)` over an empty selection sends exactly that, and a silent fallback there is a billed city-wide result wearing an id batch's name. Search for the listings you want narrowed first, then hydrate the ids that come back.

## What this API does not cover

This endpoint is read-only listing data. There is no account access, no messaging or contact-form submission, no listing creation, and no payments. The source publishes no agency directory, so agencies appear only as an attribution field on a listing and as the `company_id` search filter. The source also ships no facet metadata: filters work, but no facet counts or ranges come back with them, so any facet UI has to be built from paginated results.

- **Documentation:** [https://scrappa.co/docs/metrocuadrado-api/metrocuadrado_search](https://scrappa.co/docs/metrocuadrado-api/metrocuadrado_search)
- **API group:** Metrocuadrado API
- **Endpoint:** `GET https://scrappa.co/api/metrocuadrado/search`

## Authentication

Send your Scrappa API key in the `X-API-KEY` request header. Paid endpoints also support accountless x402 payments when called without an API key.

## Parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `business_type` | string | No | venta (for sale) or arriendo (for rent). Lowercase. |
| `property_type` | string | No | Numeric code or comma-joined codes: 1 apartment, 2 house, 3 office, 4,15 plot, 5 consulting room, 6 retail, 7 farm, 8 storage, 9 apartment building, 10 office building, 14 studio apartment. |
| `status` | string | No | usado, nuevo, or both comma-joined. |
| `city` | string | No | City token from the source location vocabulary, for example bogota or medellin. Not a URL fragment. |
| `zone` | string | No | Zone token from the source location vocabulary. |
| `neighborhood` | string | No | Neighbourhood token from the source location vocabulary, for example chapinero. A working filter for targeting an area; not a partition. To subdivide a large cell use a lat/long radius grid, 3 km or tighter. |
| `bedrooms` | string | No | Comma-joined bedroom counts. 0 is a literal zero-bedroom filter. |
| `bathrooms` | string | No | Comma-joined bathroom counts. |
| `stratum` | string | No | Comma-joined Colombian stratum numbers. |
| `keyword` | string | No | Free-text term matched against the listing. |
| `company_id` | string | No | Restrict to one publisher or agency id. |
| `area_min` | integer | No | Minimum area in square metres. |
| `area_max` | integer | No | Maximum area in square metres. |
| `price_min` | integer | No | Inclusive minimum price in Colombian pesos. Applied to the sale price under business_type=venta and to the rent under business_type=arriendo. |
| `price_max` | integer | No | Inclusive maximum price in Colombian pesos, on the same field as price_min. Must not be below it. |
| `latitude` | number | No | Radius search centre latitude. Must be sent with longitude and distance. Default route only: rich=true with a radius is rejected with a 422. |
| `longitude` | number | No | Radius search centre longitude. Must be sent with latitude and distance. Default route only: rich=true with a radius is rejected with a 422. |
| `distance` | integer | No | Distance in kilometres. Must be sent with latitude and longitude. Default route only: rich=true with a radius is rejected with a 422. |
| `sort` | string | No | price_asc, price_desc, date_newest, bedrooms, or area. The rich route supports the two price sorts only. |
| `page` | integer | No | 1-based page. A page starting past the 10000 row ceiling returns a non-billable offset_ceiling error. |
| `per_page` | integer | No | Results per page, up to 48. Not accepted together with rich=true. |
| `rich` | boolean | No | Return the full source card instead of the compact card. One lookup either way. Not accepted together with ids, latitude, longitude or distance: those are default-route only and combining them is rejected with a 422. |
| `ids` | string | No | Up to 50 comma-joined listing ids to hydrate in one lookup, for example 20622-M6020965. At least one id is required: an empty ids value is rejected with a 422 rather than answered as an unfiltered search. Default route only: the rich route answers it with an empty page, so rich=true with ids is rejected with a 422. An id batch is a single fixed window upstream, so no other parameter applies to it: every filter, page and per_page sent alongside ids is rejected with a 422 rather than dropped. |

## Example request

```bash
#!/bin/bash

curl -X GET \
    -H "x-api-key: YOUR_API_KEY_HERE" \
    "https://scrappa.co/api/metrocuadrado/search?business_type=venta&property_type=1&city=bogota&neighborhood=chapinero&sort=price_asc"
```

## Example response

```json
{
    "success": true,
    "source": "mobile",
    "total_hits": 770,
    "returned": 48,
    "page": 1,
    "page_size": 48,
    "from": 0,
    "exceeds_offset_ceiling": false,
    "subdivide_by": null,
    "results": [
        {
            "id": "20622-M6020965",
            "title": "Apartamento en venta, Chapinero, Bogot\u00e1",
            "url": "https://www.metrocuadrado.com/inmueble/apartamento-venta/chapinero/20622-M6020965",
            "image": "https://multimedia.metrocuadrado.com/20622-M6020965/20622-M6020965_1_p.jpg",
            "city": "Bogota",
            "status": "usado",
            "price_sale": 1650000000,
            "price_lease": null,
            "admin_fee": 1080000,
            "area_m2": 380,
            "bedrooms": 5,
            "bathrooms": 5,
            "garages": 4,
            "admin_included": true,
            "spotlight": false
        }
    ],
    "meta": {
        "endpoint_family": "search",
        "fanout_axes": [
            "business_type",
            "property_type",
            "city",
            "neighborhood"
        ]
    }
}
```

## Errors

| Status | Error | Description |
| --- | --- | --- |
| 422 | Error | Unknown filter, an unknown or bare 4 property type code, a URL fragment instead of a token, or a partial radius filter. Nothing is charged. |
| 503 | Error | The source was unreachable or answered with an error status. Not charged, safe to retry. |

## More Scrappa resources

- [API documentation](https://scrappa.co/docs)
- [Full LLM-readable API reference](https://scrappa.co/llms-full.txt)
- [OpenAPI specification](https://scrappa.co/docs/api.json)
- [Pricing](https://scrappa.co/pricing)
