# Listings API (DD 1.7)

> Reference for integrations still on RESO Data Dictionary 1.7. New integrations should use Data Dictionary 2.1.

Source: https://docs.cdatalabs.com/listings/v1/  
Last updated: 2026-11-12

The API is compliant to the
[RESO Web API](https://www.reso.org/reso-web-api/)
standard for transfer of real estate data. The data follows the
[RESO 1.7 Data Dictionary](https://dd.reso.org/DD1.7/)
standard for field naming, grouping and data type definition.

Data retrieval largely follows
[OData](https://www.odata.org/)
protocol with the addition of custom querying options to enhance
aggregation queries.

## Authentication

Access to the API utilizes
[OAuth2](https://oauth.net/).

Client credentials (an ID and a secret) are issued to each user of the API and are exchanged
for a bearer access token. Tokens expire after 24 hours (`expires_in: 86400`), but
because of caching (see below) a returned token can have as little as ~23 hours left.

> **Token caching:** API Gateway caches the authorization result for 1 hour.
> Any authentication request within that hour returns the same cached token, not a new one,
> so re-authenticating does not reset the 24-hour expiry.

**Example:** a client first authenticates at 10:00 AM.

| Time | Request | Result |
| --- | --- | --- |
| 10:00 AM | Client authenticates | New token issued, valid until 10:00 AM tomorrow. API Gateway caches it for 1 hour. |
| 10:30 AM | Client re-authenticates | Cached token returned, ~23.5 hours remaining. |
| 10:59 AM | Client re-authenticates | Cached token returned, ~23 hours remaining. |
| 11:00 AM | Cache expires; the next request reaches Cognito | New token issued, valid for a full 24 hours. |

To request a token, Base64-encode the Client ID and Client Secret joined by a colon
(`client_id:client_secret`):

```
Base64Encode("5pq9999coididi613e222o1nnpp:fhkoq33a69d8191j1tercv35037clb9e5a7d215e64e4e")
= 85e1b0e7539a4a96b00ee02b335aa6418c580917a6b4667f6f7a6fe2149536041569924dfbe4a7df33f==
```

You can use an online tool such as
[Base64Encode](https://www.base64encode.org/)
for the encoding.

The end point for the token must include two parameters with optional scope parameter:

1. "grant\_type" = "client\_credentials"
2. "client\_id" = "5pq9999coididi613e222o1nnpp"
3. To authenticate successfully, if providing scope parameter, ensure the scope is set to either:  
   "scope" = "" (empty string) or "scope" = "https://api.cdatalabs.com/Api.Read" depending on your integration requirements.

Example request to retrieve access token:

```
curl -X POST -H "Content-Type: application/x-www-form-urlencoded" \
-H "Authorization: Basic 85e1b0e7539a4a96b00ee02b335aa6418c580917a6b4667f6f7a6fe2149536041569924dfbe4a7df33f==" \
-H "Host: authenticate.cdatalabs.com" \
"https://authenticate.cdatalabs.com/oauth2/token?grant_type=client_credentials&client_id=5pq9999coididi613e222o1nnpp"
```

Authentication response:

```
{
"access_token": "**********************************",
"expires_in": 86400,
"token_type": "Bearer"
}
```

## Usage

Once the access token is retrieved, it is needed in each call to
the Constellation Data Labs API under the
**Authorization** HTTP header:

```
curl -X GET -i -H "Host: listings.cdatalabs.com" \
-H "Authorization: Bearer **********************************" \
"https://listings.cdatalabs.com/odata/Property?$top=5&$select=ListingKey,StreetNumberNumeric"
```

> *Recommendation*:
> *It is recommended to include 'Accept-Encoding' header with
> value 'gzip, deflate, br'. This will compress the response
> into a zip/brotli format. This should improve the latency in
> transit by compressing the size. Client will have to
> decompress the response before processing it:*

| Header | Value |
| --- | --- |
| Accept-Encoding | gzip, deflate, br |

## Metadata

The API has an endpoint to retrieve metadata:

<https://listings.cdatalabs.com/odata/$metadata>

This endpoint allows caller to know available fields and data
types for each entity.

```
<?xml version="1.0" encoding="utf-8" ?>
<edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx">
    <edmx:DataServices>
        <Schema Namespace="Cirrus.OData.Domain.Models" xmlns="http://docs.oasis-open.org/odata/ns/edm">
            <EntityType Name="Property">
                <Property Name="AccessibilityFeatures" Type="Collection(Edm.String)" />
                <Property Name="Appliances" Type="Collection(Edm.String)" />
                <Property Name="ArchitecturalStyle" Type="Collection(Edm.String)" />
                ...
            </EntityType>
            <EntityType Name="OpenHouse" />
            <EntityType Name="Member" />
            <EntityType Name="Office" />
            <EntityType Name="Media" />
            <EntityType Name="Dom" />
        </Schema>
        <Schema Namespace="org.reso.metadata.enums" xmlns="http://docs.oasis-open.org/odata/ns/edm">
            <EnumType Name="ArchitecturalStyle" UnderlyingType="Edm.Int64" />
            <EnumType Name="Appliances" />
            ...
        </Schema>
    </edmx:DataServices>
</edmx:Edmx>
```

## Entities

The API supports several entity types, primarily
**Property** which represents a real estate listing.
Other entity types such as **Media** represents
supplementary data to a listing such as photos.

| Entity | Description | URL |
| --- | --- | --- |
| Property | Real Estate Listing | <https://listings.cdatalabs.com/odata/Property> |
| Member | MLS Agent | <https://listings.cdatalabs.com/odata/Member> |
| Office | MLS Office | <https://listings.cdatalabs.com/odata/Office> |
| Media | Photos, Virtual Tours, etc. | <https://listings.cdatalabs.com/odata/Media> |
| OpenHouse | Open House Events | <https://listings.cdatalabs.com/odata/OpenHouse> |
| Dom | Days on Market | <https://listings.cdatalabs.com/odata/Dom> |
| PropertyUnitTypes | Property Units | <https://listings.cdatalabs.com/odata/PropertyUnitTypes> |
| PropertyRooms | Property Rooms | <https://listings.cdatalabs.com/odata/PropertyRooms> |
| RawMlsProperty | Raw Mls Data Fields | <https://listings.cdatalabs.com/odata/RawMlsProperty> |

## Basic Requests

Following OData protocol, the following examples show how
combining options and filters can retrieve a desired subset of
real estate data results.

```
GET /odata/Property?$select=...&$filter=...&$top=...&$orderby=...&$ignorenulls=...
```

API response structure

```
{
    "@odata.context": "...",
    "@odata.count": 0,
    "@odata.totalCount": 0,
    "value": [],
    "group": [],
    "@odata.nextLink": "...",
    "debug": { ... },
}
```

| Field | Description |
| --- | --- |
| @odata.context | annotation of the request |
| @odata.count | number of records returned in response, default is 10 |
| @odata.totalCount | total number of records found in result set |
| value | result set from query |
| group | used for aggregation results |
| @odata.nextLink | used for server side, allows user to cycle through entire result set |
| debug | hidden by default, used for troubleshooting and insight into underlying search criteria |

### Select

By default, the entire set of fields are returned, however, we can
whitelist specific fields in the response using
**$select**. Whitelist fields are specified in a
comma separated list.

Example request:

```
GET /odata/Property?$select=ListingId,StandardStatus
```

Example response:

```
{
    "@odata.context": "https://listings.cdatalabs.com/odata/$metadata#Property",
    "@odata.count": 10,
    "@odata.totalCount": 100,
    "value": [
        {
        "ListingId": "1",
        "StandardStatus": "Active"
        },
        {
        "ListingId": "2",
        "StandardStatus": "Active"
        }
        ...
    ]
}
```

### Top

By default, the first 10 results are returned in any API request,
however we can alter the number of results by using the
**$top** option followed by a non-negative integer
value.

Example request:

```
GET /odata/Property?$top=1
```

> *Recommendation: It is recommended to not exceed $top=1000 with
> $ignorenulls=true flag. Exceeding this limit will cause
> slowness of response payloads*

### Skip

Note: It is ideal and more performant to use the
**SkipToken** approach to paging through result sets

Often in combination with **$top**, the
**$skip** option allows for client side paging, also
using a non-negative integer we can cycling through the entire
result set.

Example request:

```
GET /odata/Property?$top=5&skip=2
```

**Important:** When using the $top and $skip methods to page through the results set, please be aware that there are limits to how many records can be skipped. These limits vary depending on the entity you are querying. Below are the specific limits for each entity:
  
  
- Property: 1,000,000 records
  
- Office: 500,000 records
  
- Media: 50,000 records
  
- OpenHouse: 500,000 records
  
- PropertyRooms: 50,000 records
  
- DaysOnMarket: 50,000 records
  
- Member: 500,000 records
  
- RawMLSProperty: 50,000 records
  
  
These limits are enforced to ensure optimal performance. For larger result sets, consider using the @odata.nextLink to paginate through the data efficiently.

### Ignore Nulls (preferred)

By default, the response payload will include all fields defined
in the metadata where **null** is returned when there
is no value. The **$ignorenulls** boolean flag will
omit fields which have no values thus reducing payload size

Example request:

```
GET /odata/Property?$ignorenulls=true
```

### Ignore Case

By default, searches are case sensitive. The **$ignorecase** boolean flag will do case-insensitve searches for supported text fields.

Example request:

```
GET /odata/Property?$filter=BuildingName eq 'The Great House'&$ignorecase=true
```

All supported fields are mentioned below:

| **Property** | | | | |
| --- | --- | --- | --- | --- |
| BuildingName | BuyerAgentEmail | BuyerAgentFirstName | BuyerAgentFullName | BuyerAgentLastName |
| BuyerAgentMiddleName | BuyerAgentMlsId | BuyerOfficeEmail | BuyerOfficeMlsId | BuyerOfficeName |
| CityRegion | CoBuyerAgentEmail | CoBuyerAgentFirstName | CoBuyerAgentFullName | CoBuyerAgentLastName |
| CoBuyerAgentMiddleName | CoBuyerAgentMlsId | CoBuyerOfficeEmail | CoBuyerOfficeMlsId | CoBuyerOfficeName |
| CoListAgentEmail | CoListAgentFirstName | CoListAgentFullName | CoListAgentLastName | CoListAgentMiddleName |
| CoListAgentMlsId | CoListOfficeEmail | CoListOfficeMlsId | CoListOfficeName | CountyOrParish |
| ElementarySchool | ElementarySchoolDistrict | HighSchool | HighSchoolDistrict | ListAgentEmail |
| ListAgentFirstName | ListAgentFullName | ListAgentLastName | ListAgentMlsId | ListingId |
| ListOfficeEmail | ListOfficeMlsId | ListOfficeName | MiddleOrJuniorSchoolDistrict | SchoolDistrict |
| StreetDirPrefix | StreetName | StreetNumber | StreetNumberNumeric |
| StreetSuffix | SubdivisionName | UnitNumber | UnparsedAddress |
| **Office** | | | | |
| --- | --- | --- | --- | --- |
| MainOfficeMlsId | OfficeCity | OfficeEmail | OfficeMlsId | OfficeName |
| **Member** | | | | |
| --- | --- | --- | --- | --- |
| MemberEmail | MemberFirstName | MemberFullName | MemberLastName | MemberMiddleName |
| MemberMlsId |

### Ignore Pascal

By default, when doing an apply=groupby or a nested query, the api applies a Pascal (upper camel) case to the results in order to eliminate case-sensitive duplication.
This can result in the display of values that are not case identical to the actual values. The **$ignorepascal** boolean flag will shut off this default behavior and the actual value with its native case will be maintained in the results.

Example requests:

```
    GET /OData/Property?$filter=OriginatingSystemName eq 'ARMLS'&$apply=groupby((MlsStatus))&$top=&$ignorepascal=true
```

```
    GET /OData/Property?$filter=OriginatingSystemName eq 'ARMLS'&$top=0&$nested=MlsStatus&$ignorepascal=true
```

### Filter

Most common option is **$filter**, typically at least
one filter is used when retrieving data, often used are comparison
operators to filter results by an MLS (Data Source) via the
**OriginatingSystemName** field.

The following is a list of supported operators and expressions. In
future additional OData
[expressions](https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html)
could be added to the API.

#### Comparison Operators

Note: single quotation is required for string values

| Operator | Description | Example |
| --- | --- | --- |
| **eq** | Equal | GET /odata/Property?$filter=OriginatingSystemName eq 'CRMLS' |
| **ne** | Not Equal | GET /odata/Property?$filter=StandardStatus ne 'Active' |
| **gt** | Greater Than | GET /odata/Property?$filter=ListPrice gt 100000 |
| **ge** | Greater Than or Equal | GET /odata/Property?$filter=ListPrice ge 100000 |
| **lt** | Less Than | GET /odata/Property?$filter=ListPrice lt 200000 |
| **le** | Less Than or Equal | GET /odata/Property?$filter=ListPrice le 200000 |
| **has** | Has enumerable | GET /odata/Property?$filter=Appliances has 'Freezer' |
| **in** | Is a member of | GET /odata/Property?$filter=StandardStatus in ('Active','Sold') |

The any expression is required for fields which are collections as
defined in the metadata endpoint where the field is defined as
**Type="Collection(Edm.String)"** (e.g. Appliances).

### Boolean Operators

Note: boolean operator is expressed in
**lower cased**

Often used to chain multiple comparison expressions, the boolean
operators **and** and **or** can precise
subset of real estate listings

| Operator | Description | Example |
| --- | --- | --- |
| **and** | Logical AND | GET /odata/Property?$filter=OriginatingSystemName eq 'CRMLS' and StandardStatus eq 'Active' |
| **or** | Logical OR | GET /odata/Property?$filter=OriginatingSystemName eq 'VirginIslands' or OriginatingSystemName eq 'WesternKY' |

### Using Standard Lookup Values

Lookups (as defined in
[Lookups-RESO Data Dictionary 1.7-RESO](https://ddwiki.reso.org/display/DDW17/Lookups)
) are enumerations where normally their values do not contain
special characters or spaces. In situations where they do, they
can be filtered using the standard lookup value defined in the
RESO data dictionary along with the prefix
'org.reso.metadata.enums'.

Example requests:

```
    GET /odata/Property?$filter=PropertyType eq 'Commercial Sale'
    GET /odata/Property?$filter=StandardStatus eq 'Active Under Contract'
    GET /odata/Property?$filter=PropertySubType eq 'Single Family Residence'

    OR

    GET /odata/Property?$filter=org.reso.metadata.enums.PropertyType eq 'Commercial Sale'
    GET /odata/Property?$filter=org.reso.metadata.enums.StandardStatus eq 'Active Under Contract'
    GET /odata/Property?$filter=org.reso.metadata.enums.PropertySubType eq 'Single Family Residence'
```

### Functions

#### startswith()

In support of partial and autocomplete queries, the
**startswith()** provides the ability to return
results of partial values, such as beginning of a street name or
postal/zip code.

Example requests:

```
GET /odata/Property?$filter=startswith(PostalCode, '902')
GET /odata/Property?$filter=startswith(PostalCode, '9021')
GET /odata/Property?$filter=startswith(PostalCode, '90210')
```

#### contains()

The **contains()** function complements the
**startswith()** function as it allows for wildcard
searches with the use of the asterisk symbol \*, this can be
expressed before, after, or within a search term

Example requests:

```
 GET /odata/Property?$filter=contains(SubdivisionName, 'Park*')
 GET /odata/Property?$filter=contains(SubdivisionName, '*Arlington')
 GET /odata/Property?$filter=contains(SubdivisionName, 'P*rk')
 GET /odata/Property?$filter=contains(SubdivisionName, '*Arlington Park*')
```

#### matchesPattern()

The **matchesPattern()** function matches exact phrases in the correct order,
allowing precise filtering of text fields. It can also allow small variations in word positions
using a slop parameter, which specifies the maximum number of words that can be out of order or
between matched terms.

Example requests:

```
GET /odata/Property?$filter=matchesPattern(PublicRemarks, 'solar panel')
GET /odata/Property?$filter=matchesPattern(PublicRemarks, 'five story')
GET /odata/Property?$filter=matchesPattern(PublicRemarks, 'lake park')
```

### Geospatial Search

#### Radius

A radius search returns listings within a given distance from a fixed point on a map. The function used for the radius search is "geo.distance", which accepts two parameters:

- The location of the property in question (using either Location or RawMlsLocation).
- The fixed point (longitude/latitude).

The resulting distance is then compared to a distance value in meters using the "lt" (less than) operator.

Example requests:

```
GET /odata/Property?$filter=geo.distance(Location, geography'POINT(-82.5154 40.7584)') lt 1000.0
GET /odata/Property?$filter=geo.distance(RawMlsLocation, geography'POINT(-82.5154 40.7584)') lt 1000.0
```

#### Polygon

A Polygon is a series of coordinates (longitude/latitude pairs) which represents basic shapes, with the first and last pair being the same, forming a closed shape. The function used for the polygon search is "geo.distance", and it accepts two parameters.

- The location of the property in question (using either Location or RawMlsLocation).
- The series of coordinates (longitude/latitude pairs separated by a comma).

Example requests:

```
GET /odata/Property?$filter=geo.intersects(Location, geography'POLYGON((-82.5294685 40.7700118, -82.5081825 40.7698818, -82.5064659 40.7493377, -82.5272369 40.7489476, -82.5294685 40.7700118))')
GET /odata/Property?$filter=geo.intersects(RawMlsLocation, geography'POLYGON((-82.5294685 40.7700118, -82.5081825 40.7698818, -82.5064659 40.7493377, -82.5272369 40.7489476, -82.5294685 40.7700118))')
```

### Expand

In OData, the **$expand** allows for retrieval of
supplementary entity data where there is an association to the
primary entity. Typically **$expand** would be used
where **Property** is the primary EntityType. Expand
can retrieve multiple sub-entities using a comma separated list.

Example requests:

```
GET /odata/Property?$expand=Media
GET /odata/Property?$expand=Media,OpenHouse
```

> *Recommendation: It is recommended to not exceed $top=500 with
> $expand as $expand increases the payload size and query time,
> potentially resulting in a degradation of service.*

### Expand on Member and Office variants

When using **$expand** on Member and Office
there is support for returning the List/CoList and Buyer/CoBuyer (for sold listings)
information if available.

Member variants:

```
              GET /odata/Property?$expand=BuyerMember
              GET /odata/Property?$expand=CoBuyerMember
              GET /odata/Property?$expand=ListMember
              GET /odata/Property?$expand=CoListMember
```

Office variants:

```
              GET /odata/Property?$expand=BuyerOffice
              GET /odata/Property?$expand=CoBuyerOffice
              GET /odata/Property?$expand=ListOffice
              GET /odata/Property?$expand=CoListOffice
```

> *By default, $expand=Member is the equivalent to $expand=ListMember and $expand=Office is the equivalent to $expand=ListOffice.*

### Select in Expand

The **$select** option within
**$expand** allows for the retrieval of specific
fields from the expanded resources. This can help in reducing the
payload size by selecting only the necessary fields.

Example request:

```
GET /odata/Property?$expand=Office($select=OfficeType)
```

### Paging (SkipToken)

When making a request to the API that yields a larger amount of
results than what is presentable in the response payload, the
response payload will include the node
**"@odata.nextLink"** with a hyperlink to the next
set of results. This **"@odata.nextLink"** node will
reappear until all results have been retrieved.

Example response:

```
{
    "@odata.context": "https://listings.cdatalabs.com/odata/$metadata#Property",
    "@odata.count": 10,
    "@odata.totalCount": 2295,
    "value": [ ... ],
    "group": [],
    "@odata.nextLink": "https://listings.cdatalabs.com/odata/Property/skiptoken/FGluY2x1ZGVfY29udGV4dF91dWlkDnF1ZXJ5VGhlbkZldGNoBRYxd3h4RWFXRlQwYWtDbGZjdkd0aTNBAA",
}
```

### Ordering

To sort results, the **$orderby** option can be used,
providing the desired sort field and direction, if direction is
not provided by default it is assumed the sort order is ascending.
Multiple order by expressions can be provided by a comma separated
list.

Example requests:

```
GET /odata/Property?$orderby=ModificationTimestamp
GET /odata/Property?$orderby=ModificationTimestamp desc
GET /odata/Property?$orderby=ModificationTimestamp desc, ListPrice asc
```

Within each Resource (Property, Member, Office, ect) the Key field (ListingKey, MemberKey, Officekey, ect) is unique, and the oData api will use that to order by default.
It is AWS's recommendation that a unique field be used in the order by when scrolling through data.
For example, let's say you try to order by a field like Id or a number that can have duplicates (e.g., values: 1, 2, 2, 3, 4, 5, 5).
If you use **pagination**, here's what can go wrong:

- Page 1: returns 1, 2
- Page 2: returns 3, 4
- You **missed the second 2**
- Page 3: returns 5, 5

Because 2 appeared twice and you didn't use a unique field, the system couldn't guarantee that both 2s would appear on the same page. So one got skipped.

### Aggregation

There are two approaches to generating an aggregation result,
using:

1. using **$apply=groupby()**
2. using **$nested**

OData standard protocol prefers usage of the former, however this
does not appear to support nested aggregation results, thus there
is the latter option.

Aggregation results are returned in the
**group:[]** node, it is often useful to use the
**$top=0** flag to clear the
**value:[]** results as this may not be needed during
aggregation requests.

With **$apply=groupby()** multiple aggregate results
can be returned provided a comma separated list.

Example requests:

```
GET /odata/Property?$apply=groupby((StateOrProvince))&$top=0
GET /odata/Property?$apply=groupby((StateOrProvince,City))&$top=0
```

Example results:

```
{
    "@odata.context":"https://listings.cdatalabs.com/odata/$metadata#Property",
    "@odata.count":0,
    "@odata.totalCount":2590,
    "value":[ ... ],
    "group":[
      {
         "groupby":"StateOrProvince",
         "count":3,
         "buckets":[
            {
               "key":"IL",
               "count":3,
               "sub_aggregate":[
                  ...
               ]
            },
            {
               "key":"KY",
               "count":2584,
               "sub_aggregate":[
                  ...
               ]
            },
            {
               "key":"TN",
               "count":3,
               "sub_aggregate":[
                  ...
               ]
            }
         ]
      }
    ]
}
```

### Nested

In addition to the **$apply=groupby()** approach, the
**$nested** approach can support nested results, it
can also handle fields which are collection of strings.

Example requests:

```
GET /odata/Property?$nested=PropertyType,PropertySubType,ArchitecturalStyle
```

Example response:

```
{
    "@odata.context":"https://listings.cdatalabs.com/odata/$metadata#Property",
    "@odata.count":0,
    "@odata.totalCount":10000,
    "value":[

    ],
    "group":[
      {
         "groupby":"PropertyType",
         "count":21,
         "buckets":[
            {
               "key":"CLE",
               "count":95,
               "sub_aggregate":[
                  {
                     "groupby":"PropertySubType",
                     "count":2,
                     "buckets":[
                        {
                           "key":"Business",
                           "count":1,
                           "sub_aggregate":[
                              {
                                 "groupby":"ArchitecturalStyle",
                                 "count":1,
                                 "buckets":[
                                    {
                                       "key":"Other (see remarks)",
                                       "count":1,
                                       "sub_aggregate":[

                                       ]
                                    }
                                 ]
                              }
                           ]
                        },
                        {
                           "key":"Unimproved Land",
                           "count":3,
                           "sub_aggregate":[
                              {
                                 "groupby":"ArchitecturalStyle",
                                 "count":2,
                                 "buckets":[
                                    {
                                       "key":"Other (see remarks)",
                                       "count":2,
                                       "sub_aggregate":[

                                       ]
                                    },
                                    {
                                       "key":"Traditional",
                                       "count":1,
                                       "sub_aggregate":[

                                       ]
                                    }
                                 ]
                              }
                           ]
                        }
                     ]
                  }
               ]
            },
            {
               "key":"TWN",
               "count":6,
               "sub_aggregate":[
                  {
                     "groupby":"PropertySubType",
                     "count":1,
                     "buckets":[
                        {
                           "key":"Townhouse",
                           "count":6,
                           "sub_aggregate":[
                              {
                                 "groupby":"ArchitecturalStyle",
                                 "count":0,
                                 "buckets":[

                                 ]
                              }
                           ]
                        }
                     ]
                  }
               ]
            }
         ]
      }
    ]
}
```

### Nested Averages

**$nested** aggregation also can return averages on numeric fields such as ListPrice, for example the average listing price per state code.

Example requests:

```
GET /odata/Property?$filter=StandardStatus eq 'Active'&$top=0$nested=StateOrProvince,avg(ListPrice)
```

Example response:

```
{
{
    "@odata.context": "https://listings.cdatalabs.com/odata/$metadata#Property",
    "@odata.count": 0,
    "@odata.totalCount": 3796083,
    "value": [],
    "group": [
        {
            "groupby": "StateOrProvince",
            "count": 63,
            "buckets": [
                {
                    "key": "AB",
                    "count": 44767,
                    "sub_aggregate": [
                        {
                            "aggregation": "average_aggregation",
                            "value": 637813.4183623195
                        }
                    ]
                },
                {
                    "key": "AK",
                    "count": 5365,
                    "sub_aggregate": [
                        {
                            "aggregation": "average_aggregation",
                            "value": 529860.7882419836
                        }
                    ]
                },
                {
                    "key": "AL",
                    "count": 49280,
                    "sub_aggregate": [
                        {
                            "aggregation": "average_aggregation",
                            "value": 368739.4216983966
                        }
                    ]
                }
                ...
            ]
        }
    ]
}
```

### Time Range Queries

Queries can contain filter on date ranges for DateTime field types

Example Requests:

```
GET /odata/Property?$ignorenulls=true&$filter=ModificationTimestamp ge 2025-01-01T00:00:00Z
GET /odata/Property?$ignorenulls=true&$filter=ModificationTimestamp gt 2025-01-01T00:00:00Z and ModificationTimestamp lt 2025-02-01T00:00:00Z
```

### How to do a full job?

A full job requires the first request to a sort by
**$orderby=ListingKey asc** this acts as an initial
marker to retrieve subsequent records via the @odata.nextLink url

Example

First request:

```
GET /odata/Property?$ignorenulls=true&$filter=OriginatingSystemName eq 'CRMLS'&$top=300&$orderby=ListingKey asc
```

Use the @odata.nextLink url returned from the first request for
the rest of the requests:

```
GET /odata/Property/skiptoken/FGluY2x1ZGVfY29udGV4dF91dWlkDnF1ZXJ5VGhlbkZldGNoBRZGYmFTT0FSRVExeTRfckMxZ1loOEhnAAAAAAm0TVYWajZEYmttQ1JSWEdIUTlfRDRHNnB0dxY0NUZaQzBJcFI2NmU5N1h5NHFpWXZ3AAAAAAnwnR4WTVQ3WEhKLU9Ua215d2xxWW94SFcydxZGYmFTT0FSRVExeTRfckMxZ1loOEhnAAAAAAm0TVcWajZEYmttQ1JSWEdIUTlfRDRHNnB0dxY0NUZaQzBJcFI2NmU5N1h5NHFpWXZ3AAAAAAnwnR8WTVQ3WEhKLU9Ua215d2xxWW94SFcydxY0NUZaQzBJcFI2NmU5N1h5NHFpWXZ3AAAAAAnwnSAWTVQ3WEhKLU9Ua215d2xxWW94SFcydw==/?$filter=OriginatingSystemName%20eq%20%27CRMLS%27&$ignorenulls=true&$top=300
```

Keep cycling through the entire result set until the @odata.count
is 0

### How to do an Incremental Download?

To track updates such as listing changes in price, status, or other listing details use the **ModificationTimestamp** field. For updates specifically related to
listing media (e.g., photos), refer to the **PhotosChangeTimestamp** field.

(Incremental downloads can also apply to other resources such as Media, OpenHouse, Member, Office, etc. in the same way using the **ModificationTimestamp** field)

Example

```
GET /odata/Property?$filter=OriginatingSystemName eq 'CRMLS' and ModificationTimestamp ge 2025-06-01T00:00:00Z
GET /odata/Property?$filter=OriginatingSystemName eq 'CRMLS' and PhotosChangeTimestamp ge 2025-06-01T00:00:00Z
GET /odata/Property?$filter=OriginatingSystemName eq 'CRMLS' and (ModificationTimestamp ge 2025-06-01T00:00:00Z or PhotosChangeTimestamp ge 2025-06-01T00:00:00Z)
```

To ensure you capture all relevant updates across different resources (such as **Member**, **Office**, **OpenHouse**, etc.),
you must reference the **ModificationTimestamp** field in each respective resource. Note that the **ModificationTimestamp** in the Property
resource only reflects changes made directly to the property record itself. It does not update when related resources like Member, Office, or OpenHouse change.

An alternative method is to use the highest **ModificationTimestamp** value returned in your current results as the starting point
(**ge - greater than or equal to**) for your next incremental download (delta).

To avoid missing any updates due to timing discrepancies, it's recommended to subtract a few minutes from this timestamp in your next request.

### MLS Sync

We recommend a sync process be run once or twice per day against the Property resource (less often against Member, Office, or OpenHouse,
but using similar methodology) in which you request a Full download of specific key fields from a given data source. It's also recommended
that for Property, the Sync be run more frequently, specifically for listings with on market standard statuses, such as Active, Pending, etc,
with a separate Sync, run less frequently, looking only at off market statuses, such as Closed, Withdrawn, etc.

Example Requests:

```
GET /odata/Property?$ignorenulls=true&$filter=OriginatingSystemName eq 'CRMLS'and StandardStatus in ('Active','Pending','Active Under Contract','Coming Soon')&$top=300&$select=ListingId,ListingKey,ModificationTimestamp,PhotosCount,PhotosChangeTimestamp&$orderby=ListingKey asc
```

Use the @odata.nextLink url returned from the first request for the rest of the requests, for example:

```
GET /odata/Property/skiptoken/FGluY2x1ZGVfY29udGV4dF91dWlkDnF1ZXJ5VGhlbkZldGNoBRZGYmFTT0FSRVExeTRfckMxZ1loOEhnAAAAAAm0TVYWajZEYmttQ1JSWEdIUTlfRDRHNnB0dxY0NUZaQzBJcFI2NmU5N1h5NHFpWXZ3AAAAAAnwnR4WTVQ3WEhKLU9Ua215d2xxWW94SFcydxZGYmFTT0FSRVExeTRfckMxZ1loOEhnAAAAAAm0TVcWajZEYmttQ1JSWEdIUTlfRDRHNnB0dxY0NUZaQzBJcFI2NmU5N1h5NHFpWXZ3AAAAAAnwnR8WTVQ3WEhKLU9Ua215d2xxWW94SFcydxY0NUZaQzBJcFI2NmU5N1h5NHFpWXZ3AAAAAAnwnSAWTVQ3WEhKLU9Ua215d2xxWW94SFcydw==/?$filter=OriginatingSystemName%20eq%20%27CRMLS%27and%20StandardStatus%20in%20%28%27Active%27%2C%27Pending%27%2C%27Active%20Under%20Contract%27%2C%27Coming%20Soon%27%29&$ignorenulls=true&$top=300
```

Once you've captured this full snapshot of summary key field information, the data can subsequently be used to compare against your local data
repository in order to find discrepancies (missing or data needing updating--or removal), which can subsequently be acquired by running queries using the
Key field values.

```
GET /odata/Property?$ignorenulls=true&$filter=OriginatingSystemName eq 'CRMLS' and ListingKey in ('130720008079957678492','130720007831448890874','130720007690647937713')"
```

Other syncing methodologies could include versions where you do a full of the summary key field information only for a certain time window based on
one of the timestamp fields in the data and then only sync said data using the technique described above. However, this technique wouldn't necessarily
tell you what data to remove.

## Images

### Retrieve images

There are two ways to retrive images:

1. The recommended way to retrive image Urls is to use $expand on
   Property entity. In this way, you can get all the image records
   for one listing, and can use this data to compare with the
   records in your system to do the add/update/delete operations.

PhotosChangeTimestamp is the field in Property entity to reflect
the last photo modification timestamp and can be used to get the
list of listings with latest photo updates.

Example requests:

```
GET /odata/Property?$ignorenulls=true&$expand=Media&$select=ListingId,PhotosCount&$filter=OriginatingSystemName eq 'CRMLS' and PhotosChangeTimestamp ge 2025-06-06T16:53:07+00:00
```

Example response:

```
{
    "@odata.context": "http://listings.cdatalabs.com/$metadata#Property",
    "@odata.count": 50,
    "@odata.totalCount": 70,
    "value": [
        {
            "ListingId": "PW22122540",
            "OriginatingSystemName": "CRMLS",
            "PhotosCount": 4,
            "StandardStatus": "Active",
            "Media": [
                {
                    "Id": "13071194064517",
                    "Order": 0,
                    "MediaType": "jpeg",
                    "MediaURL": "https://photos.prod.cirrussystem.net/1307/731c82121af9e429dfa966711c59b46d/2505719132.jpeg",
                    "MediaModificationTimestamp": "2025-06-08T01:51:13.103441Z",
                    "ModificationTimestamp": "2025-06-08T01:51:13+00:00",
                    "OriginatingSystemName": "CRMLS",
                    "ResourceRecordID": "PW22122540",
                    "MediaCategory": "Photo",
                    "ResourceRecordKey": "13074115592807",
                    "MediaKey": "13071194064517"
                },
                {
                    "Id": "13072357035296",
                    "Order": 1,
                    "MediaType": "jpeg",
                    "MediaURL": "https://photos.prod.cirrussystem.net/1307/731c82121af9e429dfa966711c59b46d/604979049.jpeg",
                    "MediaModificationTimestamp": "2025-06-08T01:51:13.1653589Z",
                    "ModificationTimestamp": "2025-06-08T01:51:13+00:00",
                    "OriginatingSystemName": "CRMLS",
                    "ResourceRecordID": "PW22122540",
                    "MediaCategory": "Photo",
                    "ResourceRecordKey": "13074115592807",
                    "MediaKey": "13072357035296"
                },
                {
                    "Id": "13073563813694",
                    "Order": 2,
                    "MediaType": "jpeg",
                    "MediaURL": "https://photos.prod.cirrussystem.net/1307/731c82121af9e429dfa966711c59b46d/510761608.jpeg",
                    "MediaModificationTimestamp": "2025-06-08T01:51:13.2318988Z",
                    "ModificationTimestamp": "2025-06-08T01:51:13+00:00",
                    "OriginatingSystemName": "CRMLS",
                    "ResourceRecordID": "PW22122540",
                    "MediaCategory": "Photo",
                    "ResourceRecordKey": "13074115592807",
                    "MediaKey": "13073563813694"
                },
                {
                    "Id": "1307524091547",
                    "Order": 3,
                    "MediaType": "jpeg",
                    "MediaURL": "https://photos.prod.cirrussystem.net/1307/731c82121af9e429dfa966711c59b46d/4053895032.jpeg",
                    "MediaModificationTimestamp": "2025-06-08T01:51:13.3700629Z",
                    "ModificationTimestamp": "2025-06-08T01:51:13+00:00",
                    "OriginatingSystemName": "CRMLS",
                    "ResourceRecordID": "PW22122540",
                    "MediaCategory": "Photo",
                    "ResourceRecordKey": "13074115592807",
                    "MediaKey": "1307524091547"
                }
            ],
        }            
    ],
    "group": [],
    "@odata.nextLink": "https://listings.cdatalabs.com/odata/Property/skiptoken/FGluY2x1ZGVfY29udGV4dF91dWlkDnF1ZXJ5VGhlbkZldGNoBRZ6ZlBObmtkcVQwaVN1VUNMNC1nWHN3AAAAAAE873EWQi1OZTg5a0hRMXVwTXhmMUR6NWw2URZEUU9BblpIbVR1Nm5lOXBXT2FhaXh3AAAAAAFNXXEWcWkxY3hhUTRUcnFIMkZsZENwWktadxZjSWtRMl85ZlRaaTdFWVJsM0o1VjFRAAAAAAFTZWsWeHdfTmJXcDlSTW1fZVkxTVhjXzNWURZ6ZlBObmtkcVQwaVN1VUNMNC1nWHN3AAAAAAE873IWQi1OZTg5a0hRMXVwTXhmMUR6NWw2URZ6ZlBObmtkcVQwaVN1VUNMNC1nWHN3AAAAAAE873MWQi1OZTg5a0hRMXVwTXhmMUR6NWw2UQ==/?$expand=Media&$filter=OriginatingSystemName%20eq%20%27CRMLS%27%20and%20PhotosChangeTimestamp%20ge%202025-06-06T16%3A53%3A07.0000000Z%20and%20PhotosCount%20eq%204&$ignorenulls=true&$select=ListingId,PhotosCount,PhotosChangeTimestamp"
}
```

2. Retrieve images from Media entity. Currently we only have the
   active photos in the Media entity. If a image is removed from
   the Media, you will not be able to find out unless you reference
   the PhotosCount in Property entity.

Example requests:

GET Example requests:

```
GET /odata/Media?$filter=OriginatingSystemName eq 'CRMLS' and ModificationTimestamp gt 2025-06-08T18:08:18.5086332-07:00&$top=100&$ignorenulls=true
```

Example response:

```
{
    "@odata.context": "http://listings.cdatalabs.com/$metadata#Media",
    "@odata.count": 100,
    "@odata.totalCount": 40720,
    "value": [
        {
            "Id": "1307474260391",
            "Order": 0,
            "MediaType": "jpeg",
            "MediaURL": "https://photos.prod.cirrussystem.net/1307/aea51671d925e754134c87346ecb824b/141986726.jpeg",
            "MediaModificationTimestamp": "2025-06-09T12:31:11.544528Z",
            "ModificationTimestamp": "2025-06-09T12:31:16+00:00",
            "OriginatingSystemName": "CRMLS",
            "ResourceRecordID": "IV22118726",
            "MediaCategory": "Photo",
            "ResourceRecordKey": "13075796572",
            "MediaKey": "1307474260391"
        },
        {
            "Id": "13073608336386",
            "Order": 1,
            "MediaType": "jpeg",
            "MediaURL": "https://photos.prod.cirrussystem.net/1307/aea51671d925e754134c87346ecb824b/19583124.jpeg",
            "MediaModificationTimestamp": "2025-06-09T12:31:11.7175839Z",
            "ModificationTimestamp": "2025-06-09T12:31:16+00:00",
            "OriginatingSystemName": "CRMLS",
            "ResourceRecordID": "IV22118726",
            "MediaCategory": "Photo",
            "ResourceRecordKey": "13075796572",
            "MediaKey": "13073608336386"
        },
        {
            "Id": "13072399412764",
            "Order": 2,
            "MediaType": "jpeg",
            "MediaURL": "https://photos.prod.cirrussystem.net/1307/aea51671d925e754134c87346ecb824b/264290378.jpeg",
            "MediaModificationTimestamp": "2025-06-09T12:31:11.8101757Z",
            "ModificationTimestamp": "2025-06-09T12:31:16+00:00",
            "OriginatingSystemName": "CRMLS",
            "ResourceRecordID": "IV22118726",
            "MediaCategory": "Photo",
            "ResourceRecordKey": "13075796572",
            "MediaKey": "13072399412764"
        }
    ],
    "group": [],
    "@odata.nextLink": "https://listings.cdatalabs.com/odata/Media/skiptoken/FGluY2x1ZGVfY29udGV4dF91dWlkDnF1ZXJ5VGhlbkZldGNoBRZEUU9BblpIbVR1Nm5lOXBXT2FhaXh3AAAAAAFnDsoWcWkxY3hhUTRUcnFIMkZsZENwWktadxZ6ZlBObmtkcVQwaVN1VUNMNC1nWHN3AAAAAAFVQ_wWQi1OZTg5a0hRMXVwTXhmMUR6NWw2URZEUU9BblpIbVR1Nm5lOXBXT2FhaXh3AAAAAAFnDssWcWkxY3hhUTRUcnFIMkZsZENwWktadxZEUU9BblpIbVR1Nm5lOXBXT2FhaXh3AAAAAAFnDswWcWkxY3hhUTRUcnFIMkZsZENwWktadxZ6ZlBObmtkcVQwaVN1VUNMNC1nWHN3AAAAAAFVQ_0WQi1OZTg5a0hRMXVwTXhmMUR6NWw2UQ==/?$filter=OriginatingSystemName%20eq%20%27CRMLS%27%20and%20ModificationTimestamp%20gt%202025-06-09T01%3A08%3A18.5086332Z&$ignorenulls=true&$top=100"
}
```

### Download Image By Url

Note: The image url represents one unique image based on the
content of the image file, so you don't need download the image
again if the url is the same.

##### Dowload original image by the url from the response:

<https://photos.prod.cirrussystem.net/1307/731c82121af9e429dfa966711c59b46d/2505719132.jpeg>

##### Download image by predefined sizes:

Add the parameter d={size} to the image url.  
Size values:
l(Large, Width=1024), s(Small, Width=480), t(Thumbnail,
Width=150)  
Download thumbnail image example:

<https://photos.prod.cirrussystem.net/1307/731c82121af9e429dfa966711c59b46d/2505719132.jpeg?d=t>

##### Dynamic size image download:

1. By width only with d={Width}. Example:  
   <https://photos.prod.cirrussystem.net/1307/731c82121af9e429dfa966711c59b46d/2505719132.jpeg?d=600>
2. By width and height with d={Width}x{Height}. Exmaple:
     
   <https://photos.prod.cirrussystem.net/1307/731c82121af9e429dfa966711c59b46d/2505719132.jpeg?d=500x320>

### Request Trace ID (Mandatory Header)

All media requests must include the request-trace-id header.
This header is used to identify the client making the request. The value of the header must be the clientId.

##### Header format:

request-trace-id:

##### Example:

request-trace-id: client123

### Private Image Handling

To accommodate MLSs that now provide private images that cannot be made available,
we have updated the Permission field in the Media resource. This field will indicate
whether a media record is private.

For media records marked as private:

- The MediaURL field will not be available.
- All other relevant fields will still be accessible.

This ensures that if a listing has, for example, PhotosCount = 5, but 4 of those
images are private, you will still receive 5 media records. The Permission field
will allow you to distinguish which records are private.

You can filter out private images using the Permission field in the $expand clause:

Example request:

```
GET /odata/Property?$ignorenulls=false&$top=1&$select=UnparsedAddress,ListingId,StandardStatus,PhotosCount&$expand=Media($filter=Permission ne 'Private')&$filter=OriginatingSystemName eq 'MLSL' and ListingId eq 'ML81984127'
```

Example response:

```
{
                "@odata.context": "https://listings.cdatalabs.com/odata/$metadata#Property",
                "@odata.count": 1,
                "@odata.totalCount": 1,
                "value": [
                    {
                        "ListingId": "ML81984127",
                        "PhotosCount": 14,
                        "StandardStatus": "Pending",
                        "UnparsedAddress": "223 Winwood AVE",
                        "Media": [
                            {
                                "Id": "4713961373184638389",
                                "Order": 0,
                                "MediaType": "jpeg",
                                "MediaStatus": "Active",
                                "MediaURL": "https://photos.qa.cirrussystem.net/471/07afb4ab0e9eb013ef46e43e492c920f/2629865066.jpeg",
                                "MediaModificationTimestamp": "2025-03-05T13:40:37+00:00",
                                "ModificationTimestamp": "2025-03-05T13:40:40+00:00",
                                "EntityEventSequenceNumeric": 638767788420000000,
                                "OriginatingSystemName": "MLSL",
                                "ResourceRecordID": "ML81984127",
                                "MediaCategory": "Photo",
                                "ResourceRecordKey": "4716632792944226302",
                                "MediaKey": "4713961373184638389"
                            }
                        ]
                    }
                ],
                "group": []
            }
```

Note: In the example above, we're using the filter `Permission ne 'Private'` in the expand clause
to only return public images. The response shows PhotosCount is 14, but only public images are included
in the Media array.

You can also specifically request only private images:

Example request for private photos only:

```
GET /odata/Property?$ignorenulls=false&$top=1&$select=UnparsedAddress,ListingId,StandardStatus,PhotosCount&$expand=Media($filter=Permission eq 'Private')&$filter=OriginatingSystemName eq 'MLSL' and ListingId eq 'ML81984127'
```

Example response showing only private photos:

```
{
              "@odata.context": "https://listings.cdatalabs.com/odata/$metadata#Property",
              "@odata.count": 1,
              "@odata.totalCount": 1,
              "value": [
                  {
                      "ListingId": "ML81984127",
                      "PhotosCount": 14,
                      "StandardStatus": "Pending",
                      "UnparsedAddress": "223 Winwood AVE",
                      "Media": [
                          {
                              "Order": 1,
                              "MediaType": "jpeg",
                              "MediaModificationTimestamp": "2025-03-05T13:40:38+00:00",
                              "ModificationTimestamp": "2025-03-05T13:40:40+00:00",
                              "OriginatingSystemName": "MLSL",
                              "Permission": "Private",
                              "ResourceRecordID": "ML81984127",
                              "ResourceRecordKey": "4716632792944226302",
                              "MediaKey": "4716521329102635573"
                          },
                          {
                              "Order": 2,
                              "MediaType": "jpeg",
                              "MediaModificationTimestamp": "2025-03-05T13:40:37+00:00",
                              "ModificationTimestamp": "2025-03-05T13:40:40+00:00",
                              "OriginatingSystemName": "MLSL",
                              "Permission": "Private",
                              "ResourceRecordID": "ML81984127",
                              "ResourceRecordKey": "4716632792944226302",
                              "MediaKey": "4715718752208067584"
                          }
                          ...
                      ]
                  }
              ],
              "group": []
          }
```

Note: When querying for private images using `Permission eq 'Private'`
the response does not include the MediaURL field. The MediaURL field will not be available for private images.
