API documentation
Scrape Google Maps programmatically: start jobs, poll status, and download business leads as CSV, XLSX, or JSON over a simple REST API.
5 countries · nearly 4,000 business categories · up to 23 fields per lead.
Overview
The Maps Harvest API lets you start and manage Google Maps scrape jobs programmatically, with 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 included on every plan, including Free: each scrape uses your account's own credits, exactly as in the dashboard. Webhooks and JSON downloads are on the Growth plan and above. All endpoints accept and return JSON. Dates are ISO 8601 (UTC).
https://api.mapsharvest.comAuthentication
Every request must include an Authorization header with a Bearer token. Two token types are accepted:
Generated from the dashboard → Integrations → API access. Recommended for server-side integrations.
curl https://api.mapsharvest.com/jobs/abc123 \
-H "Authorization: Bearer mh_live_your_key_here"Short-lived token from your Supabase session. Used by the dashboard itself. Fine for quick scripts.
curl https://api.mapsharvest.com/jobs/abc123 \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR..."Rate limits
Every plan, Enterprise included, runs one scrape at a time. Starting another while one is queued or running returns 429 Too Many Requests; start it once the current job finishes or is stopped. There is no per-minute rate limit on read endpoints (/jobs, /fields, etc.).
| Limit | Value |
|---|---|
Scrapes at a time | 1 per account, every plan |
API keys per account | 10 active keys |
Results per city | Up to about 120 (a Google Maps limit). API default 60; the dashboard asks for 120 |
Webhook payload size | Unbounded (streamed) |
POST /scrape
POSTStart a new scrape job. Returns a job_id immediately; the scrape runs asynchronously.
| Field | Type | Required | Description |
|---|---|---|---|
query | string | ✓ | Business type to search (e.g. "dentists") |
states | string | ✓ | Comma-separated state or region names (e.g. "Florida,Texas") |
country | string | us · uk · canada · australia · germany (default us). | |
cities | string | Comma-separated city names. Omit to scrape all cities in the states. | |
max_results_per_city | integer | Cap per city. Default 60 for API calls (the dashboard asks for 120). Google Maps shows up to about 120 per city. | |
preset | string | Field preset: essentials · standard · full · custom | |
fields | string | Comma-separated field keys when preset=custom. | |
category_filter | string | Strict category to match (exact GMB category name). | |
related_categories | string | Comma-separated additional categories to include. | |
min_rating | number | Only keep businesses with rating ≥ this value. | |
min_reviews | integer | Only keep businesses with review count ≥ this value. | |
max_reviews | integer | Only keep businesses with review count ≤ this value. | |
require_website | boolean | true = only businesses with a website. | |
exclude_website | boolean | true = only businesses without a website. | |
require_phone | boolean | true = only businesses with a phone number. | |
exclude_phone | boolean | true = only businesses without a phone number. | |
webhook_url | string | URL to POST results to when the job finishes (Growth plan and above). | |
webhook_trigger | string | completed · stopped · any · manual (default manual). | |
webhook_fields | string[] | Array of field names to include in the webhook payload. | |
campaign_id | string (uuid) | Assign this scrape to a campaign. | |
name | string | Human-readable label for this scrape run (max 200 chars). | |
resume_job_id | string | ID of a previous job to resume from where it left off. |
{
"job_id": "a7f3c2d1e8b94f56...",
"status": "queued"
}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}
GETPoll the live status of a scrape job.
| Field | Type | Description |
|---|---|---|
job_id | string | Unique job identifier |
status | string | queued · running · stopping · stopped · completed · failed |
rows_written | integer | Leads collected so far |
cities_done | integer | Cities fully scraped |
cities_total | integer | Total cities to scrape |
cities_remaining | integer | Cities left |
last_city | string | null | Most recently scraped city |
started_at | number | Unix timestamp when the job started |
eta_seconds | number | null | Estimated seconds remaining |
error | string | null | Error message if status is failed |
curl https://api.mapsharvest.com/jobs/a7f3c2d1e8b94f56 \
-H "Authorization: Bearer mh_live_..."GET /jobs/{id}/download
GETDownload the results of a completed (or stopped) job. Format is controlled by theformat query parameter. Available formats depend on your plan.
| Param | Values | Default |
|---|---|---|
format | csv · xlsx · json | csv |
# CSV (all plans)
curl "https://api.mapsharvest.com/jobs/a7f3c2d1e8b94f56/download?fmt=csv" \
-H "Authorization: Bearer mh_live_..." \
-o leads.csv
# JSON (Growth+ only)
curl "https://api.mapsharvest.com/jobs/a7f3c2d1e8b94f56/download?fmt=json" \
-H "Authorization: Bearer mh_live_..." \
-o leads.jsonPOST /scrape/{id}/stop
POSTSignal a running job to stop gracefully. The job transitions to stopping and then stopped. Any leads already collected are preserved and downloadable.
curl -X POST https://api.mapsharvest.com/scrape/a7f3c2d1e8b94f56/stop \
-H "Authorization: Bearer mh_live_..."GET /fields
GETReturns the full catalog of available data fields, grouped by tier, along with preset definitions and their credit costs.
curl https://api.mapsharvest.com/fields \
-H "Authorization: Bearer mh_live_..."GET /cities
GETReturns all cities available for the given states. Used to build the city picker in the dashboard.
| Param | Type | Description |
|---|---|---|
states | string | Comma-separated state names (e.g. states=Florida,Texas) |
curl "https://api.mapsharvest.com/cities?states=Florida,Texas" \
-H "Authorization: Bearer mh_live_..."GET /api-keys
GETList all API keys for the authenticated account. Key hashes are never returned.
{
"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
POSTCreate 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.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | ✓ | Label for this key (max 100 chars). e.g. "Zapier integration" |
{
"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}
DELETEPermanently delete an API key. Any integrations using it will stop authenticating immediately. Returns 204 No Content on success.
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.
| Field | Type | Description |
|---|---|---|
query | string | Search query used to find this business |
name | string | Business name |
category | string | Primary Google Business category |
phone | string | Phone number |
address | string | Full street address |
city | string | City |
state | string | State |
latitude | number | Latitude, parsed from the Google Maps URL |
longitude | number | Longitude, parsed from the Google Maps URL |
rating | number | Average star rating (1–5) |
review_count | integer | Total number of reviews |
google_maps_url | string | Direct Google Maps URL |
scraped_at | string | ISO 8601 timestamp of when the row was scraped |
| Field | Type | Description |
|---|---|---|
website | string | Business website URL |
website_domain | string | Root domain (e.g. example.com) |
plus_code | string | Google Plus Code location |
| Field | Type | Description |
|---|---|---|
description | string | Business description from Google |
hours | string | Opening hours (JSON string) |
price_level | string | $ $$ $$$ $$$$ |
temporarily_closed | boolean | Whether the business is temporarily closed |
review_breakdown | string | Star-count histogram, e.g. "5★:120, 4★:45..." |
last_updated_by_owner | string | Listing freshness, e.g. "Updated by this business 3 weeks ago" |
service_options | string | e.g. "Dine-in; Takeaway; Delivery" |
Error codes
| Status | Meaning |
|---|---|
400 Bad Request | Missing or invalid request parameters. |
401 Unauthorized | Missing, invalid, or revoked auth token. |
403 Forbidden | Your plan does not include this feature. |
404 Not Found | Job or resource does not exist (or belongs to another user). |
429 Too Many Requests | You already have a job running (every plan runs one job at a time), or you already have 10 API keys. |
500 Internal Server Error | Something went wrong on our end. Retry after a moment. |
All error responses include a JSON body with a detail field explaining the reason:
{ "detail": "Webhooks require a Growth plan or higher" }Code examples
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?fmt=csv", headers=HEADERS)
with open("leads.csv", "wb") as f:
f.write(csv.content)
print("Saved leads.csv")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?fmt=csv`, { headers });
require("fs").writeFileSync("leads.csv", Buffer.from(await csv.arrayBuffer()));
console.log("Saved leads.csv");
}
run();# 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?fmt=csv" \
-H "Authorization: Bearer mh_live_..." -o leads.csv
echo "Done: leads.csv ready"Local Worker (CLI)
EnterpriseThe Maps Harvest CLI runs your searches on your own computer instead of ours. This gives you:
- Results appear in your dashboard automatically after upload
- Credits deducted only on successful completion
Requires Python 3.9+ and pip. One-time Playwright browser install needed.
# 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# 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| Flag | Default | Description |
|---|---|---|
--states / -s | (required) | Comma-separated US state names or abbreviations |
--cities / -c | all cities | Comma-separated city names to restrict the scrape |
--max-results / -n | 60 | Max leads per city (up to 120) |
--output / -o | ./results/<job_id>.csv | Where to save the CSV |
--name | Human-readable label shown in the dashboard | |
--campaign-id | Link this job to an existing campaign UUID | |
--preset | core | Field preset: core · cold_caller · email_outreach · full |
--min-rating | Skip businesses rated below this (e.g. 4.0) | |
--min-reviews | Skip businesses with fewer reviews than this | |
--max-reviews | Skip businesses with more reviews than this | |
--require-website | off | Skip businesses with no website |
--require-phone | off | Skip businesses with no phone |
--headless / --no-headless | --headless | Show or hide the Chrome window |
# 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- Register: the CLI calls
POST /jobs/local-startwith your query and states. The backend validates your Enterprise plan and returns ajob_idplus the list of cities to scrape. - Scrape: Playwright launches on your machine, navigates Google Maps, and writes results to a local CSV. Progress is sent to
PATCH /jobs/{id}/progressafter each city so your dashboard updates live. - 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). - View results: the job appears in your Maps Harvest dashboard exactly like any server-side scrape. Download in CSV, XLSX, or JSON at any time.
| Problem | Fix |
|---|---|
No API key configured | Run: mapsharvest config --api-key mh_live_... |
403 Local worker is an Enterprise feature | Upgrade your plan at mapsharvest.com/pricing |
playwright: command not found | Run: pip install playwright && playwright install chromium |
0 results for a city | Google Maps may have stopped returning results for a while. Wait a few minutes, or try --no-headless to watch what happens. |
Upload fails but CSV is saved | Your local file is at ./results/<job_id>.csv. You can re-upload or import it manually. |