Docs
Call the REST API or connect an AI assistant to find local businesses with owners, emails, and phone numbers.
Quick Start
Four steps take you from a niche to a list of enriched leads. Each example uses the placeholder key lp_example_key; replace it with your own.
1. Create an API key
Press Get API Key, sign in, and create a key on the API keys screen. The REST API is on every plan, including Free. Keep the key private: it spends your credits.
2. Start a search
Send a niche and a location. Replace 1015116 with the location_code that GET /locations?q= returns for your place. A search reserves its depth in credits, charges 1 credit for each business it returns, and refunds the rest in the same response. The response holds a job_id and the raw Google Maps businesses.
curl -X POST https://app.prospectlocally.com/api/v1/search \
-H "x-api-key: lp_example_key" \
-H "content-type: application/json" \
-d '{ "keyword": "plumber", "location_code": 1015116, "depth": 100 }'3. Poll the job
Enrichment (owner, emails, phones, and website data) continues in the background. Poll the job every 5 seconds until status is completed or failed;enriching means keep waiting. Polling costs nothing.
curl https://app.prospectlocally.com/api/v1/job/JOB_ID \
-H "x-api-key: lp_example_key"4. Fetch the results
Read the leads once the job has completed. The format parameter picks JSON or one of the CSV layouts, and reading results costs nothing.
curl "https://app.prospectlocally.com/api/v1/job/JOB_ID/results?format=json" \
-H "x-api-key: lp_example_key"MCP Server
The MCP server lets an AI assistant run searches, enrich leads, start campaigns, and export for you. It uses Streamable HTTP at this address:
https://app.prospectlocally.com/api/mcp- ChatGPT, claude.ai, and GoHighLevel connect by OAuth. Add the URL with no key, sign in, and press Allow.
- Claude Code, Cursor, and Codex send your API key as a bearer token in the
Authorizationheader.
For Claude Code, one command adds the server:
claude mcp add --transport http prospect-locally https://app.prospectlocally.com/api/mcp \
--header "Authorization: Bearer lp_example_key"An assistant sees and spends only the data and credits of the account that signed in or owns the key. Export links last 1 hour; ask the assistant for a new one when it runs out.
Connect your AI
Copy the setup prompt for your tool, paste it into the chat, and ask for leads in plain words.
Instructions for ChatGPT
Add the connector once, sign in, and ask ChatGPT for leads in plain words.
Connect to Prospect Locally so you can find, enrich and export local business leads for me. Add https://app.prospectlocally.com/api/mcp as a custom connector in ChatGPT. Do not enter an API key. Sign in to Prospect Locally when ChatGPT asks, then press Allow. Read the REST docs at https://prospectlocally.com/docs and the skill file at https://prospectlocally.com/local-prospects-skill.md. Work in this order: find_location to resolve the place, search_businesses to start a search, search_status until it finishes, then get_results to read the leads. For many cities, start a campaign and use export_campaign to download the CSV. Tell me the credit cost before searching, enriching or starting a campaign.
The 19 tools
The server offers these tools. Tools that spend credits say so in their description, so the assistant can quote the cost before it acts.
| Tool | Inputs | Returns | Credits |
|---|---|---|---|
| check_credits | none | plan, credits left, allowance, reset date | 0 |
| find_location | query (2+ characters) | up to 8 locations | 0 |
| search_businesses | keyword, location_code, depth (100, 300, 500, 700; default 300), exact_category_key | the POST /search body | up to depth; unused credits come back |
| search_status | job_id | the poll body and credits used | 0 |
| get_results | job_id, has_website, has_email, has_phone, has_mobile_phone, limit (default 50, max 200), page | trimmed leads | 0 |
| enrich_domains | leads[] of { domain, business_name, city, state } (1 to 10) | the POST /enrich body with each result trimmed | 1 per domain |
| find_campaign_locations | query, country, parent_code | campaign location items | 0 |
| preview_campaign | keyword, exact_category_key, name, max_leads, include[], exclude[] | planned location count, depth, maximum credits | 0 |
| create_campaign | keyword, exact_category_key, name, max_leads, include[], exclude[], campaign_id | the campaign | 0 |
| start_campaign | campaign_id (starts or resumes) | the campaign | 1 per business found |
| pause_campaign | campaign_id, action (pause or stop) | the campaign | 0 |
| list_campaigns | page, page_size | campaigns with status, progress, leads, credits | 0 |
| get_campaign | campaign_id | campaign detail | 0 |
| update_campaign | campaign_id, name, max_leads, include[], exclude[] | the campaign | 0 |
| get_campaign_leads | campaign_id, page, page_size (default 50, max 200), search | trimmed leads | 0 |
| export_campaign | campaign_id, format (simple, advanced, gohighlevel, instantly, oneclick) | export ID and a download link | 0 |
| get_export | export_id | status and a download link valid for 1 hour | 0 |
| add_search_to_campaign | campaign_id, job_id | the campaign | 0 |
| verify_phones | numbers[] (E.164, 1 to 100) | the POST /verify body | 5 per number; free for 30 days after a lookup |
Agent Skill
The skill file teaches an AI agent how to use Prospect Locally: the rules to follow before it spends credits, the endpoints, the tools, and the errors. Save it as CLAUDE.md or add it to your agent's instructions. The latest copy is always at /local-prospects-skill.md.
# Prospect Locally: agent skill
Save this file as `CLAUDE.md` (or add it to your agent's instructions) so the agent knows how to find, enrich, and export local business leads with Prospect Locally. The latest copy is always at https://prospectlocally.com/local-prospects-skill.md. The full API reference is at https://prospectlocally.com/docs.
## Rules to follow every time
1. Use a campaign when the user wants leads from many areas, such as a state, a country, or a list of cities. A campaign is saved, resumable, and removes duplicates across its locations.
2. Use POST /search for a one-off search, or a search of a few leads in one place, where the user wants the answer in the same conversation.
3. Enrich at most 10 leads in one call to POST /enrich or the enrich_domains tool. Split a longer list into batches of 10.
4. Before you search, enrich, start a campaign, or verify phone numbers, tell the user the credit cost and ask for a yes. Never spend credits on a guess, and never repeat a paid call the user did not ask for.
## What one lead contains
A search for a niche, such as `plumber`, in a place returns the businesses on Google Maps. Every lead is then enriched in the background. One lead holds:
- The Google Maps listing: `name`, `category`, `additional_categories`, `rank`, `gbp_url`, `place_id`, `google_cid`, `website`, and `website_status`.
- Contact data in `contact`: `address`, `city`, `state`, `state_code`, `country`, `lat`, `lng`, `work_hours`, `all_emails[]` (each with `is_role_based` and `is_free_provider`), and `all_phones[]` (each with a number, a `line_type` of mobile, landline, or voip, and a `carrier`).
- The owner: `owner`, `owner_title`, `owner_email`, and `contacts[]` for other named people.
- Reputation in `reputation`: `rating`, `reviews`, `review_snippet`, and `is_claimed`.
- Website data in `web`: `tech` (CMS, analytics, frameworks), `socials` (Facebook, Instagram, LinkedIn, YouTube, Yelp, TikTok), `has_blog`, and `meta_description`.
- Better Business Bureau data in `bbb`: `rating`, `accredited`, and `url`.
Every field is `null` when the data is unknown. Say so to the user instead of guessing a value, and never invent an email address or a phone number.
## Connect
- REST base URL: `https://app.prospectlocally.com/api/v1`. Send the header `x-api-key: lp_example_key` (use the user's own key; keys start with `lp_` and the user creates them on the API keys screen). The REST API is on every plan, including Free.
- MCP server: `https://app.prospectlocally.com/api/mcp` over Streamable HTTP. ChatGPT, claude.ai, and GoHighLevel connect by OAuth: the user adds the URL with no key, signs in, and presses Allow. Claude Code, Cursor, and Codex send `Authorization: Bearer lp_example_key`.
- An agent sees and spends only the credits and data of the account that signed in. Never ask the user to paste a key into a chat when the OAuth sign-in works.
- Try the API with no key and no credits at `https://app.prospectlocally.com/api/v1/demo/*`. Every demo response includes `"demo": true`, and the fixture job ID is `demo-job-plumbers-irvine`.
## Choose the right call
1. Find the `location_code` with `GET https://app.prospectlocally.com/api/v1/locations?q=austin texas` (key required, no credits, up to 8 results).
2. For one place, call `POST https://app.prospectlocally.com/api/v1/search`. For many places, create a campaign.
3. If the niche matches a Google Maps category, look it up with `GET https://app.prospectlocally.com/api/search/categories?q=website designer` (no key, no credits, up to 3 matches). A keyword equal to a category name turns on exact mode, which keeps and charges only businesses whose primary category matches. Pass `exact_category_key: null` to force a broad search.
4. Tell the user the credit cost and wait for a yes before step 2.
## REST endpoints
All paths start with `https://app.prospectlocally.com/api/v1` unless noted.
| Method and path | Inputs | Output |
| --- | --- | --- |
| `GET /locations` | `q` (2+ characters) | up to 8 `{ location_code, name, region, country, type, full_name }` |
| `GET /api/search/categories` | `q` (2 to 80 characters), no key | up to 3 `{ category_key, display_name }` |
| `POST /search` | `keyword`, `location_code`, `depth` (100, 300, 500, or 700; default 300), `exact_category_key` | `job_id` and the raw businesses at once |
| `GET /job/:jobId` | none | `{ job_id, status, keyword, total_businesses, enriched_count, failed_count, progress, created_at, completed_at }` |
| `GET /job/:jobId/results` | `format`, the four filters | leads in the chosen format |
| `POST /enrich` | `leads[]` of `{ domain, business_name?, city?, state? }`, 1 to 10 | `{ total, succeeded, failed, results, usage }` |
| `POST /verify` | `numbers[]` in E.164, 1 to 100 | `{ total, verified, failed, results, usage }` |
| `POST /campaigns` | `keyword`, `exact_category_key`, `campaign_id`, `name`, `max_leads` (minimum 100), `scope` with `include` and `exclude` location codes | the campaign; nothing starts and no credits are spent |
| `POST /campaigns/preview` | the same body | `planned_location_count`, `planned_depth`, `next_search_credits`, maximum credits |
| `GET /campaign-locations` | none, or `q` and `country`, or `parent_code` | location items with `has_children` and `eligible_count` |
| `GET /campaigns`, `GET /campaigns/:id` | `page`, `page_size` | campaigns, or one campaign with its plan and stats |
| `GET /campaigns/:id/plan`, `/searches`, `/leads` | `page`, `page_size` | the ordered locations, the child searches, the deduplicated leads |
| `PATCH /campaigns/:id` | `name`, `max_leads`, `scope` | the campaign (not allowed once completed or stopped) |
| `POST /campaigns/:id/actions/start`, `/pause`, `/resume`, `/stop` | none | the campaign; stop is permanent |
| `POST /campaigns/:id/searches` | `{ "job_id": "..." }` | adds a finished one-off search with the same keyword |
| `GET /campaigns/:id/export` | `format` | CSV of the deduplicated leads |
`POST /search` returns the raw Google Maps rows at once. Enrichment (owner, emails, phones, website data) continues in the background, and you read it from the results endpoint when the job is `completed`.
## MCP tools
The server offers 19 tools. The names below are exact.
| Tool | Inputs | Returns | Credits |
| --- | --- | --- | --- |
| `check_credits` | none | plan, credits left, allowance, reset date | 0 |
| `find_location` | query (2+ characters) | up to 8 locations | 0 |
| `search_businesses` | keyword, location_code, depth (100, 300, 500, 700; default 300), exact_category_key | the POST /search body | up to depth; unused credits come back |
| `search_status` | job_id | the poll body and credits used | 0 |
| `get_results` | job_id, has_website, has_email, has_phone, has_mobile_phone, limit (default 50, max 200), page | trimmed leads | 0 |
| `enrich_domains` | leads[] of { domain, business_name, city, state } (1 to 10) | the POST /enrich body with each result trimmed | 1 per domain |
| `find_campaign_locations` | query, country, parent_code | campaign location items | 0 |
| `preview_campaign` | keyword, exact_category_key, name, max_leads, include[], exclude[] | planned location count, depth, maximum credits | 0 |
| `create_campaign` | keyword, exact_category_key, name, max_leads, include[], exclude[], campaign_id | the campaign | 0 |
| `start_campaign` | campaign_id (starts or resumes) | the campaign | 1 per business found |
| `pause_campaign` | campaign_id, action (pause or stop) | the campaign | 0 |
| `list_campaigns` | page, page_size | campaigns with status, progress, leads, credits | 0 |
| `get_campaign` | campaign_id | campaign detail | 0 |
| `update_campaign` | campaign_id, name, max_leads, include[], exclude[] | the campaign | 0 |
| `get_campaign_leads` | campaign_id, page, page_size (default 50, max 200), search | trimmed leads | 0 |
| `export_campaign` | campaign_id, format (simple, advanced, gohighlevel, instantly, oneclick) | export ID and a download link | 0 |
| `get_export` | export_id | status and a download link valid for 1 hour | 0 |
| `add_search_to_campaign` | campaign_id, job_id | the campaign | 0 |
| `verify_phones` | numbers[] (E.164, 1 to 100) | the POST /verify body | 5 per number; free for 30 days after a lookup |
`get_results` and `get_campaign_leads` return trimmed leads (owner, emails, phones with line type, socials, website status, rating, address) so one call stays small. Ask for the next page instead of raising the limit above 200.
Campaigns created through MCP get a default `max_leads` of 500. Tell the user the cap and raise it only when they ask.
## Poll, then read the results
- Poll `GET /job/:jobId` every 5 seconds. Stop when `status` is `completed` or `failed`; `enriching` means keep waiting. Polling costs nothing.
- Read results with `GET /job/:jobId/results?format=json`. The formats are `json`, `simple`, `advanced`, `ghl`, `instantly`, and `oneclick`. Campaign exports use `advanced`, `simple`, `gohighlevel`, `instantly`, and `oneclick`.
- Four boolean filters narrow the results and combine with AND: `has_website`, `has_email`, `has_phone`, and `has_mobile_phone`. `true` requires the data, `false` requires its absence, and an omitted filter does nothing.
- For a campaign, start it, check `GET /campaigns/:id` now and then, and read `/leads` or export it. A paused campaign resumes after the user adds credits. A campaign in `needs_attention` has a location that failed three retries; show the user and ask before you resume it.
- Export links last 1 hour. Fetch a new one with `get_export` when a link has expired.
## Credits
- One credit pool per account. A search costs 1 credit per business it returns. `POST /enrich` costs 1 per domain. Verify costs 5 per number, and a number verified in the last 30 days is free.
- A search reserves its `depth` before any provider call, keeps only the businesses it returns, and refunds the rest in the same response. The response reports `credits_reserved`, `leads_charged`, `credits_refunded`, and `leads_remaining`. A failed search refunds everything it reserved.
- A search needs 100 available credits to start. The one exception is a single-location search at depth 100, which can run on a balance of 1 to 99.
- A failed `/enrich` domain is refunded and counted in `usage.leads_refunded`. A failed verify lookup is refunded too.
- Polling, reading results, and exporting cost nothing.
- Check the balance with `check_credits` before you quote a cost, and say what the balance will be afterwards.
## Errors
| Status | Meaning | What to do |
| --- | --- | --- |
| 400 | Bad request; the body lists the valid options | Fix the input and try again |
| 401 | The key is missing or invalid | Ask the user for a valid key; do not retry |
| 429 | Not enough credits, or too many requests | See below |
| 500 | Server error | Wait, then retry once; if it fails again, tell the user |
A 429 with `code: "insufficient_credits"` also carries `required_credits` and `available_credits`. Tell the user and stop; they can add credits on the Billing screen. Any other 429 is the rate limit: 60 requests a minute per key, and 120 a minute for job polling. Wait for the number of seconds in the `Retry-After` header.
## Example
```
curl -X POST https://app.prospectlocally.com/api/v1/search \
-H "x-api-key: lp_example_key" \
-H "content-type: application/json" \
-d '{ "keyword": "plumber", "location_code": 1015116, "depth": 100 }'
```
Before you run a call like this one, say: "This search reserves up to 100 credits and charges 1 for each business it returns. Shall I go ahead?"
Authentication
Every REST call sends your API key in the x-api-key header. A key starts with lp_, and you create and revoke keys on the API keys screen of your dashboard. A key reads only the jobs and campaigns of its own account, and every plan, including Free, can use the API. An account holds at most 5 unrevoked API keys by default; revoke one to create another.
curl https://app.prospectlocally.com/api/v1/job/JOB_ID \
-H "x-api-key: lp_example_key"If the key is missing or invalid, the API answers 401. Ask for a valid key and do not retry the call. MCP clients that cannot use OAuth send the same key as Authorization: Bearer lp_example_key.
The examples in these docs use the placeholder lp_example_key. Never paste a real key into a chat, a public repository, or a page you share.
Locations
Find the location_code that a search needs. The call matches the text you send against the location list loosely, so miami, denver co, and austin texas all work. It returns up to 8 results, sorted by relevance with US cities first.
- Authentication
- The
x-api-keyheader - Credits
- 0. Looking up a location is free.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
| q | string | Yes | The place to look for, at least 2 characters. |
Example request
curl "https://app.prospectlocally.com/api/v1/locations?q=miami" \
-H "x-api-key: lp_example_key"Example response
{
"results": [
{
"location_code": 1015116,
"name": "Miami",
"region": "Florida",
"country": "US",
"type": "City",
"full_name": "Miami, Florida, US"
}
]
}Errors
| Status | When it happens |
|---|---|
| 400 | q is missing or shorter than 2 characters. |
| 401 | The x-api-key header is missing or the key is invalid or revoked. After 20 unknown keys from one address in a minute, the address gets 429 until the minute ends. |
| 429 | More than 60 requests in a minute on this key. The Retry-After header says how many seconds to wait. |
| 500 | An error on our side. Retry the call after a short wait. |
Categories
Look up the Google Maps category that matches a niche. The call returns up to 3 matches: an exact name first, then the other matches ordered by business count. The path is outside /api/v1 and the call needs no key.
In a search or a campaign, a keyword that equals a category name (ignoring case) turns on exact mode: only businesses whose primary category equals it are kept and charged. Send exact_category_key with no keyword to fill the keyword with the category name, or send exact_category_key: null with a keyword to force a broad search.
- Authentication
- None. The call needs no key.
- Credits
- 0. Looking up a category is free.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
| q | string | Yes | The niche to match, 2 to 80 characters. |
Example request
curl "https://app.prospectlocally.com/api/search/categories?q=website%20designer"Example response
{
"categories": [
{ "category_key": "website_designer", "display_name": "Website designer" }
]
}Errors
| Status | When it happens |
|---|---|
| 400 | q is missing, shorter than 2 characters, or longer than 80. |
| 500 | An error on our side. Retry the call after a short wait. |
Search
Search Google Maps for one niche in one location. The call answers at once with the raw businesses and a job_id; the status is enriching while the owner, emails, phones, and website data load in the background. Poll the job, then read the results.
A search needs at least 100 available credits. The one exception is a depth-100 search, which can run on a final balance of 1 to 99 and returns no more businesses than it reserved. You can run 3 searches at the same time.
- Authentication
- The
x-api-keyheader - Credits
- The search reserves
depthcredits before it calls any provider, charges 1 credit for each business it returns, and refunds the rest in the same response. The response reportscredits_reserved,leads_charged,credits_refunded, andleads_remaining. A failed search refunds the whole reservation. An exact category search charges only the businesses it keeps.
Body fields
| Name | Type | Required | Description |
|---|---|---|---|
| keyword | string | Yes, unless exact_category_key is set | The niche, 2 to 80 characters, for example plumber. |
| location_code | integer | Yes | A code from GET /locations. |
| depth | integer | No | The most results to read from Google Maps: 100, 300, 500, or 700. The default is 300. |
| exact_category_key | string or null | No | A category_key from GET /api/search/categories. Send null to force a broad search. |
| Idempotency-Key (header) | string | No | 1 to 255 visible ASCII characters that you choose for this request. A retry with the same key within 24 hours answers with the first request's result and charges nothing again. Send one whenever you might retry after a timeout. |
Example request
curl -X POST https://app.prospectlocally.com/api/v1/search \
-H "x-api-key: lp_example_key" \
-H "content-type: application/json" \
-d '{ "keyword": "plumber", "location_code": 1015116, "depth": 100 }'Example response
{
"job_id": "b1d9c6f2-7a34-4e58-9c1b-2f6d8e0a4c37",
"status": "enriching",
"keyword": "plumber",
"total_businesses": 62,
"businesses": [
{
"name": "Harbor Light Plumbing",
"phone": "+13055550142",
"website": "https://harborlightplumbing.example",
"address": "100 Example Street, Miami, FL 33101",
"city": "Miami",
"state": "Florida",
"country_iso_code": "US",
"rating": 4.8,
"reviews": 212,
"rating_distribution": { "1": 3, "2": 2, "3": 6, "4": 31, "5": 170 },
"cid": "1234567890123456789",
"google_cid": "1234567890123456789",
"place_id": "ChIJexample000000000001",
"category": "Plumber",
"additional_categories": ["Water heater installation service"],
"latitude": 25.7617,
"longitude": -80.1918,
"is_claimed": true,
"rank_group": 1,
"rank_absolute": 1,
"main_image": "https://img.example/harborlight.jpg",
"total_photos": 48,
"snippet": "Licensed plumber serving Miami since 1998",
"review_snippet": "Fixed our leak the same day.",
"price_level": "moderate",
"book_online_url": null,
"work_hours": null,
"searched_city": "Miami, Florida, US"
}
],
"usage": {
"credits_reserved": 100,
"leads_charged": 62,
"credits_refunded": 38,
"leads_remaining": 9938
}
}Errors
| Status | When it happens |
|---|---|
| 400 | A field is missing or invalid. A depth other than 100, 300, 500, or 700 gets an answer that lists the valid values. |
| 401 | The x-api-key header is missing or the key is invalid or revoked. After 20 unknown keys from one address in a minute, the address gets 429 until the minute ends. |
| 422 | The Idempotency-Key was used with a different body in the last 24 hours (code: "idempotency_key_reused"). Nothing is charged. |
| 429 | The balance cannot cover the reservation (code: "insufficient_credits"), or the key sent more than 60 requests in a minute. No provider call runs and no credit is spent. |
| 500 | The search failed. The full reservation is refunded. |
Campaigns
A campaign is a saved search of one niche over a country, a region, or a list of cities. It searches every location at depth 100, one location at a time, and you can pause it, resume it, and read its leads as they arrive. default_depth and depth_overrides are accepted and ignored.
A campaign is in one of seven statuses: draft, ready, running, paused, needs_attention, completed, or stopped. A failed location is retried after 30 seconds, then after 2.5 minutes, then after 5 minutes; when the third retry fails, the campaign moves to needs_attention. The campaign pauses when credits run out, and resuming it after you add credits continues from every missed location. The campaign stops starting locations once it has charged max_leads unique leads; the cap is not an exact final row count.
- Authentication
- The
x-api-keyheader - Credits
- Creating, previewing, editing, reading, and exporting cost nothing. A running campaign searches one location at a time. Each location search reserves 100 credits (or the whole balance when fewer than 100 remain) and charges 1 credit for each business it has not charged before in the campaign. Repeated businesses and unused credits are refunded at once.
Body fields
| Name | Type | Required | Description |
|---|---|---|---|
| keyword | string | Yes, unless exact_category_key is set | The niche, for example plumber. |
| exact_category_key | string or null | No | A category_key from the categories lookup. Send null to force a broad search. |
| campaign_id | string (UUID) | No | A UUID you generate. Retrying a create with the same value returns the existing campaign. |
| name | string | No | A label for the campaign. |
| max_leads | integer | No | The cap on unique leads charged, at least 100. |
| scope | object | Yes | include lists location codes of countries, regions, or cities, in order. exclude lists descendant codes to leave out. A country or region expands into its searchable locations. |
Example request
curl -X POST https://app.prospectlocally.com/api/v1/campaigns \
-H "x-api-key: lp_example_key" \
-H "content-type: application/json" \
-d '{
"keyword": "plumber",
"name": "Plumbers in Florida",
"max_leads": 500,
"scope": { "include": [21142], "exclude": [] }
}'Example response
{
"campaign_id": "3f0d6c1e-52a4-4b2a-9d3e-6a1c2f7b8e90",
"id": "3f0d6c1e-52a4-4b2a-9d3e-6a1c2f7b8e90",
"name": "Plumbers in Florida",
"keyword": "plumber",
"exact_category_key": null,
"country": "US",
"include": [21142],
"exclude": [],
"max_leads": 500,
"status": "ready",
"pause_reason": null,
"planned_location_count": 412,
"planned_depth": 100,
"locations_planned": 412,
"locations_done": 0,
"locations_failed": 0,
"leads_charged": 0,
"credits_used": 0,
"duplicates_refunded": 0,
"last_error": null,
"created_at": "2026-10-23T14:00:00.000Z",
"updated_at": "2026-10-23T14:00:00.000Z",
"started_at": null,
"finished_at": null
}Errors
| Status | When it happens |
|---|---|
| 400 | A field is invalid, for example max_leads below 100. The answer holds the sentence of the first broken rule and, when a field is at fault, its name in path. |
| 401 | The x-api-key header is missing or the key is invalid or revoked. After 20 unknown keys from one address in a minute, the address gets 429 until the minute ends. |
| 404 | The campaign id is unknown, belongs to another account, or is not a UUID (error: Campaign not found). An attach with a job_id that is not one of your searches answers Campaign or search not found. |
| 409 | code campaign_id_unavailable: a create call sent a campaign_id that another account holds. Send another UUID. |
| 409 | code campaign_not_editable: a PATCH or an attach on a completed or stopped campaign. The body carries status. |
| 409 | code invalid_status: an action that does not apply to the campaign's status, for example start on a running campaign. The body carries status, the status the campaign has now. |
| 409 | code search_not_attachable: an attach of a search that is unfinished (not_finished), has another keyword (keyword_mismatch), or is not a one-off search (not_a_search). The body carries reason. |
| 429 | A start, or a resume after a credit pause, when the balance cannot pay for the next location. The body is the one Search answers: code: "insufficient_credits", required_credits, and available_credits. Nothing starts and no credit is spent. A key past its rate limit also gets 429. |
| 500 | An error on our side. Retry the call after a short wait. |
| 500 | code provider_unavailable: a start or a resume while the Google Maps provider is unavailable. The body carries retry_after_seconds, and the campaign keeps its status. |
All campaign calls
Every path below starts with https://app.prospectlocally.com/api/v1. A key reads only the campaigns of its own account.
| Method | Path | What it does |
|---|---|---|
| POST | /campaigns | Create a campaign. Nothing starts and no credits are spent. |
| POST | /campaigns/preview | Preview a campaign from the same body. Nothing is saved and nothing is charged. |
| GET | /campaign-locations | Browse locations. With no parameters it lists the country roots; q (with an optional country) searches by name; parent_code lists the children of a region. The answer is { locations }, and each item has location_code, location_name, location_type, country_iso_code, has_children, and eligible_count. A children list stops at 1,000 places and a name search at 50; a list that goes on past its limit also has truncated: true. |
| GET | /campaigns | List your campaigns, newest first, 20 to a page (page, page_size up to 100). The answer is { results, total, page, page_size }. |
| GET | /campaigns/:id | Read one campaign with its stats, its 20 latest activity entries, the first page of its plan (250 locations), and the first page of its searches (100). |
| GET | /campaigns/:id/plan | Read the ordered locations, 250 to a page. The answer has the page fields of the list. |
| GET | /campaigns/:id/searches | Read the child searches, 100 to a page. The answer has the page fields of the list. |
| GET | /campaigns/:id/leads | Read the deduplicated leads, 100 to a page. The answer has the page fields of the list and expired_leads. It answers 409 results_restoring while archived searches come back. |
| PATCH | /campaigns/:id | Change name, max_leads, or scope while the campaign is draft, ready, running, paused, or needs_attention. The active and finished locations stay as they are. A completed or stopped campaign cannot be edited. |
| POST | /campaigns/:id/actions/start | Start a ready campaign. |
| POST | /campaigns/:id/actions/pause | Pause a running campaign. The location that is running finishes first. |
| POST | /campaigns/:id/actions/resume | Resume a paused or needs_attention campaign. |
| POST | /campaigns/:id/actions/stop | Stop a campaign for good. |
| POST | /campaigns/:id/searches | Attach a finished one-off search with the same keyword. The body is { "job_id": "<uuid>" }. The answer is the campaign with newly_attached, leads_added, and locations_marked. |
| GET | /campaigns/:id/export?format= | Export the deduplicated leads as CSV. The format is advanced, simple, gohighlevel, instantly, or oneclick. It answers 409 results_restoring while archived searches come back, and 409 campaign_not_finished while a location is still fetching or enriching. |
Preview before you start
The preview takes the body of a create call and answers with the planned location count, the planned depth, the credits the next search needs, the most credits the campaign could use, and your available credits now.
curl -X POST https://app.prospectlocally.com/api/v1/campaigns/preview \
-H "x-api-key: lp_example_key" \
-H "content-type: application/json" \
-d '{ "keyword": "plumber", "scope": { "include": [21142] } }'{
"planned_location_count": 412,
"planned_depth": 100,
"next_search_credits": 100,
"max_credits": 41200,
"available_credits": 1000
}Start, pause, resume, and stop
Each action answers with the campaign after the action. A start, and a resume after a credit pause, first check that the balance can pay for the next location; when it cannot, the call answers 429 and nothing starts.
curl -X POST https://app.prospectlocally.com/api/v1/campaigns/3f0d6c1e-52a4-4b2a-9d3e-6a1c2f7b8e90/actions/start \
-H "x-api-key: lp_example_key"{
"campaign_id": "3f0d6c1e-52a4-4b2a-9d3e-6a1c2f7b8e90",
"id": "3f0d6c1e-52a4-4b2a-9d3e-6a1c2f7b8e90",
"name": "Plumbers in Florida",
"keyword": "plumber",
"exact_category_key": null,
"country": "US",
"include": [21142],
"exclude": [],
"max_leads": 500,
"status": "running",
"pause_reason": null,
"planned_location_count": 412,
"planned_depth": 100,
"locations_planned": 412,
"locations_done": 0,
"locations_failed": 0,
"leads_charged": 0,
"credits_used": 0,
"duplicates_refunded": 0,
"last_error": null,
"created_at": "2026-10-23T14:00:00.000Z",
"updated_at": "2026-10-23T14:09:30.000Z",
"started_at": "2026-10-23T14:05:00.000Z",
"finished_at": null
}List campaigns
The list answers with one page of summaries, newest first. total counts all your campaigns, not only this page. The plan, searches, and leads lists answer with the same four page fields.
curl "https://app.prospectlocally.com/api/v1/campaigns?page=1&page_size=20" \
-H "x-api-key: lp_example_key"{
"results": [
{
"campaign_id": "3f0d6c1e-52a4-4b2a-9d3e-6a1c2f7b8e90",
"id": "3f0d6c1e-52a4-4b2a-9d3e-6a1c2f7b8e90",
"name": "Plumbers in Florida",
"keyword": "plumber",
"status": "running",
"locations_done": 3,
"locations_planned": 412,
"leads_charged": 118,
"credits_used": 118,
"created_at": "2026-10-23T14:00:00.000Z"
}
],
"total": 1,
"page": 1,
"page_size": 20
}Read one campaign
The campaign answers with all the fields of the create answer, plus stats, events (newest first), and the first pages of plan and searches. Read the other pages from /campaigns/:id/plan and /campaigns/:id/searches. archive counts the searches behind the leads that moved to the archive 14 days after they finished. Reading the leads list or the export brings them back: while they come back, both answer 409 with code: "results_restoring" and Retry-After: 60. The leads list reports expired_leads, the leads of searches older than 12 months, which are gone.
curl https://app.prospectlocally.com/api/v1/campaigns/3f0d6c1e-52a4-4b2a-9d3e-6a1c2f7b8e90 \
-H "x-api-key: lp_example_key"{
"campaign_id": "3f0d6c1e-52a4-4b2a-9d3e-6a1c2f7b8e90",
"id": "3f0d6c1e-52a4-4b2a-9d3e-6a1c2f7b8e90",
"name": "Plumbers in Florida",
"keyword": "plumber",
"exact_category_key": null,
"country": "US",
"include": [21142],
"exclude": [],
"max_leads": 500,
"status": "running",
"pause_reason": null,
"planned_location_count": 412,
"planned_depth": 100,
"locations_planned": 412,
"locations_done": 3,
"locations_failed": 0,
"leads_charged": 118,
"credits_used": 118,
"duplicates_refunded": 14,
"last_error": null,
"created_at": "2026-10-23T14:00:00.000Z",
"updated_at": "2026-10-23T14:09:30.000Z",
"started_at": "2026-10-23T14:05:00.000Z",
"finished_at": null,
"stats": {
"locations_done": 3,
"locations_failed": 0,
"locations_planned": 412,
"leads_charged": 118,
"leads_in_pool": 118,
"credits_used": 118,
"duplicates_refunded": 14
},
"archive": {
"archived": 0,
"restoring": 0,
"expired": 0
},
"events": [
{
"id": "7c1e2a90-3b4d-4f56-8a7b-9c0d1e2f3a4b",
"kind": "location_done",
"message": "Miami finished with 62 leads.",
"created_at": "2026-10-23T14:09:30.000Z"
},
{
"id": "5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d",
"kind": "started",
"message": "The campaign started.",
"created_at": "2026-10-23T14:05:00.000Z"
}
],
"plan": {
"results": [
{
"n": 1,
"location_code": 1015116,
"location_name": "Miami",
"location_type": "City",
"status": "done",
"attempt": 1,
"next_attempt_at": null,
"search_id": "b1d9c6f2-7a34-4e58-9c1b-2f6d8e0a4c37",
"attached": false,
"error_code": null,
"error_message": null
}
],
"total": 412,
"page": 1,
"page_size": 250
},
"searches": {
"results": [
{
"id": "b1d9c6f2-7a34-4e58-9c1b-2f6d8e0a4c37",
"kind": "campaign_location",
"attached": false,
"keyword": "plumber",
"status": "completed",
"location_names": ["Miami"],
"leads_charged": 62,
"credits_used": 62,
"created_at": "2026-10-23T14:05:01.000Z",
"finished_at": "2026-10-23T14:06:12.000Z"
}
],
"total": 3,
"page": 1,
"page_size": 100
}
}Browse locations
Use the location codes in scope.include and scope.exclude. eligible_count is the number of searchable locations under the item, and has_children says whether parent_code lists more.
curl "https://app.prospectlocally.com/api/v1/campaign-locations?q=florida&country=US" \
-H "x-api-key: lp_example_key"{
"locations": [
{
"location_code": 21142,
"location_name": "Florida",
"location_type": "State",
"country_iso_code": "US",
"has_children": true,
"eligible_count": 412
}
]
}Attach a finished search
Attach adds a finished one-off search with the campaign's keyword to the campaign's leads, and charges and refunds nothing. The answer is the campaign with three more fields: newly_attached (false when the search was attached before), leads_added, and locations_marked.
curl -X POST https://app.prospectlocally.com/api/v1/campaigns/3f0d6c1e-52a4-4b2a-9d3e-6a1c2f7b8e90/searches \
-H "x-api-key: lp_example_key" \
-H "content-type: application/json" \
-d '{ "job_id": "b1d9c6f2-7a34-4e58-9c1b-2f6d8e0a4c37" }'{
"campaign_id": "3f0d6c1e-52a4-4b2a-9d3e-6a1c2f7b8e90",
"id": "3f0d6c1e-52a4-4b2a-9d3e-6a1c2f7b8e90",
"name": "Plumbers in Florida",
"keyword": "plumber",
"exact_category_key": null,
"country": "US",
"include": [21142],
"exclude": [],
"max_leads": 500,
"status": "running",
"pause_reason": null,
"planned_location_count": 412,
"planned_depth": 100,
"locations_planned": 412,
"locations_done": 3,
"locations_failed": 0,
"leads_charged": 118,
"credits_used": 118,
"duplicates_refunded": 14,
"last_error": null,
"created_at": "2026-10-23T14:00:00.000Z",
"updated_at": "2026-10-23T14:09:30.000Z",
"started_at": "2026-10-23T14:05:00.000Z",
"finished_at": null,
"newly_attached": true,
"leads_added": 62,
"locations_marked": 1
}Poll Status
Read the progress of a search job. Poll every 5 seconds until status is completed or failed; enriching means keep waiting. This call allows 120 requests a minute per key, twice the limit of the other endpoints.
- Authentication
- The
x-api-keyheader - Credits
- 0. Polling is free.
Path parameter
| Name | Type | Required | Description |
|---|---|---|---|
| jobId | string | Yes | The job_id that the search returned. |
Example request
curl https://app.prospectlocally.com/api/v1/job/b1d9c6f2-7a34-4e58-9c1b-2f6d8e0a4c37 \
-H "x-api-key: lp_example_key"Example response
{
"job_id": "b1d9c6f2-7a34-4e58-9c1b-2f6d8e0a4c37",
"status": "enriching",
"keyword": "plumber",
"total_businesses": 62,
"enriched_count": 41,
"failed_count": 1,
"progress": 68,
"created_at": "2026-10-23T14:02:11Z",
"completed_at": null
}Errors
| Status | When it happens |
|---|---|
| 401 | The x-api-key header is missing or the key is invalid or revoked. After 20 unknown keys from one address in a minute, the address gets 429 until the minute ends. |
| 429 | More than 120 requests in a minute on this key. Wait for the Retry-After seconds. |
| 500 | An error on our side. Retry the call after a short wait. |
Get Results
Read the enriched leads of a job. The format parameter picks JSON or one of five CSV layouts: simple, advanced, ghl (GoHighLevel), instantly (rows without an email are dropped), and oneclick. A CSV answer uses the text/csv type with headers for the attachment, the result format, and the result count. The Advanced CSV has the same columns in the same order as the Advanced export in the app.
Four filters narrow the results and combine with AND. true requires the data, false requires its absence, and an omitted filter does nothing.
- Authentication
- The
x-api-keyheader - Credits
- 0. Reading results is free.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
| format | string | No | json, simple, advanced, ghl, instantly, or oneclick. |
| has_website | boolean | No | The business has a website URL. |
| has_email | boolean | No | At least one email was found. |
| has_phone | boolean | No | At least one phone of any type was found. |
| has_mobile_phone | boolean | No | At least one phone is labelled mobile. |
Example request
curl "https://app.prospectlocally.com/api/v1/job/b1d9c6f2-7a34-4e58-9c1b-2f6d8e0a4c37/results?format=json&has_email=true" \
-H "x-api-key: lp_example_key"Example response
{
"job_id": "b1d9c6f2-7a34-4e58-9c1b-2f6d8e0a4c37",
"total": 1,
"results": [
{
"id": "0b8e5d3c-6f21-4a97-8d14-5c2e7a9b1f60",
"job_id": "b1d9c6f2-7a34-4e58-9c1b-2f6d8e0a4c37",
"name": "Harbor Light Plumbing",
"category": "Plumber",
"website": "https://harborlightplumbing.example",
"owner": "Dana Whitfield",
"owner_title": "Owner",
"email": "[email protected]",
"phone": "+13055550142",
"phone_type": "landline",
"mobile_number": "+13055550177",
"contact": {
"city": "Miami",
"state_code": "FL",
"all_emails": [
{ "email": "[email protected]", "is_role_based": false, "is_free_provider": false }
],
"all_phones": [
{ "number": "+13055550142", "line_type": "landline", "carrier": "Example Telecom", "verified": null }
]
},
"web": { "socials": { "facebook": "https://facebook.example/harborlightplumbing" } },
"bbb": { "rating": "A+", "accredited": true }
}
]
}Errors
| Status | When it happens |
|---|---|
| 400 | The format or a filter has a value the API does not support. The answer lists the valid options. |
| 401 | The x-api-key header is missing or the key is invalid or revoked. After 20 unknown keys from one address in a minute, the address gets 429 until the minute ends. |
| 404 | The job id is unknown, belongs to another account, is not a UUID, or names an enrich request or a campaign location rather than a one-off search (error: Job not found). |
| 409 | code: "results_restoring": the search was archived and is being restored; call again after the Retry-After of 60 seconds. code: "job_not_finished": a CSV format was asked for while the job is still enriching; retry when the job status is completed. JSON is served at any time. |
| 410 | code results_expired: the search is older than the 12-month archive, so its results are gone. |
| 429 | More than 60 requests in a minute on this key. The Retry-After header says how many seconds to wait. |
| 429 | The key already has 2 downloads open (code: "too_many_downloads"). Finish one and retry after the Retry-After seconds. A download that is still open after 2 minutes is ended, so read the body as it arrives. |
| 500 | An error on our side. Retry the call after a short wait. |
Enrich
Enrich 1 to 10 websites that you already know, without a Google Maps search. The call runs synchronously and answers with the owner, emails, phones, and website data of each domain. Each result has the lead shape without the fields that only a Maps search fills: no reputation, rank, or main_image.
- Authentication
- The
x-api-keyheader - Credits
- 1 credit for each domain. The whole batch is charged up front in one deduction. A failed domain is refunded and counted in
usage.leads_refunded.
Body fields
| Name | Type | Required | Description |
|---|---|---|---|
| leads | array | Yes | 1 to 10 items. A null body or a null leads value returns 400. |
| leads[].domain | string | Yes | A bare hostname such as example.com. IP addresses, IPv6 addresses, reserved hostnames, and protocol prefixes return 400. |
| leads[].business_name | string | No | Helps match the owner. |
| leads[].city | string | No | Helps match the owner. |
| leads[].state | string | No | Helps match the owner. |
| Idempotency-Key (header) | string | No | 1 to 255 visible ASCII characters that you choose for this request. A retry with the same key within 24 hours answers with the first request's result and charges nothing again. Send one whenever you might retry after a timeout. |
Example request
curl -X POST https://app.prospectlocally.com/api/v1/enrich \
-H "x-api-key: lp_example_key" \
-H "content-type: application/json" \
-d '{ "leads": [{ "domain": "harborlightplumbing.com", "business_name": "Harbor Light Plumbing" }] }'Example response
{
"total": 1,
"succeeded": 1,
"failed": 0,
"results": [
{
"domain": "harborlightplumbing.com",
"status": "succeeded",
"result": {
"name": "Harbor Light Plumbing",
"owner": "Dana Whitfield",
"email": "[email protected]",
"phone_type": "landline"
},
"error": null
}
],
"usage": { "leads_charged": 1, "leads_refunded": 0, "leads_remaining": 9937 }
}Errors
| Status | When it happens |
|---|---|
| 400 | A domain is not a bare hostname, the body is null, or leads is null or not 1 to 10 items. |
| 401 | The x-api-key header is missing or the key is invalid or revoked. After 20 unknown keys from one address in a minute, the address gets 429 until the minute ends. |
| 422 | The Idempotency-Key was used with a different body in the last 24 hours (code: "idempotency_key_reused"). Nothing is charged. |
| 429 | The balance cannot cover the whole batch (code: "insufficient_credits"), the account already has 50 domains being enriched (code: "too_many_enrich", with open and limit), or the rate limit applies. No partial charge is made. |
| 500 | An error on our side. Retry the call after a short wait. |
Verify
Look up the live line type and carrier of 1 to 100 phone numbers. The result replaces the free label everywhere the number appears. Verify runs on paid plans only; a Free account gets a 403 with code: "paid_plan_required" before any lookup runs.
- Authentication
- The
x-api-keyheader - Credits
- 5 credits for each number looked up, reserved up front in one deduction. A number verified in the last 30 days returns
cachedand is free. A lookup that fails on a transport error, a 422, or an answer with no carrier data is refunded.
Body fields
| Name | Type | Required | Description |
|---|---|---|---|
| numbers | array of strings | Yes | 1 to 100 phone numbers in E.164 format, for example +13055550142. |
| Idempotency-Key (header) | string | No | 1 to 255 visible ASCII characters that you choose for this request. A retry with the same key within 24 hours answers with the first request's result and charges nothing again. Send one whenever you might retry after a timeout. |
Example request
curl -X POST https://app.prospectlocally.com/api/v1/verify \
-H "x-api-key: lp_example_key" \
-H "content-type: application/json" \
-d '{ "numbers": ["+13055550142", "+13055550177"] }'Example response
{
"total": 2,
"verified": 1,
"failed": 0,
"results": [
{
"number": "+13055550142",
"status": "verified",
"line_type": "landline",
"carrier": "Example Telecom",
"verified_at": "2026-10-23T14:05:40Z",
"error": null
},
{
"number": "+13055550177",
"status": "cached",
"line_type": "mobile",
"carrier": "Example Mobile",
"verified_at": "2026-10-12T09:30:02Z",
"error": null
}
],
"usage": { "credits_charged": 5, "credits_refunded": 0, "leads_remaining": 9932 }
}Errors
| Status | When it happens |
|---|---|
| 400 | numbers is missing, has more than 100 entries, or holds a value that is not E.164. |
| 401 | The x-api-key header is missing or the key is invalid or revoked. After 20 unknown keys from one address in a minute, the address gets 429 until the minute ends. |
| 403 | The account is on the Free plan: code: "paid_plan_required" and the error "Verify needs a paid plan." No lookup runs and no credit is spent. |
| 409 | The request with this Idempotency-Key is still looking numbers up (code: "request_in_progress"). Retry after the Retry-After seconds; nothing is charged again. |
| 422 | The Idempotency-Key was used with a different body in the last 24 hours (code: "idempotency_key_reused"). Nothing is charged. |
| 429 | The balance cannot cover the whole request (code: "insufficient_credits"). No lookup runs. The rate limit also answers 429. |
| 500 | An error on our side. Retry the call after a short wait. |
Demo Endpoints
Try the API with no key and no credits. The demo endpoints answer from fixed fixtures, never call a provider, and never touch your balance. Every response includes "demo": true.
The fixture job is demo-job-plumbers-irvine. Its businesses are fictional, with websites on reserved .example domains. Every path below starts with https://app.prospectlocally.com/api/v1.
The demo answers are fixed, so a few inputs behave differently from the live API. The location codes 999000001 to 999000003 are fictional: only the demo endpoints know them, and the live Search refuses them. Only the domains of the fixture businesses enrich, for example exampleplumbing.example; any other valid domain answers failed with enrichment_failed. Only +19495550101, +19495550102, and +19495550103 verify; every other number fails with demo_number.
| Method | Path | What it does |
|---|---|---|
| GET | /demo/locations?q= | Look up locations, as GET /locations does. |
| POST | /demo/search | A search that returns the fixture job. |
| GET | /demo/job/:jobId | The job status. Add ?status=enriching to see a job that is still running. |
| GET | /demo/job/:jobId/results | The fixture leads in every format and with every filter. |
| POST | /demo/enrich | An enrichment of fixture domains. /demo/enhance is an alias. |
| POST | /demo/verify | A verification of fixture numbers. |
Example request
curl https://app.prospectlocally.com/api/v1/demo/job/demo-job-plumbers-irvine/results?format=jsonExample response
{
"demo": true,
"job_id": "demo-job-plumbers-irvine",
"total": 10,
"results": [
{
"id": "00000000-0000-4000-8000-000000000001",
"job_id": "demo-job-plumbers-irvine",
"name": "Example Plumbing Co",
"category": "Plumber",
"website": "https://exampleplumbing.example",
"owner": "Sam Example",
"owner_title": "Owner",
"email": "[email protected]",
"phone": "+19495550102",
"phone_type": "landline",
"mobile_number": "+19495550101",
"contact": {
"city": "Irvine",
"state_code": "CA",
"all_emails": [
{ "email": "[email protected]", "is_role_based": false, "is_free_provider": false }
],
"all_phones": [
{ "number": "+19495550102", "line_type": "landline", "carrier": "Example Telecom", "verified": null }
]
},
"web": { "socials": { "facebook": "https://facebook.example/exampleplumbing" } },
"bbb": { "rating": "A+", "accredited": true }
}
]
}Data Fields
A lead has more than 50 distinct fields. Every field is null when the data is unknown, so check for null before you use a value, and never guess one. The tables below follow the JSON format; the CSV formats use the column names of the export you pick. summary is always null and services is always empty.
Top level
| Field | Type | Description |
|---|---|---|
| id | string | The lead id. |
| job_id | string | The search job that found the lead. |
| name | string | The business name. |
| category | string | The primary Google Maps category. |
| additional_categories[] | string[] | The other Google Maps categories of the business. |
| website | string | The business website. Null when the listing has none or links to a social or directory page. |
| website_status | string | The result of the website check, for example ok, no_website, dead, or blocked. |
| gbp_url | string | The Google Maps profile URL. |
| google_cid | string | The Google CID. A string of digits; the alias cid holds the same value. |
| place_id | string | The Google Place ID. |
| logo | string | The logo URL read from the website. |
| main_image | string | The main image of the Google Maps listing. |
| rank | integer | The position in the Google Maps results. |
| owner | string | The owner's full name, read from the website and the BBB profile. |
| owner_first_name | string | The owner's first name, with honorifics removed. |
| owner_last_name | string | The owner's last name, with suffixes removed. |
| owner_title | string | The owner's title, for example Owner, Founder, or President. |
| owner_verified | boolean | True when the website and the BBB profile name the same person. |
| owner_email | string | An email whose local part matches the owner's name. Null when none matches. |
| contact_2_name | string | The second person named, flattened from contacts[]. |
| contact_2_title | string | The second person's title. |
| contact_3_name | string | The third person named, flattened from contacts[]. |
| contact_3_title | string | The third person's title. |
| email_contact_name | string | The first name read from a personal-looking email address. Never read from a role word such as info. |
| summary | null | Always null. The API does not write summaries. |
| string | The best-match email: the owner's, then a named address on the business domain, then a role address, then a free-provider address. | |
| phone | string | The Google profile number, or the best discovered number when the profile has none. |
| phone_type | string | The line type of phone: mobile, landline, voip, or toll_free. |
| mobile_number | string | The first mobile number found. |
| created_at | string | When the lead was created, as an ISO 8601 timestamp. |
contacts[]
Everyone named on the website or the BBB profile, owner first, then by title strength, with duplicates merged.
| Field | Type | Description |
|---|---|---|
| contacts[].name | string | The person's name. |
| contacts[].title | string | The person's title. |
| contacts[].sources[] | string[] | Where the name was found. |
| contacts[].email | string | The person's email, when one matches. |
bbb
Better Business Bureau data for US and Canadian businesses. The whole object is null when nothing matches.
| Field | Type | Description |
|---|---|---|
| bbb.url | string | The BBB profile URL. |
| bbb.rating | string | The BBB rating, A+ to F. |
| bbb.accredited | boolean | True when the business is BBB accredited. |
| bbb.business_started | string | When the business started. |
| bbb.entity_type | string | The entity type, for example a corporation. |
contact
Where the business is and how to reach it. A phone number carries verified once you verify it; a number that was never verified has verified set to null.
| Field | Type | Description |
|---|---|---|
| contact.address | string | The street address. |
| contact.city | string | The city. |
| contact.state | string | The state or region name. |
| contact.state_code | string | The state code, derived for the US and Canada. |
| contact.country | string | The country. |
| contact.lat | number | The latitude. |
| contact.lng | number | The longitude. |
| contact.work_hours | object | The opening hours for each day. |
| contact.contact_url | string | The contact page URL. |
| contact.all_emails[] | object[] | Every email found, with the three fields below. |
| contact.all_emails[].email | string | The address. |
| contact.all_emails[].is_role_based | boolean | True for addresses such as info or sales. |
| contact.all_emails[].is_free_provider | boolean | True for addresses at a free provider such as Gmail. |
| contact.all_phones[] | object[] | Every phone found, the Google number first, deduplicated by digits. |
| contact.all_phones[].number | string | The number. International numbers keep their format. |
| contact.all_phones[].line_type | string | mobile, landline, voip, or toll_free. |
| contact.all_phones[].carrier | string | The carrier name. |
| contact.all_phones[].verified | object | Set after Verify: at, line_type, and carrier. Null before. |
reputation
| Field | Type | Description |
|---|---|---|
| reputation.rating | number | The Google rating. |
| reputation.reviews | integer | The number of Google reviews. |
| reputation.rating_distribution | object | The count of reviews for each rating from 1 to 5. |
| reputation.review_snippet | string | A sample review. |
| reputation.is_claimed | boolean | True when the owner claimed the Google listing. |
services
Always empty. The object is present so a client can rely on its shape; lists are [] and the other fields are null.
| Field | Type | Description |
|---|---|---|
| services.list[] | string[] | Always []. |
| services.specialties[] | string[] | Always []. |
| services.areas[] | string[] | Always []. |
| services.selling_points[] | string[] | Always []. |
| services.customer_types | null | Always null. |
| services.year_established | null | Always null. |
| services.employees | null | Always null. |
web
What the crawler read from the business website.
| Field | Type | Description |
|---|---|---|
| web.tech | object | The technology found: cms, cdn, server, chat, analytics[], frameworks[], advertising[], and other[]. |
| web.socials | object | Profile URLs: facebook, instagram, linkedin, youtube, yelp, and tiktok. Null for a profile that was not found. |
| web.has_blog | boolean | True when the site has a blog or news section. |
| web.meta_description | string | The meta description of the home page. |
| web.pages_crawled | integer | How many pages the crawler read. |
| web.website_content | string | The first 1,000 characters of visible text. |
| web.pages[] | object[] | Each page read, with url, title, and meta_description. |
| web.theme_color | string | The theme color of the site. |
| web.schema_org[] | object[] | The schema.org data found on the pages. |
searched_location
| Field | Type | Description |
|---|---|---|
| searched_location.city | string | The location the search asked about. |
| searched_location.location_code | integer | The location code the search used. |
Raw search rows
The immediate answer of POST /search carries the Google Maps fields only. Enrichment adds the lead fields above while the job runs.
| Field | Type | Description |
|---|---|---|
| name | string | The business name. |
| phone | string | The Google profile number. |
| website | string | The website URL. |
| address | string | The full address. |
| city | string | The city. |
| state | string | The state or region. |
| country_iso_code | string | The two-letter country code. |
| rating | number | The Google rating. |
| reviews | integer | The number of reviews. |
| rating_distribution | object | The reviews for each rating from 1 to 5. |
| cid | string | The Google CID. |
| google_cid | string | The same value as cid. |
| place_id | string | The Google Place ID. |
| category | string | The primary category. |
| additional_categories | string[] | The other categories. |
| latitude | number | The latitude. |
| longitude | number | The longitude. |
| is_claimed | boolean | True when the owner claimed the listing. |
| rank_group | integer | The position among the map results. |
| rank_absolute | integer | The position among all results. |
| main_image | string | The main image URL. |
| total_photos | integer | The number of photos. |
| snippet | string | The snippet Google shows. |
| review_snippet | string | A sample review. |
| price_level | string | inexpensive, moderate, expensive, very_expensive, or null. |
| book_online_url | string | The booking URL. |
| work_hours | object | The opening hours for each day. |
| searched_city | string | The location the search asked about. |
Plans
Every plan, including Free, includes API access, the full enrichment pipeline, and all five CSV formats. The plans differ in price and credits only.
| Plan | Monthly price | Yearly price | Credits | API access |
|---|---|---|---|---|
| Free | $0 | Not offered | 100 credits once, for the life of the account | Yes |
| Starter | $39 | $29 a month, billed $348 a year | 10,000 credits every month | Yes |
| Agency | $97 | $79 a month, billed $948 a year | 50,000 credits every month | Yes |
| Scale | $297 | $229 a month, billed $2,748 a year | 200,000 credits every month | Yes |
- A search costs 1 credit for each business it returns, and Enrich costs 1 credit for each domain. Verify costs 5 credits for each number, and a number verified in the last 30 days is free.
- Polling a job, reading results, and exporting cost nothing.
- Paid credits renew every month on the anniversary of the subscription, including on a yearly plan, and they do not roll over. Free credits never reset.
- Credits are spent in this order: plan credits, then free credits, then add-on credits. A paid subscriber can buy an add-on pack of 10,000 credits for $39; add-on credits never reset or expire.
Errors
A failed call answers with an HTTP status and a JSON body that holds an error message. Seven statuses cover every failure, and 429 has two causes.
400: bad request
The body or a parameter is invalid. The answer lists the valid options, for example the four depth values or the supported result formats. Fix the input and send the call again.
{
"error": "depth must be one of the valid values",
"valid_options": [100, 300, 500, 700]
}401: missing or invalid key
The x-api-key header is missing, or the key is invalid or revoked. Do not retry the call; create a valid key first.
{
"error": "Missing or invalid API key"
}404: not found
The job or campaign id is unknown, belongs to another account, or is not a UUID. The body names what was not found, for example Job not found.
409: conflict
The call conflicts with the current state. Get Results answers 409 with code: "results_restoring" while an archived search is restored, and a Retry-After header of 60 seconds says when to call again. The campaign calls answer 409 for an action that does not fit the campaign's status; each code is listed on its endpoint.
410: results expired
The search is older than the 12-month archive, so Get Results has nothing to return. The body carries code: "results_expired". Do not retry the call.
429: not enough credits
The balance cannot cover the call, and the API refuses it before any provider call, so no credit is spent. The body carries code: "insufficient_credits", the credits the call needs in required_credits, and the credits you have in available_credits. A search needs at least 100 available credits, except a depth-100 search on a final balance of 1 to 99. Add credits, or send a smaller call.
{
"error": "Not enough lead credits -- 300 required to search",
"code": "insufficient_credits",
"required_credits": 300,
"available_credits": 100
}429: rate limit
A key can send 60 requests a minute on every endpoint except job polling, which allows 120. Past the limit the API answers 429 with a Retry-After header that holds the seconds to wait. Both limits count each key separately.
HTTP/1.1 429 Too Many Requests
Retry-After: 12
{
"error": "Rate limit exceeded",
"code": "rate_limited"
}500: server error
An error on our side. A failed search refunds its whole reservation. Retry the call after a short wait.
{
"error": "Something went wrong on our side"
}