# API REST v1

API JSON para leer el inventario del Multilistado, administrar tus propiedades y recibir leads desde tu sitio o CRM. Gratis para agentes verificados.

Fuente: https://multilistado.mx/developers/api · Actualizado: 2026-09-30 · Otro idioma: https://multilistado.mx/developers/api.md?lang=en

## Resumen

| | |
|---|---|
| URL base | `https://multilistado.mx/api/v1` |
| Formato | JSON (UTF-8). En `POST`/`PUT` envía `Content-Type: application/json` |
| Autenticación | `Authorization: Bearer pbm_<prefijo>_<secreto>` |
| Permisos | `read` (todas las consultas) · `write` (crear, editar, publicar, crear leads) |
| Límite | 600 solicitudes cada 10 minutos por key |
| Descripción OpenAPI 3.0 | [/api/v1/openapi.json](https://multilistado.mx/api/v1/openapi.json) · [referencia interactiva](https://multilistado.mx/developers/api/reference) |

## Autenticación

1. Entra a [Portal → Más → API](https://multilistado.mx/portal/api-keys) (requiere licencia verificada).
2. En **Crear key** escribe una **Etiqueta** (p. ej. «Mi CRM»), elige **Permisos**: «Lectura y escritura» o «Solo lectura», y pulsa **Generar**.
3. Copia la key: **se muestra una sola vez**. Tiene la forma `pbm_` + 8 caracteres + `_` + 48 caracteres. Puedes revocarla en cualquier momento.

Envíala en la cabecera `Authorization`. Por seguridad **no se aceptan keys en la URL** (`?api_key=` responde 401) y la API no habilita CORS: llámala **desde tu servidor**, nunca desde el navegador de tus visitantes (la key quedaría expuesta). Para mostrar propiedades en un sitio sin programar usa el [widget](https://multilistado.mx/developers/widget).

Crea tu API key gratis en https://multilistado.mx/portal/api-keys (agentes verificados).

Las keys de **socios RESO** (permiso `partner`) solo sirven en la [RESO Web API](https://multilistado.mx/developers/reso); en `/api/v1` responden 403.

## Cliente mínimo

Guarda tu key en una variable de entorno (`MULTILISTADO_API_KEY`) y usa este pequeño cliente en los ejemplos siguientes. Cada ejemplo continúa el anterior.

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

```javascript
// Node.js 18+ (archivo .mjs). Nunca en el navegador: expondría tu 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+ con la extensión curl.
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+ con 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. Buscar propiedades en el MLS

`GET /properties` devuelve propiedades **publicadas** de todo el Multilistado (y las tuyas), una página a la vez.

```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} resultados, página ${pagination.page} de ${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'], " resultados", 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"], "resultados")
for l in page["content"]:
    print(l["title"], "·", l["operations"][0]["formatted_amount"] if l["operations"] else "", "·", l["agent"]["name"])
```

Respuesta (recortada, ilustrativa):

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

### Filtros

| Parámetro | Descripción |
|---|---|
| `q` | Texto libre (en español, sin acentos) |
| `operation` | `sale` (tiene precio de venta) · `rental` (tiene precio de renta) |
| `type` | `house`, `apartment`, `land`, `office`, `commercial`, `warehouse`, `ranch`, `building`, `other`. Repite el parámetro para varios: `type=house&type=apartment` |
| `state` | Estado, nombre exacto: `Baja California` |
| `city`, `neighborhood` | Ciudad y colonia (coincidencia parcial, sin acentos) |
| `min_price`, `max_price` | Precio (el de venta con `operation=sale`, el de renta con `rental`; si no, cualquiera). No convierte monedas |
| `currency` | `MXN` o `USD`: solo propiedades con precio en esa moneda |
| `min_bedrooms`, `min_bathrooms`, `min_parking` | Mínimos |
| `min_construction`, `min_lot` | m² mínimos de construcción y terreno |
| `features` | Debe tener **todas** estas amenidades (etiquetas en español de `/meta`); repite el parámetro |
| `sw_lat`, `sw_lng`, `ne_lat`, `ne_lng` | Rectángulo del mapa (coordenadas públicas aproximadas) |
| `lat`, `lng`, `radius_km` | Radio alrededor de un punto |
| `updated_since` | Fecha ISO 8601: solo cambios desde entonces (sincronización incremental) |
| `sort` | `newest` (por omisión), `price_asc`, `price_desc`, `updated`, `relevance` (con `q`). Las destacadas van primero |
| `page`, `limit` | Página (desde 1) y tamaño (por omisión 24, máximo 100) |

**Qué incluye:** propiedades publicadas de agentes verificados, y las tuyas en cualquier estado. **No incluye** importaciones con licencia de otros MLS, propiedades cuyo propietario no autorizó sitios de otros agentes (`idx_opt_out`) ni cuentas de prueba.

## 2. Ver una propiedad

`GET /properties/{id}` acepta el `id` (UUID) o el `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, 'fotos');
```

```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"]), "fotos")
```

## 3. Crear una propiedad

`POST /properties` (permiso `write`) crea la propiedad en tu inventario como **borrador** (`draft`). Obligatorios: `title` (mín. 5 caracteres), `city`, `state` y `sale_price` y/o `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"
}'
# Guarda el "id" de la respuesta:
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"])
```

Campos que puedes enviar: `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` y `external_id` (tu ID interno; se devuelve como `source_id` y es único por agente: si ya existe, la API responde `409 conflict` con el `id` de esa propiedad para que la actualices con `PUT`). Los precios aceptan números o texto como `"4,500,000"`. Las coordenadas se guardan exactas, pero **se publican aproximadas** salvo que `show_exact_address` sea `true`. Detalle completo en la [referencia](https://multilistado.mx/developers/api/reference).

## 4. Actualizar

`PUT /properties/{id}` es **parcial**: envía solo lo que cambia.

```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. Imágenes

`POST /properties/{id}/images` agrega imágenes **por URL pública** (hasta 60 por llamada, en orden). La primera se usa como portada si no hay otra. Las imágenes se enlazan, no se copian: **mantenlas en línea** (tu servidor, CDN o almacenamiento público http/https). En API v1 no hay carga binaria de archivos; para subir fotos desde tu computadora usa el portal. Enviar `images` en un `PUT` **reemplaza** las imágenes por URL de la propiedad.

```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.tu-sitio.com/fotos/1042/fachada.jpg", "https://cdn.tu-sitio.com/fotos/1042/sala.jpg"]}'
```

```javascript
const media = await api('POST', `/properties/${id}/images`, { urls: ['https://cdn.tu-sitio.com/fotos/1042/fachada.jpg', 'https://cdn.tu-sitio.com/fotos/1042/sala.jpg'] });
console.log(media.media.length, 'imágenes');
```

```php
$media = mlx('POST', "/properties/$id/images", ['urls' => ['https://cdn.tu-sitio.com/fotos/1042/fachada.jpg', 'https://cdn.tu-sitio.com/fotos/1042/sala.jpg']]);
echo count($media['media']), ' imágenes', PHP_EOL;
```

```python
media = api("POST", f"/properties/{listing_id}/images", json={"urls": ["https://cdn.tu-sitio.com/fotos/1042/fachada.jpg", "https://cdn.tu-sitio.com/fotos/1042/sala.jpg"]})
print(len(media["media"]), "imágenes")
```

## 6. Publicar y despublicar

| Llamada | Nuevo estado | Webhook |
|---|---|---|
| `POST /properties/{id}/publish` | `published` | `listing.published` |
| `POST /properties/{id}/unpublish` | `draft` | `listing.unpublished` |
| `DELETE /properties/{id}` | `withdrawn` (se conserva, no se borra) | `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"))
```

Tu inventario completo (todos los estados, hasta 500, sin paginación): `GET /my/properties` (filtra con `?status=draft`).

## 7. Crear un lead desde tu sitio

`POST /leads` (permiso `write`) registra una solicitud recibida en tu propio sitio o formulario. El lead se asigna a ti; `property_id` (UUID o slug) solo se vincula si administras esa propiedad. Recibes el aviso por correo como con cualquier lead. `name` es obligatorio.

```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\": \"Me interesa la casa.\", \"property_id\": \"$LISTING_ID\", \"source\": \"mi-sitio\"}"
```

```javascript
const lead = await api('POST', '/leads', { name: 'Ana López', email: 'ana@example.com', phone: '+52 664 123 4567', message: 'Me interesa la casa.', property_id: id, source: 'mi-sitio' });
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' => 'Me interesa la casa.', 'property_id' => $id, 'source' => 'mi-sitio']);
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": "Me interesa la casa.", "property_id": listing_id, "source": "mi-sitio"})
print(lead["id"], lead["listing_title"])
```

## 8. Tus leads

`GET /leads` devuelve tus últimos 500 leads (de tu sitio de Multilistado, el widget, portales y la API), del más nuevo al más antiguo.

```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"])
```

Campos: `id`, `listing_id`, `listing_title`, `listing_slug`, `name`, `email`, `phone`, `message`, `source` (`widget:<id>`, `api`, tu etiqueta…), `status`, `kind` (`info` o `tour` = solicitud de visita), `lang`, `interest`, `budget_min`, `budget_max`, `follow_up_at`, `tour_at`, `tour_status`, `showing_at`, `id_status` (identificación del comprador: `verified`/`pending`), `awaiting_id`, `created_at`, `updated_at`. Para recibirlos al instante usa [webhooks](https://multilistado.mx/developers/webhooks).

## Otros endpoints

| Endpoint | Devuelve |
|---|---|
| `GET /me` | Dueño de la key y permisos |
| `GET /meta` | Tipos, estados de publicación, estados de México, amenidades y monedas (sin key) |
| `GET /agents` | Directorio de agentes verificados públicos (hasta 1000) |
| `GET /agencies` | Inmobiliarias activas (hasta 1000) |
| `GET /locations` | Ciudades y estados con inventario publicado (top 200) |
| `GET /my/properties` | Tu inventario en todos los estados |

## Paginación

`/properties` responde `pagination: { page, pages, total, limit }`. Pide la siguiente página con `page=2`, `page=3`… hasta `pages`. Para **sincronizar** un inventario grande, guarda la hora de tu última sincronización y pide solo los cambios con `updated_since=2026-09-30T00:00:00Z&sort=updated`. Las listas `/my/properties`, `/leads`, `/agents` y `/agencies` no se paginan (tienen un tope fijo).

## Errores

Los errores responden JSON con `error` (código estable) y, casi siempre, `message`:

```json
{ "error": "unauthorized", "message": "Invalid or revoked API key." }
```

| HTTP | `error` | Causa |
|---|---|---|
| 401 | `unauthorized` | Falta la key, es inválida o revocada, la cuenta no está verificada, o la mandaste en la URL |
| 403 | `forbidden` | La key no tiene permiso `write`, la propiedad no es tuya, o es una key de socio RESO |
| 404 | `not_found` | No existe o tu key no la puede ver |
| 409 | `conflict` | Ya tienes una propiedad con ese `external_id`; la respuesta incluye su `id` |
| 422 | `validation` | Datos inválidos; detalle en `errors: [...]` (mensajes en inglés) |
| 429 | `rate_limited` | Límite superado; espera los segundos de `Retry-After` |
| 500 | `server_error` | Error nuestro: reintenta más tarde y avísanos si persiste |

## Límites

- **600 solicitudes cada 10 minutos por key.** Cada respuesta trae `X-RateLimit-Limit` y `X-RateLimit-Remaining`; al pasarte recibes 429 con `Retry-After` (segundos). El conteo es aproximado (por proceso del servidor): trata 600 como tu presupuesto.
- Páginas de hasta 100 propiedades; hasta 60 imágenes por llamada; cuerpo JSON de hasta 2 MB.
- **Guarda en caché como máximo 12 horas** y respeta los [lineamientos de uso](https://multilistado.mx/developers/guidelines).

## Qué no expone la API

- **Comisiones compartidas** (`shared_commission`) y demás datos de comisión: puedes escribirlos, pero solo los ven agentes verificados dentro de Multilistado; nunca salen en la API, feeds ni RESO.
- **Coordenadas exactas** y la **dirección** si el agente no eligió mostrarlas (`location.exact: false`).
- Datos privados del propietario, del comprador (identificaciones) y tokens internos de visitas.
