# Boardwalk Land Use Intelligence API

Boardwalk turns city and county meeting minutes into structured records about development
projects — what is proposed, where, by whom, and what was decided.

**Base URL:** `https://api.boardwalkai.com/api/v1`
**Documentation:** https://boardwalkai.com/docs/api/

## Read this before you spend anything

- **County, State, and National: every plan includes 100,000 export records a month, the same number on a county plan, a state plan, and National. 1 export credit per new or changed record delivered through the API, MCP, CSV, or the app. An unchanged record already delivered is free. Counts are free. Find Contact Info is separate: 1 credit per project the first time this account receives email, phone, or a personal LinkedIn profile. Standalone API Only: 1 credit per delivered record; Find Contact Info is included on records already purchased. Free trial: 50 record-export credits (search, CSV, MCP) and 5 Find Contact Info lookups, counted separately. Empty contact lookups cost nothing.**
- **`GET /projects/count` is free and takes the same filters as the search route.** Call it
  first. It does not deliver a record, so it spends neither counter. On County, State, and
  National, each new or changed record then counts toward the shared monthly export cap;
  Find Contact Info is a separate credit. On API Only and the free trial, new or changed
  record deliveries use record credits. A one-state plan is that state; a county plan those
  counties; National and API Only are nationwide. `GET /locations/states` marks each state
  `entitled`. Empty contact lookups cost nothing. Hitting the export cap returns
  `429 export_limit_reached` and charges nothing.
- **On API Only, a paid page is delivered whole or refused whole — never trimmed.**
  Each page is billed whole while it fits the balance; the first page that does not fit
  is refused with `402 insufficient_credits`, a quote, and no charge. A free-trial page
  can be shortened by the trial balance; check `meta.trialTruncation`.
- **A 402 tells you how to recover.** `quote.affordableRecordCount` is the `limit` to retry
  with — it buys what the balance still affords — and `quote.countUrl` re-prices the set
  for free so you can narrow. A `credits_exhausted` refusal carries no quote: the balance
  cannot pay for even one record.
- **Errors and zero-result queries cost nothing.** Neither does re-receiving a record the
  account already holds unchanged.

## Authentication

```
Authorization: Bearer bwk_live_YOUR_KEY
```

Keys beginning `bwk_live_` read the production corpus and can spend credits.
`bwk_test_` is reserved for a separate sandbox environment that is **not serving
data yet** — a test-prefix key is refused with `503 sandbox_unavailable` on
every route today, including the free ones, so use a `bwk_live_` key for
evaluation. Keys issued before the Boardwalk rename
begin `csk_` and are still accepted. A web session or login token is
**not** a credential for this API.

## Writes need an Idempotency-Key

`POST /enrichments` and `POST /alerts/{alertId}/run` **require** an
`Idempotency-Key` header. Without one they return `400 invalid_request` with
`error.param: "Idempotency-Key"` and nothing happens. Use any opaque string up
to 255 characters of `A-Z a-z 0-9 _ . : -`, one per distinct request. Re-sending
the same key replays the original result and charges nothing extra — that is the
safe way to retry. Reusing one key for a *different* request is `409
idempotency_key_reuse`.

## Connect Boardwalk to ChatGPT

Add Boardwalk once, install it, then select it in Chat or Work.

Server URL: `https://mcp.boardwalkai.com/mcp`. Use OAuth. No API key.

Provider instructions checked October 4, 2026.

### Personal account

Developer mode is documented for Plus, Pro, Business, Enterprise and Education on the web. Your workspace may restrict it.

1. On the web, open Settings → Security and login and enable Developer mode.

2. Open ChatGPT Plugins → +. Name it Boardwalk and paste the server URL. Choose OAuth and automatic/dynamic client registration if offered, then create it. Leave optional client ID and secret blank.

3. Open Boardwalk in your personal Plugins directory, install it with the + button, and sign in to Boardwalk when prompted.

4. Start a new Chat or Work conversation. Select Boardwalk from the tools menu, or type @Boardwalk where available.

### Company account

Workspace policy controls who can add, publish, install or use plugins. Ask your admin if Boardwalk is missing.

1. A workspace admin adds Boardwalk using the Personal account steps, with the server URL and OAuth.

