RESO Web API (OData)

Recursos estándar del RESO Data Dictionary 2.0 para que MLS, vendors IDX y CRMs de Estados Unidos consuman inventario mexicano sin desarrollos a la medida. Solo lectura.

Read in English · Markdown · Actualizado

Resumen

Raíz del serviciohttps://multilistado.mx/reso/odata
Metadatos[/reso/odata/$metadata](https://multilistado.mx/reso/odata/$metadata) (EDMX/XML)
Token OAuth2POST https://multilistado.mx/reso/oauth/token (client credentials)
EstándaresWeb API Core 2.0.0 (también probado 2.1.0) + Data Dictionary 2.0 · OData 4.0, JSON odata.metadata=minimal
RecursosProperty (+ $expand=Media), Media, Member, Office, Lookup
Límite600 solicitudes cada 10 minutos por credencial (429 con Retry-After)
EstadoDisponible, solo lectura. Aún no certificada por RESO (las pruebas oficiales pasan en nuestros entornos de prueba)

Quién puede usarla

CredencialCómo se obtieneQué ve
Key de agente (read)Tú mismo en Portal → APITus 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 IDXSolo 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.

Formulario
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"
HTTP Basic
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

curl
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ónDetalle
$filtereq ne gt ge lt le, in (…), has, and/or/not, paréntesis, contains(), startswith(), endswith(), now(); colecciones con any()/all()
$selectLista de campos; un campo desconocido responde 400
$orderbyCualquier campo escalar, asc/desc
$top / $skipProperty: 100 por omisión, máximo 500. Media, Member y Office: 200 por omisión, máximo 1000. Lookup: 1000
$count=trueTotal en @odata.count
$expand=MediaSolo en Property
@odata.nextLinkSin $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)
$formatSolo 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.

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