API overview
Base URL, authentication with API keys and sessions, conventions, errors, quotas, pagination and versioning for the MapDove REST API.
The MapDove REST API exposes everything the app does: reference data, place-level foot traffic, market intelligence, the SDSS models, saved projects and account management. It is JSON over HTTPS, with GeoJSON for geometry.
The API is included on Pro, Business and Enterprise plans. The Free plan has no API keys and an API quota of zero.
Base URL
https://api.mapdove.com/api/v1
All paths in this reference are relative to /api/v1 unless noted (the health check also lives at the root). For local development the API listens on http://localhost:8088, so the same path is http://localhost:8088/api/v1/....
Authentication
Every data endpoint — cities, places, market, SDSS, projects — requires authentication. There are two mechanisms.
API keys (server-to-server)
Organization API keys are the way to integrate MapDove into your own systems. Send the key as a bearer token:
Authorization: Bearer ms_live_<key>
- Keys belong to the organization, not to a user, and act with the permissions of the user who created them.
- Every request authenticated with a key counts as one API call toward the organization’s monthly quota.
- Keep keys on the server. Never embed them in browser or mobile code.
Sessions (browser)
The MapDove app uses a session cookie, ms_session (httpOnly, SameSite=Lax, Secure in production), set by POST /auth/login or POST /auth/signup. A session token may also be sent as Authorization: Bearer <token>. Sessions are intended for interactive use; for automation use API keys. See Account endpoints.
Key management
| Action | How |
|---|---|
| Create | Settings → API keys (owners and admins), or POST /keys with { "name": "..." } from a signed-in session |
| List | GET /keys — returns name, prefix, created, last used and revoked timestamps |
| Revoke | DELETE /keys/:id — takes effect immediately |
Key management, membership, billing and profile endpoints accept sessions only; an API key cannot create or revoke other keys. See Account endpoints.
The full secret (ms_live_…) is returned once, at creation. MapDove stores only a hash; if you lose a key, revoke it and create a new one. Plans limit the number of active keys (Pro 3, Business 10, Enterprise unlimited). Rotate keys by creating the new one, deploying it, then revoking the old one.
Your first request
curl https://api.mapdove.com/api/v1/cities \
-H "Authorization: Bearer $MAPDOVE_KEY"
const res = await fetch('https://api.mapdove.com/api/v1/cities', {
headers: { Authorization: `Bearer ${process.env.MAPDOVE_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).error.message);
const cities = await res.json();
console.log(cities.map((c) => c.slug));
import os, requests
r = requests.get(
"https://api.mapdove.com/api/v1/cities",
headers={"Authorization": f"Bearer {os.environ['MAPDOVE_KEY']}"},
timeout=30,
)
r.raise_for_status()
print([c["slug"] for c in r.json()])
Conventions
| Topic | Convention |
|---|---|
| Format | JSON request and response bodies (Content-Type: application/json); CSV for export.csv |
| Dates | YYYY-MM-DD |
| Timestamps | RFC 3339, e.g. 2026-11-01T14:03:22Z |
| IDs | Users, orgs, keys, projects, alerts: UUID strings. Cities, categories, brands: integers with string slugs (use slugs in paths and params). Places and hexes: integers |
| Coordinates | lng, lat in decimal degrees (WGS 84) |
| GeoJSON | FeatureCollection in EPSG:4326; coordinates [lng, lat] |
| Units | Fields suffixed _km, _km2 are metric; income is in the city’s currency (USD or AED) |
| Percentages | *_pct fields are percentages (4.7 = 4.7%); share, overlap and market_share are fractions (0–1) |
| Empty results | Empty arrays, never null lists |
Date ranges
Analytics endpoints accept from and to. Defaults and rules:
todefaults to the last data day (2026-10-31) and is capped to it.fromdefaults totominus 89 days — the last 90 days of data.- The plan’s
history_daysclamps the range: on Free, nothing earlier than 90 days before the last data day; on paid plans, 365 days. Out-of-range values are clamped, not rejected. fromaftertoreturns400 bad_request.- The previous period (for
change_pct,visits_prev) has the same length and ends the day beforefrom.
Errors
Errors use one envelope with a matching HTTP status:
{ "error": { "code": "plan_limit", "message": "Your free plan includes 25 analyses per month. Upgrade to run more." } }
| Status | code |
Meaning |
|---|---|---|
| 400 | bad_request |
Invalid parameter or body (bad date, unknown criterion, point outside the city, too many rings) |
| 401 | unauthorized |
Missing, invalid or revoked credentials |
| 401 | totp_required |
Login only: two-factor code missing or invalid |
| 402 | plan_limit |
A plan limit or quota is reached (API calls, analyses, keys, seats, alerts, exports not included) |
| 403 | forbidden |
Authenticated, but not allowed (role, suspended workspace, API key on a session-only endpoint) |
| 404 | not_found |
The resource does not exist or is not visible to your organization |
| 409 | conflict |
Duplicate, e.g. an email that is already registered |
| 429 | rate_limited |
Too many requests in a short window |
| 500 | internal |
Unexpected server error — safe to retry with backoff |
Show message to humans; branch on code in code.
Quotas and rate limits
Monthly quotas are per organization and reset on the first day of each calendar month (UTC):
| Plan | API calls / month | SDSS analyses / month |
|---|---|---|
| Free | 0 | 25 |
| Pro | 10,000 | 500 |
| Business | 100,000 | unlimited |
| Enterprise | unlimited | unlimited |
- Each request authenticated with an API key counts as one API call, including failed ones.
- Each successful SDSS call (
/sdss/*) also counts one analysis. - Over quota, requests return 402
plan_limituntil the next month or an upgrade. Do not retry 402s automatically. - Check current usage with
GET /org/usage(reference).
Short-term throttling. To protect the service, bursts may be throttled with 429 rate_limited. Retry with exponential backoff and jitter:
import random, time, requests
def get_with_backoff(url, headers, attempts=6):
for n in range(attempts):
r = requests.get(url, headers=headers, timeout=60)
if r.status_code not in (429, 500, 502, 503):
return r
time.sleep(min(30, 2 ** n) + random.random())
r.raise_for_status()
SDSS calls are compute-heavy (a Huff run can take around a second); run them sequentially or with low concurrency, and cache results — store them as projects if you need them later.
Pagination
List endpoints that can return many rows accept limit; GET /places also accepts offset for paging:
| Endpoint | Default limit |
Max | offset |
|---|---|---|---|
GET /places |
100 | 500 | yes |
GET /rankings |
50 | 500 | no — top N only |
GET /places/:id/cross-shopping |
10 | 50 | no |
Values above the maximum are clamped to it.
GET /places returns { total, items }; iterate with offset += limit until offset >= total:
async function allPlaces(city, key) {
const out = [];
for (let offset = 0; ; offset += 500) {
const url = `https://api.mapdove.com/api/v1/places?city=${city}&limit=500&offset=${offset}`;
const page = await (await fetch(url, { headers: { Authorization: `Bearer ${key}` } })).json();
out.push(...page.items);
if (offset + 500 >= page.total) return out;
}
}
Each page is one API call.
Versioning
The version is in the path (/api/v1). Within v1 we only make additive changes: new endpoints, new optional parameters, new response fields. Clients should ignore unknown fields. Breaking changes ship as a new version with advance notice in the changelog.
Reference
- Reference data — health, stats, plans, cities, hexes, neighborhoods, categories, brands, segments
- Places — places, visits, hourly, trade areas, cross-shopping, export, nearby, compare
- Market — overview, rankings, brand performance
- SDSS — suitability, Huff, catchment, compare sites
- Projects and alerts — projects, favorites, alerts
- Account — auth, org, members, keys, usage, billing