Getting started
Introduction
Centris provides a Data Distribution REST API certified by the Real Estate Standards Organization (RESO). This API lets third-party systems get access to MLS data such as listings, real estate agents, etc.
This document describes how to access and use the API.
Configuration
Test environment
Key | Value |
---|---|
Data Distribution Root URL | https://stg-datadistributionqc.centristst.ca/ |
Identity Provider Root URL | https://stg-accounts.centristst.ca/ |
Production environment
Key | Value |
---|---|
Data Distribution Root URL | https://datadistributionqc.centris.ca/ |
Identity Provider Root URL | https://accounts.centris.ca/ |
Client credentials
Centris will provide keys that will let you access the API.
- Client ID: This is equivalent to your username.
- Client secret: This is equivalent to your password.
Accessing the API
Authentication
The API is protected using an OpenID Connect (OIDC) layer from its Identity Provider.
When a system is authenticated through the OIDC flow, an access token is returned which represents the identity of that system. This token must be provided in each request to the API. If the token is not provided, expired or invalid, the API will return a 401 - Unauthorized
response.
To get an access token, you will need to use the Client Credentials flow.
1
2
3
4
POST https://accounts.centris.ca/connect/token
grant_type=client_credentials
&client_id=your_client_id
&client_secret=your_client_secret
If the credentials are valid, the Identity Provider will return a 200 - OK
with an access token.
The access token is a standard JSON Web Token (JWT) that looks like eyJhbGciOiJiIsIm…
Because the access token has a specific lifetime and will expire, you must make sure to request a new one at the appropriate time. The expires_in
property lets you know when (in seconds) this token will expire.
To provide the access token, simply specify it in the Authorization
header of the API request.
1
2
GET https://datadistributionqc.centris.ca/v1/odata/$metadata
Authorization: Bearer your_access_token
Assuming the request is authorized (see below), you should receive a 200 - OK
response if the operation completed successfully.
Authorization
Once a request is authenticated, it must go through the next validation process which is the authorization. This process validates that the authenticated user is authorized to access the requested resource. If the request is unauthorized, the API will return a 403 - Forbidden
response.
Rate limits
The number of requests to the API is limited to ensure its stability. If the system detects excessive usage, a 429 – Too Many Requests
response will be returned. This doesn’t mean that your request is invalid or unauthorized; it simply means that you should retry the same request later. To help you identify when you should retry, the response will have important information.
The body of the response will look like the following.
1
2
3
4
{
"message": "Quota excedeed",
"details": "Quota exceeded. Maximum allowed: 20 per 1m. Please try again in 41 second(s)."
}
The headers of the response will contain the following.
1
Retry-After: 41
You can also try to avoid 429 – Too Many Requests
responses before the limit is reached by looking at the headers of 200 – OK
responses which will contain the following.
1
2
3
X-Rate-Limit-Limit: 1d
X-Rate-Limit-Remaining: 480
X-Rate-Limit-Reset: 2024-03-26T16:42:30.7526366Z
Status codes
Status code | Description |
---|---|
200 - OK | Everything is working as expected. |
400 – Bad Request | The request is considered invalid; update your request. |
401 – Unauthorized | The request could not be authenticated; provide a valid access token. |
403 – Forbidden | The request could be authenticated but access to the resource is forbidden. |
404 – Not Found | The requested content doesn’t exist or is no longer available. |
429 – Too Many Requests | The maximum number of requests was exceeded; wait before retrying the request. |
500 – Internal Server Error | An unexpected error has occurred; this may be temporary or not. |
Querying the API
A word about RESO
RESO (Real Estate Standards Organization) is an independent and nonprofit organization which has the mission to:
“create and promote the adoption and utilization of standards that drive efficiency throughout the real estate industry.”
With the collaboration of multiple different organizations, RESO defines standards that help reduce the frictions between systems.
This is done via two main standards.
-
RESO Data Dictionary: Contains the terminology to be used for real estate data. For example, the list price of a listing should be named
ListPrice
. For more information, see RESO’s official article. -
RESO Web API: Contains the communication protocol to be used to exchange real estate data. For example, it should be a REST API which follows the OData standard. For more information, see RESO’s official article.
Each year, RESO updates their standards to keep up with the evolving market.
With its certification, Centris aligns with RESO’s standards while also including non-standard resources and fields (known as local resources) which are not yet supported by RESO. These local resources may be integrated in RESO’s standards in the future.
For more information regarding the certification process, see RESO’s official website. You can also access RESO’s Data Dictionary from their wiki.
Functionalities of OData
Summary
The API follows the conventions defined in the standard OData specification. OData (Open Data Protocol) defines ways to interact with a REST API and was selected by RESO as the query language to reduce frictions between different systems.
Because OData is a standard, there are libraries in different programming languages that are built to ease the integration with an OData API. We suggest that you take some time to search if there is one that fits your needs. In any case, it is still very easy to integrate with the API.
An OData query may look like the following.
1
/Property?$select=ListingKey,ListPrice&$filter=StandardStatus eq 'Active'&$orderby=ModificationTimestamp&$count=true
This query returns the listing key and list price of active listings ordered by modification timestamp and includes the total count.
1
2
3
4
5
6
7
8
9
{
"@odata.count": 133,
"value":
[
{ "ListPrice": 123412.00, "ListingKey": "14689588" },
{ "ListPrice": 567632.00, "ListingKey": "14239483" },
{ "ListPrice": 890221.00, "ListingKey": "23659600" }
]
}
Operators
OData offers a broad range of operators but only a few of them are generally used. The following operators are supported.
Operator | Description |
---|---|
$select | Defines the fields to project. |
$filter | Defines the filter to apply. |
$orderby | Defines the order to apply. |
$count | Defines whether to include the total count. |
$expand | Defines relational navigations to include. |
$skip | Defines the number of records to skip. |
$top | Defines the number of records to return. |
See OData’s official website for a comprehensive list of the conventions.
Getting the data model
Route
1
GET /v1/odata/$metadata
Description
Gets the data model of the system.
- An optional parameter
$format
can be provided to get the response in JSON instead of XML.$format=application/json
Example
1
GET https://datadistributionqc.centris.ca/v1/odata/$metadata
From this endpoint you can retrieve important information on the data model such as.
- The resources available (e.g.
Member
,Office
,Property
, etc.) - The fields available (e.g.
Member.MemberKey
,Member.MemberFirstName
,Property.ListPrice
, etc.) - The data type of the fields (e.g.
String
,Decimal
) along with their size information (e.g.Max length
,Precision
, etc.) - The navigation fields (e.g.
Member.MemberSocialMedia
, etc.)
Getting a collection of resources
Route
1
GET /v1/odata/Property
This example uses the
Property
resource (a listing), but any other resource could be used instead (e.g.Member
,Office
, etc.)
Description
Gets a collection of property resources (listings).
-
An optional parameter
$select
can be provided to project the results. (e.g.$select=ListingKey,ModificationTimestamp
) -
An optional parameter
$filter
can be provided to filter the results. (e.g.$filter=StandardStatus eq 'Active'
) -
An optional parameter
$orderby
can be provided to order the results. (e.g.$orderby=ModificationTimestamp
) -
An optional parameter
$count
can be provided to get the total count of the results. (e.g.$count=true
) -
An optional parameter
$expand
can be provided to include related data. (e.g.$expand=ListAgent
)
Example
1
GET https://datadistributionqc.centris.ca/v1/odata/Property?$select=ListingKey,ModificationTimestamp&$filter=StandardStatus eq 'Active'&$orderby=ModificationTimestamp&$count=true&$expand=ListAgent(select=MemberFullName)
If no results match the request, a
200 – OK
response with an empty collection will be returned.
Only some fields can be used with the
$orderby
and$filter
operators. The API will return a400 – Bad Request
with details of the error in the response if an unauthorized field is used.
Getting a resource by its key
Route
1
GET /v1/odata/Property('12345')
This example uses the
Property
resource (a listing), but any other resource could be used instead (e.g.Member
,Office
, etc.)
Description
Gets a single property resource (listing).
-
An optional parameter
$select
can be provided to project the results. (e.g.$select=ListingKey,ModificationTimestamp
) -
An optional parameter
$expand
can be provided to include related data. (e.g.$expand=ListAgent
)
Example
1
GET https://datadistributionqc.centris.ca/v1/odata/Property('12345')?$select=ListingKey,ModificationTimestamp&$expand=ListAgent(select=MemberFullName)
If no result match the request, a
404 – Not Found
response will be returned.
Paginating through results
Server-side pagination
OData offers a simple way for systems to paginate through result sets using the nextLink
field. If more results are available, a generated nextLink
field will be included in the response; much like a cursor. To paginate through the complete result set, you can simply follow the next links (i.e. executing a new request using the returned url) until there is no nextLink
.
A response may look like the following.
1
2
3
4
5
6
7
8
9
{
"value":
[
{ "ListPrice": 123412.00, "ListingKey": "14689588" },
{ "ListPrice": 567632.00, "ListingKey": "14239483" },
{ "ListPrice": 890221.00, "ListingKey": "23659600" }
],
"@odata.nextLink": "https://datadistributionqc.centris.ca/v1/odata/Property?$select=ListingKey%2CListPrice&$filter=StandardStatus%20eq%20%27Active%27&$orderby=ModificationTimestamp&$skip=10"
}
You can see that the operators of the original request were included in the nextLink
with the addition of the $skip
operator.
If you provide the
$top
operator, thenextLink
will not be returned as the result set is considered complete.
Client-side pagination
You can also implement your own pagination using a combination of the $filter
, $top
and $orderby
operators. You would need to provide the last value from the response to the $filter
operator.
For example,
1
/Property?$filter=ModificationTimestamp gt 2024-02-05T17:03:58Z
By default, the collection is ordered by the following fields.
ModificationTimestamp
- Ascending- Resource key (e.g.
MemberKey
,ListingKey
, etc.) - Ascending
Working with multiple languages
Centris supports both French and English in its MLS data. The API can distribute the data in the different languages while still being non-breaking with RESO’s standards.
For example, even if Member.MemberFirstName
would be Member.Prénom
in French, the field MemberFirstName
is still used; we only translate the payload of the response.
By default, the payload returned by the API is provided in English.
There are two ways to get the content in French.
Using the Accept-Language header
An optional header Accept-Language
can be provided to receive the payload in a different language (e.g. Accept-Language: fr
). You will need to do multiple requests to get the content in all languages which may not be practical depending on your use case.
Using the Translations expansion
An optional query parameter $expand=Translations
can be provided to include all translations of the resource (e.g. /Member?$expand=Translations
). You will get all translations in a single request but will need to do some interpretation of the results.
The response may look like this.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
{
"MemberKey": "1234",
"MemberStatus": "Active",
"MemberIntroduction": "My introduction in English",
"MemberCorporationType": "My corporation type",
"Translations":
[
{
"Locale": "fr",
"Values":
[
{
"FieldName": "MemberIntroduction",
"Value": "Mon introduction en Français"
},
{
"FieldName": "MemberCorporationType",
"Value": "Mon type de corporation"
}
]
},
{
"Locale": "en",
"Values":
[
{
"FieldName": "MemberIntroduction",
"Value": "My introduction in English"
},
{
"FieldName": "MemberCorporationType",
"Value": "My corporation type"
}
]
}
]
}
You can see that there are two locales being returned (fr
and en
) which both contains two translated fields (MemberIntroduction
and MemberCorporationType
).
If you expand on a resource that itself has translations, you will need to expand the translations on that resource as well; multiple expansions can be nested in a single request (e.g.
Property?$expand=Translations,Expenses(expand=Translations)
).
Replication
Summary
While the API allows live querying, its main objective is to allow other systems to replicate MLS data so that they can use it on their own. There are a few concepts to consider regarding replication.
Initializing your system
This is when your system doesn’t have any MLS data yet. You will request the different resources from an empty starting point and paginate through the result sets. For example, for the Property
resource, you would start with /Property
and follow the nextLink
.
Keeping your system up to date
This is when your system has been initialized and needs to stay updated.
- Getting the latest changes by continuously pulling: Your system keeps track of the last
ModificationTimestamp
of the different resources and requests records updated after this timestamp. For theProperty
resource, your request may look like this.
1
/Property?$filter=ModificationTimestamp gt 2024-02-05T17:03:58Z
- Receiving the latest changes using webhooks: Instead of continuously pulling the data, the API can let you know when a record has been updated. Note that this is optional. Your system could receive something like this.
1
2
3
4
5
{
"ResourceName": "Property",
"ResourceRecordKey": "18634444",
"ResourceRecordUrl": "https://datadistributionqc.centris.ca/v1/odata/Property('18634444')"
}
- Reconciling your system: This is the process of comparing the complete list of resource keys and modification timestamps between your system and the API. This is a safety net in case your system may have missed an update. For the
Property
resource, your request may look like this.
1
/Property?$select=ListingKey,ModificationTimestamp