API
Google Maps scraper API
By the MapsData team · Last updated
The MapsData API lets you submit a scrape, track it, and download the result from your own code. It is a small, deliberately boring JSON API: nine endpoints, bearer authentication, and the same validation and quota rules the dashboard uses. Anything you can start from the search form you can start from a script.
Quickstart
Three requests, start to finish. Create a key in Settings, then export it:
export MAPSDATA_KEY="mdk_live_..."1. Check the category is scrapable
A scrape's query has to be a real Google Maps category, not
free text. This is the single most common reason a submission is rejected, so
it is worth one call up front.
curl -s https://mapsdata.ai/api/v1/categories/validate?name=plumber \
-H "Authorization: Bearer $MAPSDATA_KEY"
{"valid": true, "canonical": "Plumber", "suggestions": []}Submit the canonical spelling exactly as returned.
2. Start the scrape
curl -s -X POST https://mapsdata.ai/api/v1/jobs \
-H "Authorization: Bearer $MAPSDATA_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "Plumber", "states": ["Texas"], "total_limit": 500}'
{"id": "8f2c...", "status": "queued", "progress": 0, ...}You get 202 Accepted and a job id straight away. The scrape
itself runs on our workers and takes anywhere from a few minutes to several
hours. Add "dry_run": true to run every validation and quota check
and see the resolved plan without creating a job or spending anything.
3. Poll, then download
curl -s https://mapsdata.ai/api/v1/jobs/8f2c... \
-H "Authorization: Bearer $MAPSDATA_KEY"
{"id": "8f2c...", "status": "completed", "progress": 100,
"lead_count": 480, "email_count": 151,
"downloads": {"csv": true, "xlsx": true, ...}}Once status is completed, ask for a link. It is a
short-lived signed URL, returned as JSON rather than a redirect:
curl -s "https://mapsdata.ai/api/v1/jobs/8f2c.../download?format=csv" \
-H "Authorization: Bearer $MAPSDATA_KEY"
{"url": "https://...", "expires_at": "...", "filename": "Plumber.csv"}The link expires in fifteen minutes. Request a new one rather than storing it. We also email you when a scrape finishes, so polling is a convenience, not the only way to find out.
The same flow in Python and JavaScript
Python, with requests:
import os, time, requests
API = "https://mapsdata.ai/api/v1"
H = {"Authorization": f"Bearer {os.environ['MAPSDATA_KEY']}"}
cat = requests.get(f"{API}/categories/validate", params={"name": "plumber"}, headers=H).json()
job = requests.post(f"{API}/jobs", headers=H, json={
"query": cat["canonical"], "states": ["Texas"], "total_limit": 500,
}).json()
while True:
status = requests.get(f"{API}/jobs/{job['id']}", headers=H).json()
if status["status"] in ("completed", "failed", "cancelled"):
break
time.sleep(60)
link = requests.get(f"{API}/jobs/{job['id']}/download", params={"format": "csv"}, headers=H).json()
print(link["url"])JavaScript (Node 18 or later, built-in fetch):
const API = "https://mapsdata.ai/api/v1";
const H = { Authorization: `Bearer ${process.env.MAPSDATA_KEY}`, "Content-Type": "application/json" };
const cat = await (await fetch(`${API}/categories/validate?name=plumber`, { headers: H })).json();
const job = await (await fetch(`${API}/jobs`, {
method: "POST", headers: H,
body: JSON.stringify({ query: cat.canonical, states: ["Texas"], total_limit: 500 }),
})).json();
let status;
do {
await new Promise((r) => setTimeout(r, 60_000));
status = await (await fetch(`${API}/jobs/${job.id}`, { headers: H })).json();
} while (!["completed", "failed", "cancelled"].includes(status.status));
const link = await (await fetch(`${API}/jobs/${job.id}/download?format=csv`, { headers: H })).json();
console.log(link.url);Poll every minute or two at most; polling and lifecycle explains why, and errors and rate limits covers retries.
What the file contains
The CSV has one row per business email with up to 17 columns: name, email, phone, website, domain, address, city, region, zip code, rating, review count, categories, business status, claimed, opening hours, a Google Maps link and the category you searched. Every column is described in the data fields guide.
Endpoints
| Method | Path | What it does |
|---|---|---|
| GET | /api/v1/categories | Search the Google Maps category list |
| GET | /api/v1/categories/validate | Resolve one term to its canonical spelling |
| GET | /api/v1/regions | Valid region names for a country |
| GET | /api/v1/clients | Client labels this key may file scrapes under |
| GET | /api/v1/usage | This cycle's allowance and per-scrape ceiling |
| POST | /api/v1/jobs | Submit a scrape |
| GET | /api/v1/jobs | List scrapes, newest first |
| GET | /api/v1/jobs/{id} | One scrape's current state |
| POST | /api/v1/jobs/{id}/cancel | Cancel a queued or running scrape |
| GET | /api/v1/jobs/{id}/download | A short-lived signed download link |
The machine-readable contract is at
https://mapsdata.ai/api/v1/openapi.json. It is an OpenAPI 3.1
document covering exactly these paths and nothing else, so you can generate a
client from it.
Conventions
- Base URL is
https://mapsdata.ai/api/v1. Everything is HTTPS. - Authentication is
Authorization: Bearer <key>on every request. See authentication. - Bodies are JSON. Unknown fields are rejected rather than ignored, so a typo in a field name is a loud error instead of a silent no-op.
- Errors all share one envelope with a stable
code. See errors and rate limits. - Times are ISO 8601 with an offset.
Where to go next
- Authentication: creating keys, scopes, and keeping them safe.
- Submitting a scrape: categories, regions, limits, and safe retries.
- Polling and lifecycle: how long scrapes take and how to watch one sensibly.
- Errors and rate limits: the full code table and what to do about each.
- Connect an AI agent: use MapsData from Claude, Cursor or another assistant over MCP.
Build on MapsData
The REST API and the MCP server are included on every paid plan, with the same allowance as the app. Subscribe or start a trial, then create a key in Settings.
See plans and API access