Skip to content
Scrappa Get API key
Metrocuadrado API 1 credit/request

Metrocuadrado Search API Documentation

GET https://scrappa.co/api/metrocuadrado/search?business_type=venta&property_type=1&city=bogota&neighborhood=chapinero&sort=price_asc

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.

Metrocuadrado Search API Documentation 1 credit/request

Endpoint

Request preview
GET
https://scrappa.co/api/metrocuadrado/search?business_type=venta&property_type=1&city=bogota&neighborhood=chapinero&sort=price_asc
Auth header
x-api-key
Cost
1 credit/request
Response preview
200 OK
{
    "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",
...

Parameters

Start with the required fields, then add optional filters only when your use case needs them.

Runnable path

This endpoint has no required query parameters.

23 optional filters available.

business_type string Optional

venta (for sale) or arriendo (for rent). Lowercase.

Example value venta
property_type string Optional

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.

Example value 1
status string Optional

usado, nuevo, or both comma-joined.

Example value example
city string Optional

City token from the source location vocabulary, for example bogota or medellin. Not a URL fragment.

Example value bogota
zone string Optional

Zone token from the source location vocabulary.

Example value example
neighborhood string Optional

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.

Example value chapinero
bedrooms string Optional

Comma-joined bedroom counts. 0 is a literal zero-bedroom filter.

Example value example
bathrooms string Optional

Comma-joined bathroom counts.

Example value example
stratum string Optional

Comma-joined Colombian stratum numbers.

Example value example
keyword string Optional

Free-text term matched against the listing.

Example value running shoes
company_id string Optional

Restrict to one publisher or agency id.

Example value 1234567890
area_min integer Optional

Minimum area in square metres.

Example value 10
area_max integer Optional

Maximum area in square metres.

Example value 10
price_min integer Optional

Inclusive minimum price in Colombian pesos. Applied to the sale price under business_type=venta and to the rent under business_type=arriendo.

Example value 10
price_max integer Optional

Inclusive maximum price in Colombian pesos, on the same field as price_min. Must not be below it.

Example value 10
latitude number Optional

Radius search centre latitude. Must be sent with longitude and distance. Default route only: rich=true with a radius is rejected with a 422.

Example value 10
longitude number Optional

Radius search centre longitude. Must be sent with latitude and distance. Default route only: rich=true with a radius is rejected with a 422.

Example value 10
distance integer Optional

Distance in kilometres. Must be sent with latitude and longitude. Default route only: rich=true with a radius is rejected with a 422.

Example value 10
sort string Optional

price_asc, price_desc, date_newest, bedrooms, or area. The rich route supports the two price sorts only.

Example value price_asc
page integer Optional

1-based page. A page starting past the 10000 row ceiling returns a non-billable offset_ceiling error.

Example value 1
per_page integer Optional

Results per page, up to 48. Not accepted together with rich=true.

Example value 10
rich boolean Optional

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.

Example value true
ids string Optional

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 value example

Request Examples

PHP
<?php

$curl = curl_init();

curl_setopt_array($curl, [
    CURLOPT_URL => "https://scrappa.co/api/metrocuadrado/search?business_type=venta&property_type=1&city=bogota&neighborhood=chapinero&sort=price_asc",
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_ENCODING => "",
    CURLOPT_MAXREDIRS => 10,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
    CURLOPT_CUSTOMREQUEST => "GET",
    CURLOPT_HTTPHEADER => [
        "x-api-key: YOUR_API_KEY_HERE"
    ],
]);

$response = curl_exec($curl);
$err = curl_error($curl);

curl_close($curl);

if ($err) {
    echo "cURL Error #:" . $err;
} else {
    echo $response;
}
PHP
<?php

use Illuminate\Support\Facades\Http;

$response = Http::timeout(30)
    ->withHeaders(['x-api-key' => 'YOUR_API_KEY_HERE'])
    ->get('https://scrappa.co/api/metrocuadrado/search?business_type=venta&property_type=1&city=bogota&neighborhood=chapinero&sort=price_asc');

if ($response->successful()) {
    echo $response->body();
} else {
    echo "Error: " . $response->status();
}
JavaScript
const options = {
    method: 'GET',
    headers: {
        'x-api-key': 'YOUR_API_KEY_HERE'
    }
};

fetch('https://scrappa.co/api/metrocuadrado/search?business_type=venta&property_type=1&city=bogota&neighborhood=chapinero&sort=price_asc', options)
    .then(response => {
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        return response.text();
    })
    .then(data => console.log(data))
    .catch(error => console.error('Error:', error));
JavaScript
const axios = require('axios');

const options = {
    method: 'GET',
    url: 'https://scrappa.co/api/metrocuadrado/search?business_type=venta&property_type=1&city=bogota&neighborhood=chapinero&sort=price_asc',
    headers: {
        x-api-key: 'YOUR_API_KEY_HERE',
    }
};

try {
    const response = await axios(options);
    console.log(response.data);
} catch (error) {
    console.error('Error:', error.message);
}
Ruby
require 'net/http'
require 'uri'

uri = URI.parse("https://scrappa.co/api/metrocuadrado/search?business_type=venta&property_type=1&city=bogota&neighborhood=chapinero&sort=price_asc")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == 'https'

request = Net::HTTP::Get.new(uri.request_uri)
request['x-api-key'] = 'YOUR_API_KEY_HERE'

begin
    response = http.request(request)
    puts response.body
rescue => e
    puts "Error: #{e.message}"
end
Python
import http.client
import json

conn = http.client.HTTPSConnection("scrappa.co")

headers = {
    'x-api-key': 'YOUR_API_KEY_HERE',
}

try:
    conn.request("GET", "/api/metrocuadrado/search?business_type=venta&property_type=1&city=bogota&neighborhood=chapinero&sort=price_asc", headers=headers)
    res = conn.getresponse()
    data = res.read()
    print(data.decode("utf-8"))
except Exception as e:
    print(f"Error: {e}")
finally:
    conn.close()
Python
import requests

headers = {
    'x-api-key': 'YOUR_API_KEY_HERE',
}

try:
    response = requests.get('https://scrappa.co/api/metrocuadrado/search?business_type=venta&property_type=1&city=bogota&neighborhood=chapinero&sort=price_asc', headers=headers)
    response.raise_for_status()
    print(response.text)
except requests.exceptions.RequestException as e:
    print(f"Error: {e}")
Java
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;

public class ApiExample {
    public static void main(String[] args) {
        OkHttpClient client = new OkHttpClient();

        Request request = new Request.Builder()
            .url("https://scrappa.co/api/metrocuadrado/search?business_type=venta&property_type=1&city=bogota&neighborhood=chapinero&sort=price_asc")
        .addHeader("x-api-key", "YOUR_API_KEY_HERE")
            .build();

        try (Response response = client.newCall(request).execute()) {
            if (response.isSuccessful()) {
                System.out.println(response.body().string());
            } else {
                System.out.println("Error: " + response.code());
            }
        } catch (IOException e) {
            System.out.println("Error: " + e.getMessage());
        }
    }
}
Go
package main

import (
    "fmt"
    "net/http"
    "io/ioutil"
)

func main() {
    client := &http.Client{}
    req, err := http.NewRequest("GET", "https://scrappa.co/api/metrocuadrado/search?business_type=venta&property_type=1&city=bogota&neighborhood=chapinero&sort=price_asc", nil)
    if err != nil {
        fmt.Println("Error creating request:", err)
        return
    }
    req.Header.Set("x-api-key", "YOUR_API_KEY_HERE")

    resp, err := client.Do(req)
    if err != nil {
        fmt.Println("Error making request:", err)
        return
    }
    defer resp.Body.Close()

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println("Error reading response:", err)
        return
    }

    fmt.Println(string(body))
}
Terminal
#!/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"
C#
using System;
using System.Net.Http;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        using var client = new HttpClient();
        client.DefaultRequestHeaders.Add("x-api-key", "YOUR_API_KEY_HERE");

        try
        {
            var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "https://scrappa.co/api/metrocuadrado/search?business_type=venta&property_type=1&city=bogota&neighborhood=chapinero&sort=price_asc"));
            var content = await response.Content.ReadAsStringAsync();
            Console.WriteLine(content);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error: {ex.Message}");
        }
    }
}
TypeScript
import axios from 'axios';

