Overview
| Base URL | https://multilistado.mx/api/v1 |
| Format | JSON (UTF-8). Send Content-Type: application/json on POST/PUT |
| Authentication | Authorization: Bearer pbm_<prefix>_<secret> |
| Scopes | read (all queries) · write (create, edit, publish, create leads) |
| Rate limit | 600 requests per 10 minutes per key |
| OpenAPI 3.0 description | /api/v1/openapi.json · interactive reference |
Authentication
- Go to Portal → More → API (requires a verified license).
- Under Create key type a Label (e.g. "My CRM"), choose Permissions: "Read and write" or "Read only", and click Generate.
- Copy the key: it is shown only once. It looks like
pbm_+ 8 characters +_+ 48 characters. You can revoke it at any time.
Send it in the Authorization header. For security, keys in the URL are refused (?api_key= returns 401) and the API does not enable CORS: call it from your server, never from your visitors' browsers (the key would be exposed). To show listings on a website without coding, use the widget.
Free for licensed agents and agencies. Create your account, verify your license and generate your API key or widget in minutes.
RESO partner keys (partner scope) only work on the RESO Web API; on /api/v1 they get 403.
Minimal client
Store your key in an environment variable (MULTILISTADO_API_KEY) and use this small client in the examples below. Each example continues the previous one.
export MULTILISTADO_API_KEY="pbm_..." # your key
API=https://multilistado.mx/api/v1
curl -s "$API/me" -H "Authorization: Bearer $MULTILISTADO_API_KEY"// Node.js 18+ (.mjs file). Never in the browser: it would expose your key.
const API = 'https://multilistado.mx/api/v1';
async function api(method, path, body) {
const res = await fetch(API + path, {
method,
headers: { Authorization: `Bearer ${process.env.MULTILISTADO_API_KEY}`, ...(body ? { 'Content-Type': 'application/json' } : {}) },
body: body ? JSON.stringify(body) : undefined
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(`${res.status} ${data.message || data.error || ''} ${(data.errors || []).join(' ')}`);
return data;
}
const me = await api('GET', '/me');
console.log(me.name, me.scopes);<?php
// PHP 7.4+ with the curl extension.
function mlx(string $method, string $path, ?array $body = null): array {
$ch = curl_init('https://multilistado.mx/api/v1' . $path);
$headers = ['Authorization: Bearer ' . getenv('MULTILISTADO_API_KEY'), 'Accept: application/json'];
if ($body !== null) {
$headers[] = 'Content-Type: application/json';
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $headers, CURLOPT_TIMEOUT => 30]);
$raw = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$data = json_decode((string) $raw, true) ?: [];
if ($code >= 400 || $raw === false) {
throw new RuntimeException($code . ' ' . ($data['message'] ?? $data['error'] ?? curl_error($ch)) . ' ' . implode(' ', $data['errors'] ?? []));
}
return $data;
}
$me = mlx('GET', '/me');
echo $me['name'], PHP_EOL;# Python 3.8+ with requests (pip install requests)
import os, requests
API = "https://multilistado.mx/api/v1"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['MULTILISTADO_API_KEY']}"
def api(method, path, **kwargs):
r = session.request(method, API + path, timeout=30, **kwargs)
data = r.json() if r.content else {}
if not r.ok:
raise RuntimeError(f"{r.status_code} {data.get('message') or data.get('error')} {' '.join(data.get('errors', []))}")
return data
me = api("GET", "/me")
print(me["name"], me["scopes"])1. Search the MLS
GET /properties returns published listings from the whole Multilistado (plus your own), one page at a time.
curl -s -G "$API/properties" -H "Authorization: Bearer $MULTILISTADO_API_KEY" \
--data-urlencode "city=Tijuana" --data-urlencode "operation=sale" --data-urlencode "type=house" \
--data-urlencode "min_price=2000000" --data-urlencode "sort=price_asc" --data-urlencode "limit=10"const qs = new URLSearchParams({ city: 'Tijuana', operation: 'sale', type: 'house', min_price: '2000000', sort: 'price_asc', limit: '10' });
const { pagination, content } = await api('GET', `/properties?${qs}`);
console.log(`${pagination.total} results, page ${pagination.page} of ${pagination.pages}`);
for (const l of content) console.log(l.title, '·', l.operations[0]?.formatted_amount, '·', l.agent.name);$qs = http_build_query(['city' => 'Tijuana', 'operation' => 'sale', 'type' => 'house', 'min_price' => 2000000, 'sort' => 'price_asc', 'limit' => 10]);
$page = mlx('GET', "/properties?$qs");
echo $page['pagination']['total'], " results", PHP_EOL;
foreach ($page['content'] as $l) {
echo $l['title'], ' · ', $l['operations'][0]['formatted_amount'] ?? '', ' · ', $l['agent']['name'], PHP_EOL;
}page = api("GET", "/properties", params={"city": "Tijuana", "operation": "sale", "type": "house",
"min_price": 2000000, "sort": "price_asc", "limit": 10})
print(page["pagination"]["total"], "results")
for l in page["content"]:
print(l["title"], "·", l["operations"][0]["formatted_amount"] if l["operations"] else "", "·", l["agent"]["name"])Response (trimmed, illustrative):
{
"pagination": { "page": 1, "pages": 3, "total": 27, "limit": 10 },
"content": [{
"id": "07e25392-…", "slug": "casa-en-venta-playas-de-tijuana", "url": "https://multilistado.mx/p/casa-en-venta-playas-de-tijuana",
"title": "Casa en venta en Playas de Tijuana", "property_type": "house", "status": "published",
"operations": [{ "type": "sale", "amount": 4350000, "currency": "MXN", "formatted_amount": "$4,350,000 MXN" }],
"bedrooms": 3, "bathrooms": 2.5, "parking": 2, "construction_m2": 180, "lot_m2": 200,
"location": { "address": null, "neighborhood": "Playas de Tijuana", "city": "Tijuana", "state": "Baja California", "lat": 32.519, "lng": -117.119, "exact": false },
"images": [{ "url": "https://…/fachada.jpg", "thumb": "https://…/fachada.jpg" }],
"agent": { "name": "…", "slug": "…", "phone": "…", "url": "https://multilistado.mx/agente/…" },
"agency": null, "updated_at": "2026-09-30T14:07:28.671Z", "published_at": "2026-09-30T14:04:19.703Z"
}]
}Filters
| Parameter | Description |
|---|---|
q | Free text (Spanish stemming, accent-insensitive) |
operation | sale (has a sale price) · rental (has a rent price) |
type | house, apartment, land, office, commercial, warehouse, ranch, building, other. Repeat for several: type=house&type=apartment |
state | State, exact name: Baja California |
city, neighborhood | City and neighborhood (partial match, accent-insensitive) |
min_price, max_price | Price (sale price with operation=sale, rent with rental, otherwise either). Currencies are not converted |
currency | MXN or USD: only listings priced in that currency |
min_bedrooms, min_bathrooms, min_parking | Minimums |
min_construction, min_lot | Minimum built / lot area in m² |
features | Must have all these amenities (Spanish labels from /meta); repeat the parameter |
sw_lat, sw_lng, ne_lat, ne_lng | Map rectangle (public, approximate coordinates) |
lat, lng, radius_km | Radius around a point |
updated_since | ISO 8601 time: only changes since then (incremental sync) |
sort | newest (default), price_asc, price_desc, updated, relevance (with q). Featured listings come first |
page, limit | Page (from 1) and size (default 24, max 100) |
Included: published listings of verified agents, and yours in any status. Excluded: licensed imports from other MLSs, listings whose owner did not authorize other agents' websites (idx_opt_out) and test accounts.
2. Get one listing
GET /properties/{id} takes the id (UUID) or the slug.
curl -s "$API/properties/casa-en-venta-playas-de-tijuana" -H "Authorization: Bearer $MULTILISTADO_API_KEY"const one = await api('GET', `/properties/${content[0].id}`);
console.log(one.title, one.url, one.images.length, 'photos');$one = mlx('GET', '/properties/' . $page['content'][0]['id']);
echo $one['title'], ' ', $one['url'], PHP_EOL;one = api("GET", f"/properties/{page['content'][0]['id']}")
print(one["title"], one["url"], len(one["images"]), "photos")3. Create a listing
POST /properties (write scope) creates the listing in your inventory as a draft. Required: title (min. 5 characters), city, state and sale_price and/or rent_price.
curl -s -X POST "$API/properties" -H "Authorization: Bearer $MULTILISTADO_API_KEY" -H "Content-Type: application/json" -d '{
"title": "Casa en venta en Playas de Tijuana", "description": "Casa de 3 recámaras a dos cuadras de la playa.",
"property_type": "house", "sale_price": 4500000, "sale_currency": "MXN",
"bedrooms": 3, "bathrooms": 2.5, "parking": 2, "construction_m2": 180, "lot_m2": 200,
"city": "Tijuana", "state": "Baja California", "neighborhood": "Playas de Tijuana",
"lat": 32.5201, "lng": -117.1205, "features": ["Vista al mar", "Jardín"], "external_id": "crm-1042"
}'
# Save the "id" from the response:
LISTING_ID="…"const created = await api('POST', '/properties', {
title: 'Casa en venta en Playas de Tijuana', description: 'Casa de 3 recámaras a dos cuadras de la playa.',
property_type: 'house', sale_price: 4500000, sale_currency: 'MXN',
bedrooms: 3, bathrooms: 2.5, parking: 2, construction_m2: 180, lot_m2: 200,
city: 'Tijuana', state: 'Baja California', neighborhood: 'Playas de Tijuana',
lat: 32.5201, lng: -117.1205, features: ['Vista al mar', 'Jardín'], external_id: 'crm-1042'
});
const id = created.id;
console.log(created.status, created.url); // draft https://multilistado.mx/p/…$created = mlx('POST', '/properties', [
'title' => 'Casa en venta en Playas de Tijuana', 'description' => 'Casa de 3 recámaras a dos cuadras de la playa.',
'property_type' => 'house', 'sale_price' => 4500000, 'sale_currency' => 'MXN',
'bedrooms' => 3, 'bathrooms' => 2.5, 'parking' => 2, 'construction_m2' => 180, 'lot_m2' => 200,
'city' => 'Tijuana', 'state' => 'Baja California', 'neighborhood' => 'Playas de Tijuana',
'lat' => 32.5201, 'lng' => -117.1205, 'features' => ['Vista al mar', 'Jardín'], 'external_id' => 'crm-1042',
]);
$id = $created['id'];
echo $created['status'], ' ', $created['url'], PHP_EOL;created = api("POST", "/properties", json={
"title": "Casa en venta en Playas de Tijuana", "description": "Casa de 3 recámaras a dos cuadras de la playa.",
"property_type": "house", "sale_price": 4500000, "sale_currency": "MXN",
"bedrooms": 3, "bathrooms": 2.5, "parking": 2, "construction_m2": 180, "lot_m2": 200,
"city": "Tijuana", "state": "Baja California", "neighborhood": "Playas de Tijuana",
"lat": 32.5201, "lng": -117.1205, "features": ["Vista al mar", "Jardín"], "external_id": "crm-1042",
})
listing_id = created["id"]
print(created["status"], created["url"])Writable fields: title, description, title_en, description_en, property_type, sale_price, sale_currency, rent_price, rent_currency, rent_period (monthly, weekly, daily, yearly), bedrooms, bathrooms, half_bathrooms, parking, construction_m2, lot_m2, year_built, floors, features, address, neighborhood, city, municipality, state, postal_code, lat, lng, show_exact_address, idx_opt_out, video_url, virtual_tour_url, exclusive, shared_commission, internal_id, images and external_id (your own ID; returned as source_id and unique per agent: if it already exists the API returns 409 conflict with that listing's id so you can PUT instead). Prices accept numbers or text such as "4,500,000". Coordinates are stored exactly but published approximately unless show_exact_address is true. Full detail in the reference.
4. Update
PUT /properties/{id} is partial: send only what changes.
curl -s -X PUT "$API/properties/$LISTING_ID" -H "Authorization: Bearer $MULTILISTADO_API_KEY" -H "Content-Type: application/json" \
-d '{"sale_price": 4350000, "title_en": "House for sale in Playas de Tijuana"}'const updated = await api('PUT', `/properties/${id}`, { sale_price: 4350000, title_en: 'House for sale in Playas de Tijuana' });
console.log(updated.operations[0].formatted_amount); // $4,350,000 MXN$updated = mlx('PUT', "/properties/$id", ['sale_price' => 4350000, 'title_en' => 'House for sale in Playas de Tijuana']);
echo $updated['operations'][0]['formatted_amount'], PHP_EOL;updated = api("PUT", f"/properties/{listing_id}", json={"sale_price": 4350000, "title_en": "House for sale in Playas de Tijuana"})
print(updated["operations"][0]["formatted_amount"])5. Images
POST /properties/{id}/images adds images by public URL (up to 60 per call, in order). The first becomes the cover if there is none. Images are linked, not copied: keep them online (your server, CDN or public http/https storage). API v1 has no binary upload; to upload photos from your computer use the portal. Sending images in a PUT replaces the listing's URL images.
curl -s -X POST "$API/properties/$LISTING_ID/images" -H "Authorization: Bearer $MULTILISTADO_API_KEY" -H "Content-Type: application/json" \
-d '{"urls": ["https://cdn.your-site.com/photos/1042/front.jpg", "https://cdn.your-site.com/photos/1042/living.jpg"]}'const media = await api('POST', `/properties/${id}/images`, { urls: ['https://cdn.your-site.com/photos/1042/front.jpg', 'https://cdn.your-site.com/photos/1042/living.jpg'] });
console.log(media.media.length, 'images');$media = mlx('POST', "/properties/$id/images", ['urls' => ['https://cdn.your-site.com/photos/1042/front.jpg', 'https://cdn.your-site.com/photos/1042/living.jpg']]);
echo count($media['media']), ' images', PHP_EOL;media = api("POST", f"/properties/{listing_id}/images", json={"urls": ["https://cdn.your-site.com/photos/1042/front.jpg", "https://cdn.your-site.com/photos/1042/living.jpg"]})
print(len(media["media"]), "images")6. Publish and unpublish
| Call | New status | Webhook |
|---|---|---|
POST /properties/{id}/publish | published | listing.published |
POST /properties/{id}/unpublish | draft | listing.unpublished |
DELETE /properties/{id} | withdrawn (kept, not deleted) | listing.unpublished |
curl -s -X POST "$API/properties/$LISTING_ID/publish" -H "Authorization: Bearer $MULTILISTADO_API_KEY"
# {"ok":true,"status":"published"}console.log(await api('POST', `/properties/${id}/publish`)); // { ok: true, status: 'published' }print_r(mlx('POST', "/properties/$id/publish"));print(api("POST", f"/properties/{listing_id}/publish"))Your whole inventory (every status, up to 500, not paginated): GET /my/properties (filter with ?status=draft).
7. Create a lead from your site
POST /leads (write scope) records an inquiry received on your own website or form. The lead is assigned to you; property_id (UUID or slug) is linked only if you manage that listing. You get the usual email notice. name is required.
curl -s -X POST "$API/leads" -H "Authorization: Bearer $MULTILISTADO_API_KEY" -H "Content-Type: application/json" \
-d "{\"name\": \"Ana López\", \"email\": \"ana@example.com\", \"phone\": \"+52 664 123 4567\", \"message\": \"I am interested in the house.\", \"property_id\": \"$LISTING_ID\", \"source\": \"my-site\"}"const lead = await api('POST', '/leads', { name: 'Ana López', email: 'ana@example.com', phone: '+52 664 123 4567', message: 'I am interested in the house.', property_id: id, source: 'my-site' });
console.log(lead.id, lead.listing_title);$lead = mlx('POST', '/leads', ['name' => 'Ana López', 'email' => 'ana@example.com', 'phone' => '+52 664 123 4567', 'message' => 'I am interested in the house.', 'property_id' => $id, 'source' => 'my-site']);
echo $lead['id'], ' ', $lead['listing_title'], PHP_EOL;lead = api("POST", "/leads", json={"name": "Ana López", "email": "ana@example.com", "phone": "+52 664 123 4567",
"message": "I am interested in the house.", "property_id": listing_id, "source": "my-site"})
print(lead["id"], lead["listing_title"])8. Your leads
GET /leads returns your latest 500 leads (from your Multilistado site, the widget, portals and the API), newest first.
curl -s "$API/leads" -H "Authorization: Bearer $MULTILISTADO_API_KEY"const { content: leads } = await api('GET', '/leads');
for (const l of leads.slice(0, 5)) console.log(l.created_at, l.name, l.kind, l.source, l.listing_title);$leads = mlx('GET', '/leads')['content'];
foreach (array_slice($leads, 0, 5) as $l) {
echo $l['created_at'], ' ', $l['name'], ' ', $l['kind'], ' ', $l['source'], PHP_EOL;
}leads = api("GET", "/leads")["content"]
for l in leads[:5]:
print(l["created_at"], l["name"], l["kind"], l["source"], l["listing_title"])Fields: id, listing_id, listing_title, listing_slug, name, email, phone, message, source (widget:<id>, api, your label…), status, kind (info or tour = showing request), lang, interest, budget_min, budget_max, follow_up_at, tour_at, tour_status, showing_at, id_status (buyer ID: verified/pending), awaiting_id, created_at, updated_at. To get them instantly use webhooks.
Other endpoints
| Endpoint | Returns |
|---|---|
GET /me | Key owner and scopes |
GET /meta | Property types, listing statuses, Mexican states, amenities and currencies (no key needed) |
GET /agents | Public verified agent directory (up to 1000) |
GET /agencies | Active agencies (up to 1000) |
GET /locations | Cities and states with published inventory (top 200) |
GET /my/properties | Your inventory in every status |
Pagination
/properties returns pagination: { page, pages, total, limit }. Request the next page with page=2, page=3… up to pages. To sync a large inventory, store the time of your last sync and request only changes with updated_since=2026-09-30T00:00:00Z&sort=updated. /my/properties, /leads, /agents and /agencies are not paginated (fixed caps).
Errors
Errors return JSON with error (a stable code) and usually message:
{ "error": "unauthorized", "message": "Invalid or revoked API key." }| HTTP | error | Cause |
|---|---|---|
| 401 | unauthorized | Missing, invalid or revoked key, unverified account, or key sent in the URL |
| 403 | forbidden | Key lacks write, the listing is not yours, or it is a RESO partner key |
| 404 | not_found | Does not exist or your key cannot see it |
| 409 | conflict | You already have a listing with that external_id; the response includes its id |
| 422 | validation | Invalid data; details in errors: [...] |
| 429 | rate_limited | Limit exceeded; wait the Retry-After seconds |
| 500 | server_error | Our error: retry later and tell us if it persists |
Limits
- 600 requests per 10 minutes per key. Every response carries
X-RateLimit-LimitandX-RateLimit-Remaining; over the limit you get 429 withRetry-After(seconds). Counting is approximate (per server process): treat 600 as your budget. - Pages of up to 100 listings; up to 60 images per call; JSON bodies up to 2 MB.
- Cache for at most 12 hours and follow the usage guidelines.
What the API does not expose
- Shared commissions (
shared_commission) and other commission data: you can write them, but only verified agents inside Multilistado see them; they never leave through the API, feeds or RESO. - Exact coordinates and the address when the agent did not choose to show them (
location.exact: false). - Private owner data, buyer data (IDs) and internal showing tokens.