Skip to content

Data API

Filtering

Filter grammar for the Parcel Data API search endpoint. OR within a field, AND across fields.

Pass a filters object in the body of any POST /data/{dataset}/search request. Each key is a filterable field for that dataset. Three value shapes are supported.


Field filter

Field filter shape
{
"filters": {
"<field>": {
"value": "<scalar> | [<scalar>, ...]",
"exact_match": false,
"exclude": false
}
}
}
PropertyTypeRequiredDefaultNotes
valuescalar or arrayyesAn array of values means OR (match any).
exact_matchbooleannofalseText fields only. Exact case-insensitive match instead of fuzzy. Not accepted on enum-backed fields (a closed vocabulary is always exact); passing it there is a 400.
excludebooleannofalseNegates the filter (exclude records that match).

Example: projects in one of two stages

Request body
{
"filters": {
"stage": {
"value": ["approved", "permitted"]
}
}
}

Range filter

Range filter shape
{
"filters": {
"<field>": {
"min": "<number | YYYY-MM-DD>",
"max": "<number | YYYY-MM-DD>"
}
}
}

Both min and max are optional and inclusive. Omit either to make the range open-ended. Numeric fields accept integers or floats. Date fields use YYYY-MM-DD (day-granular, UTC).

Example: projects with 50 to 200 residential units

Request body
{
"filters": {
"residential_units": {
"min": 50,
"max": 200
}
}
}

Example: signals filed in 2025

Request body
{
"filters": {
"filed_at": {
"min": "2025-01-01",
"max": "2025-12-31"
}
}
}

Relational filter

A relational filter matches records that have at least one related record satisfying the inner criteria. The outer key is the relational key for the dataset (e.g. has_account on projects); the value is itself a filters object using the related dataset’s filterable fields.

Relational filter shape
{
"filters": {
"<relational_key>": {
"<related_field>": { "value": "..." }
}
}
}

Example: projects that have a developer account named “Suffolk”

Request body
{
"filters": {
"has_account": {
"role": { "value": "developer" },
"name": { "value": "Suffolk", "exact_match": false }
}
}
}

To match records that have NO related record satisfying criteria, use the without_* relational keys. On projects: without_account, without_signal, without_contact. On accounts: without_project, without_signal, without_contact. Each takes the same inner filters as its has_* twin and compiles to NOT EXISTS.

This is different from a field-level exclude inside a has_* block: has_account: { role: { value: "engineer", exclude: true } } matches projects that have an account whose role is NOT engineer (they have some non-engineer account), whereas without_account: { role: { value: "engineer" } } matches projects that have NO engineer account at all.

Example: projects with no engineer account

Request body
{
"filters": {
"without_account": {
"role": { "value": "engineer" }
}
}
}

Combining filters

Conditions across different top-level keys (including relational blocks) are combined with AND. Values within a single field filter are combined with OR.

result = (field_A matches any of its values)
AND (field_B matches any of its values)
AND (relational_key_C: related record matches inner criteria)

Combined example: approved Boston projects with 50+ units that have a GC on the team

Search projects
curl -X POST https://api.parcelengineering.com/api/v1/data/projects/search \
-H "Authorization: Bearer pcl_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"filters": {
"stage": { "value": "approved" },
"city": { "value": "Boston" },
"residential_units": { "min": 50 },
"has_account": {
"role": { "value": "gc" }
}
},
"sort": { "field": "last_signal_at", "order": "desc" },
"limit": 25
}'

Unknown keys and invalid enum values

Passing a key that is not a valid filter field for the dataset returns a 400 error. Passing a value outside an enum field’s allowed set returns the same 400 validation shape; the allowed values appear in a nested invalid_enum_value issue. See Errors for both response shapes.


Per-dataset filterable fields

Each dataset page lists its filter keys, range keys, and relational keys.

DatasetFilter keysRange keysRelational keys
accountsaccount_type, city, state, namenonehas_project, without_project, has_signal, without_signal, has_contact, without_contact
contactsname, title, email, account_idnoneat_account, on_project
projectsstage, primary_use, city, state, nameresidential_units, gross_floor_area_sf, cost_of_construction_usd, last_signal_athas_account, without_account, has_signal, without_signal, has_contact, without_contact
signalssignal_type, project_idfiled_at, discovered_atfor_project, for_account

Discovering fields

Filter vocabularies (which fields exist, which enum values are valid, what relational sub-fields accept) are served by the fields dataset rather than duplicated in documentation or tool schemas. Call POST /data/fields/search with a dataset to get the full vocabulary in one response, or fetch a single descriptor with GET /data/fields/{dataset}/{key}. Over MCP, the same discovery flow is search_fields({dataset: "projects"}) first, then compose the search; invalid enum values are rejected by the API with a 400 naming the allowed values.