API Documentation

Access agent, office and listing data across subscribed MLS databases.

Base URL https://api.restats.com
No endpoints match that search.

Getting Started

Authentication

The API uses two credential types. Which one you send depends on the endpoint.

CredentialHeaderUsed by
API KeyAPI-Key: your_api_key /authorization/*, /users/*, /databases/*
BearerAuthorization: Bearer <token> /transactions/*, /agents/*
Getting a Bearer token

Call /authorization/login with your API key and a username. The token expires after 600 minutes (10 hours) of inactivity. Every successful request to a Bearer endpoint resets the timer, so a token in regular use does not expire. Requests rejected before they reach the data, such as a bad parameter, do not reset it. If a valid token already exists for that username, the same token is returned rather than a new one.

Data endpoints share a common pattern

List endpoints accept fields, filter, order_column and sort, and return the same envelope. The paged ones also accept rows and skip:

{
    "status": "OK",
    "entires": 1250,          // total matching rows, ignoring rows/skip
    "result": [ { ... } ]     // this page of results
}
Paging: rows is capped at 100 per request. Use skip to page through larger result sets, and entires to know when to stop. /databases/MLSList is the exception: it takes neither parameter and always returns every matching row.
sort is required whenever you send order_column. order_column on its own returns 403 Invalid sort parameter.

Rate Limits

Limits

Every request is counted twice: against the credential it presents (your Bearer token, or your API key on the key-authenticated endpoints) and against the source IP address. Two time windows apply to each. Exceeding any of them returns 429.

Per credential
EndpointPer minutePer hour
Most endpoints1206,000
/transactions/TransactionsHistorical1005,000
/agents/AgentsHistorical1005,000

The two historical endpoints read far larger tables, so they are limited more tightly.

Per IP address
Per minutePer hour
All endpoints40020,000
The address limit is deliberately looser than the credential limit, and it is not lowered per endpoint. It exists to bound credential guessing, not to shape normal load. If several of your users share one outbound address, they share this allowance, so a busy integration should watch it as well as the per-credential limit.
Response Headers
HeaderMeaning
Retry-AfterSeconds until the current window resets
X-RateLimit-LimitThe limit that was exceeded
X-RateLimit-RemainingAlways 0. These three headers are sent only on a 429; successful responses carry no rate-limit headers
Example Response
HTTP/1.1 429 Too Many Requests
Retry-After: 37
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0

{
    "status": "error",
    "message": "Rate limit exceeded: 100 requests per minute. Retry in 37 seconds."
}
Handling a 429: wait the number of seconds given in Retry-After, then retry. Windows are fixed rather than rolling, so the counter resets at the top of each minute and each hour rather than gradually. A request rejected with a 429 is still counted, so retrying before the window resets pushes the reset no further out but does not help either. Wait for Retry-After.
Paging large result sets: rows is capped at 100, so a large export needs many requests. At 100 requests per minute that is 10,000 rows per minute. Space the calls out rather than bursting, and stop on the first 429.

Errors

Error responses

Every failure returns JSON with the same two fields, and an HTTP status code that matches. Check the HTTP status code. A request that failed never returns 200.

{
    "status": "error",
    "message": "Either you do not have access to the selected database or database name is wrong."
}
Status codes
CodeMeaningWhat to do
200Success. The body carries status, entires and resultRead the results
401The API-Key was missing or wrong, or a parameter on a /users/* request failed validation Check the key and the request body. Do not retry unchanged
403The Bearer token is missing, invalid or idle-expired; or the user is not assigned the database; or a parameter is not accepted Read message. Request a new token only if it names the token
429A rate limit was exceeded Wait for Retry-After, then retry. See Rate Limits
500Something failed on our side Retry once. If it persists, send us the request and the time
503The token could not be verified right now, which is a fault on our side rather than a problem with the token Retry shortly with the same token. Do not re-authenticate
Do not treat 403 as "get a new token". Most 403 responses are about the database or a parameter, not the token. The message says which. Re-authenticating on every 403 wastes a call and will not fix an unassigned database or a bad filter.
The one exception to "failures are never 200" is a repeat login: /authorization/login answers 200 with status: "error" when a valid token already exists, and that response contains the token. Treat any 200 carrying a token as a successful login.

Authorization

POST/authorization/loginAPI Key

Retrieve a Bearer token tied to a username, for use with the transaction and agent endpoints.

Parameters
NameDescription
usernamerequiredUsername to be logged in
user_iprequiredIP address of the end user
Example Request
POST /authorization/login
API-Key: your_api_key

{
    "username": "test_user",
    "user_ip": "127.0.0.1"
}
Example Response

First login for that username, or the first after the previous token expired:

{
    "token": "BearerTokenHere"
}

When a valid token already exists, the same token comes back in a different envelope, still with HTTP 200:

{
    "status": "error",
    "message": "A valid token already exist for the user",
    "token": "BearerTokenHere"
}
Read token, not status. The second response carries status: "error" for historical reasons, but it is a success: the token in it is valid and ready to use. Treat any 200 containing a token as a successful login.

Databases

GET/databases/MLSListAPI Key

List the MLS databases available for subscription.

One row per MLS, not per database. Several MLS organisations can share a single DATABASE_NAME, so filtering on one name usually returns several rows that differ only by MLS_NAME. Use DATABASE_NAME as the value to pass to the other endpoints; it is not a unique key here. SHORT_CODE and CITY are often empty.
This endpoint returns every matching row. It does not accept rows or skip, and is not paged.
Parameters
NameDescription
fieldsoptionalComma-separated fields to return
filteroptionale.g. DATABASE_NAME eq TORONTO ON
order_columnoptionalField to sort by
sortoptional1 ascending, 2 descending
Example Request
GET /databases/MLSList?filter=DATABASE_NAME eq TORONTO ON&order_column=DATABASE_NAME&sort=2
API-Key: your_api_key
Example Response
{
    "status": "OK",
    "entires": 5,
    "result": [
        // 2 of the 5 rows shown
        {
            "DATABASE_NAME": "TORONTO ON",
            "MLS_NAME": "Brampton Real Estate Board",
            "SHORT_CODE": "TRREB",
            "CITY": "",
            "STATE": "ON",
            "LAST_UPDATED": "2026-08-14 08:23:47"
        },
        {
            "DATABASE_NAME": "TORONTO ON",
            "MLS_NAME": "Toronto Real Estate Board (TRREB)",
            "SHORT_CODE": "TRREB",
            "CITY": "",
            "STATE": "ON",
            "LAST_UPDATED": "2026-08-17 14:25:19"
        }
    ]
}

Users

POST/users/CreateUserAPI Key

Create a new user and assign MLS databases to them.

Parameters
NameDescription
full_namerequiredFull name of the user
office_namerequiredOffice name
office_addressrequiredOffice address
cityrequiredCity
staterequiredTwo-letter state or province code, e.g. ON
postal_coderequiredPostal or ZIP code
countryrequiredUSA or Canada
mls_namerequiredArray of MLS names
mls_member_idrequiredArray of MLS member IDs
mls_office_idrequiredArray of MLS office IDs
database_namerequired Array of databases to assign. See /databases/MLSList. Must align positionally with the other arrays, and contain only databases related to the MLS names sent.
Approval: databases not already assigned to any active user are submitted for approval rather than assigned immediately. The response says which.
Example Request
POST /users/CreateUser
API-Key: your_api_key

{
    "full_name": "John Doe",
    "office_name": "Example Realty",
    "office_address": "1234 Elm St",
    "city": "Waterloo",
    "state": "ON",
    "postal_code": "N2L 3G1",
    "country": "Canada",
    "mls_name": ["MLS1", "MLS2"],
    "mls_member_id": ["12345", "67890"],
    "mls_office_id": ["Office1", "Office2"],
    "database_name": ["TORONTO ON", "OTTAWA ON"]
}
Example Response
{
    "status": "OK",
    "message": "User was successfully created.",
    "username": "John Doe"
}

PUT/users/UpdateUserStatusAPI Key

Activate or deactivate a user. Deactivating revokes all of that user's Bearer tokens.

Deactivating also releases databases. Any database assigned to this user that no other active user holds is disabled at the same time, and the response message lists them. Those databases have to be reassigned with /users/AssignDatabases when the user is reactivated; reactivating on its own does not restore them.
The default is to deactivate. A request that omits activate, or sends a value this endpoint does not recognise, deactivates the user and returns 200. Always send the parameter explicitly.
Parameters
NameDescription
usernamerequiredUsername of the user
activateoptional1 to activate. Anything else deactivates, including 0, an unexpected value, and omitting the parameter. Both 1 and "1" activate. Default: deactivate
Example Request
PUT /users/UpdateUserStatus
API-Key: your_api_key

{
    "username": "test_user",
    "activate": 0
}
Example Response
{
    "status": "OK",
    "message": "User was successfully deactivated."
}

PUT/users/AssignDatabasesAPI Key

Assign additional databases to an existing user, or disable databases already assigned.

Approval: as with /users/CreateUser, a database that no active user already holds is submitted for approval rather than assigned immediately. A single request can therefore assign some databases and queue others; the response message says which happened. Databases awaiting approval are not queryable until approved.
Parameters
NameDescription
usernamerequiredUsername of the user
database_namerequiredArray of database names
mls_namerequiredArray of MLS names. Not needed when disable_database is 1
mls_member_idrequiredArray of MLS member IDs. Not needed when disabling
mls_office_idrequiredArray of MLS office IDs. Not needed when disabling
disable_databaseoptional Send 1 to disable the databases in database_name. All of them must currently be active and assigned to the user.
Example Request
PUT /users/AssignDatabases
API-Key: your_api_key

{
    "username": "test_user",
    "mls_name": ["MLS1", "MLS2"],
    "mls_member_id": ["12345", "67890"],
    "mls_office_id": ["Office1", "Office2"],
    "database_name": ["TORONTO ON", "OTTAWA ON"]
}
Example Response
{
    "status": "OK",
    "message": "Databases are successfully assigned.",
    "username": "test_user"
}

POST/users/SetOfficeCriteriaAPI Key

Define named office groups, each matched by one or more wildcard patterns against office names. /transactions/MigrationGraph reports agents gained and lost separately for each group, with everything unmatched counted under others. The groups are reporting buckets, not ownership: a group can just as well track a competing brokerage as the user's own.

Parameters
NameDescription
usernamerequiredUser the criteria belong to
officesrequiredArray of office objects
offices[].office_namerequiredLabel for the group. Appears as the key in the MigrationGraph response
offices[].criteriarequiredArray of wildcard patterns matched against office names, e.g. Re/Max*. * matches any sequence of characters
offices[].office_addressoptionalNarrows the group to a single office address. Only one address per group
Example Request
POST /users/SetOfficeCriteria
API-Key: your_api_key

{
    "username": "john_doe",
    "offices": [
        {
            "office_name": "Re/Max",
            "office_address": "60 That St",
            "criteria": ["Re/Max*", "re/Max*", "Re/max*"]
        },
        {
            "office_name": "Coldwell Banker",
            "office_address": "24 The Other St",
            "criteria": ["Coldwell*", "coldwell*", "COLDWELL*"]
        }
    ]
}
Example Response
{
    "status": "OK",
    "message": "Office criteria for the user is successfully added."
}

PUT/users/UpdateOfficeCriteriaAPI Key

Replace the wildcard patterns for office groups already set up for a user. The new set replaces the old one entirely. To add a new group, use /users/SetOfficeCriteria instead.

Parameters
NameDescription
usernamerequiredUser the criteria belong to
officesrequiredArray of office objects
offices[].office_idrequiredFrom /users/GetOfficeCriteria
offices[].office_namerequiredName of the office
offices[].criteriarequiredArray of wildcard patterns. Replaces the existing set
offices[].office_addressoptionalSend empty to remove the address criterion
Example Request
PUT /users/UpdateOfficeCriteria
API-Key: your_api_key

{
    "username": "john_doe",
    "offices": [
        {
            "office_id": "1",
            "office_name": "Re/Max",
            "office_address": "60 That St",
            "criteria": ["Re/Max*", "re/Max*", "Re/max*"]
        }
    ]
}
Example Response
{
    "status": "OK",
    "message": "Office criteria for the user is successfully updated."
}

GET/users/GetOfficeCriteriaAPI Key

Retrieve the office criteria set for a user, including the office IDs needed to update or delete them.

Parameters
NameDescription
usernamerequiredUser whose criteria to retrieve
Example Request
GET /users/GetOfficeCriteria?username=test_user
API-Key: your_api_key
Example Response
{
    "status": "OK",
    "result": [
        {
            "office_id": "1",
            "office_name": "Re/Max",
            "office_address": "24 The Other St.",
            "criteria_list": [
                { "criteria_id": "1", "criteria": "Re/Max*" },
                { "criteria_id": "2", "criteria": "re/Max*" },
                { "criteria_id": "3", "criteria": "Re/max*" }
            ]
        }
    ]
}

DELETE/users/DeleteOfficeCriteriaAPI Key

Delete an office and all criteria attached to it.

Parameters
NameDescription
usernamerequiredUsername of the user
office_idrequiredFrom /users/GetOfficeCriteria
Example Request
DELETE /users/DeleteOfficeCriteria
API-Key: your_api_key

{
    "username": "test_user",
    "office_id": 58
}
Example Response
{
    "status": "OK",
    "message": "Office ID was successfully deleted.",
    "office_id": 58
}

Transactions

GET/transactions/AgentsBearer

Agent production for the last 12 months, with optional property, office-history and co-agent detail.

Parameters
NameDescription
database_namerequirede.g. TORONTO ON
rowsoptionalRows to return, max 100
skipoptionalRows to skip
fieldsoptionalComma-separated fields to return
filteroptionale.g. MEMBERNAME eq John Doe
order_columnoptionalField to sort by
sortoptional1 ascending, 2 descending
historical_columnsoptional1 to include lifetime and 5-year columns
property_dataoptional1 to attach a TRANSACTIONS array per agent
property_filteroptionale.g. DATEOFSALE eq 2025-01-01
agent_office_dataoptional1 to attach AGENT_OFFICES, the agent's office by year for up to the past 6 years. How many years are actually present depends on how long we have held the database
agent_office_filteroptionale.g. OFFICE eq Realty Executives Associates
coagent_dataoptional1 to attach COAGENTS, where available
Example Request
GET /transactions/Agents?database_name=OREGON COAST OR&rows=1&filter=MEMBERNAME eq John Doe&historical_columns=1&property_data=1&agent_office_data=1
Authorization: Bearer BearerTokenHere
Example Response
{
    "status": "OK",
    "entires": 50000,
    "result": [
        {
            "MEMBERNAME": "John Doe",
            "MEMBERMLSID": "FAKE-MLS-1234",
            "MEMBERTITLE": "Realtor",
            "MEMBEREMAIL": "fake.email@example.com",
            "MEMBERMOBILEPHONE": "555-000-0000",
            "MEMBERDIRECTPHONE": "555-123-4567",
            "OFFICENAME": "Fictional Brokerage",
            "OFFICEADDRESS1": "123 Fake St",
            "OFFICECITY": "Faketown",
            "OFFICEKEY": "OFFICE-KEY-001",
            "COMPANY": "Fictional Realty",
            "SIDES_LAST_12": "15",
            "TRANSACTIONS_LAST_12": "450",
            "TOTAL_CLOSED_VOLUME_LAST_12": "190000000.00",
            "AVG_CLOSE_PRICE_LAST12": "650000.00",
            "AVGDOM_LAST12": "45.67",
            "LISTSIDEPCT_LAST_12": "8.34",
            "CLOSED_VOL_PERCENTILE": "1.56",
            "RESIDENTIAL_LEASE_COUNT_LAST_12": "12",
            "COMMERCIAL_TRANSACTION_COUNT_LAST_12": "3",
            "LAND_TRANSACTION_COUNT_LAST_12": "1",
            "LAST_LEASE_ACTIVITY_DATE": "2026-05-29",
            "LAST_COMMERCIAL_ACTIVITY_DATE": "",
            "LAST_LAND_ACTIVITY_DATE": "",
            "MLS_TOTAL_CLOSED": "2400",
            "5_YR_TOT_LISTINGS": "1500",
            "AGENT_OFFICES": [
                { "OFFICENAME": "Office 1", "YEAR": 2025, "OFFICE_ID": "OF1" },
                { "OFFICENAME": "Office 2", "YEAR": 2024, "OFFICE_ID": "OF2" }
            ],
            "TRANSACTIONS": [
                {
                    "PROPERTYTYPE": "Residential",
                    "PROPERTYSUBTYPE": "Bungalow 1 Storey",
                    "AGENTROLE": "Listing Agent",
                    "DATEOFSALE": "2025-01-01",
                    "PROPERTYADDRESS": "123 Fake St, Faketown",
                    "PROPERTYSOLDPRICE": "650000",
                    "OFFICENAME": "Fictional Brokerage",
                    "LISTING_ID": "X1234567",
                    "AGENT_ID": "FAKE-MLS-1234",
                    "OFFICE_ID": "OF1"
                }
            ]
        }
    ]
}
Lease, commercial and land activity comes from the MLS RESO feed as six fields: three 12-month counts (RESIDENTIAL_LEASE_COUNT_LAST_12, COMMERCIAL_TRANSACTION_COUNT_LAST_12, LAND_TRANSACTION_COUNT_LAST_12) and the date of the most recent deal of each kind (LAST_LEASE_ACTIVITY_DATE, LAST_COMMERCIAL_ACTIVITY_DATE, LAST_LAND_ACTIVITY_DATE). The count and its date describe the same 12-month window, so a date is empty exactly when its count is 0 and never otherwise. An agent commonly has a lease count with no commercial or land dates at all. The window ends at the database's most recent data date, not today, so on a database that is a few weeks behind, a date can fall more than 12 calendar months in the past and still be inside the counted window. All six exist only on databases with RESO API access. On any other database the keys are absent from the response entirely, rather than returned as 0, and naming one in fields returns 403 Unknown field. Check for the key before reading it.
The field list varies by database. With no fields parameter this endpoint returns the database's columns, so the exact set differs between boards and is wider than the example above. A few internal columns are always withheld, among them ID, PRIMARY_COUNTY, OFFICE_LATITUDE, OFFICE_LONGITUDE and the 1 to 4 year listing totals. Send fields to pin the response to a fixed shape.
Two computed fields are also available: DIST_TO_OFFICE, the distance in kilometres from your configured office to the agent's office, and OWN_COMPANY, 1 when the agent's office name matches your own. Both can be named in fields, filter and order_column. They are computed from the office recorded on your own user account (the office_name and office_address given at /users/CreateUser), not from /users/SetOfficeCriteria, which is a separate setting used by /transactions/MigrationGraph.
When attaching arrays with property_data, agent_office_data or coagent_data, MEMBERNAME must be one of the fields you request. It is included by default; if you send an explicit fields list, add it there too or the request returns 403.
COAGENTS is not available on most databases. Sending coagent_data=1 where the database has no co-agent data fails the whole request with 403 Co-Agents data is not available for this database, rather than omitting the key. Send it only where you know co-agent data exists, and note that property_filter does not apply to it.

GET/transactions/TransactionsHistoricalBearer

Listing-level data for the last 3 years. What a database returns depends on how we receive it from the MLS, and there are two cases - see Data sources below.

Coverage: available for every database you are subscribed to. A database you are not subscribed to returns 403.
Data sources

Both sources return the same field names in the same shape. The difference is which fields carry data, and it is a property of the database, not of the request.

RESO feedMLS download
StatusesEvery status the MLS publishes - active, pending, under contract, sold, expired, withdrawnClosed sales only. Listings that never sold are not included
3-year window measured onLIST_DATECLOSE_DATE
Always empty-UNDER_CONTRACT_DATE, ORIGINAL_PRICE, MODIFICATION_TIMESTAMP, and the four …_KEY fields
LIST_DATE and CDOMAlways presentVaries. Some MLS downloads supply them and some do not, and within a single database the older years often do not – so these two can be populated on recent rows and empty on earlier ones. Where an MLS supplies a list date we cannot trust, it is left empty rather than published
Incremental syncYes, on MODIFICATION_TIMESTAMPNot available. The download carries no per-listing change stamp
Why the window is measured on CLOSE_DATE here, even where a list date exists. A few MLS downloads do supply a usable list date and it is published as LIST_DATE – but it never moves the window, for two reasons.

Coverage. Even on the databases with a clean list date it is typically present only for the most recent years, because the earlier yearly extracts do not carry the column at all. On the best such database it covers under 40% of the window, so a list-date window would silently drop the majority of the rows you are entitled to.

What the data is. This source is closed sales only. Three years of transactions means three years of closings. Measuring on the list date would admit a sale that closed last month after four years on the market, and drop one that closed inside the window but was listed long before it – published list dates on this source reach back several years beyond the window for exactly that reason.

CLOSE_DATE is populated on every row of every MLS-download database, so the window is complete and measured the same way on all of them. Listing and agent/office ..._KEY fields are a separate matter: this source has none, so they are returned empty rather than estimated.
Parameters
NameDescription
database_namerequirede.g. OREGON COAST OR
rowsoptionalRows to return, max 100. Default 100
skipoptionalRows to skip. Default 0. Maximum 100,000 on this endpoint; to read past that, narrow the result set with a filter rather than paging through it (see Reading the whole window below)
fieldsoptionalComma-separated subset of the fields below
filteroptionale.g. STATUS eq Active
order_columnoptionalField to sort by
sortoptional1 ascending, 2 descending
Returned Fields
FieldDescription
PROPERTY_ADDRESSFull street address of the property
CITYCity
STATEState or province
COUNTYCounty or parish
ZIPPostal or ZIP code
PROPERTY_TYPEe.g. Residential, Land, Commercial Sale
PROPERTY_SUB_TYPEe.g. Single Family Residence, Condominium
LIST_DATEDate the listing was entered into the MLS. On MLS-download databases this is present only where that MLS supplies a usable one, and it never moves the 3-year window – see Data sources above
UNDER_CONTRACT_DATEDate an offer was accepted. Empty while still on market. Empty on MLS-download databases
CLOSE_DATEDate the sale closed. Empty unless the listing has closed. Populated on every row of every MLS-download database, and the 3-year window is measured on it for that source – including on the databases that also supply a LIST_DATE
ORIGINAL_PRICEOriginal list price. Some boards do not publish this at all, in which case it is empty on every row
LIST_PRICECurrent or final list price - what the property was asked for
CLOSE_PRICEWhat the property actually sold for. Empty unless the listing has closed. Some MLSs do not distribute sale prices at all - NORTH TEXAS is one, so it is empty on almost every row there
DOMDays on market. Calculated where the MLS does not supply it
CDOMCumulative days on market. Not every MLS publishes it, and on MLS-download databases it can be present for recent years and empty for earlier ones. A negative figure is treated as not reported and returned empty rather than as zero
LISTING_IDMLS listing number
LIST_AGENT_MLS_IDMLS ID of the listing agent
LIST_AGENT_FULL_NAMEName of the listing agent
LIST_AGENT_KEYMLS key of the listing agent
LIST_OFFICE_MLS_IDMLS ID of the listing office
LIST_OFFICE_KEYMLS key of the listing office
LIST_OFFICE_NAMEName of the listing office
LIST_OFFICE_ADDRESSStreet address of the listing office, from the MLS office roster. See the coverage note below
BUYER_AGENT_MLS_IDMLS ID of the buyer's agent. Populated mainly on Closed listings and empty on most others, since a board only publishes it once a deal completes
BUYER_AGENT_FULL_NAMEName of the buyer's agent
BUYER_AGENT_KEYMLS key of the buyer's agent
BUYER_OFFICE_MLS_IDMLS ID of the buyer's office
BUYER_OFFICE_KEYMLS key of the buyer's office
BUYER_OFFICE_NAMEName of the buyer's office
BUYER_OFFICE_ADDRESSStreet address of the buyer's office, from the MLS office roster. See the coverage note below
STATUSListing status. See below
MODIFICATION_TIMESTAMPWhen the MLS last changed this listing. Use it to fetch only what has changed since your last sync. Refreshed monthly. Empty on MLS-download databases, which carry no per-listing change stamp
Identifying a row without an MLS ID. Some MLSs do not publish an agent or office ID. Both sides therefore carry a name and a key as well: *_MLS_ID, *_FULL_NAME and *_KEY for the agent, and the same three plus an address for the office. The IDs, keys and names are populated on essentially every row, so use those to identify an entry and treat the address as supplementary.
Office address coverage varies by database. The address is the one office field that is not on the listing itself; it is looked up from the MLS office roster, so a row gets one only when its office appears there. Measured coverage: NORTH TEXAS 99.7% of listing offices and 88% of buyer offices; WILLAMETTE VALLEY OR 90% / 79%; OREGON DATASHARE OR 84% / 80%; OREGON COAST OR 69% / 67%; MYFLORIDA FL 55% / 33%. Where there is no match the field is empty; every other field on the row is unaffected. The office name, MLS ID and key are unaffected by this and are present on essentially every row, so an entry is still identifiable when the address is blank.
Buyer-side fields are empty on listings that have not sold. An Active or Pending listing has no buyer agent or buyer office yet, so all seven BUYER_* fields come back empty. They fill in once the listing reaches Closed.
Status Values

STATUS is the board's RESO StandardStatus, passed through exactly as the MLS publishes it. The set of values is standard, but spacing varies between boards:

  • Active: on market
  • Active Under Contract or ActiveUnderContract: offer accepted, still showing
  • Pending: under contract
  • Closed: sold
  • Expired: listing period ended
  • Canceled: cancelled by the seller
  • Withdrawn: withdrawn from market
  • Hold: temporarily off market
  • Coming Soon or ComingSoon: published before going active
Filters match exactly, so use the spelling your board uses. MYFLORIDA FL stores Active Under Contract and Coming Soon; NORTH TEXAS stores ActiveUnderContract and ComingSoon. Filtering on the wrong spelling returns an empty result rather than an error. To see the values a board actually uses, request fields=STATUS and page through, or ask us for the list.
Example Request
GET /transactions/TransactionsHistorical?database_name=OREGON COAST OR&rows=1&filter=STATUS eq Active&order_column=LIST_DATE&sort=2
Authorization: Bearer BearerTokenHere
Example Response
{
    "status": "OK",
    "entires": 1657,
    "result": [
        {
            "PROPERTY_ADDRESS": "383 N Lagoon Street, Rockaway Beach, OR 97136",
            "CITY": "Rockaway Beach",
            "STATE": "OR",
            "COUNTY": "Tillamook",
            "ZIP": "97136",
            "PROPERTY_TYPE": "Residential",
            "PROPERTY_SUB_TYPE": "Manufactured On Land",
            "LIST_DATE": "2026-08-29",
            "UNDER_CONTRACT_DATE": "",
            "CLOSE_DATE": "",
            "ORIGINAL_PRICE": "",
            "LIST_PRICE": "374000.00",
            "CLOSE_PRICE": "",
            "DOM": "1",
            "CDOM": "",
            "LISTING_ID": "26-2393",
            "LIST_AGENT_MLS_ID": "sel",
            "LIST_AGENT_FULL_NAME": "Sierra E Lauder",
            "LIST_AGENT_KEY": "20251031170210933290000000",
            "LIST_OFFICE_MLS_ID": "kwcl",
            "LIST_OFFICE_KEY": "20251031170134722494000000",
            "LIST_OFFICE_NAME": "Keller Williams Sunset Corridor - Coast Life",
            "LIST_OFFICE_ADDRESS": "",
            "BUYER_AGENT_MLS_ID": "",
            "BUYER_AGENT_FULL_NAME": "",
            "BUYER_AGENT_KEY": "",
            "BUYER_OFFICE_MLS_ID": "",
            "BUYER_OFFICE_KEY": "",
            "BUYER_OFFICE_NAME": "",
            "BUYER_OFFICE_ADDRESS": "",
            "STATUS": "Active",
            "MODIFICATION_TIMESTAMP": "2026-08-30 09:14:52"
        }
    ]
}

The example above is an Active listing, so every BUYER_* field is empty: there is no buyer yet. On a Closed listing both sides are filled:

{
    "LISTING_ID": "LC-100425",
    "STATUS": "Closed",
    "CLOSE_DATE": "2026-02-14",
    "LIST_PRICE": "525000.00",
    "CLOSE_PRICE": "509000.00",
    "LIST_AGENT_MLS_ID": "Illt",
    "LIST_AGENT_FULL_NAME": "Laura Gillott",
    "LIST_AGENT_KEY": "20251031144251293986000000",
    "LIST_OFFICE_MLS_ID": "KWM",
    "LIST_OFFICE_KEY": "20251031144139664175000000",
    "LIST_OFFICE_NAME": "Keller Williams Realty Mid-Willamette",
    "BUYER_AGENT_MLS_ID": "BLP",
    "BUYER_AGENT_FULL_NAME": "Barbara D Le Pine",
    "BUYER_AGENT_KEY": "20251031151704949407000000",
    "BUYER_OFFICE_MLS_ID": "ACRE",
    "BUYER_OFFICE_KEY": "20251031144150169564000000",
    "BUYER_OFFICE_NAME": "Advantage Real Estate"
}

Fields omitted above are unchanged from the first example.

An MLS-download database returns the same fields, but every row is a closed sale and the fields that source does not supply come back empty:

{
    "LISTING_ID": "26-799",
    "PROPERTY_ADDRESS": "Lakewinds Rd 409",
    "CITY": "Alexander City",
    "STATE": "",
    "COUNTY": "ELMORE",
    "ZIP": "35010",
    "PROPERTY_TYPE": "Single Family Residence",
    "PROPERTY_SUB_TYPE": "",
    "LIST_DATE": "",
    "UNDER_CONTRACT_DATE": "",
    "CLOSE_DATE": "2026-08-06",
    "ORIGINAL_PRICE": "",
    "LIST_PRICE": "1695000.00",
    "CLOSE_PRICE": "1600000.00",
    "DOM": "36",
    "CDOM": "",
    "STATUS": "Closed",
    "LIST_AGENT_MLS_ID": "DAMON STORY",
    "LIST_AGENT_FULL_NAME": "DAMON STORY",
    "LIST_AGENT_KEY": "",
    "LIST_OFFICE_NAME": "Lake Martin Realty Willow Point",
    "LIST_OFFICE_ADDRESS": "",
    "LIST_OFFICE_KEY": "",
    "BUYER_AGENT_MLS_ID": "Judith Jager",
    "BUYER_AGENT_FULL_NAME": "Judith Jager",
    "BUYER_AGENT_KEY": "",
    "BUYER_OFFICE_NAME": "Lake Martin Realty Willow Point",
    "BUYER_OFFICE_KEY": "",
    "MODIFICATION_TIMESTAMP": ""
}
Agent IDs on databases without usable ones. A few MLS downloads either publish no agent ID at all, or publish one that does not identify an individual agent - a branch or group code reused across many people. On those databases LIST_AGENT_MLS_ID, BUYER_AGENT_MLS_ID and AGENT_MEMBER_ID all carry the agent's name instead, which is the only identifier that distinguishes them. Publishing the unusable code would merge unrelated agents into a single record.

The choice is made per database and applied consistently across both endpoints, so LIST_AGENT_MLS_ID and BUYER_AGENT_MLS_ID always join to AGENT_MEMBER_ID in /agents/AgentsHistorical whichever form a database uses. Matching is case-insensitive. Most MLS-download databases publish proper agent IDs and carry them as usual.

STATE and LIST_OFFICE_ADDRESS are empty above because this MLS did not report them on that listing, not because the field is unsupported.
Reading the whole window

This endpoint holds up to about 1.2 million listings, and skip is capped at 100,000. You reach the rest by filtering, not by paging deeper: take one month at a time and page within it. The busiest single month on the largest database is about 38,000 listings, so a month always fits inside the cap with room to spare.

for each month in your 3-year window:
    skip = 0
    repeat:
        GET /transactions/TransactionsHistorical?database_name=MYFLORIDA FL
            &filter=LIST_DATE ge 2024-01-01 and LIST_DATE lt 2024-02-01
            &order_column=LIST_DATE&sort=1&rows=100&skip={skip}
        skip = skip + 100
    until fewer than 100 rows come back

This is the same number of requests as paging straight through, but every request stays fast. A request at skip=1,000,000 makes the database walk past a million rows before returning anything and takes over ten times as long as the first page; a month slice never does that.

Page each month in one pass rather than pausing for hours between requests. The underlying data is rebuilt when the database refreshes, and a slice paged across a refresh can repeat or miss rows. Month-sized slices finish quickly enough that this is not a practical concern.
/agents/AgentsHistorical has no such cap. It is far smaller, so page it straight through with skip and no filter.
Keeping a copy in sync
RESO-feed databases only. MLS-download databases carry no per-listing change stamp, so MODIFICATION_TIMESTAMP is empty on every row and cannot be used as a sync cursor. Re-read those with a CLOSE_DATE range instead, as described under Reading the whole window.

Once you hold a full copy, never re-download it to pick up changes. Fetch only what has changed since your last run, using MODIFICATION_TIMESTAMP:

GET /transactions/TransactionsHistorical?database_name=MYFLORIDA FL&rows=100&filter=MODIFICATION_TIMESTAMP ge 2026-08-30 00:00:00&order_column=MODIFICATION_TIMESTAMP&sort=1
Authorization: Bearer BearerTokenHere

Store the highest MODIFICATION_TIMESTAMP you received and use it as the starting point next time. A listing whose status moves from Active to Pending to Closed gets a new timestamp each time, so this catches updates to listings you already hold as well as new ones.

ApproachRequestsTypical time
Re-download the full 3-year window~11,500hours
Daily sync on MODIFICATION_TIMESTAMP~10seconds

Figures are for MYFLORIDA FL, the largest database, which carries about 1.15 million listings in the window and sees roughly 800 change on an average day.

How often to sync. We refresh from the MLS feeds monthly, in the first week of each month. MODIFICATION_TIMESTAMP does not advance between refreshes, so polling more often than weekly returns nothing new and spends your rate limit for no result. Syncing once after the first week of the month is enough.
Paging the first full copy: use date ranges rather than a large skip. A request with skip=1000000 makes the database walk past a million rows before returning anything, and gets slower the deeper you go. Pulling a month at a time keeps every request fast and is roughly ten times cheaper overall.

Slice on LIST_DATE for a RESO-feed database (filter=LIST_DATE ge 2024-01-01 and LIST_DATE lt 2024-02-01) and on CLOSE_DATE for an MLS-download one (filter=CLOSE_DATE ge 2024-01-01 and CLOSE_DATE lt 2024-02-01). Slicing an MLS-download database on LIST_DATE will silently miss rows: on those databases that field is empty on some or all rows, so a date range over it cannot cover the window. CLOSE_DATE is populated on every row of every MLS-download database. Both columns are indexed, so either is fast on the database it applies to.
Finding an agent's listings: an agent can appear on either side of a deal. Query LIST_AGENT_MLS_ID and BUYER_AGENT_MLS_ID separately and combine the results. Both join to AGENT_MEMBER_ID in /agents/AgentsHistorical.

GET/transactions/InactiveAgentsBearer

MLS members with no transactions in the last 12 months, taken from the MLS member roster.

Coverage: available for roughly half of the databases, since it depends on the MLS publishing a full member roster. Where it is not available the request returns 403 Inactive agents list is not available for <database>.
Parameters
NameDescription
database_namerequirede.g. TORONTO ON
rowsoptionalRows to return, max 100
skipoptionalRows to skip
fieldsoptionalComma-separated fields to return
filteroptionale.g. MEMBERNAME eq John Doe
order_columnoptionalField to sort by
sortoptional1 ascending, 2 descending
Example Request
GET /transactions/InactiveAgents?database_name=TORONTO ON&rows=1
Authorization: Bearer BearerTokenHere
Example Response
{
    "status": "OK",
    "entires": 10000,
    "result": [
        {
            "MEMBERID": "FAKE-MLS-1234",
            "MEMBERNAME": "John Doe",
            "MEMBERTITLE": "Realtor",
            "EMAIL": "fake.email@example.com",
            "MEMBERMOBILEPHONE": "000-000-0000",
            "MEMBERDIRECTPHONE": "000-000-0000",
            "OFFICEID": "OFFICE-ID-001",
            "OFFICENAME": "Fictional Brokerage",
            "OFFICEADDRESS": "123 Fake St",
            "OFFICECITY": "Faketown",
            "OFFICEPOSTALCODE": "F1K 2B3",
            "OFFICESTATE": "BC",
            "HOME_ADDRESS_AVAILABLE": "123 Fake St"
        }
    ]
}

GET/transactions/MigrationGraphBearer

Agents gained and lost for each office group configured for the user, counted from brokerage moves recorded in the database. Offices matching none of the groups are aggregated under others.

Prerequisite: office criteria must be set via /users/SetOfficeCriteria first, or this returns 403.
Window and population: counts cover the last 12 months and include only agents who appear at more than one office in that period. An agent who stayed put is not counted on either side, so gained and lost describe movement between offices rather than headcount. Totals across all groups are close to balanced by construction: one agent's move is another office's loss.
Parameters
NameDescription
database_namerequirede.g. TORONTO ON
Example Request
GET /transactions/MigrationGraph?database_name=TORONTO ON
Authorization: Bearer BearerTokenHere
Example Response
{
    "Re/Max":          { "gained": 27,  "lost": 50  },
    "Coldwell Banker": { "gained": 210, "lost": 241 },
    "others":          { "gained": 600, "lost": 596 }
}

Agents

GET/agents/AgentsHistoricalBearer

Agents with activity in the last 3 years. Includes agents who have since left the MLS, which the member roster no longer carries. Both sides of a deal count, so an agent who only ever represented buyers is still listed.

Coverage: available for every database you are subscribed to. A database you are not subscribed to returns 403. An agent is included when their most recent activity falls inside the 3-year window - their most recent listing on a RESO-feed database, their most recent closed sale on an MLS-download one.
On MLS-download databases: OFFICE_KEY is always empty, and where the MLS publishes no agent IDs at all AGENT_MEMBER_ID carries the agent's name instead, because it is the only identifier that database has. It still joins to LIST_AGENT_MLS_ID and BUYER_AGENT_MLS_ID in /transactions/TransactionsHistorical, which carry the same value.
Parameters
NameDescription
database_namerequirede.g. OREGON COAST OR
rowsoptionalRows to return, max 100. Default 100
skipoptionalRows to skip. Default 0
fieldsoptionalComma-separated subset of the fields below
filteroptionale.g. OFFICE_NAME eq RealtyNET, LLC
order_columnoptionalField to sort by
sortoptional1 ascending, 2 descending
Returned Fields
FieldDescription
AGENT_MEMBER_IDMLS member ID. The identifier agents see in their own MLS. On MLS-download databases that publish no agent IDs, this is the agent's name instead
AGENT_NAMEFull name
AGENT_EMAILEmail address, where published
AGENT_MOBILEBest available phone for the agent. Taken from their most recent listing, preferring direct, then mobile, then preferred, then office; falling back to the member roster's mobile, preferred or direct number. It is not guaranteed to be a mobile line
AGENT_OFFICEMLS office ID of the agent's office
OFFICE_KEYMLS office key. Empty for a small share of agents, where neither the listing nor the office roster carries one
OFFICE_NAMEOffice name
OFFICE_ADDRESSStreet address of the office, from the MLS office roster. Empty where that roster has no entry for the office
Example Request
GET /agents/AgentsHistorical?database_name=OREGON COAST OR&rows=1&order_column=AGENT_NAME&sort=1
Authorization: Bearer BearerTokenHere
Example Response
{
    "status": "OK",
    "entires": 640,
    "result": [
        {
            "AGENT_MEMBER_ID": "AHE",
            "AGENT_NAME": "Aaron Heath",
            "AGENT_EMAIL": "aaron.heath@exprealty.com",
            "AGENT_MOBILE": "503-664-6134",
            "AGENT_OFFICE": "exp",
            "OFFICE_KEY": "20251031170128612353000000",
            "OFFICE_NAME": "eXp Realty LLC",
            "OFFICE_ADDRESS": "1321 NE Broadway, Portland, OR, 97232"
        }
    ]
}
Contact details come from the agent's own listings first. Each field is taken independently, from the most recent listing that carries a value for it, so an agent's email and phone can come from two different listings. The MLS member roster is used only for fields no listing supplies at all. This favours the most recent thing the agent actually published over the roster, which is often staler, and it is why agents who have left the MLS still have contact details here. Any field can be empty where neither source publishes it.

Filter Operators

Building a filter

Filters take the form FIELD operator value. Combine conditions with and.

filter=MEMBERNAME eq John Doe
filter=STATUS eq Active and LIST_PRICE ge 500000
filter=PRODUCTION_ZIP_CODE mc 63303,63376
Operators
OperatorMeaningExample
eqEqualsSTATUS eq Closed
neNot equalsSTATUS ne Expired
gtGreater thanDOM gt 90
geGreater than or equalLIST_PRICE ge 500000
ltLess thanDOM lt 30
leLess than or equalLIST_PRICE le 250000
mcMatches any of a listZIP mc 97391,97365
Only and is supported. The word or is accepted but treated as and, so it never widens a result set. To combine alternatives, make one request per condition and merge the results, or use mc where you are matching one field against several values.
Values may contain and or or. OFFICE_NAME eq Better Homes and Gardens Realty Partners is read as a single condition, not as two. A word is only treated as a separator when what follows it is itself a complete FIELD operator value condition.
Quoting and special characters

Quotes around a value are optional and are stripped: OFFICE_NAME eq "RealtyNET, LLC" and OFFICE_NAME eq RealtyNET, LLC are the same request. Apostrophes are preserved, so MEMBERNAME eq John O'Brien matches that agent. Commas are literal except after mc, where they separate the list.

Field names

On /transactions/TransactionsHistorical and /agents/AgentsHistorical, use the field names documented for that endpoint. Case does not matter: STATUS eq Active and status eq Active behave identically.

Naming a field the endpoint does not publish returns 403, with a message that depends on where the name appeared: Unknown filter NAME from filter, and Unknown field NAME from fields or order_column.

sort is required whenever order_column is sent. Sending order_column on its own returns 403 Invalid sort parameter. Sending sort on its own is ignored.
On /transactions/Agents, /transactions/InactiveAgents and /databases/MLSList, order_column must name a field you also asked for. If you send fields, include the sort field in that list or the request returns 403 Unknown field, even for a column the endpoint really publishes.
On /transactions/Agents, the five historical columns (MLS_TOTAL_CLOSED, FIRST_TRANS_DATE, FIRST_TRANS_DATE_CURRENT_OFFICE, 5_YR_TOT_LISTINGS, 5_YR_CLOSED_PCT) also require historical_columns=1. Naming one in fields or order_column without it returns 403.
Numeric field names

Some fields on /transactions/Agents contain # or $. Write these as _units and _dollars in a filter. Use yrchange_units for YRCHANGE#, pct_change_dollars for PCT_CHANGE$.