API Documentation
Access agent, office and listing data across subscribed MLS databases.
https://api.restats.comGetting Started
Authentication
The API uses two credential types. Which one you send depends on the endpoint.
| Credential | Header | Used by |
|---|---|---|
| API Key | API-Key: your_api_key |
/authorization/*, /users/*, /databases/* |
| Bearer | Authorization: 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
}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
| Endpoint | Per minute | Per hour |
|---|---|---|
| Most endpoints | 120 | 6,000 |
/transactions/TransactionsHistorical | 100 | 5,000 |
/agents/AgentsHistorical | 100 | 5,000 |
The two historical endpoints read far larger tables, so they are limited more tightly.
Per IP address
| Per minute | Per hour | |
|---|---|---|
| All endpoints | 400 | 20,000 |
Response Headers
| Header | Meaning |
|---|---|
Retry-After | Seconds until the current window resets |
X-RateLimit-Limit | The limit that was exceeded |
X-RateLimit-Remaining | Always 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."
}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.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
| Code | Meaning | What to do |
|---|---|---|
200 | Success. The body carries status,
entires and result | Read the results |
401 | The 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 |
403 | The 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 |
429 | A rate limit was exceeded | Wait for Retry-After, then retry. See
Rate Limits |
500 | Something failed on our side | Retry once. If it persists, send us the request and the time |
503 | The 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 |
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./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
| Name | Description | |
|---|---|---|
username | required | Username to be logged in |
user_ip | required | IP 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"
}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.
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.rows or skip, and is not paged.Parameters
| Name | Description | |
|---|---|---|
fields | optional | Comma-separated fields to return |
filter | optional | e.g. DATABASE_NAME eq TORONTO ON |
order_column | optional | Field to sort by |
sort | optional | 1 ascending, 2 descending |
Example Request
GET /databases/MLSList?filter=DATABASE_NAME eq TORONTO ON&order_column=DATABASE_NAME&sort=2
API-Key: your_api_keyExample 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
| Name | Description | |
|---|---|---|
full_name | required | Full name of the user |
office_name | required | Office name |
office_address | required | Office address |
city | required | City |
state | required | Two-letter state or province code, e.g. ON |
postal_code | required | Postal or ZIP code |
country | required | USA or Canada |
mls_name | required | Array of MLS names |
mls_member_id | required | Array of MLS member IDs |
mls_office_id | required | Array of MLS office IDs |
database_name | required | 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. |
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.
/users/AssignDatabases when the user is reactivated;
reactivating on its own does not restore them.activate,
or sends a value this endpoint does not recognise, deactivates the user and returns 200. Always
send the parameter explicitly.Parameters
| Name | Description | |
|---|---|---|
username | required | Username of the user |
activate | optional | 1 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.
/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
| Name | Description | |
|---|---|---|
username | required | Username of the user |
database_name | required | Array of database names |
mls_name | required | Array of MLS names. Not needed when disable_database is 1 |
mls_member_id | required | Array of MLS member IDs. Not needed when disabling |
mls_office_id | required | Array of MLS office IDs. Not needed when disabling |
disable_database | optional | 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
| Name | Description | |
|---|---|---|
username | required | User the criteria belong to |
offices | required | Array of office objects |
offices[].office_name | required | Label for the group. Appears as the key in the MigrationGraph response |
offices[].criteria | required | Array of wildcard patterns matched against office names, e.g. Re/Max*. * matches any sequence of characters |
offices[].office_address | optional | Narrows 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
| Name | Description | |
|---|---|---|
username | required | User the criteria belong to |
offices | required | Array of office objects |
offices[].office_id | required | From /users/GetOfficeCriteria |
offices[].office_name | required | Name of the office |
offices[].criteria | required | Array of wildcard patterns. Replaces the existing set |
offices[].office_address | optional | Send 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
| Name | Description | |
|---|---|---|
username | required | User whose criteria to retrieve |
Example Request
GET /users/GetOfficeCriteria?username=test_user
API-Key: your_api_keyExample 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
| Name | Description | |
|---|---|---|
username | required | Username of the user |
office_id | required | From /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
| Name | Description | |
|---|---|---|
database_name | required | e.g. TORONTO ON |
rows | optional | Rows to return, max 100 |
skip | optional | Rows to skip |
fields | optional | Comma-separated fields to return |
filter | optional | e.g. MEMBERNAME eq John Doe |
order_column | optional | Field to sort by |
sort | optional | 1 ascending, 2 descending |
historical_columns | optional | 1 to include lifetime and 5-year columns |
property_data | optional | 1 to attach a TRANSACTIONS array per agent |
property_filter | optional | e.g. DATEOFSALE eq 2025-01-01 |
agent_office_data | optional | 1 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_filter | optional | e.g. OFFICE eq Realty Executives Associates |
coagent_data | optional | 1 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 BearerTokenHereExample 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"
}
]
}
]
}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.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.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.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.
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 feed | MLS download | |
|---|---|---|
| Statuses | Every status the MLS publishes - active, pending, under contract, sold, expired, withdrawn | Closed sales only. Listings that never sold are not included |
| 3-year window measured on | LIST_DATE | CLOSE_DATE |
| Always empty | - | UNDER_CONTRACT_DATE, ORIGINAL_PRICE, MODIFICATION_TIMESTAMP, and the four …_KEY fields |
LIST_DATE and CDOM | Always present | Varies. 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 sync | Yes, on MODIFICATION_TIMESTAMP | Not available. The download carries no per-listing change stamp |
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
| Name | Description | |
|---|---|---|
database_name | required | e.g. OREGON COAST OR |
rows | optional | Rows to return, max 100. Default 100 |
skip | optional | Rows 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) |
fields | optional | Comma-separated subset of the fields below |
filter | optional | e.g. STATUS eq Active |
order_column | optional | Field to sort by |
sort | optional | 1 ascending, 2 descending |
Returned Fields
| Field | Description |
|---|---|
PROPERTY_ADDRESS | Full street address of the property |
CITY | City |
STATE | State or province |
COUNTY | County or parish |
ZIP | Postal or ZIP code |
PROPERTY_TYPE | e.g. Residential, Land, Commercial Sale |
PROPERTY_SUB_TYPE | e.g. Single Family Residence, Condominium |
LIST_DATE | Date 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_DATE | Date an offer was accepted. Empty while still on market. Empty on MLS-download databases |
CLOSE_DATE | Date 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_PRICE | Original list price. Some boards do not publish this at all, in which case it is empty on every row |
LIST_PRICE | Current or final list price - what the property was asked for |
CLOSE_PRICE | What 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 |
DOM | Days on market. Calculated where the MLS does not supply it |
CDOM | Cumulative 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_ID | MLS listing number |
LIST_AGENT_MLS_ID | MLS ID of the listing agent |
LIST_AGENT_FULL_NAME | Name of the listing agent |
LIST_AGENT_KEY | MLS key of the listing agent |
LIST_OFFICE_MLS_ID | MLS ID of the listing office |
LIST_OFFICE_KEY | MLS key of the listing office |
LIST_OFFICE_NAME | Name of the listing office |
LIST_OFFICE_ADDRESS | Street address of the listing office, from the MLS office roster. See the coverage note below |
BUYER_AGENT_MLS_ID | MLS 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_NAME | Name of the buyer's agent |
BUYER_AGENT_KEY | MLS key of the buyer's agent |
BUYER_OFFICE_MLS_ID | MLS ID of the buyer's office |
BUYER_OFFICE_KEY | MLS key of the buyer's office |
BUYER_OFFICE_NAME | Name of the buyer's office |
BUYER_OFFICE_ADDRESS | Street address of the buyer's office, from the MLS office roster. See the coverage note below |
STATUS | Listing status. See below |
MODIFICATION_TIMESTAMP | When 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 |
*_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.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.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 marketActive Under ContractorActiveUnderContract: offer accepted, still showingPending: under contractClosed: soldExpired: listing period endedCanceled: cancelled by the sellerWithdrawn: withdrawn from marketHold: temporarily off marketComing SoonorComingSoon: published before going active
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 BearerTokenHereExample 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": ""
}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 backThis 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.
/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
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 BearerTokenHereStore 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.
| Approach | Requests | Typical time |
|---|---|---|
| Re-download the full 3-year window | ~11,500 | hours |
Daily sync on MODIFICATION_TIMESTAMP | ~10 | seconds |
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.
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.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.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.
403 Inactive agents list is not available for <database>.Parameters
| Name | Description | |
|---|---|---|
database_name | required | e.g. TORONTO ON |
rows | optional | Rows to return, max 100 |
skip | optional | Rows to skip |
fields | optional | Comma-separated fields to return |
filter | optional | e.g. MEMBERNAME eq John Doe |
order_column | optional | Field to sort by |
sort | optional | 1 ascending, 2 descending |
Example Request
GET /transactions/InactiveAgents?database_name=TORONTO ON&rows=1
Authorization: Bearer BearerTokenHereExample 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.
/users/SetOfficeCriteria first, or this returns 403.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
| Name | Description | |
|---|---|---|
database_name | required | e.g. TORONTO ON |
Example Request
GET /transactions/MigrationGraph?database_name=TORONTO ON
Authorization: Bearer BearerTokenHereExample 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.
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.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
| Name | Description | |
|---|---|---|
database_name | required | e.g. OREGON COAST OR |
rows | optional | Rows to return, max 100. Default 100 |
skip | optional | Rows to skip. Default 0 |
fields | optional | Comma-separated subset of the fields below |
filter | optional | e.g. OFFICE_NAME eq RealtyNET, LLC |
order_column | optional | Field to sort by |
sort | optional | 1 ascending, 2 descending |
Returned Fields
| Field | Description |
|---|---|
AGENT_MEMBER_ID | MLS 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_NAME | Full name |
AGENT_EMAIL | Email address, where published |
AGENT_MOBILE | Best 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_OFFICE | MLS office ID of the agent's office |
OFFICE_KEY | MLS office key. Empty for a small share of agents, where neither the listing nor the office roster carries one |
OFFICE_NAME | Office name |
OFFICE_ADDRESS | Street 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 BearerTokenHereExample 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"
}
]
}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,63376Operators
| Operator | Meaning | Example |
|---|---|---|
eq | Equals | STATUS eq Closed |
ne | Not equals | STATUS ne Expired |
gt | Greater than | DOM gt 90 |
ge | Greater than or equal | LIST_PRICE ge 500000 |
lt | Less than | DOM lt 30 |
le | Less than or equal | LIST_PRICE le 250000 |
mc | Matches any of a list | ZIP mc 97391,97365 |
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.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./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./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$.