API reference
MapRouter holds your data. You create a dataset, upload a file to it, and run an ingest that reprojects, geocodes and indexes it; from then on the same dataset answers bounding-box queries, draws as tiles, and appears to any MCP client using your key. Responses are GeoJSON, so QGIS, geopandas and MapLibre read them without an SDK.
Authentication
Pass your key as a bearer token. Keys look like mr_live_… and are shown once at issue time; we store only a hash, so a lost key is replaced rather than recovered. A key comes with your first purchase of credit — see pricing — or email us. The site’s dashboard and maps need no key: sign up free with just an email.
export MAPROUTER_KEY="mr_live_..."
curl https://api.maprouter.ai/v1/me \
-H "Authorization: Bearer $MAPROUTER_KEY"404 rather than a permission error — the two are indistinguishable from outside, which is the point.Your first dataset
A dataset is a named container you own. Create it first; the id is yours to choose and is lowercase letters, digits and underscores, starting with a letter.
curl -X POST https://api.maprouter.ai/v1/datasets \
-H "Authorization: Bearer $MAPROUTER_KEY" \
-H "Content-Type: application/json" \
-d '{"dataset_id": "sites", "title": "Our sites"}'
# → 201 · { "dataset": { "status": "draft", … } }
# Repeating it returns 200 and the same record, so a
# retried create is not an error.GET /v1/datasets/{id} returns the record and its last few jobs. The status tells you where it is:
| Status | Means |
|---|---|
draft | The record exists. Nothing has been uploaded to it yet. |
uploaded | A source file is stored and ready to ingest. |
ingesting | A job is running. Any previously published version keeps serving. |
ready | A version is published and servable, named by the record’s live_version. |
failed | The last job did not finish. The job record says at which stage. |
A dataset can be ingesting while an earlier version is still ready and serving — re-uploading never takes the live version down, which is why live_version is its own field rather than implied by the status.
Endpoints
Everything below is deployed and serving. Nothing here is aspirational.
Datasets
| Method | Path | Key | Description |
|---|---|---|---|
| POST | /v1/datasets | Required | Create a dataset. Repeating the same id is a no-op, not an error. |
| GET | /v1/datasets/{id} | Required | The record: status, source file, live version, and the last few ingest jobs. |
| PATCH | /v1/datasets/{id} | Required | Correct the title, or the column roles ingest inferred. |
| DELETE | /v1/datasets/{id} | Required | Unpublish, then delete every object under it. |
| PUT | /v1/datasets/{id}/source | Required | Upload the file in one request. Up to 50 MB. |
| POST | /v1/datasets/{id}/source/parts | Required | Begin a multipart upload, for anything larger. |
| PUT | /v1/datasets/{id}/source/parts/{n} | Required | Send one part. |
| POST | /v1/datasets/{id}/source/complete | Required | Assemble the parts into the source file. |
| DELETE | /v1/datasets/{id}/source/parts | Required | Abandon a multipart upload in progress. |
| POST | /v1/datasets/{id}/ingest | Required | Queue the pipeline. Returns 202 and a job id to poll. |
| GET | /v1/datasets/{id}/jobs/{job} | Required | Stage-by-stage progress of one ingest job. |
Reading
| Method | Path | Key | Description |
|---|---|---|---|
| GET | /v1/collections | Required | Your published datasets, plus any shared reference layers. |
| GET | /v1/collections/{id} | Required | Collection metadata, OGC API - Features shape. |
| GET | /v1/collections/{id}/items?bbox= | Required | Features intersecting a bounding box, as GeoJSON. |
Agents
| Method | Path | Key | Description |
|---|---|---|---|
| POST | /mcp | Required | MCP over Streamable HTTP — the tool surface an agent connects to. |
Compute
| Method | Path | Key | Description |
|---|---|---|---|
| POST | /v1/spatial/isochrone | Required | Travel-time contours from a point. |
Account
| Method | Path | Key | Description |
|---|---|---|---|
| GET | /v1/health | — | Status and the edge location serving you. |
| GET | /v1/me | Required | Key metadata — user, balance snapshot, rate limit. |
| GET | /v1/credits | Required | Remaining credit. |
Uploading a file
Send the file you already have: CSV, GeoJSON, Shapefile, GeoPackage, File geodatabase, GeoParquet or KML. It does not need to be in WGS84, and it does not need coordinates at all if it carries addresses.
# one shot, up to 50 MB
curl -X PUT \
"https://api.maprouter.ai/v1/datasets/sites/source?filename=sites.csv" \
-H "Authorization: Bearer $MAPROUTER_KEY" \
--data-binary @sites.csv
# → 200 · { "dataset": { "status": "uploaded", … } }Above 50 MB, use the multipart endpoints. There are no presigned URLs here on purpose: a presigned upload needs a long-lived, account-wide storage credential held by the very service whose job is keeping accounts apart, so a large upload is many ordinary authenticated requests instead.
# 1. begin — returns the minimum part size and a note
curl -X POST \
"https://api.maprouter.ai/v1/datasets/sites/source/parts?filename=parcels.gpkg" \
-H "Authorization: Bearer $MAPROUTER_KEY"
# 2. send each part, in any order
curl -X PUT https://api.maprouter.ai/v1/datasets/sites/source/parts/1 \
-H "Authorization: Bearer $MAPROUTER_KEY" \
--data-binary @part-001
# 3. assemble
curl -X POST https://api.maprouter.ai/v1/datasets/sites/source/complete \
-H "Authorization: Bearer $MAPROUTER_KEY"begin response states it, because learning it from a failed complete means having uploaded the whole file first.DELETE …/source/parts abandons an upload in progress.Ingest
Ingest is a job, not a request. It reprojects, geocodes and repartitions, which takes minutes, so POST /ingest returns 202 and a job id and the work happens somewhere it can be retried.
curl -X POST https://api.maprouter.ai/v1/datasets/sites/ingest \
-H "Authorization: Bearer $MAPROUTER_KEY"
# → 202 · { "job_id": "job_9f3c…", "status": "running" }
curl https://api.maprouter.ai/v1/datasets/sites/jobs/job_9f3c… \
-H "Authorization: Bearer $MAPROUTER_KEY"A job reports progress against these stages, in order:
| Stage | What it does |
|---|---|
inspect | Work out the format, read the schema, detect the coordinate system, and count the rows that will need geocoding. |
geocode | Resolve the address column in batches, for files that carry addresses rather than coordinates. |
normalise | Reproject to WGS84, derive a stable feature id, and infer which column plays which role. |
partition | Write GeoParquet in spatial order, sized so a point query reads kilobytes rather than gigabytes. |
manifest | Index every partition with its extent, feature count and byte size, so a read never has to list storage. |
tiles | Build PMTiles, so the dataset can be drawn as well as queried. |
validate | Check the output against the read path’s contract before anything becomes visible. |
publish | Add the catalogue entry and stamp the version live. |
Nothing becomes visible until validate passes. It checks the pipeline’s output against the read path’s own contract — that every feature is in the partition that claims it, that the statistics a query prunes on are actually present, that the column roles name columns that exist — so a bad ingest fails at publish rather than surfacing later as a feature with no name.
Column roles
Your columns are yours, so something has to record which one names a feature and which one is an address. Otherwise a tool can only answer {"id": "f0042", "distance_m": 120}, which tells a model nothing it can say back to a person. Ingest infers the mapping from column names, and PATCH overrides it.
| Role | Is |
|---|---|
label | The column that names a feature in prose. |
address | A street address. Also what gets geocoded when there are no coordinates. |
category | A type or class — what filtering is worth doing on. |
highlight | Columns worth showing unasked, in order. |
noun | What one row is called, singular and plural: ["well", "wells"]. |
curl -X PATCH https://api.maprouter.ai/v1/datasets/sites \
-H "Authorization: Bearer $MAPROUTER_KEY" \
-H "Content-Type: application/json" \
-d '{
"semantics": {
"label": "site_name",
"address": "situs_addr",
"category":"asset_type",
"noun": ["site", "sites"]
}
}'A correction is stored on the dataset record as well as in the published version, so it survives a re-ingest — having fixed which column is the address once, you do not fix it again after every upload. A role naming a column that does not exist fails at publish.
Querying features
Published datasets appear as OGC API - Features collections. A bounding box is min_lon,min_lat,max_lon,max_lat, and it is required — a query with no extent would read the whole dataset.
curl "https://api.maprouter.ai/v1/collections/sites/items\
?bbox=-97.75,30.25,-97.70,30.30" \
-H "Authorization: Bearer $MAPROUTER_KEY"GET /v1/collections lists what your key can see: your own published datasets, plus any shared reference layers, each flagged as one or the other. Two response headers, X-Partitions-Read and X-Row-Groups, report what the query actually touched.
POST /v1/spatial/isochrone draws on your balance. See pricing for the rate and for what is coming.Agents and MCP
POST /mcp speaks MCP over Streamable HTTP. Point a client at it with your key and it sees your datasets and nobody else’s — there is no fixed catalogue, which is why list_datasets is the tool an agent calls first.
{
"mcpServers": {
"maprouter": {
"url": "https://api.maprouter.ai/mcp",
"headers": {
"Authorization": "Bearer mr_live_..."
}
}
}
}| Tool | Takes a dataset | Returns |
|---|---|---|
list_datasets | — | What datasets exist, what one row of each represents, the area each covers, and which columns carry the name, address and category. An agent calls this first. |
search_features | Yes | Records near a coordinate, nearest first, with distance and bearing already computed. Filterable by name or category when the dataset declares those columns. |
feature_at | Yes | The record whose area contains a coordinate — the parcel, zone, territory or catchment it falls inside. Tested against real geometry, never a bounding box. |
describe_location | — | A coordinate summarised across every dataset the key can see: what covers it, and what is nearby. |
Distance, bearing and containment are computed before the answer leaves the gateway, and containment is tested against real geometry rather than a bounding box. A model asked to do that arithmetic will eventually get it wrong and say so confidently.
Errors
Errors return { "error": { "type", "message" } }. Branch on type, not the message.
Request
| Status | Type | Cause |
|---|---|---|
400 | invalid_request | A required field is missing, or the body is empty. |
400 | invalid_dataset_id | Ids are lowercase letters, digits and underscores, starting with a letter. |
401 | missing_api_key | No Authorization header, or malformed. |
401 | invalid_api_key | Key not recognised. |
402 | insufficient_credits | Balance has reached zero. |
429 | rate_limited | Over the rate limit for your key. |
503 | auth_unavailable | Authentication temporarily unreachable. Retry. |
Datasets
| Status | Type | Cause |
|---|---|---|
404 | dataset_not_found | No dataset of that id under your key. Create it first. |
404 | job_not_found | No ingest job of that id on this dataset. |
409 | dataset_conflict | The dataset’s current state forbids this — an ingest is running, say. Retry after it finishes. |
402 | free_tier_limit | An account without credit is at a free limit: 10 datasets, 250 MB of files, or 20 processing runs a day. Credit lifts them. |
409 | no_upload_in_progress | A part or a completion arrived with no multipart upload open. |
413 | upload_too_large | Over 50 MB in a single request. Use the multipart endpoints. |
400 | upload_incomplete | The assembled parts were rejected — almost always a part-size violation. |
409 dataset_conflict is worth singling out: nothing about the request is malformed, it is the dataset’s current state that makes it impossible — an ingest is running, say. The same request usually succeeds once that finishes.
In development
Written down because a roadmap you can read is worth more than a surprise. None of this is billed for, and none of it is live.
- Operations for agents. Map chat asks questions of a map by running operations in your browser. The same operations over MCP need somewhere to run for an agent with no browser, and that is next — until then a map’s MCP endpoint answers the point tools.
- Combining layers. Joining one layer to another by location, and zones around features, each producing a layer you can map and ask about in turn.
- Bulk extract. A bounding box over a large area is currently refused rather than answered. Now that the data is yours, the right answer is a link to your own partitions.
- Metered ingest and storage. Both are unbilled today. Rates go on the pricing page before they take effect, and never apply to credit already bought.