Pull every permit into the tools you already use (your CRM, data warehouse, spreadsheet, or a custom script) instead of exporting CSVs by hand. The API is a simple, read-only REST endpoint that takes an API key.
Create a key in Settings → Integrations. You'll see the full key once. Store it somewhere safe. Send it as a Bearer token on every request:
curl https://YOUR_DOMAIN/api/v1/me \
-H "Authorization: Bearer bm_live_xxxxxxxxxxxxxxxxxxxx"Keep keys server-side. A request with a missing or revoked key returns 401. Revoke a key any time from the same screen; it stops working immediately.
/api/v1/meVerify a key & view its scopes/api/v1/permitsList / filter / pull permits/api/v1/permits/{id}One permit (by id or permit_id)/api/v1/permits/{id}/contactsContacts on a permitFull machine-readable schema: /api/v1/openapi.json (drop it into Postman, Insomnia, or a code generator).
Filter and page through permits. All parameters are optional.
curl "https://YOUR_DOMAIN/api/v1/permits?city=Austin&min_value=500000&limit=50" \
-H "Authorization: Bearer $BM_KEY"Supported query parameters:
since / until: permits added to BuildMapper in a time window (ISO 8601)filed_after / filed_before: by permit filing datepulled_after / pulled_before: by the date a permit file was uploaded to BuildMapperlatest_batch=true: only the newest uploaded permit file (new files land every Monday)city, state, project_type, status, permit_classmin_value: minimum project valuebbox: geographic box: minLng,minLat,maxLng,maxLatorder: desc (default, newest first) or asclimit: 1–500 (default 100), and cursor for the next pageResponses are wrapped in a stable envelope:
{
"data": [
{
"id": "8f3c…",
"permit_id": "2026-BLD-04417",
"address": "1200 W 6th St",
"city": "Austin", "state": "TX",
"location": { "lat": 30.27, "lng": -97.75 },
"project_type": "New Construction",
"status": "Issued",
"value": 2400000,
"filed_at": "2026-06-18T00:00:00Z",
"added_at": "2026-06-19T14:02:11Z",
"builder": { "id": "…", "name": "Reyes Builds Co", "trade": "GC", "website": null },
"owner": null
}
],
"pagination": { "limit": 50, "order": "desc", "has_more": true, "next_cursor": "eyJ0cyI6…" }
}To keep pulling new permits, sort oldest-first and remember the added_at of the last record you saw. On each run, pass it back as since and page until has_more is false:
# Pseudocode for a scheduled job (e.g. every 15 min)
since = load_checkpoint() # last added_at you processed
cursor = None
while True:
params = { "order": "asc", "limit": 200, "since": since }
if cursor: params["cursor"] = cursor
res = GET("/api/v1/permits", params, bearer=BM_KEY)
for permit in res["data"]:
upsert(permit) # dedupe on permit["id"] or permit["permit_id"]
since = permit["added_at"]
if not res["pagination"]["has_more"]:
break
cursor = res["pagination"]["next_cursor"]
save_checkpoint(since)The cursor is keyset-based: pages stay consistent even while new permits arrive, so you never miss or double-count rows. Dedupe on the stable id (or the source permit_id) on your side for safety.
const res = await fetch("https://YOUR_DOMAIN/api/v1/permits?order=asc&limit=200", {
headers: { Authorization: `Bearer ${process.env.BM_KEY}` },
});
const { data, pagination } = await res.json();
console.log(`${data.length} permits, more: ${pagination.has_more}`);Requests are limited per key. Every response includes X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers; exceeding the limit returns 429with a Retry-After header. Errors share one shape:
{ "error": { "code": "unauthorized", "message": "Invalid or revoked API key." } }Status codes: 401 auth, 403 scope, 404 not found, 422 bad parameter, 429 rate limited.
Prefer events pushed to you instead of polling? Register an HTTPS endpoint in Settings → Integrations and we'll POST a JSON event as things happen:
permit.created: a new permit landed in BuildMapperproject.saved: you saved a project; we send the lead and permit to your systemEach delivery is a POST with this body, plus headers X-BuildMapper-Event, X-BuildMapper-Event-Id (idempotency key), and X-BuildMapper-Signature:
{
"id": "evt_…", // unique per event, dedupe on this
"type": "permit.created",
"created": "2026-06-23T18:00:00Z",
"data": { /* same shape as a permit (or project) from the REST API */ }
}Verify the signature to be sure it came from us. The header is t=<unix>,v1=<hmac>; recompute HMAC-SHA256 of "{t}.{raw_body}"with your endpoint's signing secret:
import crypto from "node:crypto";
function verify(rawBody, header, secret) {
const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}Respond with a 2xx within 10 seconds to acknowledge. Non-2xx or timeouts are retried with exponential backoff (up to ~24h); an endpoint that keeps failing is auto-disabled and can be re-enabled from Settings. Use the Test button there to send a sample ping and confirm your verification works.
This is v1. We only make additive changes within a version (new fields, new endpoints); anything breaking ships under a new version path. Treat unknown JSON fields as forward-compatible and ignore them.