Docs / Find Contact Info
Recipes
Find Contact Info
Buy records first, then enrich them with POST /enrichments, or turn on contact lookup inside one request. Paid plans and live trials.
Who this is for
Lead-gen teams who need a person to contact, not just a project to read about.
The rules, in one place
- Off unless you ask. Default search, detail, AI search, and CSV stay unenriched.
- Paid plans and live trials. Unpaid keys get
402 enrichment_requires_paid_plan. A live trial spends the separate 5-lookup grant. Names, titles, and companies from the filing still ship without the toggle. The website CSV Find Contact Info toggle stays on paid plans. - It does not spend export records.
POST /enrichmentstakes project IDs you already hold. A new or changed record was already counted when you pulled it. The lookup draws the Find Contact Info allowance instead. - You pay for a contact that is found. 1 credit per project the first time email, phone, or a personal LinkedIn profile comes back. Ten lookups that find nothing cost 0. Ten lookups that find seven contacts cost 7.
Enrich records you already hold
The flagship flow: pull the records that match your thesis, then enrich exactly the ones you kept. The pipeline runs about a minute per lead, so it is a job you poll, not a request you wait on.
# 1. Queue enrichment for purchased projects (free, 202 Accepted)
# Idempotency-Key is REQUIRED: a retry replays the same job instead of
# submitting a second one (400 invalid_request without it).
POST /api/v1/enrichments
Idempotency-Key: enrich-sunbelt-2026-08-27
{ "projectIds": [10754108, 10754109, 10754110] }
# 2. Poll progress until status is completed (queued | running | completed | failed)
GET /api/v1/enrichments/57
# → { "status": "running", "requested": 100, "processed": 57, "found": 41, "pending": 43, ... }
# 3. Read the contacts back by ID, so this is a re-delivery and not a new search
# An id selection suppresses the default date window, so a purchase with an
# older meeting date still comes back.
GET /api/v1/projects/search?projectIds=10754108,10754109,10754110&include=contacts&contactOptions={"cachedOnly":true}A job takes at most 250 project ids; a longer list is a 400 invalid_request on projectIds carrying details.maxProjectsPerJob. A full month's allowance is therefore more than one job, so split it. IDs you never purchased are skipped and named under skipped.notPurchased. Buy them through any billed route first. IDs already enriched recently are skipped free under skipped.alreadyEnriched. found counts the projects where a usable person email, phone, or personal LinkedIn profile was found. API Only includes enrichment on purchased records; trials have a separate five-project grant. The allowance is reported in three different shapes, so read the one the route you called actually returns: the submit response carries an allowance object (used / limit / remaining / resetsAt); the poll carries contactBilling (reserved, charged, released, qualifyingProjects), plus the flat counters allowanceUsed and allowanceRemaining, and no allowance object at all; and GET /account/credits carries it under data.enrichment as monthlyAttemptsLimit / attemptsUsed / attemptsRemaining / resetsAt. A finished job settles the allowance to newly unlocked projects with qualifying contact channels, so a project that came back empty costs you nothing, and if the per-request ceilings stop a job early the poll body says so: notAttempted counts the projects never tried, they were not charged against the allowance, and resubmitting those ids creates a new maximum hold. Polling costs zero: meta.creditsUsed stays 0 while contactBilling reports the cumulative job settlement. Workers process projects in parallel; estimatedCompletion is a polling hint, not a deadline.
How to turn it on
Same feature as the website export toggle. Pick one path:
# CSV from the API: same job as the map Export button
GET /api/v1/projects/search?format=csv&include=contacts&states=UT
# JSON live lookup: cannot run inside a GET
POST /api/v1/projects/ai-search/jobs
{ "query": "multifamily continued for parking", "states": ["UT"], "limit": 50, "include": "contacts" }
# JSON, contacts we already hold: no new vendor lookup
GET /api/v1/projects/search?include=contacts&contactOptions[cachedOnly]=true&states=UTThe same search, with Find Contact Info switched on
curl -X POST 'https://api.boardwalkai.com/api/v1/projects/ai-search/jobs' \
-H 'Authorization: Bearer bwk_live_YOUR_KEY' \
-H 'Content-Type: application/json' \
-d '{
"query": "multifamily projects that were continued for parking concerns",
"states": [
"UT"
],
"limit": 50,
"include": "contacts",
"contactOptions": {
"minConfidence": "high"
}
}'Response · HTTP 202
{
"requestId": 918509,
"status": "queued",
"pollUrl": "https://api.boardwalkai.com/api/v1/projects/ai-search/jobs/918509",
"meta": {
"creditsReserved": 50,
"limit": 50,
"trialTruncation": null,
"requestId": "req_01K1QF3M0000EXAMPLE0001"
}
}The response shape on this page is the one the endpoint builds: the keys, their nesting, and their types are asserted against the shipping code by a test. The numbers inside it are the scenario being walked through, not a measurement of the corpus.
A GET that asks for a live lookup returns 400 enrichment_requires_async and names the AI-search job and the CSV export. representation=compact cannot carry contacts; use standard or full.
Optional knobs
| Option | Values | Effect |
|---|---|---|
contactOptions.minConfidence | high, medium | The floor for a contact to ship. Defaults to medium. There is no low. |
contactOptions.cachedOnly | boolean | JSON only: return contacts we already hold. No new lookups. Not valid on format=csv. |
contactOptions.companyOnly | boolean | Company-level contacts only. |
contactOptions.maxProjects | positive integer | Cap how many of the returned projects are looked up. |
An unknown key or an unknown value is a 400 that costs nothing, before anything runs. Sending cachedonly instead of cachedOnly fails loudly rather than quietly running the most expensive path on the platform.
Reading the result
JSON records carry a contactEnrichment block whether or not you asked, so clients branch on its value rather than its existence:
contactEnrichment.status | Meaning |
|---|---|
not_requested | You did not ask. The contacts array still carries anyone named on the filing itself. |
pending | Requested, not finished. |
completed | We looked. contactsFound says what we found, including zero. |
failed | The lookup could not be completed. You were still charged 1 credit for the record, which you received. |
CSV files put the looked-up person on the contact columns (name, title, email, phone, LinkedIn). Staff names may appear; staff email and phone never do.
The rules that do not bend
- Government staff contact details are never returned, on any tier. Planners, clerks, and commissioners appear in
publicOfficialswith their name, title, and organisation, and no contact channel. - Suppressed contacts are removed everywhere. Anyone can ask us to stop distributing their contact details, and once suppressed they do not ship to anyone.
Next
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.