IMS API Documentation

RESTful API for accessing property, member, and office data with OData query support

https://ims-api.restats.com

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

  1. Obtain your API credentials (client_id and client_secret)
  2. Generate an access token using the /authorization/token endpoint
  3. Make requests to /v1/{resource} endpoints with your token
  4. 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
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)

JSON
{
  "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:

HTTP Header
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

HTTP Request
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.

Bash
curl -X GET "https://ims-api.restats.com/v1/MlsCatalog?\$search=Aiken" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
JSON
{
  "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

FieldTypeDescription
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.

JSON Request
{
  "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

FieldRequiredDescription
FirstName, LastName YesThe broker the credentials belong to.
OfficeName, OfficeAddress, City, StateCode YesOffice 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[] YesAt 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 YesMLS 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:

FormExampleWhen 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

JSON Response
{
  "SubmissionId": "sub_aabbccddee11",
  "SubmittedAt": 1791140559495,
  "BrokerMatchedExisting": false,
  "Requests": [
    {
      "Mls": "UnlockMLS - Austin Board of Realtors",
      "Database": "AUSTIN TX",
      "Status": "auto_approved",
      "Reason": null
    }
  ]
}
FieldDescription
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

StatusMeaning
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.

Bash
curl -X GET "https://ims-api.restats.com/v1/Credentials?\$status=pending" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
JSON
{
  "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
    }
  ]
}
FieldDescription
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

ParameterValues
$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.

JSON
{
  "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
  }
}
FieldDescription
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.

Bash
curl -X GET "https://ims-api.restats.com/v1/Credentials?\$requestId=apr_1a2b3c4d5e6f" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Why a request may be pending

ReasonMeaningWhat 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.

Bash
curl -X GET "https://ims-api.restats.com/v1/Subscription" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
JSON
{
  "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
    }
  ]
}
FieldDescription
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.

Bash
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.

JSON
{
  "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."
}
StatusMeaning
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

Bash
curl -X GET "https://ims-api.restats.com/v1/Users" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
JSON
{
  "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"
    }
  ]
}
FieldDescription
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.

Bash
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.

JSON
{
  "UserId": "apu_7c1d4e9a2b",
  "Active": false,
  "DatabasesStopped": ["TWIN CITIES MN"],
  "DatabasesStillAvailable": ["SOCAL CA"],
  "DatabasesServing": [],
  "DatabasesAwaitingReview": [],
  "RequestIds": []
}
FieldDescription
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.
JSON
{
  "UserId": "apu_7c1d4e9a2b",
  "Active": true,
  "DatabasesServing": ["SOCAL CA"],
  "DatabasesAwaitingReview": ["TWIN CITIES MN"],
  "DatabasesStopped": [],
  "DatabasesStillAvailable": [],
  "RequestIds": ["apr_1a2b3c4d5e6f"]
}
FieldDescription
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.

Example
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

Simple Filter
$filter=OriginatingSystemName eq 'TRREB' and ListPrice gt 500000
Multiple Conditions
$filter=OriginatingSystemName eq 'TRREB' and ListPrice gt 300000 and ListPrice lt 800000 and CloseDate ge '2024 -01-01'
With Grouping
$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.

Example
$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.

Single Field
$orderby=ListPrice desc
Multiple Fields
$orderby=CloseDate desc,ListPrice asc

$top - Result Limit

Limit the number of records returned. Default is 100 records, maximum is 5000 records per request.

Example
$top=50

$skip - Pagination Offset

Skip a specified number of records (useful for pagination).

Example - Page 2
$top=100&$skip=100

Combining Parameters

You can combine multiple OData parameters in a single request:

Complete Example
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

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

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)

JavaScript
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

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

JSON Response
{
  "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

JSON Error
{
  "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

Response
{
  "error": "Bearer Token is missing"
}

Invalid Credentials

Status Code: 400 Bad Request

Response
{
  "error": "Invalid credentials"
}

Missing OriginatingSystemName

Status Code: 401 Unauthorized

Response
{
  "error": {
    "code": 401,
    "message": "OriginatingSystemName must be sent"
  }
}

Invalid Field Name

Status Code: 401 Unauthorized

Response
{
  "error": {
    "code": 401,
    "message": "Column InvalidFieldName is not allowed."
  }
}

$top Value Exceeded

Status Code: 401 Unauthorized

Response
{
  "error": "$top value can not exceed 5000"
}

Resource Not Found

Status Code: 404 Not Found

Response
{
  "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:

429 Response
{
  "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.

Good
$select=ListingId,ListPrice,UnparsedAddress

2. Implement Pagination

Use $top and $skip for efficient pagination instead of requesting all records.

Example
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

Python Example
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:

Incremental Sync
$filter=OriginatingSystemName eq 'TRREB' and ModificationTimestamp ge '2024-01-01T00:00:00'
$orderby=ModificationTimestamp asc