Prospecting API
The Prospecting API finds local businesses: 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.
Every route is also in the API reference and in the OpenAPI file.
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
You can also send the key in an x-api-key header instead, with no Authorization header. Both work on every path on this page.
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, or twice that withverify_emailson (the default). When it finishes, you pay 1 per business found with a phone or website, plus 1 per verified email address whenverify_emailsis on, 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; withverify_emailson, a balance below 200 does the same and returns at most half your balance. 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, twice that withverify_emailson. 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. |
| GET | /api/prospecting/categories?q= | Google business categories by name, with the key for an exact-category search. No key needed. |
| GET | /api/prospecting/campaign-locations | Browse the countries, states and cities a campaign can cover. |
| 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. |
| POST | /api/prospecting/campaigns/preview | The plan only, whatever confirm says. |
| GET | /api/prospecting/campaigns | Your campaigns, newest first. |
| 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.
Find a category
A search by keyword matches the words a person would type into Google Maps. To keep only businesses whose main Google category is exactly one category, look the category up by name and send its key as exact_category_key. This lookup needs no API key and uses no credits.
curl "https://YOUR_DASHBOARD_DOMAIN/api/prospecting/categories?q=roof"
{
"ok": true,
"categories": [
{ "key": "roofing_contractor", "name": "Roofing contractor" }
]
}
The same lookup answers at /api/search/categories?q=.
Start a search
POST /api/prospecting/searches
| Field | Required | Notes |
|---|---|---|
keyword | Unless exact_category_key is given | The kind of business, e.g. roofing contractor. |
exact_category_key | No | A category key from the categories lookup. Keeps only businesses whose main Google category is exactly this one. Leave it out to search by keyword text. |
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. |
website | No | none keeps only businesses whose Google listing shows no website; businesses whose listing shows one are left out and not charged. any (default) keeps every business. |
verify_emails | No | true (default) checks each email address; each business whose address is verified costs 1 extra lead credit. false runs no email check: addresses come back as found, without a verified status, and only the business credit is charged. |
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. A search that finds nothing costs 10 credits. The rest of the hold 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. |
page, page_size | The same paging by page number: page starts at 1, page_size is 1 to 100. offset and limit win when both are sent. |
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 | Unless exact_category_key is given | The kind of business. |
exact_category_key | No | As in a search: a category key from the categories lookup keeps only businesses whose main Google category is exactly this one. |
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. |
website | No | none keeps only businesses whose Google listing shows no website in every place; businesses whose listing shows one are left out and not charged. any (default) keeps every business. |
verify_emails | No | As in a search: true (default) adds 1 lead credit per verified email address; false runs no email check. |
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.
Preview a campaign
POST /api/prospecting/campaigns/preview takes the same fields and returns the plan only: how many places it would search, the lead estimate and the most lead credits it could hold (lead_credits_held). It ignores confirm, saves nothing, starts nothing and holds nothing.
curl -X POST https://YOUR_DASHBOARD_DOMAIN/api/prospecting/campaigns/preview \
-H "Authorization: Bearer $SHOWFIRST_API_KEY" \
-H "Content-Type: application/json" \
-d '{"keyword": "plumber", "locations": ["Tampa, Florida", "Sarasota, Florida"], "depth_per_location": 100}'
List your campaigns
GET /api/prospecting/campaigns returns your campaigns, newest first (limit defaults to 25): campaign_id, keyword, name, location_count, depth_per_location, max_leads, credits and created_at. Status is not in the list: read it with GET /api/prospecting/campaigns/{campaignId}, which also moves the campaign on to its next places.
{
"ok": true,
"campaigns": [
{ "campaign_id": "b2a7f0de-55c1-4f0e-8d0b-3c0f7c1a9e44", "keyword": "plumber", "name": null, "location_count": 3,
"depth_per_location": 100, "max_leads": 250, "credits": { "reserved": 250 }, "created_at": "2026-11-06T14:02:11.000Z" }
]
}
Browse campaign locations
GET /api/prospecting/campaign-locations browses the places a campaign can cover, for when you want to explore rather than type a name. With no query it lists countries; add country (a two-letter code) for its regions, country and region for the cities in one, or country and q to search by name. offset and limit page the list. Every place comes with the location_code a campaign takes.
curl "https://YOUR_DASHBOARD_DOMAIN/api/prospecting/campaign-locations?country=US®ion=FL" \
-H "Authorization: Bearer $SHOWFIRST_API_KEY"
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, or leads as [{"domain": "...", "business_name": "...", "city": "...", "state": "..."}].
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. |