Overview

The MapsHarvest API lets you start and manage Google Maps scrape jobs programmatically — no browser required. You can launch scrapes, poll for progress, download results in CSV, XLSX, or JSON, and manage your API keys.

API access is available on the Growth plan and above. All endpoints accept and return JSON. Dates are ISO 8601 (UTC).

Base URL: https://api.mapsharvest.com

Authentication

Every request must include an Authorization header with a Bearer token. Two token types are accepted:

API key mh_live_...

Generated from the dashboard → Integrations → API access. Recommended for server-side integrations.

bash
curl https://api.mapsharvest.com/jobs/abc123 \
  -H "Authorization: Bearer mh_live_your_key_here"
Supabase session JWT

Short-lived token from your Supabase session. Used by the dashboard itself. Fine for quick scripts.

bash
curl https://api.mapsharvest.com/jobs/abc123 \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR..."

Rate limits

Each account is limited to 5 concurrent scrape jobs. Starting a sixth while five are running returns 429 Too Many Requests. There is no per-minute rate limit on read endpoints (/jobs, /fields, etc.).

LimitValue
Concurrent scrape jobs5 per account
API keys per account10 active keys
Max results per city120 leads
Webhook payload sizeUnbounded (streamed)

POST /scrape

POST

Start a new scrape job. Returns a job_id immediately — the scrape runs asynchronously.

Request body
FieldTypeRequiredDescription
querystringBusiness type to search (e.g. "dentists")
statesstringComma-separated US state names (e.g. "Florida,Texas")
citiesstringComma-separated city names. Omit to scrape all cities in the states.
max_results_per_cityintegerCap per city. Default 120, max 120.
workersintegerParallel workers. Default 1. Ignored — server enforces 1.
presetstringField preset: essentials · standard · full · custom
fieldsstringComma-separated field keys when preset=custom.
category_filterstringStrict category to match (exact GMB category name).
related_categoriesstringComma-separated additional categories to include.
min_ratingnumberOnly keep businesses with rating ≥ this value.
min_reviewsintegerOnly keep businesses with review count ≥ this value.
max_reviewsintegerOnly keep businesses with review count ≤ this value.
require_websitebooleantrue = only businesses with a website.
exclude_websitebooleantrue = only businesses without a website.
require_phonebooleantrue = only businesses with a phone number.
exclude_phonebooleantrue = only businesses without a phone number.
webhook_urlstringURL to POST results to when the job finishes.
webhook_triggerstringcompleted · stopped · any · manual (default manual).
webhook_fieldsstring[]Array of field names to include in the webhook payload.
campaign_idstring (uuid)Assign this scrape to a campaign.
namestringHuman-readable label for this scrape run (max 200 chars).
resume_job_idstringID of a previous job to resume from where it left off.
Response
json
{
  "job_id": "a7f3c2d1e8b94f56...",
  "status": "queued"
}
Example
bash
curl -X POST https://api.mapsharvest.com/scrape \
  -H "Authorization: Bearer mh_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "query": "dentists",
    "states": "Florida,Texas",
    "preset": "standard",
    "min_rating": 4,
    "webhook_url": "https://hooks.zapier.com/hooks/catch/...",
    "webhook_trigger": "completed"
  }'

GET /jobs/{id}

GET

Poll the live status of a scrape job.

Response
FieldTypeDescription
job_idstringUnique job identifier
statusstringqueued · running · stopping · stopped · completed · failed
rows_writtenintegerLeads collected so far
cities_doneintegerCities fully scraped
cities_totalintegerTotal cities to scrape
cities_remainingintegerCities left
last_citystring | nullMost recently scraped city
started_atnumberUnix timestamp when the job started
eta_secondsnumber | nullEstimated seconds remaining
errorstring | nullError message if status is failed
Example
bash
curl https://api.mapsharvest.com/jobs/a7f3c2d1e8b94f56 \
  -H "Authorization: Bearer mh_live_..."

GET /jobs/{id}/download

GET

Download the results of a completed (or stopped) job. Format is controlled by theformat query parameter. Available formats depend on your plan.

Query parameters
ParamValuesDefault
formatcsv · xlsx · jsoncsv
Example
bash
# CSV (all plans)
curl "https://api.mapsharvest.com/jobs/a7f3c2d1e8b94f56/download?format=csv" \
  -H "Authorization: Bearer mh_live_..." \
  -o leads.csv

# JSON (Growth+ only)
curl "https://api.mapsharvest.com/jobs/a7f3c2d1e8b94f56/download?format=json" \
  -H "Authorization: Bearer mh_live_..." \
  -o leads.json

POST /jobs/{id}/stop

POST

Signal a running job to stop gracefully. The job transitions to stopping and then stopped. Any leads already collected are preserved and downloadable.

Example
bash
curl -X POST https://api.mapsharvest.com/jobs/a7f3c2d1e8b94f56/stop \
  -H "Authorization: Bearer mh_live_..."

GET /fields

GET

