RESO Web API (OData)

Standard RESO Data Dictionary 2.0 resources so US MLSs, IDX vendors and CRMs can consume Mexican inventory with no custom work. Read-only.

Leer en español · Markdown · Updated

Overview

Service roothttps://multilistado.mx/reso/odata
Metadata[/reso/odata/$metadata](https://multilistado.mx/reso/odata/$metadata) (EDMX/XML)
OAuth2 tokenPOST https://multilistado.mx/reso/oauth/token (client credentials)
StandardsWeb API Core 2.0.0 (2.1.0 also tested) + Data Dictionary 2.0 · OData 4.0, JSON odata.metadata=minimal
ResourcesProperty (+ $expand=Media), Media, Member, Office, Lookup
Rate limit600 requests per 10 minutes per credential (429 with Retry-After)
StatusAvailable, read-only. Not yet RESO-certified (the official test suites pass in our test environments)

Who can use it

CredentialHow to get itWhat it sees
Agent key (read)Yourself at Portal → APIYour listings (published, pending, closed or withdrawn; no drafts) + those of agents who consented to share with partner MLSs
**Partner, broker scope**Issued by Multilistado to an MLS, GDX node or vendor (apply)Only listings of agents who consented: Active, Pending, Closed, Withdrawn
**Partner, idx scope**Same, for IDX displayOnly Active listings of consenting agents

Each agent consents at Portal → International portals ("US MLSs and GDX network (RESO)"). Licensed imports from other MLSs, test accounts and any compensation field are never included. Coordinates are approximate and the street address is omitted unless the agent shows it.

1. Get a token

client_id = the key prefix (the 8 characters after pbm_); client_secret = the full key. Tokens last 1 hour (expires_in: 3600). You can also send the key directly as Authorization: Bearer pbm_…. Credentials in the URL (?api_key=) are refused.

Form
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" }

OAuth errors: 400 {"error":"unsupported_grant_type"} and 401 {"error":"invalid_client"}.

2. OData queries

curl
TOKEN="…"   # access_token from the previous step
R=https://multilistado.mx/reso/odata

# Active listings in Tijuana, 10 per page, with total count and photos
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"

# One listing by key
curl -s -G "$R/Property('07e25392-752f-4ca5-a441-51b4ad0625ed')" -H "Authorization: Bearer $TOKEN" \
  --data-urlencode "\$select=ListingKey,ListPrice,StandardStatus"

# Lookup values of a field
curl -s -G "$R/Lookup" -H "Authorization: Bearer $TOKEN" --data-urlencode "\$filter=LookupName eq 'PropertySubType'"

Response (trimmed, illustrative):

{
  "@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…"
}

Supported options

OptionDetail
$filtereq ne gt ge lt le, in (…), has, and/or/not, parentheses, contains(), startswith(), endswith(), now(); collections with any()/all()
$selectField list; an unknown field returns 400
$orderbyAny scalar field, asc/desc
$top / $skipProperty: default 100, max 500. Media, Member and Office: default 200, max 1000. Lookup: 1000
$count=trueTotal in @odata.count
$expand=MediaProperty only
@odata.nextLinkWithout $orderby or $skip, paging uses a $skiptoken ordered by ModificationTimestamp + key: replication-safe — no record is skipped; one that changes during the download may appear again at the end (upsert by ListingKey)
$formatjson only

OData errors: {"error":{"code":"BadRequest","message":"Unknown field Bogus"}} (400), Unauthorized (401), NotFound (404), TooManyRequests (429).

Lookups

Values are Data Dictionary 2.0 names (e.g. 'Single Family Residence', 'Square Meters'). The Lookup resource gives each one's LegacyODataValue (SingleFamilyResidence), and filters accept either form. Multi-value fields View, WaterfrontFeatures, CommunityFeatures and PetsAllowed are collections: View/any(v: v eq 'Ocean'). Local fields use the PBM_ prefix: PBM_Currency is the currency of ListPrice/LeaseAmount (MXN or USD, not converted).

3. Replication

To keep a copy: do a full download following @odata.nextLink, store the newest ModificationTimestamp, then request only changes.

Python
# Incremental replica (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"          # the mark saved by your last run
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"])   # store your record here (upsert by ListingKey)
    url, params = page.get("@odata.nextLink"), None   # nextLink already carries every parameter
print(n, "changes; next mark:", since)

Listings leaving the market change to Closed or Withdrawn (broker scope) or stop appearing (idx scope): with idx scope, delete from your copy whatever is missing from a periodic full download.

Partner rules