async function run(): Promise<void> {
    try {
        const response = await axios({
            method: 'GET',
            url: 'https://scrappa.co/api/metrocuadrado/search?business_type=venta&property_type=1&city=bogota&neighborhood=chapinero&sort=price_asc',
            headers: {
        'x-api-key': 'YOUR_API_KEY_HERE',
            },
        });

        console.log(response.data);
    } catch (error) {
        console.error('Error:', error);
    }
}

void run();
RUST
use reqwest::Client;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::new();

    let response = client
        .get("https://scrappa.co/api/metrocuadrado/search?business_type=venta&property_type=1&city=bogota&neighborhood=chapinero&sort=price_asc")
        .header("x-api-key", "YOUR_API_KEY_HERE")
        .send()
        .await?;

    println!("{}", response.text().await?);

    Ok(())
}

Response Schema

Example response fields are illustrative; inspect the JSON before integrating.

Example response fields

Scan these fields before integrating.

success source total_hits returned page page_size from exceeds_offset_ceiling +3 more

Common results fields

id title url image
JSON Response
200 OK
{
    "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

Handle these documented responses before retrying or showing customer-facing failures.

422

HTTP 422

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

HTTP 503

The source was unreachable or answered with an error status. Not charged, safe to retry.

Generate Code with AI

Copy a ready-made prompt with all the endpoint details, parameters, and example responses. Paste it into ChatGPT, Claude, or any AI assistant to instantly generate working code.

Try It Live

Test this endpoint in our interactive playground with real data.