2. In ChatGPT Plugins → Personal, the admin opens Boardwalk’s three-dot menu → Publish and chooses which workspace roles can use it.

3. Members open the workspace Plugins directory, install Boardwalk and sign in with their own Boardwalk account. Select it in a new Chat or Work conversation.

[ChatGPT setup guide](https://developers.openai.com/plugins/quickstart)

[Workspace setup](https://developers.openai.com/plugins/build/plugins)

[Developer mode availability](https://developers.openai.com/api/docs/guides/developer-mode)

[Workspace permissions](https://learn.chatgpt.com/docs/enterprise/apps-and-connectors)

## Using Boardwalk from an agent

- Boardwalk records what the public filing stated — the same facts someone would have
  after reading that agenda. Absent fields were not in the filing. That is expected for
  early pre-construction intel, not an error, not “missing data,” and not a failed
  extraction. Tell the user what is known. Do not open with “this looks wrong.”
- Search like the map: `q` on `count_projects` / `get_projects` is the map search box
  (project name, developer, words from the filing). Combine it with Region, Type, Date
  Range, Acres, Project Size, Entitlement Status, Application Type, Govt. Decisions,
  and overlays. Use `ai_search_projects` only when they need evidence from the minutes.
- List tools return **project details** (name, headline, summary, status, place, party names
  without emails, size). Do not request `representation=full` or `get_document`
  in a loop.
- Always `count_projects` (or the named preflight) before hydrating. Chat pages stay
  small (about 25, never more than 50 in one result). If they want to review or rank
  in chat and `matchCount` is over 250, tell them the number and either tighten
  filters or work in batches of 250. If they said CRM, CSV, or email — including
  after a pull they further filtered — use `create_export` with that `alertId` /
  `listId` (same extra filters) or the `projectIds` you kept. Never paste CSV rows
  into chat.
- First session: `get_preferences`, then ask types + geography. Say once: record pulls
  are free on County, State, and National plans inside the plan geography; trial and API
  Only use record credits. Find Contact Info uses 1 credit per project on geographic
  plans, is included on purchased API Only records, and has a separate 5-lookup trial
  grant. Empty fields mean the filing did not say it.
- When they name something they already saved, `list_alerts` / `list_user_lists` then
  `get_alert_matches` (no deliveryId) or `get_list_results`. Pull every match except
  dismissed — pending review stays in; helping manage those is a main use of this MCP.
  Say the name, how many are already on it, and how many are still pending review. If
  the name is unclear, or “leads” could mean a saved set or a new search, ask once —
  do not search the whole database.
- “Find / search / like X in Y” with no saved name is `count_projects` then `get_projects`.
- “Filter these / of those / among them / 2+ acres / just the offices” after a pull
  stays inside those ids — extra filters on the same pull tool (`get_alert_matches` or
  `get_list_results`). If the set is too large for chat, tighten that way first, then
  pull the subset. Never drop the set and search again.
- Do not call `run_alert` unless they asked to run it now. Do not mention deliveries
  or delivery ids to the user.
- Confirm `create_enrichment` in the transcript before starting a job. Confirm
  `delete_alert` — it deletes for every member of a shared alert, and only the
  alert's owner or the team admin may call it (other members get a 403).
- On County, State, and National plans, searching, viewing, and exporting records are
  free inside the plan geography; Find Contact Info uses **1 credit per project** when it
  first delivers email or phone. Trial and API Only record pulls can use credits. Empty
  contact lookups cost nothing. Counting, project types, locations, and alert
  configuration are free.
- `describe_data` lists questions this corpus cannot answer (denial rates, time-to-
  entitlement, project value). If the question is there, say so and stop.
- You can connect without a paid Boardwalk plan. Upgrading does not require reconnecting.
  If a tool result says an active Boardwalk trial or plan is needed, relay that
  sentence verbatim. Do not tell them to reconnect. Do not invent a `bwk_live_` key as
  the MCP fix.

## Endpoints

| Endpoint | Cost | What it does |
| --- | --- | --- |
| `GET /projects/search` | Plan dependent | County, State, and National: 1 export record per project that is new to this account or changed since the last delivery, against the shared 100,000 a month. API, MCP, CSV, and the app share that counter. An unchanged record already delivered is free. `format=csv` queues the full filter match and returns HTTP 202. Find Contact Info is a separate allowance. Standalone API Only and the free trial: 1 credit per new or changed record; unchanged owned records are free. API Only includes Find Contact Info on records already purchased. |
| `GET /projects/{id}` | Plan dependent | County, State, and National: 1 export record the first time this account receives the project, against the shared 100,000 a month. A later read of the same unchanged project is free. Email, phone, and LinkedIn stay withheld until Find Contact Info unlocks them, which spends the contact allowance, not export records. Standalone API Only and the free trial: 1 credit the first time. API Only includes Find Contact Info after the record is purchased. |
| `GET /projects/sync` | Plan dependent | County, State, and National: 1 export record per new or changed project, against the shared 100,000 a month. Unchanged records bill 0. Standalone API Only and the free trial: 1 credit per new or changed record. |
| `POST /projects/ai-search` | Free | **Not part of the public API — you cannot call this with an API key.** Any `bwk_live_`/`csk_` key is refused with `400 invalid_request` (`error.param: "endpoint"`, `details.asyncEndpoint`) before anything is parsed; use `POST /projects/ai-search/jobs` instead. On the session-authenticated web product this path is free, like structured search. |
| `POST /projects/ai-search/jobs` | Plan dependent | County, State, and National: confirmed new or changed matches count toward the shared 100,000 export records a month. Rejected candidates do not. Find Contact Info on those records is a separate job. Standalone API Only and the free trial: credits are reserved at 1 per requested record and settled against confirmed matches. API Only includes Find Contact Info after the record is purchased. |
| `GET /documents/{id}` | Plan dependent | County, State, and National: 1 export record the first time this account receives the document, against the shared 100,000 a month. A later read of the same document is free. Standalone API Only and the free trial: 1 credit the first time. |
| `GET /alerts/{alertId}/matches` | Free | Free, every time. Omit deliveryId to read every match except dismissed (pending review included) — the same set the website table shows. Passing a deliveryId re-reads a past API run. Neither path charges, and neither does include=contacts here: it reads what the alert's own lookups already found. |
| `GET /projects/count` | Free | Free, always. A count does not deliver a project record, so it does not use export records or Find Contact Info credits. On API Only and the trial it also prices the set. |
| `GET /projects/ai-search/jobs/{id}` | Free | Polling is free. The records the job delivers were charged when the job settled, not when you read them. |
| `DELETE /projects/ai-search/jobs/{id}` | Free | Cancelling is free and gives credits back, never takes them: a queued job refunds its full reservation immediately; a running job settles at the next batch boundary, charging only confirmed records and refunding the rest. |
| `GET /account/credits` | Free | Free. Your plan, your Find Contact Info allowance, and exportCredits (limit, used, remaining, resetsAt) for the shared 100,000 export records a month. |
| `POST /enrichments` | Plan dependent | County, State, and National: 1 credit per project the first time this account receives email, phone, or a personal LinkedIn profile for it. Standalone API Only: included on records this pool has already purchased — unpurchased ids are skipped as `notPurchased`. A live free trial spends the separate 5-lookup grant (not the 50 record-export credits). Empty lookups cost nothing. The maximum is held when queued, excluding already-unlocked projects; completion charges qualifying deliveries and releases the rest. Company links and names alone do not qualify. |
| `GET /enrichments/{id}` | Free | Free to poll: meta.creditsUsed stays 0. contactBilling reports the cumulative reserved, charged, released, refunded, and qualifyingProjects counts; repeated polls do not charge again. |
| `GET /health` | Free | Free and unauthenticated. A **liveness probe**, not part of the record contract: its response shape carries no schema guarantee and can change without a schema-version bump, which is why it is deliberately absent from the OpenAPI document. Do not generate a client against it or parse its body. |
| `GET /taxonomy` | Free | Free. The complete Building Type tree. |
| `GET /taxonomy/project-types` | Free | Free. |
| `GET /taxonomy/subtypes` | Free | Free. |
| `GET /taxonomy/asset-classes` | Free | Free. |
| `GET /taxonomy/action-types` | Free | Free. |
| `GET /taxonomy/action-categories` | Free | Free. |
| `GET /locations/states` | Free | Free. Resolve the ids you filter with. |
| `GET /locations/states/{id}` | Free | Free. |
| `GET /locations/states/{id}/counties` | Free | Free. |
| `GET /locations/states/{stateId}/counties/{countyId}/cities` | Free | Free. |
| `GET /locations/counties/{id}` | Free | Free. |
| `GET /locations/cities/{id}` | Free | Free. |
| `GET /analytics/datasets` | Free | Free. GET only. What the analytics plane can and cannot answer, self-described. |
| `POST /analytics/aggregate` | Free | Free. POST only (GET returns 404/405). Returns counts and group keys, never a project id, name, address, contact or document. |
| `POST /analytics/rankings` | Free | Free. POST only (GET returns 405). Same small-group suppression floor as /aggregate. |
| `GET /alerts` | Free | Free. Configuring alerts never costs credits; only delivered records do. |
| `POST /alerts` | Free | Free. |
| `GET /alerts/{alertId}` | Free | Free. |
| `PATCH /alerts/{alertId}` | Free | Free. |
| `DELETE /alerts/{alertId}` | Free | Free. |
| `POST/GET /alerts/{alertId}/preview` | Free | Free. Preview returns how many records a rule would deliver and what that would cost, not the records themselves. |
| `POST /alerts/{alertId}/run` | Free | Free. A manual run delivers matching records at no credit cost. Finding contact info on those records is a separate enrichment. |
| `GET /alerts/{alertId}/deliveries` | Free | Free. The delivery history and what each run charged. |
| `GET /alerts/{alertId}/deliveries/{deliveryId}` | Free | Free. |
| `POST /alerts/{alertId}/dismissals/{projectId}` | Free | Free. Dismissing a match stops it being re-offered. |
| `DELETE /alerts/{alertId}/dismissals/{projectId}` | Free | Free. |
| `GET /lists` | Free | Free. Every list your membership resolves — your own and the ones shared with you. |
| `GET /lists/{listId}` | Free | Free. One list's configuration and share state, plus `results.billable.credits` — what a full `/results` pull would charge right now. This is the free preflight for `/results`. |
| `GET /lists/{listId}/results` | Plan dependent | County, State, and National: 1 export record per project that is new or changed, against the shared 100,000 a month. Unchanged records already delivered are free. Standalone API Only and the free trial: 1 credit per new or changed record. Alert companions return the same match set as the alert (every match except dismissed, pending review included). Extra search filters apply only inside those existing ids. Filter and polygon lists return stored criteria plus a search link. |
| `POST /lists` | Free | Free. Creates a named membership list (the "new list" option on save). |
| `POST /lists/{listId}/projects` | Free | Free. Adds project ids to a membership-backed list. Idempotent per id. |
| `DELETE /lists/{listId}/projects/{projectId}` | Free | Free. Removes a project from a membership-backed list. |
| `GET /account/preferences` | Free | Free. The account's saved search preferences (types, geography, website). |
| `PATCH /account/preferences` | Free | Free. Update the account's saved search preferences. |
| `GET /reference/statuses` | Free | Free. Canonical status filter tokens plus display aliases (e.g. scheduled). |
| `GET /reference/owner-types` | Free | Free. The owner-type vocabulary used by alert criteria and CSV output. **Not** accepted as a `/projects/search` filter — the `ownerTypes` filter was withdrawn on 2026-08-27. |
| `GET /reference/decision-bodies` | Free | Free. Canonical values accepted by the decisionBodies filter. |
| `GET /reference/request-types` | Free | Free. Canonical values accepted by the requestTypes filter. |
| `POST /exports` | Free | Free to queue a product CSV. Pass alertId, listId, or projectIds to export a saved set (optional extra filters stay inside those ids). Omit all three and the body is a new search. Find Contact Info on those rows is 1 credit per project the first time this account receives email, phone, or a personal LinkedIn profile for it. |
| `GET /exports` | Free | Free. Lists export jobs for your team (including jobs queued with POST /exports or format=csv on search). |
| `GET /exports/{id}` | Free | Free. Poll job status until ready. |
| `GET /exports/{id}/download` | Free | Free re-download of a ready product-core CSV. Rows are free to queue. Find Contact Info on those rows, if you asked for it, was 1 credit per project unlocked. |

## Filtering

- **Geography accepts ids, USPS abbreviations or names.** `states=UT`, `states=Utah` and
  `states=1` are all valid. Naming a county or city **replaces** the state scope rather than
  narrowing within it — the states you also sent are not searched in full, and the response
  says so with a `geography_narrowed_to_smallest_scope` warning. An
  ambiguous city name is an error rather than a silently wider search.
- Values inside one parameter are an **OR**; different parameters are **AND**ed.
- **An unknown parameter is a `400`, not a silent drop.** If you are unsure of a name, read
  the filter reference at https://boardwalkai.com/docs/api/filters/ rather than guessing.
- **A request with no date filter searches the last 12 months.** Pass `dateRange=all` for the
  whole corpus. The response discloses the applied default in `meta.appliedDefaults`.
- **Search returns private development leads by default** (`leadTypes=private`, disclosed in
  `meta.appliedDefaults`) — the same view as the map. Add `leadTypes=public` for public
  projects (roads, schools, utilities, public buildings), `private_minor` for small private
  jobs, or `public_minor`; `private` is always included, so `leadTypes` only ever widens.
  Ground-up vs renovation is `constructionTypes`. Standard and full records carry
  `leadType` and `stage`; compact omits them. `developerName` (aliases `developer`,
  `company`) is an exact-phrase search over parties named on the filing.
- **Re-read records the account already bought with `projectIds`** (alias `ids`, at most 100
  per request; accepted on search, count and sync). Owned unchanged records come back at 0
  credits, and no default date window applies to an id selection. On the free
  `GET /projects/count` it must be the **only** filter.
- **On a repeat pull add `deliveryMode=new`** so records the pool already owns are withheld
  instead of filling the page. `new` withholds an owned record whether or not it changed —
  use `GET /projects/sync` when you want the updates.
- `meta.appliedFilters` is a receipt: it shows what actually ran after name resolution.
  `meta.warnings[]` is the other one — branch on its `code`, never its prose.

## Representations

`representation=compact|standard|full`. All three cost the same. Use `compact` (21 fields) for anything you are holding in
context; use `full` (85 fields) only when you need decisions with named votes, document
metadata or the source text.

## The loop to follow

1. `GET /locations/states` — free. Confirm the market exists.
2. `GET /projects/count` with the real filters — free. It prices the whole match set:
   `billable.credits` is what delivering the entire set would cost, and
   `affordability.affordable` says whether that fits the balance. `affordable: false`
   does not mean your next search call fails — it means the balance runs out somewhere
   inside the set, and `affordability.maxAffordableRecords` is how many records you can
   still collect.
3. `GET /projects/search` with the identical filters — the count response hands you
   that URL in `links.search`. On API Only, each paid page is delivered whole while
   it fits the balance; a page that does not fit is refused with a quote and no charge.
   Trial pages may be shortened; check `meta.trialTruncation`.
4. Read `meta.creditsRemaining` from the response rather than tracking a local counter —
   the credit pool is shared with the account's other users and surfaces.

On `402 insufficient_credits`, retry with `limit` set to
`error.details.quote.affordableRecordCount` — that buys what the balance still affords —
or follow `error.details.quote.countUrl`, a free URL that re-prices the exact filter set
that was refused, and narrow from there. A `credits_exhausted` refusal carries no quote:
the balance cannot pay for even one record.

## Limitations you should tell your user about

- **Coverage is per-state, not nationwide.** `GET /locations/states` is free and lists exactly
  what exists. Check it before promising a market.
- **Filtered values are rechecked before billing.** A record with no current value for a
  requested status, Building Type or measurement constraint is excluded rather than delivered
  as a null-valued match. Do not add a client-side null-value workaround.
- **An empty field means we could not establish the value**, not that the value is zero. We
  do not guess.
- **Free-trial keys are the one truncation exception.** A paid page is delivered whole or
  refused whole, so a short page on a paid key always means that is all that matched. A
  short page on a trial key may instead be the trial budget; check `meta.trialTruncation`.
- **Find Contact Info is opt-in.** Send `include=contacts` or `POST /enrichments`. Paid
  plans and live trials (the trial has 5 lookups, counted separately from the 50 record
  credits). Unpaid keys get `402 enrichment_requires_paid_plan`. Do not enrich the rest
  of the catalog.

---

_Generated from the Boardwalk API contract. Do not edit by hand — run `bin/console app:export-docs`._
