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.
Run this endpoint
Endpoint
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.
string
Optional
venta (for sale) or arriendo (for rent). Lowercase.
venta
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.
1
string
Optional
usado, nuevo, or both comma-joined.
example
string
Optional
City token from the source location vocabulary, for example bogota or medellin. Not a URL fragment.
bogota
string
Optional
Zone token from the source location vocabulary.
example
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.
chapinero
string
Optional
Comma-joined bedroom counts. 0 is a literal zero-bedroom filter.
example
string
Optional
Comma-joined bathroom counts.
example
string
Optional
Comma-joined Colombian stratum numbers.
example
string
Optional
Free-text term matched against the listing.
running shoes
string
Optional
Restrict to one publisher or agency id.
1234567890
integer
Optional
Minimum area in square metres.
10
integer
Optional
Maximum area in square metres.
10
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.
10
integer
Optional
Inclusive maximum price in Colombian pesos, on the same field as price_min. Must not be below it.
10
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.
10
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.
10
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.
10
string
Optional
price_asc, price_desc, date_newest, bedrooms, or area. The rich route supports the two price sorts only.
price_asc
integer
Optional
1-based page. A page starting past the 10000 row ceiling returns a non-billable offset_ceiling error.
1
integer
Optional
Results per page, up to 48. Not accepted together with rich=true.
10
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.
true
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
Request Examples
<?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
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();
}
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));
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);
}
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
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()
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}")
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());
}
}
}
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))
}
#!/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"
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}");
}
}
}
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();
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
{
"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.
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.
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.