# REST API v1

JSON API to read the Multilistado inventory, manage your listings and receive leads from your site or CRM. Free for verified agents.

Source: https://multilistado.mx/developers/api?lang=en · Updated: 2026-09-30 · Other language: https://multilistado.mx/developers/api.md

## 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](https://multilistado.mx/api/v1/openapi.json) · [interactive reference](https://multilistado.mx/developers/api/reference?lang=en) |

## Authentication

1. Go to [Portal → More → API](https://multilistado.mx/portal/api-keys) (requires a verified license).
2. Under **Create key** type a **Label** (e.g. "My CRM"), choose **Permissions**: "Read and write" or "Read only", and click **Generate**.
3. 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](https://multilistado.mx/developers/widget?lang=en).

Create a free API key at https://multilistado.mx/portal/api-keys (verified agents).

**RESO partner** keys (`partner` scope) only work on the [RESO Web API](https://multilistado.mx/developers/reso?lang=en); 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.

```bash
export MULTILISTADO_API_KEY="pbm_..."   # your key
API=https://multilistado.mx/api/v1
curl -s "$API/me" -H "Authorization: Bearer $MULTILISTADO_API_KEY"
```

```javascript
// 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
// 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
# 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.

```bash
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"
```

```javascript
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);
```

```php
$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;
}
```

```python
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):

```json
{
  "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`.

```bash
curl -s "$API/properties/casa-en-venta-playas-de-tijuana" -H "Authorization: Bearer $MULTILISTADO_API_KEY"
```

```javascript
const one = await api('GET', `/properties/${content[0].id}`);
console.log(one.title, one.url, one.images.length, 'photos');
```

```php
$one = mlx('GET', '/properties/' . $page['content'][0]['id']);
echo $one['title'], ' ', $one['url'], PHP_EOL;
```

```python
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`.

```bash
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="…"
```

```javascript
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/…
```

```php
$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;
```

```python
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](https://multilistado.mx/developers/api/reference?lang=en).

## 4. Update

`PUT /properties/{id}` is **partial**: send only what changes.

```bash
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"}'
```

```javascript
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
```

```php
$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;
```

```python
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.

```bash
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"]}'
```

```javascript
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');
```

```php
$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;
```

```python
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` |

```bash
curl -s -X POST "$API/properties/$LISTING_ID/publish" -H "Authorization: Bearer $MULTILISTADO_API_KEY"
# {"ok":true,"status":"published"}
```

```javascript
console.log(await api('POST', `/properties/${id}/publish`));   // { ok: true, status: 'published' }
```

```php
print_r(mlx('POST', "/properties/$id/publish"));
```

```python
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.

```bash
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\"}"
```

```javascript
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);
```

```php
$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;
```

```python
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.

```bash
curl -s "$API/leads" -H "Authorization: Bearer $MULTILISTADO_API_KEY"
```

```javascript
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);
```

```php
$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;
}
```

```python
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](https://multilistado.mx/developers/webhooks?lang=en).

## 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`:

```json
{ "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-Limit` and `X-RateLimit-Remaining`; over the limit you get 429 with `Retry-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](https://multilistado.mx/developers/guidelines?lang=en).

## 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.
