Resumen
| Raíz del servicio | https://multilistado.mx/reso/odata |
| Metadatos | [/reso/odata/$metadata](https://multilistado.mx/reso/odata/$metadata) (EDMX/XML) |
| Token OAuth2 | POST https://multilistado.mx/reso/oauth/token (client credentials) |
| Estándares | Web API Core 2.0.0 (también probado 2.1.0) + Data Dictionary 2.0 · OData 4.0, JSON odata.metadata=minimal |
| Recursos | Property (+ $expand=Media), Media, Member, Office, Lookup |
| Límite | 600 solicitudes cada 10 minutos por credencial (429 con Retry-After) |
| Estado | Disponible, solo lectura. Aún no certificada por RESO (las pruebas oficiales pasan en nuestros entornos de prueba) |
Quién puede usarla
| Credencial | Cómo se obtiene | Qué ve |
|---|---|---|
Key de agente (read) | Tú mismo en Portal → API | Tus propiedades (publicadas, en trato, cerradas o retiradas; no borradores) + las de agentes que autorizaron compartir con MLS socios |
**Socio, alcance broker** | Multilistado la emite a un MLS, GDX o vendor (solicitar) | Solo propiedades de agentes que autorizaron compartir: Active, Pending, Closed, Withdrawn |
**Socio, alcance idx** | Igual, para exhibición IDX | Solo propiedades Active de agentes que autorizaron |
El consentimiento lo da cada agente en Portal → Portales internacionales («MLS de EE. UU. y red GDX (RESO)»). Nunca se incluyen importaciones con licencia de otros MLS, cuentas de prueba ni ningún campo de compensación. Las coordenadas son aproximadas y la dirección se omite salvo que el agente la muestre.
1. Obtener un token
client_id = el prefijo de la key (los 8 caracteres después de pbm_); client_secret = la key completa. El token dura 1 hora (expires_in: 3600). También puedes enviar la key directamente como Authorization: Bearer pbm_…. Las credenciales en la URL (?api_key=) se rechazan.
curl -s -X POST https://multilistado.mx/reso/oauth/token \
-d grant_type=client_credentials \
-d client_id="$RESO_CLIENT_ID" \
--data-urlencode client_secret="$RESO_CLIENT_SECRET"curl -s -X POST https://multilistado.mx/reso/oauth/token \
-u "$RESO_CLIENT_ID:$RESO_CLIENT_SECRET" -d grant_type=client_credentials{ "access_token": "Qm9…", "token_type": "Bearer", "expires_in": 3600, "scope": "read partner idx" }Errores OAuth: 400 {"error":"unsupported_grant_type"} y 401 {"error":"invalid_client"}.
2. Consultas OData
TOKEN="…" # access_token del paso anterior
R=https://multilistado.mx/reso/odata
# Propiedades activas en Tijuana, 10 por página, con total y fotos
curl -s -G "$R/Property" -H "Authorization: Bearer $TOKEN" \
--data-urlencode "\$filter=StandardStatus eq 'Active' and City eq 'Tijuana'" \
--data-urlencode "\$select=ListingKey,ListPrice,PBM_Currency,City,PropertySubType,BedroomsTotal,ModificationTimestamp" \
--data-urlencode "\$expand=Media" --data-urlencode "\$top=10" --data-urlencode "\$count=true"
# Una propiedad por su clave
curl -s -G "$R/Property('07e25392-752f-4ca5-a441-51b4ad0625ed')" -H "Authorization: Bearer $TOKEN" \
--data-urlencode "\$select=ListingKey,ListPrice,StandardStatus"
# Catálogo (Lookup) de un campo
curl -s -G "$R/Lookup" -H "Authorization: Bearer $TOKEN" --data-urlencode "\$filter=LookupName eq 'PropertySubType'"Respuesta (recortada, ilustrativa):
{
"@odata.context": "https://multilistado.mx/reso/odata/$metadata#Property",
"@odata.count": 27,
"value": [{
"ListingKey": "07e25392-752f-4ca5-a441-51b4ad0625ed", "ListPrice": 4350000, "PBM_Currency": "MXN", "City": "Tijuana",
"PropertySubType": "Single Family Residence", "BedroomsTotal": 3, "ModificationTimestamp": "2026-09-30T14:07:28.671Z",
"Media": [{ "MediaKey": "05453dad-…", "ResourceRecordKey": "07e25392-…", "MediaURL": "https://…/fachada.jpg", "MediaCategory": "Photo", "Order": 0 }]
}],
"@odata.nextLink": "https://multilistado.mx/reso/odata/Property?%24filter=…&%24skiptoken=WyIy…"
}Opciones soportadas
| Opción | Detalle |
|---|---|
$filter | eq ne gt ge lt le, in (…), has, and/or/not, paréntesis, contains(), startswith(), endswith(), now(); colecciones con any()/all() |
$select | Lista de campos; un campo desconocido responde 400 |
$orderby | Cualquier campo escalar, asc/desc |
$top / $skip | Property: 100 por omisión, máximo 500. Media, Member y Office: 200 por omisión, máximo 1000. Lookup: 1000 |
$count=true | Total en @odata.count |
$expand=Media | Solo en Property |
@odata.nextLink | Sin $orderby ni $skip, la paginación usa $skiptoken ordenado por ModificationTimestamp + clave: segura para replicación: no se salta registros; uno que cambie durante la descarga puede volver a aparecer al final (haz upsert por ListingKey) |
$format | Solo json |
Errores OData: {"error":{"code":"BadRequest","message":"Unknown field Bogus"}} (400), Unauthorized (401), NotFound (404), TooManyRequests (429).
Catálogos (lookups)
Los valores son los nombres del Data Dictionary 2.0 (p. ej. 'Single Family Residence', 'Square Meters'). El recurso Lookup da para cada uno su LegacyODataValue (SingleFamilyResidence), y los filtros aceptan ambas formas. Los campos multivalor View, WaterfrontFeatures, CommunityFeatures y PetsAllowed son colecciones: View/any(v: v eq 'Ocean'). Campos locales con prefijo PBM_: PBM_Currency es la moneda de ListPrice/LeaseAmount (MXN o USD, sin convertir).
3. Replicación
Para mantener una copia: descarga completa siguiendo @odata.nextLink, guarda el ModificationTimestamp más reciente y después pide solo los cambios.
# Réplica incremental (Python 3.8+, requests)
import os, requests
R = "https://multilistado.mx/reso/odata"
tok = requests.post("https://multilistado.mx/reso/oauth/token", data={
"grant_type": "client_credentials", "client_id": os.environ["RESO_CLIENT_ID"], "client_secret": os.environ["RESO_CLIENT_SECRET"]}, timeout=30).json()
h = {"Authorization": f"Bearer {tok['access_token']}"}
since = "2026-01-01T00:00:00Z" # la marca guardada de tu última corrida
url, params, n = f"{R}/Property", {"$filter": f"ModificationTimestamp gt {since}", "$expand": "Media", "$top": "200"}, 0
while url:
page = requests.get(url, headers=h, params=params, timeout=60).json()
for p in page["value"]:
n += 1
since = max(since, p["ModificationTimestamp"]) # guarda aquí tu registro (upsert por ListingKey)
url, params = page.get("@odata.nextLink"), None # nextLink ya trae todos los parámetros
print(n, "cambios; próxima marca:", since)Propiedades que salen del mercado cambian a Closed o Withdrawn (alcance broker) o dejan de aparecer (alcance idx): con alcance idx, elimina de tu copia lo que no vuelva en una descarga completa periódica.
Reglas para socios
- Muestra el crédito del agente listador (
ListAgentFullName,ListOfficeName) y enlaza aListingURL. - Actualiza al menos cada 12 horas y elimina lo que ya no esté activo.
- No re-sindiques a terceros sin acuerdo por escrito.
- Programa de socios: /socios-mls · contacto@multilistado.mx.