Pagination
Offset paging with a hard ceiling, and the cursor-based sync feed for when you need every record.
GET /projects/search pages with limit and offset. limit is the page size per request (integer from 1 to 100) — not a hard ceiling on how many rows you can retrieve overall. Values below 1 or non-integers return 400 invalid_request (same posture as an unknown representation). Asking for more than 100 is clamped to 100 and the response says so in meta.warnings rather than failing. Keep paging with offset (credits still apply per returned row). offset must be a non-negative integer — negative values return 400 invalid_request (they are not silently floored to 0). You can also page with page (1-based); when offset is absent it becomes (page - 1) * limit.
CSV output
Default response format is JSON. Pass format=csv to queue a product CSV export of the full filter match — the same survivor set count quotes, not one JSON page of 100. The writer is the map Export CSV button. Core columns always ship. Add include=contacts to turn on Find Contact Info for those rows (paid plans only) and include the contact columns. Already-owned unchanged rows stay in the file and reserve 0 credits.
CSV does not stream in the search response body. You get HTTP 202 with exportJobId, absolute pollUrl / downloadUrl, and webExportsUrl (/exports in the product UI). Credits are reserved only for new or changed rows (1 each). Re-download of a ready file is free. limit / offset / page are ignored on this path — they belong to JSON paging. If nothing matched, status is no_matching_rows and no job is queued.
Know when to continue
meta.pageCount is the number of records in this response and meta.hasMore tells you whether another offset page is retrievable. Continue by adding pageCount to your current offset while hasMore is true. meta.total is a deprecated compatibility field containing offset + pageCount; despite its old name, it is not a total match count.
When you need the size of the whole selection, call the free GET /projects/count route with the same filters. On a search response, the matchCount, matchCountIsExact and matchCountBasis fields describe only what that request's bounded candidate scan could prove. Count on a large state with dateRange=all can take several seconds under load — fine for a one-shot quote, but cache identical queries in agent loops rather than re-hitting count on every page.
The honest ceiling
In practice this only bites on very broad queries. The answer is not deeper paging, it is narrowing — by geography, by date window, or by a measurement bound — and counting each narrowed variant for free until the total is one you can actually retrieve.
When you need every record: use sync
Offset paging over a changing dataset can skip records: if a record is updated and re-sorts while you are on page 4, you can miss it. GET /projects/sync walks by cursor in last-updated order, which makes it lossless, and it refuses sort, order, offset and page because a caller-chosen ordering would break that guarantee while appearing to work.
One net-new record and one that changed since you last saw it
curl 'https://api.boardwalkai.com/api/v1/projects/sync?states=UT&representation=compact&limit=2' \
-H 'Authorization: Bearer bwk_live_YOUR_KEY'Response · HTTP 200
{
"data": [
{
"id": 184203,
"recordType": "project",
"projectName": "Alta Ridge Phase II",
"boardwalkLink": "https://boardwalkai.com/map/#project=184203",
"mergedFrom": [
184203,
184987
],
"lastUpdated": "2026-06-18T09:22:41Z",
"meetingDate": "2026-06-17",
"delivery": {
"alreadyExported": false,
"updatedSinceExport": null,
"lastExportedAt": null,
"billed": true
},
"address": "3600 S Constitution Blvd, West Valley City, UT",
"city": "West Valley City",
"county": "Salt Lake",
"state": "UT",
"latitude": 40.6916,
"longitude": -112.0011,
"inOpportunityZone": true,
"inFemaFloodZone": false,
"propertyType": "residential",
"propertyTypeLabel": "Residential",
"landUses": [
{
"type": "residential",
"typeLabel": "Residential",
"subtype": "multifamily",
"subtypeLabel": "Multifamily",
"assetClass": "Apartment Building",
"assetClassLabel": "Apartment Building",
"matchedOn": []
}
],
"status": "approved_with_conditions",
"statusLabel": "Approved with Conditions"
},
{
"id": 191774,
"recordType": "project",
"projectName": "Foothill Commons",
"boardwalkLink": "https://boardwalkai.com/map/#project=191774",
"mergedFrom": [
191774
],
"lastUpdated": "2026-06-24T14:08:03Z",
"meetingDate": "2026-06-23",
"delivery": {
"alreadyExported": true,
"updatedSinceExport": true,
"lastExportedAt": "2026-07-19T02:14:55Z",
"billed": true
},
"address": null,
"city": "Provo",
"county": "Utah",
"state": "UT",
"latitude": 40.2338,
"longitude": -111.6585,
"inOpportunityZone": null,
"inFemaFloodZone": null,
"propertyType": "residential",
"propertyTypeLabel": "Residential",
"landUses": [
{
"type": "residential",
"typeLabel": "Residential",
"subtype": "multifamily",
"subtypeLabel": "Multifamily",
"assetClass": null,
"assetClassLabel": null,
"matchedOn": []
}
],
"status": "continued",
"statusLabel": "Continued"
}
],
"meta": {
"schemaVersion": "2026-09-16",
"representation": "compact",
"limit": 2,
"creditsUsed": 2,
"creditsRemaining": 284,
"delivery": {
"mode": "all",
"notPreviouslyExported": 1,
"previouslyExported": 1,
"updatedSinceExport": 1,
"billed": 2,
"notRebilled": 0,
"ledgerApplied": true
},
"sync": {
"cursor": "eyJ1cGRhdGVkQXQiOiIyMDI2LTA4LTAyVDE0OjA5OjIyWiIsImlkIjoxOTE3NzR9",
"hasMore": true,
"watermark": "2026-08-02T14:09:22+00:00",
"highWater": "2026-08-03T00:00:00+00:00",
"emitted": 2,
"pageSize": 2,
"candidatesScanned": 2
}
}
}Record bodies on this page were produced by running Boardwalk's production response mapper over a documented sample project, so the field set, the labels and the empty fields are exactly what the API emits. The project itself is a sample, not a real filing.
Carry meta.sync.cursor into the next request and stop when meta.sync.hasMore is false — the whole sync block is nested under meta.sync, alongside watermark, highWater, emitted, pageSize and candidatesScanned. A cursor is only valid for the filter set and entitlement scope it was created for; using it with a different query returns 400 invalid_cursor rather than quietly walking the wrong set.
Ready to make a call?
A free Boardwalk trial includes API access, 50 record-export credits (search, CSV, MCP), and 5 Find Contact Info lookups. Counting, taxonomy, location lookups and the analytics plane cost nothing, so you can evaluate the data before you spend a credit.