Prospecting API
The Prospecting API finds local businesses to pitch: one kind of business in one place, or one kind across many places. It runs the same searches as the Search and Campaigns tabs, uses the same lead credits, and the searches it starts show up in those tabs. It uses the same API keys as the Lead Gen API and the Site Creation API.
Using an AI agent? The MCP server has the same operations as tools, and the prospecting skill teaches coding agents to use them.
Base URL
Send requests to the same domain you use for your dashboard. Every path on this page is relative to it.
Authentication
Send your API key as a bearer token on every request:
Authorization: Bearer YOUR_API_KEY
Keys start with lgen_. To create one, open API Keys in your dashboard, type a name in the Key label box, and click + Create New Key. The full key is shown once. Treat it like a password.
A missing, unknown or revoked key gets 401. A key that is not linked to a ShowFirst account gets 403; make a new key on the API Keys page.
Searches and campaigns belong to your account. A search started with one of your keys can be read with any of them, and from the dashboard.
Lead credits
Searches use lead credits, the same ones the Search tab uses. See Lead credits.
- A search holds 1 lead credit for each business it may return: its
depth. When it finishes, you pay for the businesses it found and the rest come back. - With 1 to 99 lead credits left, a
depth100 search holds your balance and returns at most that many businesses. The plan and the reply show the hold aslead_credits_heldand the cap asmax_leads. - A campaign holds places ×
depth_per_location, ormax_leadsif that is lower. It settles the same way when it finishes or is stopped. - Looking up websites uses 1 lead credit per website.
- Nothing is held until you send
"confirm": true. A request that can't start gives the hold back straight away. - Credits settle the first time a status check sees a final status, so keep polling until the search or campaign is done.
curl https://YOUR_DASHBOARD_DOMAIN/api/prospecting/credits \
-H "Authorization: Bearer $SHOWFIRST_API_KEY"
{ "ok": true, "lead_credits": 1400 }
Endpoints
| Method | Path | What it does |
|---|---|---|
| GET | /api/prospecting/credits | Lead credits left. |
| GET | /api/prospecting/locations?q= | Place name to location_code. |
| POST | /api/prospecting/searches | Plan or start a search. |
| GET | /api/prospecting/searches/{jobId} | A search's progress. |
| GET | /api/prospecting/searches/{jobId}/results | A page of businesses, or a CSV. |
| POST | /api/prospecting/campaigns | Plan or start a campaign. |
| GET | /api/prospecting/campaigns/{campaignId} | A campaign's progress. |
| GET | /api/prospecting/campaigns/{campaignId}/results | Progress and a page of businesses, or a CSV. |
| POST | /api/prospecting/campaigns/{campaignId}/pause | Pause a campaign. |
| POST | /api/prospecting/campaigns/{campaignId}/resume | Resume a paused campaign. |
| POST | /api/prospecting/campaigns/{campaignId}/stop | Stop a campaign for good. |
| POST | /api/prospecting/enrich | Look up 1 to 10 websites. |
Find a location
Searches take a location_code. Look one up by name; the best matches come first, up to 8.
curl "https://YOUR_DASHBOARD_DOMAIN/api/prospecting/locations?q=Sarasota" \
-H "Authorization: Bearer $SHOWFIRST_API_KEY"
{
"ok": true,
"locations": [
{ "location_code": 1015192, "location_name": "Sarasota,Florida,United States", "location_type": "City" }
]
}
You can also send location as text to a search; the best match is used.
Start a search
POST /api/prospecting/searches
| Field | Required | Notes |
|---|---|---|
keyword | Yes | The kind of business, e.g. roofing contractor. |
location_code | One of these two | From /api/prospecting/locations. |
location | One of these two | A place as text, e.g. Sarasota, Florida. |
depth | No | How many businesses to look through: 100 (default), 300, 500 or 700. |
radius_miles | No | Only keep businesses within 10, 25, 50 or 100 miles. |
confirm | No | Leave it out to get the plan without starting anything. true starts the search. |
First, the plan:
curl -X POST https://YOUR_DASHBOARD_DOMAIN/api/prospecting/searches \
-H "Authorization: Bearer $SHOWFIRST_API_KEY" \
-H "Content-Type: application/json" \
-d '{"keyword": "roofing contractor", "location_code": 1015192, "depth": 300}'
{
"ok": true,
"requires_confirmation": true,
"plan": {
"keyword": "roofing contractor", "location_code": 1015192, "location_name": null,
"depth": 300, "lead_credits_held": 300, "lead_credits_balance": 1400
}
}
Then the same request with "confirm": true:
{
"ok": true,
"job_id": "6f1c2a9e-1b0d-4c55-9a51-0d6c1e1f7a20",
"status": "enriching",
"keyword": "roofing contractor",
"location_code": 1015192,
"depth": 300,
"lead_credits_held": 300
}
Check progress
Poll every 10 to 20 seconds. A search usually takes a few minutes.
curl https://YOUR_DASHBOARD_DOMAIN/api/prospecting/searches/JOB_ID \
-H "Authorization: Bearer $SHOWFIRST_API_KEY"
{
"job_id": "6f1c2a9e-1b0d-4c55-9a51-0d6c1e1f7a20",
"status": "completed",
"keyword": "roofing contractor",
"total_businesses": 212,
"enriched_count": 212,
"failed_count": 0,
"progress": 100
}
status | Meaning |
|---|---|
enriching | Still running. Keep polling. |
completed | Done. Read the results. |
no_results | Nothing matched. All held credits came back. |
failed | The search stopped. Unused credits came back. |
Read the results
GET /api/prospecting/searches/{jobId}/results returns a page of businesses, with the same fields as the results table in the Search tab.
| Query | Notes |
|---|---|
offset | Skip this many businesses. Default 0. Use next_offset from the last page. |
limit | Businesses per page, 1 to 100. Default 25. |
has_website, has_email, has_phone, has_mobile_phone | true keeps only businesses that have it, false only those without. |
has_verified_email | true keeps only businesses whose email passed the mailbox check (verified). Catch-all, unknown and unchecked emails are left out. false keeps the rest. |
format | json (default) or a CSV format below. |
curl "https://YOUR_DASHBOARD_DOMAIN/api/prospecting/searches/JOB_ID/results?limit=2&has_website=false" \
-H "Authorization: Bearer $SHOWFIRST_API_KEY"
{
"ok": true,
"job_id": "6f1c2a9e-1b0d-4c55-9a51-0d6c1e1f7a20",
"total": 38, "offset": 0, "limit": 2, "next_offset": 2,
"businesses": [
{
"name": "Gulf Coast Roof Repair",
"category": "Roofing contractor",
"website": null,
"email": "office@example.com",
"phone": "(941) 555-0142",
"owner": "Dana Ruiz",
"contact": { "address": "1200 Main St", "city": "Sarasota", "state_code": "FL" },
"reputation": { "rating": 4.8, "reviews": 61, "is_claimed": true },
"site_report": null
}
]
}
Each business has 28 fields, including place_id, gbp_url, summary, greeting_name, mobile_number, services, web (tech, socials, pages) and site_report (the website check). The example above is shortened.
Export a CSV
Send a format to get a CSV file instead. The filters work the same way.
format | What you get |
|---|---|
simple | The main columns. |
advanced | Every column. |
ghl | Ready to import into GoHighLevel. |
instantly | Ready to import into Instantly. |
batch | Ready to upload as a Lead Gen batch. |
curl -o roofers.csv "https://YOUR_DASHBOARD_DOMAIN/api/prospecting/searches/JOB_ID/results?format=simple&has_email=true" \
-H "Authorization: Bearer $SHOWFIRST_API_KEY"
Campaigns
A campaign runs one keyword across many places. POST /api/prospecting/campaigns
| Field | Required | Notes |
|---|---|---|
keyword | Yes | The kind of business. |
location_codes | One of these two | A list of location codes, up to 200. |
locations | One of these two | A list of places as text; each uses its best match. |
depth_per_location | No | 100 (default), 300, 500 or 700. |
max_leads | No | Stop after this many businesses in total. |
name | No | Shown in the Campaigns tab. |
confirm | No | Leave it out for the plan. true starts the campaign. |
idempotency_key | No | A UUID. Sending the same one again does not start a second campaign. |
curl -X POST https://YOUR_DASHBOARD_DOMAIN/api/prospecting/campaigns \
-H "Authorization: Bearer $SHOWFIRST_API_KEY" \
-H "Content-Type: application/json" \
-d '{"keyword": "plumber", "locations": ["Tampa, Florida", "Sarasota, Florida", "Bradenton, Florida"],
"depth_per_location": 100, "max_leads": 250, "confirm": true}'
{ "ok": true, "campaign_id": "b2a7f0de-55c1-4f0e-8d0b-3c0f7c1a9e44", "lead_credits_held": 250 }
Without confirm, the reply is the plan: the lead estimate, about how long it takes, and lead_credits_held.
A campaign moves on to its next places each time you check it, so poll GET /api/prospecting/campaigns/{campaignId}/results about once a minute. It returns the campaign's status and leads_found plus a page of the businesses found so far (same offset, limit, filters and format as a search). Stop polling when status is completed, stopped or failed.
curl "https://YOUR_DASHBOARD_DOMAIN/api/prospecting/campaigns/CAMPAIGN_ID/results?limit=50" \
-H "Authorization: Bearer $SHOWFIRST_API_KEY"
curl -X POST https://YOUR_DASHBOARD_DOMAIN/api/prospecting/campaigns/CAMPAIGN_ID/pause \
-H "Authorization: Bearer $SHOWFIRST_API_KEY"
needs_attention means the campaign paused because your lead credits ran low. Add credits, then resume it.
Look up websites
Have websites already? POST /api/prospecting/enrich finds emails, phones, social links and runs the website check for 1 to 10 of them. It uses 1 lead credit per website. It returns a job_id: poll it and read it like a search.
curl -X POST https://YOUR_DASHBOARD_DOMAIN/api/prospecting/enrich \
-H "Authorization: Bearer $SHOWFIRST_API_KEY" \
-H "Content-Type: application/json" \
-d '{"domains": ["gulfcoastroofing.com", "https://www.example-plumbing.com"]}'
{ "ok": true, "job_id": "0d4e9b61-7a2c-4c1e-9f35-5b8a2c6e1d07", "status": "enriching", "lead_credits_held": 2 }
You can send businesses instead, as [{"business_name": "...", "website": "...", "phone": "..."}], when you know the names.
Rate limits
Keep to about one request a second. When the search service is busy, a request gets 429; wait a few seconds and send it again. Polling every 10 to 20 seconds stays well inside the limit.
Errors
Error responses have "ok": false and an error string. Some also have a details list.
{
"ok": false,
"error": "You're out of lead credits.",
"details": ["This search needs 300 lead credits; you have 120."],
"balance": 120,
"required": 300
}
| Status | When |
|---|---|
| 400 | A field is missing or invalid, for example a depth that is not 100, 300, 500 or 700, or more than 10 websites. |
| 401 | The key is missing, unknown or revoked. |
| 402 | Not enough lead credits. Nothing started and nothing was held. |
| 403 | The key is not linked to a ShowFirst account. |
| 404 | The search or campaign is unknown or belongs to another account, or no place matched location. |
| 409 | The search has not finished yet. Keep polling. |
| 429 | The search service is busy. Try again in a moment. |
| 502, 503 | The search service had a problem. Try again in a moment. Any credits held for a request that did not start came back. |