# Property Data API (GraphQL)

> One GraphQL endpoint that joins base properties, RESO listings, schools and school districts, so you fetch a whole property picture in one request.

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

The Insights GraphQL API combines the RESO Listings with the Insights OData APIs to provide a combined result set.

**Interactive GraphiQL:** Open the [GraphiQL Explorer](https://baseproperties.dev.insights.cdatalabs.com/) to run and validate the example GraphQL queries shown in this documentation.

This API uses [GraphQL](https://graphql.org/learn), which is a query language with the ability to define precisely the data you want to fetch

The GraphQL query is resolving our Listings API (MLS data) and our Property Data API (public records) in a single call.

Learn how to use GraphQL with [How to use GraphQL](https://www.howtographql.com/)

## Authentication

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

Client Credentials (ID and secret) are provided for each user of the API, allowing for retrieval of bearer access token with preset expiry (default 3600 seconds).

We must encode the combination of Client ID and Client Secret Key separated by ":" in Base 64 format like this:

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

Can use online service [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.constellation1apis.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.constellation1apis.com" \
"https://authenticate.constellation1apis.com/oauth2/token?grant_type=client_credentials&client_id=5pq9999coididi613e222o1nnpp"
```

Authentication response:

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

## Base properties (`baseProperties`)

The **Insights Base Property** query (`baseProperties`) returns U.S. assessor and public-record style property rows from the **BaseProperties** OData entity.
Each row is keyed by **CID** together with **county FIPS** and **parcel** context. The GraphQL schema groups attributes into objects such as `location`, `characteristics`, `structure`, `tax`, `latestTransfer`, `currentLoans`, and `estimatedValue`,
plus optional history expansions: `historicalEstimate`, `taxHistory`, `salesAndTransferHistory`, and `mortgageHistory`.

### Connection result

| Field | Type | Description |
| --- | --- | --- |
| `totalCount` | `Int!` | Count of records matching the filter (from OData where available). |
| `nodes` | `[BaseProperty]` | Page of `BaseProperty` objects for the current `limit` and `offset`. |

### Endpoint

```
POST /graphql
```

### Root query (example)

```
query BasePropertyQuery(
  $filter: BasePropertyFilter!
  $limit: Int
  $offset: Int
  $orderBy: BasePropertySort
) {
  baseProperties(filter: $filter, limit: $limit, offset: $offset, orderBy: $orderBy) {
    totalCount
    nodes {
      cid
      countyFipsCode
      parcelNumber
      modificationTimestamp
      propertyType
      propertySubType
      location {
        address { unparsedAddress city stateOrProvince postalCode }
        census {
          administrative {
            countyOrParish {
              id
              name
              stateOrProvince
              country
              type
              geom
              latitude
              longitude
              version
            }
          }
        }
        gis { latitude longitude }
      }
      characteristics { lotSizeSquareFeet view poolFeatures }
      structure {
        bedroomsTotal
        bathroomsTotalInteger
        livingArea
        yearBuilt
      }
      latestTransfer { salesPrice recordingDate }
      estimatedValue { estimate confidenceScore }
      historicalEstimate(limit: 4) {
        estimateDate
        estimate
        rangeMin
        rangeMax
      }
      taxHistory {
        taxYear
        taxAnnualAmount
        taxAssessedValue
        marketValue
      }
      salesAndTransferHistory(limit: 5, orderBy: { modificationTimestamp: DESC }) {
        transactionId
        recordingDate
        salesPrice
        sellerFullName1
        buyerFullName1
        titleCompanyName
      }
      mortgageHistory(limit: 5, orderBy: { modificationTimestamp: DESC }) {
        loanId
        transactionId
        loanAmount
        interestRate
        loanType
        lenderNameBeneficiary
        recordingDate
      }
      resoProperties { listingKey }
    }
  }
}
```

### Variables (example)

```
{
  "filter": {
    "countyFipsCode": { "eq": "06037" },
    "polygon": {
      "wkt": "POLYGON((-118.5 34.0, -118.3 34.0, -118.3 34.2, -118.5 34.2, -118.5 34.0))"
    },
    "structure": { "bedroomsTotal": { "ge": 3 }, "livingArea": { "ge": 1500 } },
    "estimatedValue": { "estimate": { "lt": 1000000 } },
    "dates": { "modificationTimeStamp": { "ge": "2026-01-01" } }
  },
  "limit": 10,
  "offset": 0,
  "orderBy": {
    "latestTransfer": { "recordingDate": "DESC" },
    "estimatedValue": { "estimate": "ASC" }
  }
}
```

### How filtering works

Arguments use the input type `BasePropertyFilter`. The server forwards filters to Insights as an OData `$filter` string.

- **Flat vs nested:** You may pass a single “flat” object of conditions (e.g. `countyFipsCode` + `structure`). The resolver wraps it as `{ and: filter }` when you do not supply top-level `and` or `or`, so all clauses are combined with **AND**.
- **Explicit logic:** To use **OR** or mixed logic, structure variables with top-level `and` or `or` keys whose values are objects of field filters (same pattern as other Insights-backed queries).
- **Nested sections:** Filters mirror the schema: e.g. `location.address.city`, `structure.livingArea`, `latestTransfer.salesPrice`, `estimatedValue.estimate`, `dates.modificationTimeStamp`.
- **Polygon search:** Use top-level `polygon` with `wkt` (a WKT `POLYGON` string of longitude/latitude pairs). The server maps this to OData `geo.intersects(location, geography'POLYGON((...))')`. The ring must be closed (first and last coordinate pair identical).
- **Operators:** `StringFilter` supports `eq, contains, startswith`; `IntFilter` and `FloatFilter` support `eq, gt, ge, lt, le`; `DateTimeFilter` supports `eq, gt, ge, lt, le` (ISO date strings).
- **Enums:** `propertyType` and `propertySubType` use GraphQL enums (e.g. `Residential`, `SingleFamilyResidence`) - pass them without wrapping in `eq` at the filter field.
- **OData mapping:** GraphQL `cid` maps to OData `CID`; `dates.modificationTimeStamp` maps to OData `modificationTimestamp`.

| BasePropertyFilter field | Purpose |
| --- | --- |
| `cid`, `countyFipsCode`, `parcelNumber` | Identify or constrain parcel keys. |
| `location` | `BaseLocationFilter` -> `address` (city, postalCode, unparsedAddress, …). |
| `propertyType`, `propertySubType` | Enumerated property classification. |
| `characteristics` | Lot size and related numeric filters. |
| `structure` | Bed/bath counts, living area, year built, stories. |
| `latestTransfer` | Sale price and contract/sale date filters. |
| `estimatedValue` | AVM estimate thresholds. |
| `dates` | `BaseDatesFilter` -> `modificationTimeStamp` for record change time. |
| `polygon` | `GeoPolygonFilter` -> `wkt`: properties whose location intersects the polygon (OData `geo.intersects`). |

### How sorting works

Use optional `orderBy: BasePropertySort`. Only certain leaf fields are mapped to OData `$orderby`:

| Group | Sortable fields -> OData |
| --- | --- |
| `characteristics` | `lotSizeSquareFeet` |
| `structure` | `bedroomsTotal`, `bathroomsTotalInteger`, `livingArea` |
| `latestTransfer` | `salesPrice`, `recordingDate` |
| `estimatedValue` | `estimate` |

Each sort direction is `ASC` or `DESC` (`SortOrder` enum). Multiple groups can appear in one `orderBy` object; the server builds a comma-separated OData order list.

### How expansion works

In GraphQL, “expansion” means **requesting nested fields** in the query. There are no separate REST-style expand parameters.

- **Nested objects:** Ask for `location { address census gis }`, `tax { … }`, `occupantOwner { … }`, etc. Null objects appear when the source has no data.
- **`location.gis.parcel`:** Resolved via DataLoader / ParcelBoundary API by CID (extra round-trip when selected).
- **History fields:** Request `historicalEstimate`, `taxHistory`, `salesAndTransferHistory`, or `mortgageHistory` under each `BaseProperty` node. Each loads a related Insights history entity by CID (batched). Omit them to avoid the extra work. If Insights denies a history entity (HTTP 403), that field resolves to null and GraphQL may include an entry in `errors` while the rest of `baseProperties` still returns.
- **`resoProperties`:** When listed under `BaseProperty`, the resolver loads linked MLS/RESO listings for the same CID (batched per request). Omit this field to avoid that work.

### Pagination

| Argument | Maps to OData | Description |
| --- | --- | --- |
| `limit` | `$top` | Maximum rows returned for this call. |
| `offset` | `$skip` | Rows to skip from the start of the filtered set. |

### Operators (filter leaf inputs)

| Operator | Typical input types | Meaning |
| --- | --- | --- |
| `eq` | String, Int, date string | Equal. |
| `contains`, `startswith` | String | Substring / prefix (string filters). |
| `gt`, `ge`, `lt`, `le` | Numeric / date | Range comparisons. |
| `wkt` | `GeoPolygonFilter` | WKT `POLYGON` string (closed ring of longitude/latitude pairs). |

### `BaseProperty` - root fields (schema)

| Field | Description |
| --- | --- |
| `cid` | Constellation property identifier (U.S.). |
| `countyFipsCode` | 5-digit state + county FIPS. |
| `parcelNumber` | County assessor parcel id. |
| `modificationTimestamp` | Last modification time for the base record. |
| `propertyType`, `propertySubType` | General and detailed property classification strings. |
| `location` | `BaseLocation`: address, census geographies, GIS + optional parcel boundary. |
| `characteristics` | `BaseCharacteristics`: lot, topography, views, pool, waterfront, community features, etc. |
| `structure` | `BaseStructure`: rooms, area, year built, construction, garage, heating/cooling, etc. |
| `tax` | `BaseTax`: assessed values, tax amounts, legal description, zoning, PLSS, subdivision. |
| `occupantOwner` | `BaseOccupantOwner`: owner names, mailing address, occupant type. |
| `latestTransfer` | `BaseLatestTransfer`: deed/sale parties, price, dates, REO flags. |
| `currentLoans` | `BaseCurrentLoans`: summary plus up to five lien slots. |
| `estimatedValue` | `BaseEstimatedValue`: current AVM snapshot (estimate, range, confidence). |
| `historicalEstimate` | `[BaseHistoricalEstimate!]`: monthly AVM history from `AvmHistoryAggregate`. Args: `filter`, `limit` (default 1, max 120), `orderBy`. |
| `taxHistory` | `[BaseTaxHistory!]`: annual tax/assessment history from `AssessorHistoryAggregate`. Args: `filter` (`taxYear`), `orderBy`. |
| `salesAndTransferHistory` | `[BaseSalesAndTransferHistory!]`: deed/sale history from `SalesAndTransferHistory`. Args: `limit` (default 1, max 100), `orderBy` (`modificationTimestamp`). |
| `mortgageHistory` | `[BaseMortgageHistory!]`: mortgage/lien history from `MortgageHistoryAggregate`. Args: `limit` (default 1, max 100), `orderBy` (`modificationTimestamp`). |
| `resoProperties` | List of `RESOProperty` for this CID (requires resolver expansion). |

**Nested highlights:** `location.address` (full addressing components); `location.census` (administrative, legislative, statistical GeoIDs and boundary details); `location.gis` (lat/long, precision, `parcel` boundary); `latestTransfer.buyers/sellers` arrays; `currentLoans.loans` array of `BaseLoan`.

### Historical fields (schema detail)

History fields are requested on each `BaseProperty` node. They are not part of the root `BasePropertyFilter`; filtering and sorting for history apply only to that field’s arguments.
If the caller’s Insights credentials lack access to a history entity, that field returns null (with a GraphQL error) and the rest of the base property payload is unchanged.

#### `historicalEstimate` → `[BaseHistoricalEstimate!]`

Backed by OData `AvmHistoryAggregate`. Returns monthly AVM point estimates for the property.

| Argument | Type | Description |
| --- | --- | --- |
| `filter` | `BaseHistoricalEstimateFilter` | Optional. `estimateDate` (`DateTimeFilter`) constrains months (use first-of-month values such as `2026-06-01`). |
| `limit` | `Int` | How many monthly rows to return (default `1`, max `120`). |
| `orderBy` | `BaseHistoricalEstimateSort` | Sort by `estimateDate` (`ASC` / `DESC`). |

| Field | Description |
| --- | --- |
| `estimateDate` | First day of the estimate month (e.g. `2026-06-01`). |
| `estimate` | AVM point estimate for that month. |
| `rangeMin`, `rangeMax` | Low / high end of the estimated value range for that month. |

#### `taxHistory` → `[BaseTaxHistory!]`

Backed by OData `AssessorHistoryAggregate`. Returns annual tax and assessment history.

| Argument | Type | Description |
| --- | --- | --- |
| `filter` | `BaseTaxHistoryFilter` | Optional. `taxYear` (`IntFilter`) to constrain years. |
| `orderBy` | `BaseTaxHistorySort` | Sort by `taxYear` (`ASC` / `DESC`). |

| Field | Description |
| --- | --- |
| `taxYear` | Tax year for `taxAnnualAmount`. |
| `taxAnnualAmount` | Annual taxes due for that year. |
| `taxAssessedValue`, `taxAssessedValueLand`, `taxAssessedValueImprovements` | Assessed values for the year. |
| `marketValue`, `marketValueLand`, `marketValueImprovements` | Market values for the year. |
| `taxAnnualAmountYOYC`, `taxAssessedValueYOYC`, `marketValueYOYC` (+ percent variants) | Year-over-year change metrics. |
| `avmAnnualMedian`, `avmAnnualAverage`, `avmAnnualLow`, `avmAnnualHigh`, `avmAnnualCount` | Annual AVM aggregates for the tax year. |
| `saleYN`, `salePrice`, `saleRecordingDate` | Sale indicators tied to the tax year, when present. |
| `modificationTimestamp` | When the tax history record was last modified. |

See GraphQL introspection for the full `BaseTaxHistory` leaf list (exemptions, certification date, value-per-sqft fields, and related flags).

#### `salesAndTransferHistory` → `[BaseSalesAndTransferHistory!]`

Backed by OData `SalesAndTransferHistory`. Returns recorded sales and ownership transfers for the property.

| Argument | Type | Description |
| --- | --- | --- |
| `limit` | `Int` | How many transfer rows to return (default `1`, max `100`). |
| `orderBy` | `BaseSalesAndTransferHistorySort` | Sort by `modificationTimestamp` (`ASC` / `DESC`). |

| Field | Description |
| --- | --- |
| `transactionId` | CDL transaction identifier. |
| `documentType`, `recordingDate`, `originalDateOfContract` | Recording / contract metadata. |
| `salesPrice`, `pricePerSquareFoot` | Transfer price metrics. |
| `sellerFullName1`, `buyerFullName1` (+ secondary party fields) | Seller and buyer parties. |
| `titleCompanyName`, `totalTransferTax`, `reoStatus`, `distressedSaleYN` | Title, tax, and distress indicators. |
| `modificationTimestamp` | When the history record was last modified. |

#### `mortgageHistory` → `[BaseMortgageHistory!]`

Backed by OData `MortgageHistoryAggregate`. Returns historical mortgage and lien records for the property.

| Argument | Type | Description |
| --- | --- | --- |
| `limit` | `Int` | How many mortgage rows to return (default `1`, max `100`). |
| `orderBy` | `BaseMortgageHistorySort` | Sort by `modificationTimestamp` (`ASC` / `DESC`). |

| Field | Description |
| --- | --- |
| `loanId`, `transactionId`, `recordId` | Loan and transaction identifiers. |
| `loanAmount`, `interestRate`, `loanType`, `termMonths` | Core loan terms (see schema for the full term set). |
| `lenderNameBeneficiary`, `borrowerName1` | Lender and borrower parties. |
| `recordingDate`, `modificationTimestamp` | Recording and last-modified timestamps. |

See GraphQL introspection for the complete `BaseMortgageHistory` and `BaseSalesAndTransferHistory` field lists.

### Example history selection

```
nodes {
  cid
  estimatedValue { estimate confidenceScore }
  historicalEstimate(limit: 12, orderBy: { estimateDate: DESC }) {
    estimateDate
    estimate
    rangeMin
    rangeMax
  }
  taxHistory(filter: { taxYear: { ge: 2018 } }, orderBy: { taxYear: DESC }) {
    taxYear
    taxAnnualAmount
    taxAssessedValue
    marketValue
  }
  salesAndTransferHistory(limit: 10, orderBy: { modificationTimestamp: DESC }) {
    transactionId
    recordingDate
    salesPrice
    buyerFullName1
    sellerFullName1
  }
  mortgageHistory(limit: 10, orderBy: { modificationTimestamp: DESC }) {
    loanId
    loanAmount
    interestRate
    lenderNameBeneficiary
    recordingDate
  }
}
```

### `CensusGeo` fields (used under `location.census.*`)

| Field | Description |
| --- | --- |
| `id` | Census boundary identifier stored on the base property record. |
| `name` | Boundary display name from `CensusBoundary` (for example county/parish name). |
| `stateOrProvince` | State or province code associated with the boundary. |
| `country` | Country code for the boundary (typically `US`). |
| `type` | Census boundary classification (for example `Census County or Parish`, `Census Designated Place`). |
| `geom` | Boundary geometry payload (GeoJSON-like structure). |
| `latitude`, `longitude` | Representative coordinates for the boundary. |
| `version` | Boundary data version year. |

### Example response (abbreviated)

```
{
  "data": {
    "baseProperties": {
      "totalCount": 1,
      "nodes": [{
        "cid": "06037123456",
        "countyFipsCode": "06037",
        "parcelNumber": "1234-567-890",
        "modificationTimestamp": "2026-01-10",
        "propertyType": "Residential",
        "structure": { "bedroomsTotal": 4, "livingArea": 2500 },
        "estimatedValue": { "estimate": 1012000, "confidenceScore": 95 }
      }]
    }
  }
}
```

## RESO listings (`resoProperties`)

The `resoProperties` query returns MLS listing rows modeled as **RESO**-style groups (listing, location, structure, tax, financial, etc.).
Filters are translated to OData against the RESO listings endpoint; results expose rich nested objects on each `RESOProperty`.

### Connection result

| Field | Type | Description |
| --- | --- | --- |
| `totalCount` | `Int!` | Matching listing count when returned by the backing API. |
| `nodes` | `[RESOProperty]` | Listings for the current page. |

### Endpoint

```
POST /graphql
```

### Root query (example)

```
query ResoListings(
  $filter: RESOPropertyFilter!
  $limit: Int
  $offset: Int
  $orderBy: RESOPropertySort
) {
  resoProperties(filter: $filter, limit: $limit, offset: $offset, orderBy: $orderBy) {
    totalCount
    nodes {
      cid
      listingKey
      propertyType
      propertySubType
      listing {
        standardStatus
        listPrice
        closePrice
      }
      location {
        address {
          unparsedAddress
          city
          stateOrProvince
          postalCode
        }
      }
      baseProperty {
        countyFipsCode
        parcelNumber
      }
    }
  }
}
```

### Variables (example)

```
{
  "filter": {
    "listing": {
      "standardStatus": { "eq": "Active" }
    },
    "location": {
      "address": {
        "stateOrProvince": { "eq": "CA" }
      }
    }
  },
  "limit": 10,
  "offset": 0,
  "orderBy": {
    "price": { "listPrice": "DESC" },
    "dates": { "modificationTimestamp": "DESC" }
  }
}
```

### How filtering works

- **Input type:** `RESOPropertyFilter` with top-level `cid`, `listingKey`, `propertyType`, `location`, `listing`.
- **Listing subtree:** `listing.originatingSystemName`, `listing.standardStatus`, `listing.price` (close/list/previous prices), `listing.dates` (status/modification/close/on-market/off-market timestamps, days on market), `listing.agentOffice` (buyer/list/co agents & offices), `listing.structure`, `listing.equipment`, `listing.leaseAmount`, `listing.rawMlsPropertySubType`.
- **Location subtree:** `location.address.*` for street, city, state, postal, county, etc. - each uses RESO `StringFilter` with `eq`, `ne`, `contains`, `startsWith` as defined on that schema.
- **Operators:** Strings use comparison filters as noted above; numeric fields use `FloatFilter` and `IntFilter`; dates use `DateTimeFilter` with `eq, gt, ge, lt, le`.
- **Combining:** Same as base properties: omit top-level `and` or `or` for an implicit AND bundle, or supply explicit logical objects when your client passes structured filters.
- **Backend:** Built server-side into OData for the Property/listings service (not the same entity set as BaseProperties).

| RESOPropertyFilter branch | Role |
| --- | --- |
| `cid`, `listingKey` | Pin or scan specific listings / linked public CID. |
| `propertyType` | RESO string filter on listing property type. |
| `location` | Address-centric geographic filters. |
| `listing` | MLS workflow, pricing, dates, agents, equipment, lease, structure hints. |

### How sorting works

`orderBy: RESOPropertySort` supports nested groups:

| Group | Fields |
| --- | --- |
| `price` | `closePrice`, `listPrice`, `listPriceLow`, `originalListPrice`, `previousListPrice` |
| `dates` | `modificationTimestamp` (and other date sorts if enabled in schema) |

Use `ASC` or `DESC` per field. Only mapped fields are sent to the listings OData layer.

### How expansion works

- Select any subset of `RESOProperty` groups: `business`, `characteristics`, `equipment`, `farming`, `financial`, `hoa`, `listing`, `location`, `occupantOwner`, `structure`, `tax`, `unitTypes`, `utilities`, identifiers (`universalPropertyId`, etc.).
- **`baseProperty`:** Joins to Insights Base Property by CID - include only when you need assessor/public-record fields to avoid an extra Insights fetch per listing.

### `RESOProperty` - root groups (schema)

| Field | GraphQL type | Summary |
| --- | --- | --- |
| `cid`, `listingKey`, `propertyType`, `propertySubType` | Scalars / strings | Identification and classification. |
| `listing` | `RESOListingGroup` | Status, pricing, dates, agents, remarks, media hooks. |
| `location` | `RESOLocationGroup` | Address, schools, GIS, parcel context. |
| `structure`, `tax`, `financial`, … | RESO groups | Building details, tax roll, finance, HOA, equipment, utilities, etc. |
| `baseProperty` | `BaseProperty` | Linked assessor/public-record profile. |

See the GraphQL schema / introspection for the full list of leaf fields inside each group.

### Pagination

| Argument | Role |
| --- | --- |
| `limit` | Page size (maps to backend top/skip semantics). |
| `offset` | Rows to skip. |

## Schools (`schools`)

The `schools` query reads the Insights **Schools** OData entity. Each `School` node is one school record; filters are **flat** OData column names (no nested filter objects like Base Property).

### Connection result

| Field | Type | Description |
| --- | --- | --- |
| `totalCount` | `Int!` | Row count for the filter. |
| `nodes` | `[School]` | School records for this page. |

### Endpoint

```
POST /graphql
```

### Root query (example)

```
query SchoolsQuery(
  $filter: SchoolFilter!
  $limit: Int
  $offset: Int
  $orderBy: SchoolSort
) {
  schools(filter: $filter, limit: $limit, offset: $offset, orderBy: $orderBy) {
    totalCount
    nodes {
      id
      name
      status
      modificationTimestamp
      location {
        address { city stateOrProvince postalCode }
        gis { countyFipsCode latitude longitude }
      }
      classification { level gradeRangeMin gradeRangeMax charterYN }
      enrollment { studentsTotal studentTeacherRatio }
      district { id name status }
      testScores { testYear subjectName percentMetStandard }
      schoolRatings { schoolRating schoolRank schoolRankYear }
    }
  }
}
```

### Variables (example)

```
{
  "filter": {
    "stateOrProvince": { "eq": "CA" },
    "city": { "contains": "Los Angeles" }
  },
  "limit": 25,
  "offset": 0,
  "orderBy": { "name": "ASC" }
}
```

### How filtering works

- **Flat filters:** Every key on `SchoolFilter` maps to a single OData property (e.g. `city`, `studentsTotal`, `latitude`). There is no `location.address.city` path in the filter - use `city` directly.
- **Operators:** Same primitives as elsewhere: `StringFilter` (`eq, contains, startswith`), plus `IntFilter`, `FloatFilter`, and `BooleanFilter` for fields such as `charterYN`.
- **Combining:** Resolver wraps plain objects in `{ and: ... }` when you omit explicit `and` or `or`.
- **Strings in OData:** Insights double-quotes certain school string literals for OData compatibility - handled inside the API client.

| SchoolFilter (examples) | Purpose |
| --- | --- |
| `id`, `districtId`, `name`, `status` | Identity and lifecycle. |
| `unparsedAddress`, `city`, `stateOrProvince`, `postalCode`, … | Address matching. |
| `countyFipsCode`, `countyOrParish` | Geography filters. |
| `level`, `gradeRangeMin`, `gradeRangeMax` | Grade span. |
| `charterYN`, `magnetYN`, `privateYN`, `virtualYN` | Program flags. |
| `studentsTotal`, `studentTeacherRatio`, `graduationRate`, … | Numeric metrics. |
| `latitude`, `longitude` | Geo bounds / proximity filters. |

### How sorting works

`SchoolSort` exposes top-level fields only. Mapped OData columns include:

| Sort field | Notes |
| --- | --- |
| `name`, `city`, `stateOrProvince`, `status` | Lexical / status ordering. |
| `studentsTotal` | Enrollment size. |
| `graduationRate` | School-level graduation metric. |

Directions: `ASC` or `DESC`. Only one field per group is typical; multiple entries depend on server mapping (comma-separated OData).

### How expansion works

- **`district`:** Resolver loads `SchoolDistrict` via `districtId` + DataLoader (batched).
- **`testScores`:** Loads TestScores rows where `schoolId` equals this school’s `id`.
- **`schoolRatings`:** Loads accountability/rating rows for the school id.
- Omit these fields in the GraphQL selection set when you do not need them to reduce Insights calls.

### `School` - fields by level (schema)

| Field | Description / child type |
| --- | --- |
| `id`, `name`, `status`, `modificationTimestamp` | Core identifiers and freshness. |
| `location` | `SchoolLocation` -> `address` (`SchoolAddress`), `gis` (`SchoolGIS`). |
| `contact` | `SchoolContact` (website, phone). |
| `classification` | `SchoolClassification` (level, grades, charter/magnet/private/virtual flags). |
| `enrollment` | `SchoolEnrollment` (counts, ratios, demographics). |
| `spending` | `SchoolSpending` (per-pupil and federal/state splits). |
| `graduation` | `SchoolGraduation` (rate + year). |
| `district` | Optional `SchoolDistrict` parent. |
| `testScores` | `[TestScore!]!` - assessment outcome rows. |
| `schoolRatings` | `[SchoolRating!]!` - rating/rank rows. |

### Pagination

`limit` maps to OData `$top`, and `offset` maps to `$skip`.

## School districts (`schoolDistricts`)

The `schoolDistricts` query reads the Insights **SchoolDistricts** OData entity. Filters use **flat** district columns; expansions load related schools and test rows by foreign key.

### Connection result

| Field | Type | Description |
| --- | --- | --- |
| `totalCount` | `Int!` | District rows matching the filter. |
| `nodes` | `[SchoolDistrict]` | District records for this page. |

### Endpoint

```
POST /graphql
```

### Root query (example)

```
query SchoolDistrictsQuery(
  $filter: SchoolDistrictFilter!
  $limit: Int
  $offset: Int
  $orderBy: SchoolDistrictSort
) {
  schoolDistricts(
    filter: $filter
    limit: $limit
    offset: $offset
    orderBy: $orderBy
  ) {
    totalCount
    nodes {
      id
      name
      status
      modificationTimestamp
      location {
        address { city stateOrProvince postalCode }
        gis { countyFipsCode countyOrParish }
      }
      composition {
        gradeRangeMin
        gradeRangeMax
        numberElementarySchools
        numberMiddleSchools
        numberHighSchools
      }
      ratings { districtRating districtRank districtRankYear }
      graduation { graduationRate graduationRateYear }
      schools { id name level status }
      testScores { testYear subjectName percentMetStandard }
    }
  }
}
```

### Variables (example)

```
{
  "filter": {
    "stateOrProvince": { "eq": "TX" },
    "name": { "contains": "Independent" }
  },
  "limit": 10,
  "offset": 0,
  "orderBy": { "name": "ASC" }
}
```

### How filtering works

- **Flat filters:** Keys on `SchoolDistrictFilter` map 1:1 to OData properties (e.g. `name`, `city`, `districtRating`, `numberElementarySchools`).
- **Operators:** Identical vocabulary to schools (`eq, contains, startswith`, plus numeric comparisons).
- **Combining:** Implicit `and` wrapping when `and` or `or` is omitted at the top level.

| SchoolDistrictFilter (examples) | Purpose |
| --- | --- |
| `id`, `name`, `status` | Identity. |
| `unparsedAddress`, `city`, `stateOrProvince`, postal fields | District office address. |
| `countyFipsCode`, `countyOrParish` | Regional filters. |
| `gradeRangeMin`, `gradeRangeMax` | District-wide grade span. |
| `numberElementarySchools`, `numberMiddleSchools`, `numberHighSchools` | Composition counts. |
| `districtRating`, `districtRank`, `districtRankOf`, `districtRankYear` | Accountability metrics. |
| `graduationRate`, `graduationRateYear` | District graduation filters. |

### How sorting works

| SchoolDistrictSort field | Typical use |
| --- | --- |
| `name`, `city`, `stateOrProvince`, `status` | Lexical ordering. |
| `districtRating`, `districtRank` | Performance ordering. |
| `graduationRate` | Outcome ordering. |

### How expansion works

- **`schools`:** Batch-fetches all `School` rows whose `districtId` equals this district’s `id` (large result sets possible - use pagination on the parent query and narrow filters).
- **`testScores`:** TestScores joined with `schoolId` equal to the district id (same identifier space as documented on `TestScore`).

### `SchoolDistrict` - fields by level (schema)

| Field | Description / child type |
| --- | --- |
| `id`, `name`, `status`, `modificationTimestamp` | Core identifiers. |
| `location` | `SchoolDistrictLocation` -> `address`, `gis`. |
| `composition` | `SchoolDistrictComposition` (grade span, school counts). |
| `ratings` | `SchoolDistrictRatings`. |
| `graduation` | `SchoolDistrictGraduation`. |
| `schools` | `[School!]!` - member schools (expanded query). |
| `testScores` | `[TestScore!]!` - linked assessments. |

### Pagination

Same as schools: `limit` maps to OData `$top`, and `offset` maps to `$skip`.