Returns the full catalog of available data fields, grouped by tier, along with preset definitions and their credit costs.

Example
bash
curl https://api.mapsharvest.com/fields \
  -H "Authorization: Bearer mh_live_..."

GET /cities

GET

Returns all cities available for the given states. Used to build the city picker in the dashboard.

Query parameters
ParamTypeDescription
statesstringComma-separated state names (e.g. states=Florida,Texas)
Example
bash
curl "https://api.mapsharvest.com/cities?states=Florida,Texas" \
  -H "Authorization: Bearer mh_live_..."

GET /api-keys

GET

List all API keys for the authenticated account. Key hashes are never returned.

Response
json
{
  "keys": [
    {
      "id": "uuid",
      "name": "My Zapier integration",
      "key_prefix": "mh_live_a1b2",
      "created_at": "2026-05-24T10:00:00Z",
      "last_used_at": "2026-05-24T14:32:00Z",
      "revoked_at": null
    }
  ]
}

POST /api-keys

POST

Create a new API key. The full raw key is returned once only in the raw_key field and is never stored in plaintext. Save it immediately.

Request body
FieldTypeRequiredDescription
namestringLabel for this key (max 100 chars). e.g. "Zapier integration"
Response
json
{
  "id": "uuid",
  "name": "My Zapier integration",
  "key_prefix": "mh_live_a1b2",
  "created_at": "2026-05-24T10:00:00Z",
  "raw_key": "mh_live_a1b2c3d4e5f6..."
}

DELETE /api-keys/{id}

DELETE

Permanently delete an API key. Any integrations using it will stop authenticating immediately. Returns 204 No Content on success.

Example
bash
curl -X DELETE https://api.mapsharvest.com/api-keys/key-uuid-here \
  -H "Authorization: Bearer mh_live_..."

Fields reference

Which fields are available depends on your plan's field tier. Higher tiers include all lower-tier fields.

Core tierAll plans
FieldTypeDescription
namestringBusiness name
categorystringPrimary Google Business category
phonestringPhone number
addressstringFull street address
citystringCity
statestringState
ratingnumberAverage star rating (1–5)
review_countintegerTotal number of reviews
google_maps_urlstringDirect Google Maps URL
Standard tierStarter+
FieldTypeDescription
websitestringBusiness website URL
website_domainstringRoot domain (e.g. example.com)
hoursstringOpening hours (JSON string)
price_levelstring$ $$ $$$ $$$$
Full tierGrowth+
FieldTypeDescription
temporarily_closedbooleanWhether the business is temporarily closed
descriptionstringBusiness description from Google
plus_codestringGoogle Plus Code location
querystringSearch query used to find this business
scraped_atstringISO 8601 timestamp of when the row was scraped

Error codes

StatusMeaning
400 Bad RequestMissing or invalid request parameters.
401 UnauthorizedMissing, invalid, or revoked auth token.
403 ForbiddenYour plan does not include this feature.
404 Not FoundJob or resource does not exist (or belongs to another user).
429 Too Many Requests5 concurrent jobs already running, or 10 API keys at limit.
500 Internal Server ErrorSomething went wrong on our end. Retry after a moment.

All error responses include a JSON body with a detail field explaining the reason:

json
{ "detail": "API keys require a Growth plan or higher" }

Code examples

Python — scrape and download
python
import requests, time

API_BASE = "https://api.mapsharvest.com"
API_KEY  = "mh_live_your_key_here"
HEADERS  = {"Authorization": f"Bearer {API_KEY}"}

# 1. Start the scrape
resp = requests.post(f"{API_BASE}/scrape", headers=HEADERS, json={
    "query":  "plumbers",
    "states": "California",
    "preset": "standard",
    "min_rating": 4.0,
})
job_id = resp.json()["job_id"]
print(f"Job started: {job_id}")

# 2. Poll until done
while True:
    status = requests.get(f"{API_BASE}/jobs/{job_id}", headers=HEADERS).json()
    print(f"  {status['status']} — {status['rows_written']} leads")
    if status["status"] in ("completed", "stopped", "failed"):
        break
    time.sleep(5)

# 3. Download CSV
csv = requests.get(f"{API_BASE}/jobs/{job_id}/download?format=csv", headers=HEADERS)
with open("leads.csv", "wb") as f:
    f.write(csv.content)
print("Saved leads.csv")
Node.js — scrape and download
javascript
const API_BASE = "https://api.mapsharvest.com";
const API_KEY  = "mh_live_your_key_here";
const headers  = { Authorization: `Bearer ${API_KEY}` };

