# 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?"
