← Back to Blog
EN FR

Canadian Building Permits API: Developer Reference

Every Canadian municipality publishes building permit data separately, in its own format. Toronto uses CKAN. Calgary and Edmonton use Socrata. Vancouver uses ArcGIS. Some cities publish Excel files. None agree on field names, date formats, or permit classifications.

BuildData normalizes 3,219,264 permits from 57 cities into a single REST schema, geocodes 81% of records with lat/lng coordinates, and refreshes daily. This is the developer reference: authentication, endpoints, query examples, and design decisions worth knowing before you start.

What’s in the database

Every record shares the same schema regardless of source city. The permit_type_canonical field maps local permit names (building permit, construction neuve, renovation, etc.) to six stable categories:

Renovation dominates at 42.3%, followed by new construction at 26.1%. Additions (10.8%) and demolitions (3.1%) are also covered. 81.7% of records are geocoded using Statistics Canada’s Open Database of Addresses.

Top 10 cities by volume:

  • Montreal: 550,842
  • Calgary: 487,541
  • Toronto: 240,648
  • Edmonton: 239,904
  • Winnipeg: 158,800
  • Laval: 152,632
  • Brampton: 142,651
  • Sudbury: 114,888
  • Victoria: 107,338
  • Ottawa: 82,996

Authentication

All endpoints are accessed via RapidAPI. Two headers are required on every request:

bash
# Required on every request
X-RapidAPI-Key: YOUR_API_KEY
X-RapidAPI-Host: builddata-canadian-construction-data-api.p.rapidapi.com

Free tier: 100 requests/day. Pro: 10,000 requests/day at $29/month.

Endpoints

The API has four endpoints. The full OpenAPI 3.0.3 spec is at builddata.ca/openapi.json and imports directly into Postman, Insomnia, or any LLM tool-use framework.

API Surface
  • GET
    /permit
    Search permits. Filter by municipality, permit_type_canonical, status_canonical, q (full-text), issued_after, issued_before, lat/lng/radius_km. Cursor pagination.
  • GET
    /permit/stats
    Aggregated counts. group_by: permit_type_canonical, status_canonical, municipality. Filter by period.
  • GET
    /permit/{record_id}
    Single record by stable ID.
  • GET
    /zone
    Zoning classification at a coordinate (lat/lng). Returns zone code and description.

Query examples

New construction permits in Toronto since 2024

curl
curl -G "https://builddata-canadian-construction-data-api.p.rapidapi.com/permit" \
  --data-urlencode "municipality=toronto" \
  --data-urlencode "permit_type_canonical=new_construction" \
  --data-urlencode "issued_after=2024-01-01" \
  --data-urlencode "sort_by=date" \
  --data-urlencode "limit=20" \
  -H "X-RapidAPI-Key: YOUR_API_KEY" \
  -H "X-RapidAPI-Host: builddata-canadian-construction-data-api.p.rapidapi.com"

Proximity search (2 km radius)

curl
curl -G "https://builddata-canadian-construction-data-api.p.rapidapi.com/permit" \
  --data-urlencode "lat=43.6532" \
  --data-urlencode "lng=-79.3832" \
  --data-urlencode "radius_km=2" \
  --data-urlencode "limit=50" \
  -H "X-RapidAPI-Key: YOUR_API_KEY" \
  -H "X-RapidAPI-Host: builddata-canadian-construction-data-api.p.rapidapi.com"

Cursor pagination (for pipelines and agents)

Every list response includes a next_cursor field. Pass it as the cursor parameter on the next request. Cursors are stable across requests, even as new records are added.

python
import requests

BASE = "https://builddata-canadian-construction-data-api.p.rapidapi.com"
HEADERS = {
    "X-RapidAPI-Key": "YOUR_API_KEY",
    "X-RapidAPI-Host": "builddata-canadian-construction-data-api.p.rapidapi.com",
}

params = {"municipality": "calgary", "permit_type_canonical": "new_construction", "limit": 100}
records = []

while True:
    r = requests.get(BASE + "/permit", headers=HEADERS, params=params)
    data = r.json()
    records.extend(data["results"])
    if not data.get("next_cursor"):
        break
    params["cursor"] = data["next_cursor"]

