← 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 more than 4.0M permits from 55 cities into a single REST schema, geocodes them with lat/lng coordinates, and refreshes them 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 served from api.builddata.ca. One header is required on every request:

bash
# Required on every request
X-API-Key: YOUR_API_KEY

Free tier: 100 requests/day, records first seen more than 30 days ago. Pro: 10,000 requests/day including the last 30 days, at $29 CAD/month.

Endpoints

The core endpoints are below. 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://api.builddata.ca/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-API-Key: YOUR_API_KEY" \
  -H ""

Proximity search (2 km radius)

curl
curl -G "https://api.builddata.ca/permit" \
  --data-urlencode "lat=43.6532" \
  --data-urlencode "lng=-79.3832" \
  --data-urlencode "radius_km=2" \
  --data-urlencode "limit=50" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H ""

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://api.builddata.ca"
HEADERS = {
    "X-API-Key": "YOUR_API_KEY"
}

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://api.builddata.ca/permit/stats" \
  --data-urlencode "municipality=calgary" \
  --data-urlencode "group_by=permit_type_canonical" \
  --data-urlencode "period=1y" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H ""

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 55 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?
55 cities across 9 provinces: 21 Ontario cities (Toronto, Ottawa, Hamilton, Kitchener, and more); Quebec cities (Montreal, Laval, Quebec City, Longueuil); Alberta cities (Calgary, Edmonton, Red Deer, Lethbridge, Grande Prairie, Strathcona County); 17 BC cities (Vancouver, Victoria, Surrey, Burnaby, 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,378,722). 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, every endpoint and every city, for records first seen more than 30 days ago. Pro plan: $29 CAD/month, 10,000 requests/day, including the last 30 days.
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: 4.0M permits, 55 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 and breakdowns as of May 6, 2026 (3,378,722 total; 2,631,431 geocoded). City and permit totals elsewhere on this page were updated September 22, 2026. 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 4.0M Canadian building permits today.

Get Your Free API Key View OpenAPI Spec