BuildMapper API · v1

Permits API

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.

Authentication

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.

Endpoints

GET/api/v1/meVerify a key & view its scopes
GET/api/v1/permitsList / filter / pull permits
GET/api/v1/permits/{id}One permit (by id or permit_id)
GET/api/v1/permits/{id}/contactsContacts on a permit

Full machine-readable schema: /api/v1/openapi.json (drop it into Postman, Insomnia, or a code generator).

Listing permits

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:

Responses 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…" }
}

Getting each Monday's new permits

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.

Node example

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}`);

Rate limits & errors

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.

Webhooks (push)

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:

Each 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.

Versioning

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.