Docs ← Back to dashboard
Get API key

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:

text
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.

bash
curl https://YOUR_DASHBOARD_DOMAIN/api/prospecting/credits \
  -H "Authorization: Bearer $SHOWFIRST_API_KEY"
json
{ "ok": true, "lead_credits": 1400 }

Endpoints

MethodPathWhat it does
GET/api/prospecting/creditsLead credits left.
GET/api/prospecting/locations?q=Place name to location_code.
POST/api/prospecting/searchesPlan or start a search.
GET/api/prospecting/searches/{jobId}A search's progress.
GET/api/prospecting/searches/{jobId}/resultsA page of businesses, or a CSV.
POST/api/prospecting/campaignsPlan or start a campaign.
GET/api/prospecting/campaigns/{campaignId}A campaign's progress.
GET/api/prospecting/campaigns/{campaignId}/resultsProgress and a page of businesses, or a CSV.
POST/api/prospecting/campaigns/{campaignId}/pausePause a campaign.
POST/api/prospecting/campaigns/{campaignId}/resumeResume a paused campaign.
POST/api/prospecting/campaigns/{campaignId}/stopStop a campaign for good.
POST/api/prospecting/enrichLook 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.

bash
curl "https://YOUR_DASHBOARD_DOMAIN/api/prospecting/locations?q=Sarasota" \
  -H "Authorization: Bearer $SHOWFIRST_API_KEY"
json
{
  "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

FieldRequiredNotes
keywordYesThe kind of business, e.g. roofing contractor.
location_codeOne of these twoFrom /api/prospecting/locations.
locationOne of these twoA place as text, e.g. Sarasota, Florida.
depthNoHow many businesses to look through: 100 (default), 300, 500 or 700.
radius_milesNoOnly keep businesses within 10, 25, 50 or 100 miles.
confirmNoLeave it out to get the plan without starting anything. true starts the search.

First, the plan:

bash
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}'
json
{
  "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:

json
{
  "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.

bash
curl https://YOUR_DASHBOARD_DOMAIN/api/prospecting/searches/JOB_ID \
  -H "Authorization: Bearer $SHOWFIRST_API_KEY"
json
{
  "job_id": "6f1c2a9e-1b0d-4c55-9a51-0d6c1e1f7a20",
  "status": "completed",
  "keyword": "roofing contractor",
  "total_businesses": 212,
  "enriched_count": 212,
  "failed_count": 0,
  "progress": 100
}
statusMeaning
enrichingStill running. Keep polling.
completedDone. Read the results.
no_resultsNothing matched. All held credits came back.
failedThe 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.

QueryNotes
offsetSkip this many businesses. Default 0. Use next_offset from the last page.
limitBusinesses per page, 1 to 100. Default 25.
has_website, has_email, has_phone, has_mobile_phonetrue keeps only businesses that have it, false only those without.
has_verified_emailtrue keeps only businesses whose email passed the mailbox check (verified). Catch-all, unknown and unchecked emails are left out. false keeps the rest.
formatjson (default) or a CSV format below.
bash
curl "https://YOUR_DASHBOARD_DOMAIN/api/prospecting/searches/JOB_ID/results?limit=2&has_website=false" \
  -H "Authorization: Bearer $SHOWFIRST_API_KEY"
json
{
  "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.

formatWhat you get
simpleThe main columns.
advancedEvery column.
ghlReady to import into GoHighLevel.
instantlyReady to import into Instantly.
batchReady to upload as a Lead Gen batch.
bash
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

FieldRequiredNotes
keywordYesThe kind of business.
location_codesOne of these twoA list of location codes, up to 200.
locationsOne of these twoA list of places as text; each uses its best match.
depth_per_locationNo100 (default), 300, 500 or 700.
max_leadsNoStop after this many businesses in total.
nameNoShown in the Campaigns tab.
confirmNoLeave it out for the plan. true starts the campaign.
idempotency_keyNoA UUID. Sending the same one again does not start a second campaign.
bash
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}'
json
{ "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.

bash
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.

bash
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"]}'
json
{ "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.

json
{
  "ok": false,
  "error": "You're out of lead credits.",
  "details": ["This search needs 300 lead credits; you have 120."],
  "balance": 120,
  "required": 300
}
StatusWhen
400A field is missing or invalid, for example a depth that is not 100, 300, 500 or 700, or more than 10 websites.
401The key is missing, unknown or revoked.
402Not enough lead credits. Nothing started and nothing was held.
403The key is not linked to a ShowFirst account.
404The search or campaign is unknown or belongs to another account, or no place matched location.
409The search has not finished yet. Keep polling.
429The search service is busy. Try again in a moment.
502, 503The search service had a problem. Try again in a moment. Any credits held for a request that did not start came back.