print(f"{len(records)} permits fetched")

Aggregate stats (for dashboards and summaries)

curl
curl -G "https://builddata-canadian-construction-data-api.p.rapidapi.com/permit/stats" \
  --data-urlencode "municipality=calgary" \
  --data-urlencode "group_by=permit_type_canonical" \
  --data-urlencode "period=1y" \
  -H "X-RapidAPI-Key: YOUR_API_KEY" \
  -H "X-RapidAPI-Host: builddata-canadian-construction-data-api.p.rapidapi.com"

Record schema

json (permit record)
{
  "record_id": "23-123456",
  "municipality": "toronto",
  "address": "123 QUEEN ST W",
  "permit_type": "New Construction",
  "permit_type_canonical": "new_construction",
  "status": "Issued",
  "status_canonical": "issued",
  "issued_date": "2024-03-15",
  "work": "Erect 12-storey mixed-use building",
  "lat": 43.6487,
  "lng": -79.3958,
  "entity_type": "permit"
}

Canonical vs. raw fields: permit_type_canonical and status_canonical are normalized across all 57 cities. permit_type and status preserve the original municipal values. Use canonical fields for cross-city filters and aggregations; use raw fields when you need the source terminology.

Design decisions worth knowing

Use cursors, not offsets

The API does not support offset pagination. Every response returns next_cursor: pass it as the cursor parameter on the next request. Cursors are stable between calls, safe for agents making sequential tool calls across multiple turns.

Use /permit/stats for aggregate questions

To answer “how many new construction permits in Calgary last year,” use /permit/stats rather than paging through all records. The stats endpoint returns aggregated counts in under a second:

bash
/permit/stats?municipality=calgary&permit_type_canonical=new_construction&period=1y

Proximity search works across city boundaries

lat, lng, and radius_km work without specifying a municipality. Useful for queries near administrative borders, along transit corridors, or in metro areas that span multiple cities.

Full-text search on work descriptions

The q parameter runs full-text search across the work and address fields. Use it to find specific project types (“pool,” “demolition,” a developer name) rather than filtering by canonical type alone.

Frequently asked questions

Which cities are covered?
57 cities across 9 provinces: 28 Ontario cities (Toronto, Ottawa, Hamilton, Kitchener, and more); Quebec cities (Montreal, Laval, Quebec City, Longueuil, Saint-Jérôme); Alberta cities (Calgary, Edmonton, Red Deer, Lethbridge, Grande Prairie, Strathcona County); 13 BC cities (Vancouver, Victoria, Surrey, Burnaby, Richmond, Coquitlam, and more); plus Winnipeg, Regina, Halifax, Saint John, and St. John’s.
How often is the data updated?
Ingestion pipelines run daily at midnight UTC from each city’s open data portal. Most cities publish new permits in near real time; the typical lag between issuance and API availability is under 24 hours.
How does geocoding work?
Lat/lng coordinates come from Statistics Canada’s Open Database of Addresses. 81.7% of records are geocoded (2,631,431 of 3,219,264). Records without coordinates have lat and lng set to null and are returned normally in non-proximity queries.
Is there a free tier?
Yes. Basic plan: free, 100 requests/day, full access to all endpoints and all 57 cities. Pro plan: $29/month, 10,000 requests/day.
Where is the OpenAPI spec?
The full OpenAPI 3.0.3 spec is at builddata.ca/openapi.json. It includes all parameters, response schemas, and examples. Import directly into Postman, Insomnia, or any LLM tool-use framework.

The short version: 3.2M permits, 57 cities, one schema. No per-city integrations to write, no normalization to build. Start on the free tier, move to Pro when your pipeline is stable.

Data notes: Record counts as of May 6, 2026 (3,219,264 total; 2,631,431 geocoded). permit_type_canonical values: renovation (42.3%), new_construction (26.1%), other (14.0%), addition (10.8%), change_of_use (3.7%), demolition (3.1%). Percentages may not sum to exactly 100% due to rounding. Geocoding: Statistics Canada Open Database of Addresses (ODA).

Start querying 3.2M Canadian building permits today.

Get Your Free API Key View OpenAPI Spec