Documentation menu
Cookbook
OverviewNew permit leadsHighest-value permitsFurnace and rewire permitsPermits near a pointMegaproject pipelineProjects near a siteZoning at an addressEverything at one addressEndpoints
GET/zoneGET/geocodeGET/reverse-geocodeGET/zoningPOST/permit/estimateGET/contractor/statsGET/contractor/coverageGET/contractorGET/propertyGET/address/normalizeGET/opening_soonGET/openingGET/businessGET/business/statsGET/business/coverageGET/contractor/{name_normalized}GET/permitGET/inspectionGET/licenceGET/development_permitGET/planning_applicationGET/assessmentGET/major_projectGET/heritage_statusGET/occupancy_permitGET/permit/statsGET/inspection/statsGET/licence/statsGET/development_permit/statsGET/planning_application/statsGET/assessment/statsGET/major_project/statsGET/heritage_status/statsGET/occupancy_permit/statsGET/permit/coverageGET/inspection/coverageGET/licence/coverageGET/development_permit/coverageGET/planning_application/coverageGET/assessment/coverageGET/major_project/coverageGET/heritage_status/coverageGET/occupancy_permit/coverageGET/permit/{record_id}/historyGET/permit/{record_id}GET/inspection/{record_id}GET/licence/{record_id}GET/development_permit/{record_id}GET/planning_application/{record_id}GET/assessment/{record_id}GET/major_project/{record_id}GET/heritage_status/{record_id}GET/occupancy_permit/{record_id}Search development permits
Parameters
| Parameter | Type | Description | |
|---|---|---|---|
municipality | string | optional | Filter to one city by its slug, e.g. 'new_westminster', 'toronto', 'north_vancouver_city'.Exact match. The dataset's /coverage endpoint lists the valid slugs. 'city' is accepted as an alias. |
q | string | optional | Full-text search across the indexed fields of the record, e.g. q=roofing or q="Iron Hill".Matches business, contractor and project names, the address, the city and the permit or licence number, on every plan including free. It does not match a person's name: these datasets carry business records only. Space-separated words require all terms ("deck addition"). Use OR to match any term ("pool OR spa"), quotes for an exact phrase ("secondary suite"), and a leading minus to exclude ("renovation -interior"). |
contractor | string | optional | Filter permits by the firm that pulled them.A whole name is matched exactly against the normalized contractor name and is indexed; a fragment falls back to a substring across the contractor, company and builder fields. Cities file the same firm under several spellings, so the normalized form is the one that groups them. |
units_min | integer | optional | Permits with at least this many dwelling units.Reads whichever of units, dwelling_units, total_units or units_created the city publishes. |
floor_area_min | number | optional | Permits with at least this much floor area, in square metres.Reads floor_area or area_m2, whichever the city publishes. Cities that publish square feet are not converted and are not matched. |
storeys_min | integer | optional | Permits on a building of at least this many storeys. |
structure_type | string | optional | Filter by structure or building type as the city publishes it (exact, case-insensitive), e.g. 'Single Family Dwelling', 'Hotel'.Call /permit/stats for the values in use. |
neighbourhood | string | optional | Filter by neighbourhood (partial match).Matches whichever of neighborhood, neighbourhood or community the city publishes; Calgary calls it community. |
ward | string | optional | Filter by electoral ward as the city publishes it (exact, case-insensitive). |
zoning | string | optional | Filter by the zoning code on the record (partial match), e.g. 'R-1'.Present on permits, development permits and assessments. |
status | string | optional | Permits: the normalized status (issued, in_review, completed, expired, cancelled, unknown), same as status_canonical.Major projects: proposed, under_construction, complete, on_hold, cancelled. Business licences: the status the city publishes, e.g. APPROVED, case-insensitive. Other datasets: the status as the city publishes it. |
status_canonical | string | optional | Normalized permit status: issued, in_review, completed, expired, cancelled, unknown |
city | string | optional | Alias for municipality. |
issued_after | string | optional | Filter by date >= YYYY-MM-DD (issued_date).Aliases: date_from, date_after |
issued_before | string | optional | Filter by date <= YYYY-MM-DD.Aliases: date_to, date_before |
lat | number | optional | Latitude for proximity search.Send with lng. |
lng | number | optional | Longitude for proximity search.Send with lat. |
radius_km | number | optional | Radius in km, 0.1 to 25.Optional: lat and lng with no radius search 1 km around the point. The radius actually applied is returned as radius_km in the response, so a default is never silent. For every record at one street address rather than everything near it, use /property. |
sort_by | string | optional | Sort field: date (the record's own date, e.g. issued_date for permits) or value (the field value_min reads). |
sort_order | string | optional | Sort order: asc or desc |
limit | integer | optional | Records per page, 1 to 500.Free-tier keys are capped lower; the cap applied is the `count` in the response. |
offset | integer | optional | Records to skip. Fine for jumping to a known page;use `cursor` to walk a whole result set, because offset re-reads everything it skips and shifts when new records arrive. |
cursor | string | optional | Pagination cursor from next_cursor in a previous response.Use instead of offset for deep pagination. |
Request
curl --request GET \
--url 'https://api.builddata.ca/development_permit' \
--header 'X-API-Key: YOUR_API_KEY'import requests
url = "https://api.builddata.ca/development_permit"
headers = {"X-API-Key": "YOUR_API_KEY"}
resp = requests.get(url, headers=headers)
print(resp.json())const res = await fetch("https://api.builddata.ca/development_permit", {
headers: {
"X-API-Key": "YOUR_API_KEY",
},
});
const data = await res.json();
console.log(data);<?php
$ch = curl_init("https://api.builddata.ca/development_permit");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"X-API-Key: YOUR_API_KEY",
]);
echo curl_exec($ch);require "net/http"
require "uri"
uri = URI("https://api.builddata.ca/development_permit")
req = Net::HTTP::Get.new(uri)
req["X-API-Key"] = "YOUR_API_KEY"
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts res.bodypackage main
import (
"fmt"
"io"
"net/http"
)
func main() {
req, _ := http.NewRequest("GET", "https://api.builddata.ca/development_permit", nil)
req.Header.Add("X-API-Key", "YOUR_API_KEY")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}import java.net.URI;
import java.net.http.*;
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.builddata.ca/development_permit"))
.header("X-API-Key", "YOUR_API_KEY")
.build();
HttpResponse<String> res = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());Example response
{
"entity_type": "permit",
"count": 1,
"offset": 0,
"results": [
{
"record_id": "24 223023 PLB:00",
"municipality": "toronto",
"lat": 43.65401,
"lng": -79.42376,
"work": "Building Permit Related(PS)",
"status": "Permit Issued",
"address": "865 COLLEGE ST",
"description": "Plumbing - INTERIOR ALTERNATION OF 3RD FLOOR SUITE",
"issued_date": "2025-01-29",
"permit_type": "Plumbing(PS)",
"application_date": "2024-10-07",
"construction_value": null
}
]
}Response fields
One development_permit record. Measured over 2,193 live records sampled across municipalities. Percentages say how often each field is populated: sources publish different columns, so a field missing from a given record is normal rather than an error.
| Field | Type | Description |
|---|---|---|
address | string | Populated in 88% of sampled records. |
application_date | string | Always present. |
application_subtype | string | Populated in 76% of sampled records. |
community | string | Populated in 18% of sampled records. |
completed_date | string | Populated in 14% of sampled records. |
decision_date | string | Populated in 25% of sampled records. |
description | string | Populated in 72% of sampled records. |
fetched_at | string | When Nimbus last ingested this record. Bookkeeping, not source data. Always present. |
issued_date | string | Populated in 14% of sampled records. |
lat | string | Populated in 29% of sampled records. |
legal_description | string | Populated in 27% of sampled records. |
lng | string | Populated in 29% of sampled records. |
municipality | string | City slug. Call /{entity_type}/coverage for every slug in this dataset. Always present. |
neighborhood | string | Populated in 39% of sampled records. |
neighbourhood | string | Populated in 31% of sampled records. |
normalized_address | string | Address normalized for joining the same property across datasets. Populated in 88% of sampled records. |
permit_type | string | Populated in 48% of sampled records. |
pipeline_stage | string | Populated in 97% of sampled records. |
postal_code | string | Populated in 17% of sampled records. |
province | string | Populated in 84% of sampled records. |
record_id | string | Stable identifier for this record. Pass it to the /{entity_type}/{record_id} endpoint. Always present. |
record_number | string | Always present. |
roll_number | string | Populated in 18% of sampled records. |
status | string | Always present. |
status_canonical | string | Status mapped onto a shared vocabulary. This is what the status_canonical filter matches. Always present. |
ward | string | Populated in 32% of sampled records. |
work_type | string | Populated in 18% of sampled records. |
zoning | string | Populated in 18% of sampled records. |
46 more fields published by only one or two cities
Present on under 10% of records. Useful when you are working with a specific city, not something to rely on across the dataset.
| Field | Type | Populated |
|---|---|---|
advertised_date | string | 3.8% |
appeal_filed_date | string | 0.3% |
appealed_record_number | string | 0.3% |
application_url | string | 9.1% |
category | string | 3.0% |
construction_cost | number | 9.1% |
district | string | 9.1% |
dwelling_units_created | string | 9.1% |
dwelling_units_lost | string | 9.1% |
expiration_date | string | 4.9% |
fee | number | 9.1% |
geocode_failed | boolean | 2.1% |
hamlet | string | 2.6% |
housing_type | string | 2.4% |
inspection_outcome | string | 0.1% |
land_use_district | string | 8.8% |
land_use_district_description | string | 5.1% |
latitude | string | 9.1% |
legal | string | 8.7% |
longitude | string | 9.1% |
municipality_name | string | 8.9% |
normalized_business_name | string | 0.0% |
original_decision | string | 0.3% |
permit_name | string | 9.1% |
permitted_or_discretionary | string | 5.1% |
plan_block_lot | string | 7.1% |
primary_work_scope | string | 9.1% |
property_type | string | 0.3% |
proposed_use_code | string | 5.1% |
proposed_use_description | string | 5.3% |
published_date | string | 9.1% |
quadrant | string | 5.1% |
related_record_numbers | array | 0.1% |
stats_can_code | string | 0.0% |
structure_description | string | 9.1% |
structure_type | string | 9.1% |
subcategory | string | 3.0% |
subdivision | string | 9.1% |
total_units | integer | 2.4% |
type_of_structure | string | 9.1% |
units_created | string | 0.4% |
updated_date | string | 9.1% |
use | string | 3.9% |
variance_required | string | 9.1% |
year | string | 0.3% |
zoning_overlay | string | 9.1% |
Errors
Every error returns {"detail": "…"}, a sentence naming what was wrong and, where there is one, the fix.
| Status | When |
|---|---|
400 | The request is understood but cannot be served as asked, e.g. offset above 9500, or cursor combined with sort_by=value. The message names the fix. |
403 | Missing or invalid API key. |
404 | No such endpoint, or no record with that id. |
422 | A parameter is invalid: an unknown city slug, an impossible date, a limit above 500, an unrecognised sort field, or a misspelled parameter name. The message names the parameter and, where there is one, the nearest valid value. |
429 | Rate limit or plan quota exceeded. |
500 | Unexpected server error. |
504 | The query took too long. Narrow it with a municipality or a date range. |
Ready to build?Create a free key and start calling BuildData in minutes. No card required.
Get your API key →