Data API
Projects
Search and retrieve construction and development projects tracked in Parcel, with stage, use, size, team, and timeline data.
Projects are individual construction or development initiatives tracked by Parcel: new buildings, renovations, mixed-use developments, and more. Each project carries address and geographic data, physical attributes (floor area, unit count, construction cost), a regulatory stage, and rolling counts of associated accounts, contacts, and signals.
The Data API requires the Pro plan. Non-Pro requests receive a 403 forbidden response.
Fields
| Field | Type | Notes |
|---|---|---|
id | uuid | Unique project identifier |
name | string | Project name |
primary_address | string | null | Street address |
city | string | null | City |
state | string | null | State (two-letter code) |
neighborhood | string | null | Neighborhood within the city |
primary_use | string | null | Primary building use: multifamily, mixed_use, office, industrial, retail, hotel, institutional, educational, r_and_d, other. See Enums |
uses | string[] | All uses (may include mixed-use values) |
stage | string | null | Regulatory stage. See Project stages for allowed values: pre_filing, filed, under_review, approved, permitted, under_construction, completed |
disposition | string | null | Outcome for inactive projects: active, denied, withdrawn, expired, stalled, cancelled |
gross_floor_area_sf | int | null | Gross floor area in square feet |
residential_units | int | null | Number of residential units |
cost_of_construction_usd | int | null | Estimated construction cost in USD |
stories | int | null | Storeys a filing or the project’s own site states. Null means no source states it, not a one-storey building |
parking_spaces | int | null | Parking spaces stated. 0 is a stated zero; null means no source states it |
gc_status | string | named (a general contractor is linked), not_yet_selected (a filing states none is chosen yet), or unknown (no source says). Derived when read |
lat | number | null | Latitude |
lng | number | null | Longitude |
first_signal_at | string | null | ISO 8601 date of the earliest signal |
last_signal_at | string | null | ISO 8601 date of the most recent signal |
created_at | string | ISO 8601 timestamp, first seen in Parcel |
accounts_count | int | Number of associated accounts |
contacts_count | int | Number of associated contacts |
signals_count | int | Number of associated signals; only those matching has_signal when that filter is set |
Detail fields (GET only)
A single-project GET also returns these. Search results leave them out to keep rows small.
| Field | Type | Notes |
|---|---|---|
height_ft | int | null | Building height in whole feet. Null means no source states it |
hotel_rooms | int | null | Hotel rooms stated. 0 is a stated zero; null means no source states it |
land_area_sf | int | null | Site area in square feet. Null means no source states it |
unit_mix | object | null | { studio_or_1br, two_br, three_br_plus }. Null when no bedroom count is known; a null member is a count no source states |
affordable_units | object | null | Income-restricted units by area-median-income band: { ami_0_30, ami_30_50, ami_50_80, ami_80_120, ami_unknown }. Null when no band is known; a null member is a band no source states |
use_areas | object[] | { use_type, floor_area_sf } for each non-residential use a source states, largest first. Empty means none is stated, not that there is none |
structure_types | string[] | Construction types a source names: wood_frame, podium, steel, concrete, mass_timber, masonry, modular. Empty means none is stated |
regulatory_flags | string[] | Review paths the project went through: chapter_40b, special_permit, site_plan_review, variance, pud, design_review, wetlands_notice_of_intent, large_project_review, small_project_review |
far | object | null | Floor area ratio, { value, derived_from: "gross_floor_area_sf/land_area_sf" }. Derived when read; no source states it. Null unless both areas are known |
updates_last_30_days | int | Signals filed in the last 30 days, counted by filing date rather than when Parcel found them |
Filters
Default sort: last_signal_at desc
| Filter key | Type | Notes |
|---|---|---|
stage | field | Regulatory stage. Array value = OR |
primary_use | field | Primary building use. Array value = OR |
city | field | City name. Supports fuzzy match |
state | field | State code, e.g. "MA" |
name | field | Project name. Supports fuzzy match |
gc_status | field | named, not_yet_selected or unknown. Array value = OR |
stories_known | field | known or unknown: whether any source states a storey count. REST only; the MCP search tools leave it out |
Sort keys: last_signal_at (default, desc), name, created_at, signals_count (rank projects by activity). With a has_signal filter, signals_count counts only the signals that match it. Sorting by signals_count, or filtering with has_signal or without_signal, keeps the count inside your plan’s signal history window.
Range keys: stories, residential_units, gross_floor_area_sf, cost_of_construction_usd, last_signal_at (date, YYYY-MM-DD)
A range never matches a project whose value is unknown (null). To find projects with no stated storey count, filter stories_known to unknown.
Range filter shape: { "min": <number>, "max": <number> } for numeric fields and { "min": "YYYY-MM-DD", "max": "YYYY-MM-DD" } for last_signal_at. Both min and max are optional and inclusive.
Relational filters
Use relational filters to match projects based on properties of their related records.
| Key | Inner filter fields |
|---|---|
has_account | role, name, account_type |
without_account | role, name, account_type |
has_signal | signal_type, filed_at (range) |
without_signal | signal_type, filed_at (range) |
has_contact | role, title |
without_contact | role, title |
has_account.role and has_contact.role use different vocabularies. Account roles are developer, owner, architect, gc, engineer, law_firm, consultant, landscape_architect, other; contact roles are developer_contact, architect_contact, engineer_contact, gc_contact, agency_pm, attorney, owner_contact, other. An account role like gc in has_contact.role matches nothing.
Each without_* key takes the same inner fields as its has_* twin, but matches projects with NO related record satisfying the inner criteria (NOT EXISTS) instead of at least one. See Filtering for the semantics.
Includes
Valid include keys for GET /data/projects/{id}: accounts, contacts, signals.
Example: GET /data/projects/<id>?include=accounts&include=signals
Search example
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": { "city": { "value": "Boston" }, "stage": { "value": "approved" }, "residential_units": { "min": 50 }, "has_account": { "account_type": { "value": "developer" } } }, "sort": { "field": "last_signal_at", "order": "desc" }, "limit": 10 }'const response = await fetch( 'https://api.parcelengineering.com/api/v1/data/projects/search', { method: 'POST', headers: { 'Authorization': 'Bearer pcl_your_api_key', 'Content-Type': 'application/json', }, body: JSON.stringify({ filters: { city: { value: 'Boston' }, stage: { value: 'approved' }, residential_units: { min: 50 }, has_account: { account_type: { value: 'developer' }, }, }, sort: { field: 'last_signal_at', order: 'desc' }, limit: 10, }), });
const { data, metadata } = await response.json();{ "data": [ { "id": "8a1f3c2e-0003-4b7d-9e5a-000000000003", "name": "1250 Boylston Street", "primary_address": "1250 Boylston St", "city": "Boston", "state": "MA", "neighborhood": "Fenway", "primary_use": "mixed_use", "uses": ["residential", "retail"], "stage": "approved", "disposition": "active", "gross_floor_area_sf": 180000, "residential_units": 120, "cost_of_construction_usd": 62000000, "stories": 9, "parking_spaces": 40, "gc_status": "unknown", "lat": 42.3467, "lng": -71.0972, "first_signal_at": "2023-06-01", "last_signal_at": "2025-11-14", "created_at": "2023-06-01T09:00:00Z", "accounts_count": 3, "contacts_count": 7, "signals_count": 14 } ], "metadata": { "total": 218, "total_is_capped": false, "credits": 10 }}The data array is trimmed to one record above; a full limit: 10 page returns 10 records and costs 10 credits (1 per record returned).