IMS API Documentation
RESTful API for accessing property, member, and office data with OData query support
Overview
The IMS API provides programmatic access to Multiple Listing Service (MLS) data including properties, members, offices, and database information. The API follows RESTful principles and supports OData query parameters for flexible data retrieval.
Key Features
- RESTful Design - Simple, predictable resource URLs
- OData Support - Powerful querying with $filter, $select, $orderby, $top, and $skip
- JWT Authentication - Secure token-based authentication
- JSON Responses - Standard JSON format for all responses
- Rate Limiting - Built-in request tracking and limits
Base URL
https://ims-api.restats.com
Quick Start
- Obtain your API credentials (client_id and client_secret)
- Generate an access token using the /authorization/token endpoint
- Make requests to /v1/{resource} endpoints with your token
- Parse the JSON response
Authentication
The IMS API uses JWT (JSON Web Token) Bearer authentication. You must include a valid access token in the Authorization header of all API requests.
Getting an Access Token
To obtain an access token, make a POST request to the token endpoint with your credentials:
Endpoint
| Method | Endpoint | Description |
|---|---|---|
| POST | /authorization/token |
Generate access token |
Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
client_id |
string | Yes | Your API credential ID |
client_secret |
string | Yes | Your API credential password |
scope |
string | Yes | Must be "api" |
grant_type |
string | Yes | Must be "client_credentials" |
Example Request
curl -X POST https://ims-api.restats.com/authorization/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET" \
-d "scope=api" \
-d "grant_type=client_credentials"
Success Response (200 OK)
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
"token_type": "Bearer",
"expires_in": 9000,
"scope": "api"
}
Using the Access Token
Include the access token in the Authorization header of all API requests:
Authorization: Bearer YOUR_ACCESS_TOKEN
Token Expiration
[!] Token Lifetime: Access tokens expire after 150 minutes of inactivity.
Rate Limits
The API enforces rate limits and request quotas based on your account type. Contact your administrator for specific limits applicable to your credentials.
Endpoints
Token Generation
| Method | Endpoint | Auth Required | Description |
|---|---|---|---|
| POST | /authorization/token |
No | Generate JWT access token |
Data Retrieval
| Method | Endpoint | Auth Required | Description |
|---|---|---|---|
| GET | /v1/Property |
Yes | Retrieve property listings |
| GET | /v1/Member |
Yes | Retrieve member (agent) data |
| GET | /v1/Office |
Yes | Retrieve office data |
| GET | /v1/Database |
Yes | Retrieve available databases |
Request Format
GET /v1/{resource}?$filter={filter}&$select={fields}&$top={limit} HTTP/1.1
Host: ims-api.restats.com
Authorization: Bearer YOUR_ACCESS_TOKEN
Resources
The API provides access to four main resources. Each resource returns data from specific database tables.
Property Resource
Access property listing data including prices, addresses, and related agent/office information.
Endpoint: GET /v1/Property
Available Fields
| Field | Type | Description |
|---|---|---|
OriginatingSystemName |
string | Required in $filter. Specifies which database to query. Not a database column - use only in $filter parameter |
ListingId |
string | The well known identifier for the listing (Primary Key) |
UnparsedAddress |
string | The full street address of the property in an unparsed format |
ListPrice |
number | The current price of the property as determined by the seller and the seller's broker |
ClosePrice |
number | The amount of money paid by the purchaser to the seller for the property under the agreement |
CloseDate |
date | The date the purchase agreement was fulfilled or lease requirements were met |
ListAgentMlsId |
string | The local, well-known identifier for the listing agent |
BuyerAgentMlsId |
string | The local, well-known identifier for the buyer agent |
ListOfficeMlsId |
string | The local, well-known identifier for the listing office |
BuyerOfficeMlsId |
string | The local, well-known identifier for the buyer office |
ListOfficeEmail |
string | The email address of the listing office |
BuyerOfficeEmail |
string | The email address of the buyer's office |
ListAgentKey |
string | The unique identifier for the listing agent (foreign key to Member resource) |
BuyerAgentKey |
string | The unique identifier for the buyer's agent (foreign key to Member resource) |
CoListAgentKey |
string | The unique identifier for the co-listing agent (foreign key to Member resource) |
CoBuyerAgentKey |
string | The unique identifier for the co-buyer's agent (foreign key to Member resource) |
PropertyType |
string | The type of property (e.g., Residential, Commercial, Land) |
ModificationTimestamp |
timestamp | Date/time the property record was last modified |
Member Resource
Access agent and member information including contact details and office affiliations.
Endpoint: GET /v1/Member
Available Fields
| Field | Type | Description |
|---|---|---|
OriginatingSystemName |
string | Required in $filter. Specifies which database to query. Not a database column - use only in $filter parameter |
MemberFullName |
string | The full name of the Member. (First Middle Last) or a alternate full name (Primary Key) |
MemberEmail |
string | The email address of the Member |
MemberMlsId |
string | The local, well-known identifier for the member (Primary Key) |
OfficeMlsId |
string | The local, well-known identifier for the associated office |
OfficeName |
string | The legal name of the brokerage |
MemberMobilePhone |
string | Member's mobile phone number (format: ###-###-####) |
MemberPreferredPhone |
string | Member's preferred contact phone number (format: ###-###-####) |
MemberDirectPhone |
string | Member's direct phone line (format: ###-###-####) |
MemberLicenseNumber |
string | The real estate license number of the member |
MemberStateLicense |
string | The license of the Member. Separate multiple licenses with a comma and space |
MemberKey |
string | The unique identifier for the member |
OfficeKey |
string | The unique identifier for the associated office (foreign key to Office resource) |
MemberFirstName |
string | The first name of the member |
MemberLastName |
string | The last name of the member |
MemberCity |
string | The city of the member's address |
MemberAddress1 |
string | The first line of the member's address |
MemberAddress2 |
string | The second line of the member's address |
MemberPostalCode |
string | The postal code of the member's address |
MemberStateOrProvince |
string | The state or province of the member's address |
MemberType |
string | The type/category of member |
MemberMLSSecurityClass |
string | The MLS security class of the member |
ModificationTimestamp |
timestamp | Date/time the roster (member or office) record was last modified |
Office Resource
Access real estate office information including addresses and contact details.
Endpoint: GET /v1/Office
Available Fields
| Field | Type | Description |
|---|---|---|
OriginatingSystemName |
string | Required in $filter. Specifies which database to query. Not a database column - use only in $filter parameter |
OfficeMlsId |
string | The local, well-known identifier for the office |
OfficeName |
string | The legal name of the brokerage (Primary Key) |
OfficeAddress |
string | The street number, direction, name and suffix of the office (Primary Key) |
OfficePostalCode |
string | The postal code of the office |
OfficeCity |
string | The city of the office |
OfficePhone |
string | Office phone number (format: ###-###-####) |
OfficeBrokerKey |
string | The MemberKey of the responsible/owning broker (foreign key to Member resource) |
OfficeBrokerMlsId |
string | The MemberMlsId of the responsible/owning broker |
OfficeManagerKey |
string | The lead Office Manager for the given office (foreign key to Member resource) |
OfficeManagerMlsId |
string | The lead Office Manager for the given office |
OfficeEmail |
string | The email address of the office |
OfficeStatus |
string | The current status of the office |
OfficeStateOrProvince |
string | The state or province of the office |
MainOfficeKey |
string | The unique identifier for the main/parent office |
MainOfficeName |
string | The name of the main/parent office |
OfficeKey |
string | The unique identifier for the office |
ModificationTimestamp |
timestamp | Date/time the roster (member or office) record was last modified |
Database Resource
Retrieve list of available MLS databases you have access to.
Endpoint: GET /v1/Database
Available Fields
| Field | Type | Description |
|---|---|---|
MLSID |
integer | Database ID (Primary Key, Auto-increment) |
MLSName |
string | Database name (Primary Key) |
MLSLegalName |
string | Full legal name of MLS |
UpdateDate |
datetime | Last update timestamp |
OriginatingSystemName |
string | The originating system identifier used in Property, Member, and Office API queries |
[i] Important: The Database resource does not require OriginatingSystemName in the $filter parameter. Instead, query this endpoint first to retrieve the list of available databases. Use the OriginatingSystemName field from the response in your Property, Member, and Office queries.
Submitting MLS Credentials
API access is provided in exchange for MLS credentials. Look up the MLS you belong to, then submit the credentials for it. Every submission is reviewed, and a database is granted to your subscription once the review passes. The sections that follow cover checking a request, seeing what your subscription covers, and managing your users.
Endpoints:
GET /v1/MlsCatalog and
POST /v1/Credentials
[i] Use MlsKey. It is exact
and stable, so you can look an MLS up once, store its key,
and keep submitting against it. The other forms
(MlsName, MlsShortCode,
MlsCatalogId) remain supported for
convenience and for existing integrations.
Step 1 - Find your MLS
GET /v1/MlsCatalog returns every MLS we can
accept credentials for - all of them by default, so there is
no paging to do. Narrow it with $search,
$state and $top.
[i] $search also matches the
database. Searching a board name finds that one
board; searching a Database value finds every
MLS that feeds it. $search=TORONTO returns all
ten Ontario boards behind TORONTO ON - Barrie,
Brampton, Durham, Kawartha Lakes, London & St Thomas,
Northumberland, Peterborough, Quinte, Timmins and Toronto
itself - not just the one with "Toronto" in its name. Use
it to check whether your board is already covered.
curl -X GET "https://ims-api.restats.com/v1/MlsCatalog?\$search=Aiken" \
-H "Authorization: Bearer $ACCESS_TOKEN"
{
"count": 1,
"value": [
{
"MlsKey": 572,
"MlsCatalogId": 572,
"MlsName": "UnlockMLS - Austin Board of Realtors",
"MlsShortCode": "ABOR",
"City": "Austin",
"State": "TX",
"Database": "AUSTIN TX",
"Mapped": true
}
]
}
Response Fields
| Field | Type | Description |
|---|---|---|
MlsKey |
integer | The identifier to store and send.
Stable: it is preserved across the nightly
catalog rebuild, so it means the same MLS
indefinitely. Use this in the
mls object when submitting
credentials. |
MlsCatalogId |
integer | Internal row id. Valid for the current day only - the catalog is rebuilt nightly and this is reassigned. Do not store it. Accepted when submitting, for convenience in the same session as the lookup. |
MlsName |
string | The MLS or board name. Accepted as an
alternative to MlsKey. |
MlsShortCode |
string | Short code, where one exists. Not
unique - for example
BRIGHT covers six states - so
always pair it with State. |
City / State |
string | Location, used to disambiguate names that repeat. |
Database |
string | The internal database this MLS feeds, or
null if none yet. Several MLSs
commonly share one - ten Ontario boards feed
TORONTO ON, fifteen feed
ITSO ON - so this tells you your
local board is part of a wider dataset. |
Mapped |
boolean | false means no data has been
compiled for this MLS yet. Credentials for it
are still accepted and queued, but access
cannot be granted until the data is built. |
Step 2 - Submit credentials
POST /v1/Credentials takes the same information
as our broker form. Send one entry per MLS you hold
credentials for.
{
"FirstName": "Jane",
"LastName": "Avery",
"OfficeName": "Avery Realty Group",
"OfficeAddress": "100 Congress Ave",
"City": "Austin",
"StateCode": "TX",
"EthicsAccepted": true,
"credentials": [
{
"MlsName": "UnlockMLS - Austin Board of Realtors",
"MlsId": "ABOR-44821",
"MlsWebAddress": "https://matrix.abor.com",
"Username": "javery",
"Password": "...",
"mls": { "MlsKey": 572 }
}
]
}
Request Fields
| Field | Required | Description |
|---|---|---|
FirstName, LastName |
Yes | The broker the credentials belong to. |
OfficeName, OfficeAddress,
City, StateCode |
Yes | Office details for that broker. |
EthicsAccepted |
Yes | Must be true. Sending it records
acceptance of the Code of Ethics for this
submission, together with the version in force
at the time. A submission without it is
rejected. |
credentials[] |
Yes | At least one entry; up to 50. |
credentials[].MlsName |
Yes | The MLS as you refer to it. Informational. |
credentials[].MlsId |
Yes | The broker's own member or agent ID at that MLS. |
credentials[].MlsWebAddress |
Yes | MLS login URL. Must be http(s). |
credentials[].Username,
credentials[].Password |
Yes | The MLS login. Stored encrypted; never returned by any endpoint. |
credentials[].mls |
Yes | Which catalog MLS this is for - see below. |
Identifying the MLS
The mls object accepts any one of three forms:
| Form | Example | When to use it |
|---|---|---|
| By key | {"MlsKey": 493} |
Recommended. Exact and stable - nothing to spell, nothing to disambiguate, and it survives the nightly rebuild. If present it takes precedence over every other field. |
| By name | {"MlsName": "Aiken MLS", "State": "SC"} |
Supported. Add State (and
City) when the name is shared by
more than one board. |
| By catalog id | {"MlsCatalogId": 493} |
Exact, but only for ids read from
/MlsCatalog the same day. Prefer
MlsKey. |
| By short code | {"MlsShortCode": "BBOR", "State": "LA"} |
Convenient, but State is effectively
required - codes are not unique. |
[i] Name matching is forgiving. You do not need to reproduce a name character for character. Matching ignores letter case, accents, punctuation and symbols, so all of these find the same MLS:
Eufaula Board of REALTORS® ·
Eufaula Board of REALTORS ·
eufaula board of realtors
That matters because 174 of the names in the catalog end in
a registered-trademark sign and many contain slashes,
ampersands or parentheses. Accents fold too:
Abitibi-Temiscamingue matches
Abitibi-Témiscamingue.
[i] Ambiguity is queued, never guessed. If
what you send matches more than one MLS, the submission is
still accepted and the request is held for review with
"Reason": "ambiguous_mls". We will not pick one
for you, because filing credentials under the wrong board
would be worse than waiting. Add State or
City to resolve it yourself.
Response
{
"SubmissionId": "sub_aabbccddee11",
"SubmittedAt": 1791140559495,
"BrokerMatchedExisting": false,
"Requests": [
{
"Mls": "UnlockMLS - Austin Board of Realtors",
"Database": "AUSTIN TX",
"Status": "auto_approved",
"Reason": null
}
]
}
| Field | Description |
|---|---|
SubmissionId |
Reference for this submission. Quote it in support requests. |
BrokerMatchedExisting |
true if this broker had submitted
before. Their office details are refreshed and
the new credentials are added; nothing is
replaced or lost. |
Requests[] |
One entry per submitted credential, giving the outcome for each. |
Requests[].Database |
The database that would be granted, once known. |
Requests[].Status |
auto_approved - access granted
immediately. pending - held for
review. |
Requests[].Reason |
Why a request is pending. See below. |
Errors
| Status | Meaning |
|---|---|
400 |
A field is missing or malformed. The message names the credential and the field. |
403 |
Missing, invalid or expired bearer token. |
409 |
Your subscription is not yet linked to a credential collection group. Contact support - this is a one-time setup step on our side. |
429 |
Too many submissions. Honour
Retry-After. |
503 |
Credential submission is temporarily unavailable. Nothing was stored - retry. |
[i] Privacy. Passwords are encrypted at rest and are never returned by any endpoint, including this one. The response deliberately echoes no usernames or passwords, so your request bodies stay the only copy in transit.
Checking Your Requests
Where each of your credential submissions stands, what was decided, and the date window any approval granted.
Endpoint: GET /v1/Credentials
GET /v1/Credentials lists the requests raised by
your own submissions, with their current status. Scoped to
your subscription: there is no parameter that widens it.
curl -X GET "https://ims-api.restats.com/v1/Credentials?\$status=pending" \
-H "Authorization: Bearer $ACCESS_TOKEN"
{
"count": 1,
"value": [
{
"RequestId": "apr_6f5e4d3c2b1a",
"SubmissionId": "sub_1122334455aa",
"Mls": "UnlockMLS - Austin Board of Realtors",
"MlsKey": 572,
"Database": "AUSTIN TX",
"Status": "Pending",
"Detail": "Awaiting approval. Your credentials have been received and this database is pending activation on your subscription.",
"ActionRequired": false,
"RequestedAt": 1791237100563,
"DecidedAt": null
}
]
}
| Field | Description |
|---|---|
Status |
Pending, Approved or
Declined. |
Detail |
One sentence on where a pending request stands. Null once it has been decided. |
ActionRequired |
The field to watch.
true means you can clear it
yourself by resubmitting with a better MLS
identifier. false means it is with
us and resubmitting will not help. |
Database |
The database this request is for. Populated as
soon as we recognise the MLS,
which is the usual case.
null means we do
not publish a dataset for it
yet, and Detail
will say so. |
Query parameters
| Parameter | Values |
|---|---|
$status |
pending, approved or
declined. Omit for all. |
$top |
Maximum rows, default 200, limit 500. |
[i] Polling. This endpoint is cheap and safe
to poll, but once a day is plenty: requests are reviewed by a
person, and the ones marked ActionRequired: false
will not change any faster for being asked about.
What happened to a decided request
Every request carries a Result. It is
null while the request is pending, because a
request with no decision has no result, and fills in once it
has been decided.
{
"RequestId": "apr_1a2b3c4d5e6f",
"Mls": "UnlockMLS - Austin Board of Realtors",
"Database": "AUSTIN TX",
"Status": "Approved",
"Detail": null,
"ActionRequired": false,
"Result": {
"Outcome": "Approved",
"DecidedAt": 1791331751000,
"Database": "AUSTIN TX",
"DataFrom": "2024-01-01",
"DataUntil": null,
"Note": null
}
}
| Field | Description |
|---|---|
Result.Outcome |
Approved or
Declined. |
Result.Database |
The database granted. Null on a declined request, because nothing was granted. |
Result.DataFrom /
Result.DataUntil |
The licensed date window on that database.
null means no limit at that
end, so a pair of nulls is the full
history. An DataUntil in the
past means newer records are outside your
licence. |
Result.Note |
Why, in words, where a note was left. This is where the reason for a refusal appears. |
counts |
On a list response: how many are Pending, Approved, Declined, and how many need action from you. |
One request by id
Add $requestId to ask about a single request.
It is scoped to your subscription, so an id that is not yours
returns 404 rather than revealing that it exists.
curl -X GET "https://ims-api.restats.com/v1/Credentials?\$requestId=apr_1a2b3c4d5e6f" \
-H "Authorization: Bearer $ACCESS_TOKEN"
Why a request may be pending
| Reason | Meaning | What to do |
|---|---|---|
unmapped_mls |
We accept this MLS but have not compiled its data yet. | Nothing. It is queued for our team. |
no_data_table |
The data for this MLS is still being built. | Nothing. It is queued. |
not_active_for_subscriber |
Your subscription does not cover this database yet. | Nothing. It is queued for approval. |
ambiguous_mls |
What you sent matched more than one MLS. | Resubmit with State or
City, or use
MlsCatalogId. |
mls_key_unavailable |
You sent MlsKey but this server
cannot resolve keys yet. |
Nothing is wrong with your request. Retry, or
send MlsName meanwhile. |
unknown_mls |
No MLS matched. | Check the spelling against
/v1/MlsCatalog. |
no_mls_selected |
The mls object was missing or
empty. |
Add it and resubmit. |
subscriber_inactive |
Your subscription is not currently active. | Contact support. The request is recorded and will be actioned once the subscription is reactivated. |
[i] A pending request is not a failure.
Credentials are stored either way, and the request stays in
the queue until it is resolved. There is no need to
resubmit, except for ambiguous_mls,
unknown_mls and no_mls_selected,
which need a corrected selection from you.
Your Subscription
The databases your subscription includes, the licensed date window on each, and whether the data in it is currently up to date.
Endpoint: GET /v1/Subscription
GET /v1/Subscription lists the databases your
subscription includes, the licensed date window on each, and
whether the data in it is currently up to date. Scoped to
your own subscription.
curl -X GET "https://ims-api.restats.com/v1/Subscription" \
-H "Authorization: Bearer $ACCESS_TOKEN"
{
"count": 2,
"ExpectedThrough": "2026-09",
"DateField": "CloseDate",
"value": [
{
"Database": "GREENVILLE SC",
"Active": true,
"Brokers": 2,
"DataFrom": "2021-01-01",
"DataUntil": null,
"LatestData": "2026-09-30",
"UpToDate": true,
"MonthsBehind": 0,
"CheckedAt": 1791237100563
},
{
"Database": "TWIN CITIES MN",
"Active": true,
"Brokers": 3,
"DataFrom": null,
"DataUntil": null,
"LatestData": "2026-07-31",
"UpToDate": false,
"MonthsBehind": 2,
"CheckedAt": 1791237100563
}
]
}
| Field | Description |
|---|---|
Active |
Whether this database currently returns data to you. A database can be listed but inactive, in which case it is excluded from query results. |
DataFrom /
DataUntil |
The licensed date window, applied to
CloseDate. null means
no limit at that end, so a pair of nulls
means the full history with no date
restriction at all. |
LatestData |
The most recent CloseDate in that
database. null if we have not
measured it yet. |
UpToDate |
true when the data reaches
ExpectedThrough. |
MonthsBehind |
How many months short of that it falls.
0 when up to date. |
CheckedAt |
When LatestData was measured, in
milliseconds. See the note below. |
[i] What "up to date" means. Data is
loaded a month behind: during October we load September.
So on any day in October a current database holds data
through September, and
ExpectedThrough tells you which month that
is without you having to work it out.
[i] CheckedAt is a measurement time, not a
guarantee of freshness. Finding the latest date
is expensive on large tables, so it is measured
periodically rather than on every call.
CheckedAt tells you when it was taken, and this
endpoint remeasures as it answers, so what you get is
current.
[i] Databases are loaded through the month. Our team brings each one up to date on its own schedule rather than all at once, so a database reading as behind today may well be current tomorrow. Keep checking rather than treating one reading as final: re-reading this endpoint is the way to find out that a database you were waiting on has been updated. There is no need to poll it often, since a given database changes at most once a month.
Enabling and disabling a database
Disable one of your databases, or ask for it to be enabled again.
curl -X POST "https://ims-api.restats.com/v1/Databases" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"Database": "AUSTIN TX",
"Active": true,
"Users": ["apu_7c1d4e9a2b"]
}'
Disabling is immediate: the database stops serving for every one of your users at once.
A user whose only database this was is deactivated with it,
since an active user holding nothing serves no purpose. A
user who also holds other databases is left
active, because deactivating them would revoke
those other databases too. The response names both groups,
as UsersDeactivated and
UsersKeptActive.
Enabling takes a Users
list and raises one request per user named in it. Only those
users are affected, so enabling a database for one of its
users while leaving another deactivated is simply a matter of
naming the one. Every id must be a user already assigned to
that database; one that is not is refused by name rather
than skipped.
It is always reviewed. A disabled database is held up by nobody, so there is no existing access for it to inherit, and it stays unavailable until we approve. Approving activates the user along with the database, since a user who is still deactivated would serve nothing.
Sending Active: true for a database that is
already enabled does nothing and raises no request, and the
same call repeated while a request is open reports the
request that already exists rather than adding another.
{
"Database": "AUSTIN TX",
"Enabled": true,
"Users": ["apu_7c1d4e9a2b"],
"UsersDeactivated": [],
"UsersKeptActive": [],
"RequestIds": ["apr_1a2b3c4d5e6f"],
"Message": "AUSTIN TX has been requested. It stays unavailable until we approve it, and 1 request is now with us for review."
}
| Status | Meaning |
|---|---|
409 |
No user is assigned to that database, so there is nothing to justify enabling it. Assign one first. |
400 |
No Users list was sent on an
enable. The message lists the users assigned
to that database, so you can pick from
them. |
404 |
Your subscription does not include that database. |
[i] A database cannot be enabled on its own. Access is justified by the MLS credentials your users supply, so at least one active user has to be assigned to a database before it can serve. Assign one first if the database has none.
Use this endpoint rather than
POST /v1/Users when you mean one database:
activating a user through that endpoint affects every
database they hold, while this one affects only the
database you name.
Your Users
If your subscription is organised by user, these list your users and activate or deactivate one.
Endpoints: GET /v1/Users,
POST /v1/Users
If your subscription is organised by user, each of your users holds its own databases. These two calls list them and activate or deactivate one. A subscription whose databases are held at the subscription level rather than per user will see an empty list here, and nothing else changes.
Listing them
curl -X GET "https://ims-api.restats.com/v1/Users" \
-H "Authorization: Bearer $ACCESS_TOKEN"
{
"count": 2,
"value": [
{
"UserId": "apu_7c1d4e9a2b",
"Name": "Jane Brooks",
"Office": "Brooks Realty",
"Active": true,
"Databases": 2,
"DatabasesServing": 2,
"AddedOn": "2026-10-05 19:15:02"
},
{
"UserId": "apu_3f85b06c71",
"Name": "Sam Ortiz",
"Office": "Brooks Realty",
"Active": false,
"Databases": 1,
"DatabasesServing": 0,
"AddedOn": "2026-10-06 18:21:03"
}
]
}
| Field | Description |
|---|---|
UserId |
An opaque identifier. Pass it back exactly as given when activating or deactivating the user. It is not a number and not sequential, so it cannot be guessed or counted from, and a numeric value is rejected. |
Active |
Whether the user is activated. An inactive user serves no data. |
Databases /
DatabasesServing |
How many databases are assigned to this user, and how many of those are actually serving. They differ when something is deactivated or waiting for our review. |
Activating and deactivating
Send the UserId and the state you want.
curl -X POST "https://ims-api.restats.com/v1/Users" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"UserId": "apu_7c1d4e9a2b",
"Active": false
}'
Deactivating a user
Immediate, no review. The user's databases come down with them, with one exception that matters: a database that another one of your active users also holds keeps serving, because each user holds it separately. The response separates the two, so you are never told a database stopped when it did not.
{
"UserId": "apu_7c1d4e9a2b",
"Active": false,
"DatabasesStopped": ["TWIN CITIES MN"],
"DatabasesStillAvailable": ["SOCAL CA"],
"DatabasesServing": [],
"DatabasesAwaitingReview": [],
"RequestIds": []
}
| Field | Description |
|---|---|
DatabasesStopped |
Stopped serving. Nobody else was holding these up, so queries against them now return nothing. |
DatabasesStillAvailable |
Taken off this user, but still serving, because another of your active users also holds them. Your queries are unaffected. |
Activating a user
Reviewed per database, not per user. The user is activated either way, and each of its databases is settled on its own:
- A database that another of your active users already holds starts serving at once. There is nothing to review, because you are already entitled to it.
- A database nobody else is holding up stays dark until we approve it, and we may need to re-verify the MLS credentials behind it. One request is raised per database.
{
"UserId": "apu_7c1d4e9a2b",
"Active": true,
"DatabasesServing": ["SOCAL CA"],
"DatabasesAwaitingReview": ["TWIN CITIES MN"],
"DatabasesStopped": [],
"DatabasesStillAvailable": [],
"RequestIds": ["apr_1a2b3c4d5e6f"]
}
| Field | Description |
|---|---|
DatabasesServing |
Serving now. Query them immediately. |
DatabasesAwaitingReview |
Not serving yet. Waiting on our review, so queries against them return nothing until then. |
RequestIds |
One id per database awaiting review. Track
each with
GET /v1/Credentials using
the $requestId filter, which
reports the outcome and the date window
granted. |
Activating a user is not instant access.
Check DatabasesAwaitingReview before assuming
a database is queryable. If it is listed there, the data is
not available to you yet, and a query will return no rows
rather than an error.
OData Parameters
The API supports OData query parameters for flexible data retrieval. All parameters are optional except for OriginatingSystemName in $filter.
$filter - Filtering Data
Filter results using comparison and logical operators.
Comparison Operators
| Operator | Description | Example |
|---|---|---|
eq |
Equal to | ListPrice eq 500000 |
ne |
Not equal to | ListAgentMlsId ne '12345' |
gt |
Greater than | ListPrice gt 300000 |
ge |
Greater than or equal | ListPrice ge 300000 |
lt |
Less than | ListPrice lt 1000000 |
le |
Less than or equal | ListPrice le 1000000 |
Logical Operators
| Operator | Description | Example |
|---|---|---|
and |
Logical AND | ListPrice gt 300000 and ListPrice lt 500000 |
or |
Logical OR | ListAgentMlsId eq '123' or ListAgentMlsId eq '456' |
not |
Logical NOT | not(ListPrice gt 1000000) |
( ) |
Grouping | (ListPrice gt 300000 and ListPrice lt 500000) or ClosePrice gt 600000 |
OriginatingSystemName Requirement
[!] Required: All requests to Property, Member, and Office resources must include OriginatingSystemName in the $filter parameter to specify which MLS database to query.
GET /v1/Property?$filter=OriginatingSystemName eq 'TRREB' and ListPrice gt 500000
Database Selection Workflow
Step 1: Query GET /v1/Database to retrieve available databases
Step 2: Extract the OriginatingSystemName value from the response
Step 3: Use this value in $filter parameter for Property, Member, and Office queries
Example: If Database response includes "OriginatingSystemName": "TRREB", use it as:
$filter=OriginatingSystemName eq 'TRREB' and ListPrice gt 500000
Filter Examples
$filter=OriginatingSystemName eq 'TRREB' and ListPrice gt 500000
$filter=OriginatingSystemName eq 'TRREB' and ListPrice gt 300000 and ListPrice lt 800000 and CloseDate ge '2024 -01-01'
$filter=OriginatingSystemName eq 'TRREB' and (ListPrice gt 500000 or ClosePrice gt 600000)
$select - Field Selection
Specify which fields to return in the response. Omit this parameter to return all fields.
$select=ListingId,ListPrice,UnparsedAddress,CloseDate
[i] Performance Tip: Use $select to reduce payload size and improve response times by only requesting fields you need.
$orderby - Sorting
Sort results by one or more fields in ascending (asc) or descending (desc) order.
$orderby=ListPrice desc
$orderby=CloseDate desc,ListPrice asc
$top - Result Limit
Limit the number of records returned. Default is 100 records, maximum is 5000 records per request.
$top=50
$skip - Pagination Offset
Skip a specified number of records (useful for pagination).
$top=100&$skip=100
Combining Parameters
You can combine multiple OData parameters in a single request:
GET /v1/Property?$filter=OriginatingSystemName eq 'TRREB' and ListPrice gt 400000 and ListPrice lt 600000&$select=ListingId,ListPrice,UnparsedAddress&$orderby=ListPrice desc&$top=50&$skip=0
Code Examples
Complete examples in multiple programming languages showing authentication and data retrieval.
cURL
# Step 1: Get access token
TOKEN_RESPONSE=$(curl -s -X POST https://ims-api.restats.com/authorization/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET" \
-d "scope=api" \
-d "grant_type=client_credentials")
# Extract token
ACCESS_TOKEN=$(echo $TOKEN_RESPONSE | grep -o '"access_token":"[^"]*' | cut -d'"' -f4)
# Step 2: Query properties
curl -X GET "https://ims-api.restats.com/v1/Property?\$filter=OriginatingSystemName eq 'TRREB' and ListPrice gt 500000&\$top=10" \
-H "Authorization: Bearer $ACCESS_TOKEN"
Python
import requests
# Configuration
BASE_URL = "https://ims-api.restats.com"
CLIENT_ID = "YOUR_CLIENT_ID"
CLIENT_SECRET = "YOUR_CLIENT_SECRET"
# Step 1: Get access token
token_response = requests.post(
f"{BASE_URL}/authorization/token",
data={
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"scope": "api",
"grant_type": "client_credentials"
}
)
if token_response.status_code == 200:
access_token = token_response.json()["access_token"]
# Step 2: Query properties
headers = {"Authorization": f"Bearer {access_token}"}
params = {
"$filter": "OriginatingSystemName eq 'TRREB' and ListPrice gt 500000",
"$select": "ListingId,ListPrice,UnparsedAddress,ListOfficeEmail,BuyerOfficeEmail",
"$top": 10
}
data_response = requests.get(
f"{BASE_URL}/v1/Property",
headers=headers,
params=params
)
if data_response.status_code == 200:
properties = data_response.json()["value"]
print(f"Found {len(properties)} properties")
for prop in properties:
print(f" {prop['ListingId']}: ${prop['ListPrice']:,.0f} - {prop['UnparsedAddress']}")
else:
print(f"Error: {data_response.status_code} - {data_response.text}")
else:
print(f"Authentication failed: {token_response.status_code}")
JavaScript (Node.js)
const fetch = require('node-fetch');
// Configuration
const BASE_URL = 'https://ims-api.restats.com';
const CLIENT_ID = 'YOUR_CLIENT_ID';
const CLIENT_SECRET = 'YOUR_CLIENT_SECRET';
async function getIMSData() {
try {
// Step 1: Get access token
const tokenResponse = await fetch(`${BASE_URL}/authorization/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
scope: 'api',
grant_type: 'client_credentials'
})
});
if (!tokenResponse.ok) {
throw new Error(`Authentication failed: ${tokenResponse.status}`);
}
const { access_token } = await tokenResponse.json();
// Step 2: Query properties
const params = new URLSearchParams({
'$filter': "OriginatingSystemName eq 'TRREB' and ListPrice gt 500000",
'$select': 'ListingId,ListPrice,UnparsedAddress,ListOfficeEmail',
'$top': '10'
});
const dataResponse = await fetch(
`${BASE_URL}/v1/Property?${params}`,
{
headers: {
'Authorization': `Bearer ${access_token}`
}
}
);
if (!dataResponse.ok) {
throw new Error(`Data request failed: ${dataResponse.status}`);
}
const data = await dataResponse.json();
console.log(`Found ${data.value.length} properties`);
data.value.forEach(prop => {
console.log(` ${prop.ListingId}: $${prop.ListPrice.toLocaleString()} - ${prop.UnparsedAddress}`);
});
} catch (error) {
console.error('Error:', error.message);
}
}
getIMSData();
PHP
$clientId,
'client_secret' => $clientSecret,
'scope' => 'api',
'grant_type' => 'client_credentials'
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$tokenResponse = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode == 200) {
$tokenData = json_decode($tokenResponse, true);
$accessToken = $tokenData['access_token'];
// Step 2: Query properties
$filterQuery = http_build_query([
'$filter' => "OriginatingSystemName eq 'TRREB' and ListPrice gt 500000",
'$select' => 'ListingId,ListPrice,UnparsedAddress',
'$top' => '10'
]);
$ch = curl_init($baseUrl . '/v1/Property?' . $filterQuery);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $accessToken
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$dataResponse = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode == 200) {
$data = json_decode($dataResponse, true);
echo "Found " . count($data['value']) . " properties\n";
foreach ($data['value'] as $prop) {
echo " {$prop['ListingId']}: \${$prop['ListPrice']} - {$prop['UnparsedAddress']}\n";
}
} else {
echo "Data request failed: HTTP $httpCode\n";
}
} else {
echo "Authentication failed: HTTP $httpCode\n";
}
?>
Response Format
All successful responses return JSON with a consistent structure.
Success Response
Status Code: 200 OK
{
"value": [
{
"ListingId": "C123456",
"UnparsedAddress": "123 Main Street, Toronto, ON",
"ListPrice": 599000.00,
"ClosePrice": 595000.00,
"CloseDate": "2024-01-15",
"ListAgentMlsId": "AG12345",
"BuyerAgentMlsId": "AG67890",
"ListOfficeMlsId": "OF111",
"BuyerOfficeMlsId": "OF222",
"ListOfficeEmail": "listings@realestate-office.com",
"BuyerOfficeEmail": "buyers@realty-group.com",
"ModificationTimestamp": "2024-01-20T15:30:00"
},
{
"ListingId": "C123457",
"UnparsedAddress": "456 Oak Avenue, Toronto, ON",
"ListPrice": 725000.00,
"ClosePrice": 720000.00,
"CloseDate": "2024-01-18",
"ListAgentMlsId": "AG23456",
"BuyerAgentMlsId": "AG78901",
"ListOfficeMlsId": "OF333",
"BuyerOfficeMlsId": "OF444",
"ListOfficeEmail": "list@torontorealty.com",
"BuyerOfficeEmail": "contact@buyersagency.com",
"ModificationTimestamp": "2024-01-21T10:15:00"
}
]
}
Response Structure
| Field | Type | Description |
|---|---|---|
value |
array | Array of result objects |
[i] Note: The structure of each object in the value array depends on the resource and fields selected. See the Resources section for field details.
Error Handling
When an error occurs, the API returns an appropriate HTTP status code and a JSON error response.
Error Response Format
{
"error": {
"code": 401,
"message": "OriginatingSystemName must be sent"
}
}
HTTP Status Codes
| Code | Status | Description |
|---|---|---|
| 200 | OK | Request successful |
| 400 | Bad Request | Invalid request parameters or syntax |
| 401 | Unauthorized | Missing OriginatingSystemName, invalid credentials, or validation error |
| 403 | Forbidden | Missing or invalid authentication token |
| 404 | Not Found | Resource not found |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Server error occurred |
Common Error Examples
Missing Authentication Token
Status Code: 403 Forbidden
{
"error": "Bearer Token is missing"
}
Invalid Credentials
Status Code: 400 Bad Request
{
"error": "Invalid credentials"
}
Missing OriginatingSystemName
Status Code: 401 Unauthorized
{
"error": {
"code": 401,
"message": "OriginatingSystemName must be sent"
}
}
Invalid Field Name
Status Code: 401 Unauthorized
{
"error": {
"code": 401,
"message": "Column InvalidFieldName is not allowed."
}
}
$top Value Exceeded
Status Code: 401 Unauthorized
{
"error": "$top value can not exceed 5000"
}
Resource Not Found
Status Code: 404 Not Found
{
"error": "Resource can not be found"
}
Rate Limiting
The API tracks all requests for rate limiting and monitoring purposes.
Rate Limit Response
If you exceed the rate limit, you'll receive a 429 status code:
{
"error": "Rate limit exceeded: 1050/1000 requests per hour",
"retry_after": 3600
}
Best Practices
- Implement exponential backoff for retry logic
- Cache responses when appropriate
- Use $select to minimize payload size
- Batch requests efficiently using $top and $skip
- Monitor your usage patterns
Best Practices
Performance Optimization
1. Use $select to Limit Fields
Only request the fields you need to reduce payload size and improve response times.
$select=ListingId,ListPrice,UnparsedAddress
2. Implement Pagination
Use $top and $skip for efficient pagination instead of requesting all records.
page_size = 100
page = 1
skip = (page - 1) * page_size
params = {
'$filter': "OriginatingSystemName eq 'TRREB'",
'$top': page_size,
'$skip': skip
}
3. Filter Server-Side
Use $filter to reduce data transferred instead of filtering client-side.
4. Reuse Access Tokens
Tokens are valid for 150 minutes. Store and reuse them instead of requesting new tokens for each request.
Error Handling
Implement Retry Logic
import time
def make_request_with_retry(url, headers, max_retries=3):
for attempt in range(max_retries):
response = requests.get(url, headers=headers)
if response.status_code == 200:
return response.json()
elif response.status_code == 429: # Rate limit
retry_after = int(response.headers.get('Retry-After', 60))
time.sleep(retry_after)
else:
print(f"Error: {response.status_code}")
break
return None
Security
- Never expose your client_secret in client-side code
- Store credentials securely using environment variables
- Use HTTPS for all requests (enforced by API)
- Implement proper token refresh logic
- Monitor API usage for unusual patterns
Data Freshness
Use the ModificationTimestamp field to track when records were last updated and implement incremental syncing:
$filter=OriginatingSystemName eq 'TRREB' and ModificationTimestamp ge '2024-01-01T00:00:00'
$orderby=ModificationTimestamp asc