Beta

Sign up free and turn your files into maps today. Buying credit, for the heavier work, opens soon.

Docs API reference

API reference

v1https://api.maprouter.aiGeoJSONMCP

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.

authenticate.sh
export MAPROUTER_KEY="mr_live_..."

curl https://api.maprouter.ai/v1/me \
  -H "Authorization: Bearer $MAPROUTER_KEY"
A key sees its own datasets and nothing else. Ownership is taken from the key, never from anything in the request, so naming another account’s dataset id returns 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.

create.sh
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:

StatusMeans
draftThe record exists. Nothing has been uploaded to it yet.
uploadedA source file is stored and ready to ingest.
ingestingA job is running. Any previously published version keeps serving.
readyA version is published and servable, named by the record’s live_version.
failedThe 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

MethodPathKeyDescription
POST/v1/datasetsRequiredCreate a dataset. Repeating the same id is a no-op, not an error.
GET/v1/datasets/{id}RequiredThe record: status, source file, live version, and the last few ingest jobs.
PATCH/v1/datasets/{id}RequiredCorrect the title, or the column roles ingest inferred.
DELETE/v1/datasets/{id}RequiredUnpublish, then delete every object under it.
PUT/v1/datasets/{id}/sourceRequiredUpload the file in one request. Up to 50 MB.
POST/v1/datasets/{id}/source/partsRequiredBegin a multipart upload, for anything larger.
PUT/v1/datasets/{id}/source/parts/{n}RequiredSend one part.
POST/v1/datasets/{id}/source/completeRequiredAssemble the parts into the source file.
DELETE/v1/datasets/{id}/source/partsRequiredAbandon a multipart upload in progress.
POST/v1/datasets/{id}/ingestRequiredQueue the pipeline. Returns 202 and a job id to poll.
GET/v1/datasets/{id}/jobs/{job}RequiredStage-by-stage progress of one ingest job.

Reading

MethodPathKeyDescription
GET/v1/collectionsRequiredYour published datasets, plus any shared reference layers.
GET/v1/collections/{id}RequiredCollection metadata, OGC API - Features shape.
GET/v1/collections/{id}/items?bbox=RequiredFeatures intersecting a bounding box, as GeoJSON.

Agents

MethodPathKeyDescription
POST/mcpRequiredMCP over Streamable HTTP — the tool surface an agent connects to.

Compute

MethodPathKeyDescription
POST/v1/spatial/isochroneRequiredTravel-time contours from a point.

Account

MethodPathKeyDescription
GET/v1/health—Status and the edge location serving you.
GET/v1/meRequiredKey metadata — user, balance snapshot, rate limit.
GET/v1/creditsRequiredRemaining 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.

upload.sh
# 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.

multipart.sh
# 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"
Every part except the last must be the same size and at least 5 MiB — a storage rule, not ours. The 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.

ingest.sh
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:

StageWhat it does
inspectWork out the format, read the schema, detect the coordinate system, and count the rows that will need geocoding.
geocodeResolve the address column in batches, for files that carry addresses rather than coordinates.
normaliseReproject to WGS84, derive a stable feature id, and infer which column plays which role.
partitionWrite GeoParquet in spatial order, sized so a point query reads kilobytes rather than gigabytes.
manifestIndex every partition with its extent, feature count and byte size, so a read never has to list storage.
tilesBuild PMTiles, so the dataset can be drawn as well as queried.
validateCheck the output against the read path’s contract before anything becomes visible.
publishAdd 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.

RoleIs
labelThe column that names a feature in prose.
addressA street address. Also what gets geocoded when there are no coordinates.
categoryA type or class — what filtering is worth doing on.
highlightColumns worth showing unasked, in order.
nounWhat one row is called, singular and plural: ["well", "wells"].
semantics.sh
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.

features.sh
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.

Feature queries are not metered during the beta — only 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.

mcp.json
{
  "mcpServers": {
    "maprouter": {
      "url": "https://api.maprouter.ai/mcp",
      "headers": {
        "Authorization": "Bearer mr_live_..."
      }
    }
  }
}
ToolTakes a datasetReturns
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_featuresYesRecords near a coordinate, nearest first, with distance and bearing already computed. Filterable by name or category when the dataset declares those columns.
feature_atYesThe 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

StatusTypeCause
400invalid_requestA required field is missing, or the body is empty.
400invalid_dataset_idIds are lowercase letters, digits and underscores, starting with a letter.
401missing_api_keyNo Authorization header, or malformed.
401invalid_api_keyKey not recognised.
402insufficient_creditsBalance has reached zero.
429rate_limitedOver the rate limit for your key.
503auth_unavailableAuthentication temporarily unreachable. Retry.

Datasets

StatusTypeCause
404dataset_not_foundNo dataset of that id under your key. Create it first.
404job_not_foundNo ingest job of that id on this dataset.
409dataset_conflictThe dataset’s current state forbids this — an ingest is running, say. Retry after it finishes.
402free_tier_limitAn account without credit is at a free limit: 10 datasets, 250 MB of files, or 20 processing runs a day. Credit lifts them.
409no_upload_in_progressA part or a completion arrived with no multipart upload open.
413upload_too_largeOver 50 MB in a single request. Use the multipart endpoints.
400upload_incompleteThe 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.