async function run() {
  // 1. Start scrape
  const start = await fetch(`${API_BASE}/scrape`, {
    method: "POST",
    headers: { ...headers, "Content-Type": "application/json" },
    body: JSON.stringify({ query: "plumbers", states: "California", preset: "standard" }),
  });
  const { job_id } = await start.json();
  console.log("Job started:", job_id);

  // 2. Poll until done
  let done = false;
  while (!done) {
    await new Promise(r => setTimeout(r, 5000));
    const s = await (await fetch(`${API_BASE}/jobs/${job_id}`, { headers })).json();
    console.log(`  ${s.status} — ${s.rows_written} leads`);
    if (["completed","stopped","failed"].includes(s.status)) done = true;
  }

  // 3. Download CSV
  const csv = await fetch(`${API_BASE}/jobs/${job_id}/download?format=csv`, { headers });
  require("fs").writeFileSync("leads.csv", Buffer.from(await csv.arrayBuffer()));
  console.log("Saved leads.csv");
}

run();
cURL — one-liner
bash
# Start scrape, poll every 5s, download when done
JOB=$(curl -s -X POST https://api.mapsharvest.com/scrape \
  -H "Authorization: Bearer mh_live_..." \
  -H "Content-Type: application/json" \
  -d '{"query":"dentists","states":"Florida","preset":"essentials"}' | jq -r .job_id)

echo "Job: $JOB"

until [ "$(curl -s https://api.mapsharvest.com/jobs/$JOB -H 'Authorization: Bearer mh_live_...' | jq -r .status)" = "completed" ]; do
  sleep 5
done

curl -s "https://api.mapsharvest.com/jobs/$JOB/download?format=csv" \
  -H "Authorization: Bearer mh_live_..." -o leads.csv

echo "Done — leads.csv ready"

Local Worker (CLI)

Enterprise
🔒 This feature is available on the Enterprise plan only. The CLI is free to install, but running scrapes requires an active Enterprise subscription and a valid API key.

The MapsHarvest CLI lets you run Google Maps scrapes directly on your own machine using your own IP address and Chrome browser — no cloud worker required. This gives you:

  • Your own residential / datacenter IP — bypasses proxy detection
  • Zero queue time — scrapes start immediately
  • Results appear in your dashboard automatically after upload
  • Credits deducted only on successful completion
Installation

Requires Python 3.9+ and pip. One-time Playwright browser install needed.

bash
# 1. Install the CLI
pip install mapsharvest

# 2. Install Playwright's Chromium browser (one-time)
playwright install chromium

# 3. Save your Enterprise API key
mapsharvest config --api-key mh_live_your_key_here
Quick start
bash
# Scrape "roofers" across all cities in Florida, max 60 results per city
mapsharvest scrape "roofers" --states Florida

# Scrape specific cities with filters
mapsharvest scrape "dentists" \
  --states "Texas,Florida" \
  --cities "Miami,Houston,Dallas" \
  --max-results 100 \
  --min-rating 4.0 \
  --require-website \
  --output leads.csv

# Show the browser (useful for debugging)
mapsharvest scrape "plumbers" --states California --no-headless
All flags
FlagDefaultDescription
--states / -s(required)Comma-separated US state names or abbreviations
--cities / -call citiesComma-separated city names to restrict the scrape
--max-results / -n60Max leads per city (up to 120)
--output / -o./results/<job_id>.csvWhere to save the CSV
--nameHuman-readable label shown in the dashboard
--campaign-idLink this job to an existing campaign UUID
--presetcoreField preset: core · cold_caller · email_outreach · full
--min-ratingSkip businesses rated below this (e.g. 4.0)
--min-reviewsSkip businesses with fewer reviews than this
--max-reviewsSkip businesses with more reviews than this
--require-websiteoffSkip businesses with no website
--require-phoneoffSkip businesses with no phone
--headless / --no-headless--headlessShow or hide the Chrome window
Other commands
bash
# View your API key and config
mapsharvest config

# List your 20 most recent jobs
mapsharvest jobs list

# Download a past job as CSV / XLSX / JSON
mapsharvest jobs download <job_id>
mapsharvest jobs download <job_id> --fmt xlsx --output leads.xlsx
How it works
  1. Register — the CLI calls POST /jobs/local-start with your query and states. The backend validates your Enterprise plan and returns a job_id plus the list of cities to scrape.
  2. Scrape — Playwright launches on your machine, navigates Google Maps, and writes results to a local CSV. Progress is sent to PATCH /jobs/{id}/progress after each city so your dashboard updates live.
  3. Upload — when scraping finishes the CLI uploads the CSV to POST /jobs/{id}/local-complete. The backend stores it in Supabase Storage and deducts credits (1 per row).
  4. View results — the job appears in your MapsHarvest dashboard exactly like any server-side scrape. Download in CSV, XLSX, or JSON at any time.
Troubleshooting
ProblemFix
No API key configuredRun: mapsharvest config --api-key mh_live_...
403 Local worker is an Enterprise featureUpgrade your plan at mapsharvest.com/pricing
playwright: command not foundRun: pip install playwright && playwright install chromium
0 results for a cityGoogle Maps may have rate-limited the IP. Try --no-headless to debug or wait a few minutes.
Upload fails but CSV is savedYour local file is at ./results/<job_id>.csv — you can re-upload or import it manually.