Public REST API
Use the Mapiry REST API to programmatically create and manage maps, locations, categories, tags, and layers from your own apps and automations
Updated October 12, 2026
The Mapiry REST API lets Pro users create and manage maps, locations, categories, tags, and layers programmatically — perfect for AI workflows, automations, and integrations with your own tooling.
Looking for the endpoint list? The Interactive API Reference renders every endpoint with live request examples, authentication headers, and response schemas.
Prerequisites
- A Pro subscription (API access is a Pro feature)
- An API key created in Account Settings
Also available as MCP
Everything on this page is also exposed as an MCP server at https://mapiry.com/mcp, so MCP clients can call the API without hand-written glue. Same API key, same scopes, same rate limits, same response shapes.
See MCP Server for client configuration and the tool list. Note that API key management is deliberately not available over MCP.
Interactive API Reference
Full API reference with live try-it-out: /docs/api
The reference is generated from /api/v1/openapi.json, which is built from the same Zod schemas the API uses to validate requests. When a validation rule changes, the reference changes with it — there is no separate hand-maintained copy of the schema to fall out of date.
Authentication
All API requests require a Bearer token in the Authorization header:
Authorization: Bearer mk_live_your_key_hereAPI keys are scoped:
| Scope | Allowed Methods |
|---|---|
READ_ONLY | GET |
READ_WRITE | GET, POST, PUT, DELETE |
Creating an API Key
- Go to Account Settings → API Keys
- Click Create API Key
- Choose a name, scope, and optional expiration
- Copy the key immediately — it is shown only once
You can have up to 10 active API keys per account.
Base URL
https://mapiry.com/api/v1Rate Limits
Rate limits are per account (shared across all your keys):
| Bucket | Limit |
|---|---|
| General | 1,000 requests / hour |
Map creation (POST /maps) | 50 requests / hour |
| Bulk location creation | 20 requests / hour |
Every response includes rate limit headers:
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 997
X-RateLimit-Reset: 1740000000When a limit is exceeded you receive 429 Too Many Requests with a Retry-After header.
Endpoints Overview
Maps
| Method | Path | Description |
|---|---|---|
GET | /maps | List your maps |
POST | /maps | Create a map |
GET | /maps/:mapId | Get a map |
PUT | /maps/:mapId | Update a map |
DELETE | /maps/:mapId | Delete a map |
Locations
| Method | Path | Description |
|---|---|---|
GET | /maps/:mapId/locations | List locations |
POST | /maps/:mapId/locations | Create a location |
POST | /maps/:mapId/locations/bulk | Bulk create (up to 100) |
GET | /maps/:mapId/locations/:locId | Get a location |
PUT | /maps/:mapId/locations/:locId | Update a location |
DELETE | /maps/:mapId/locations/:locId | Delete a location |
Categories
| Method | Path | Description |
|---|---|---|
GET | /maps/:mapId/categories | List categories |
POST | /maps/:mapId/categories | Create a category |
PUT | /maps/:mapId/categories/:catId | Update a category |
DELETE | /maps/:mapId/categories/:catId | Delete a category |
Tags
| Method | Path | Description |
|---|---|---|
GET | /maps/:mapId/tags | List tags |
POST | /maps/:mapId/tags | Create a tag |
PUT | /maps/:mapId/tags/:tagId | Update a tag |
DELETE | /maps/:mapId/tags/:tagId | Delete a tag |
Layers
| Method | Path | Description |
|---|---|---|
GET | /maps/:mapId/layers | List layers |
POST | /maps/:mapId/layers | Create a layer |
PUT | /maps/:mapId/layers/:layerId | Update a layer |
DELETE | /maps/:mapId/layers/:layerId | Delete a layer |
PUT | /maps/:mapId/locations/:locId/layers | Replace a location's layer memberships |
API Keys
| Method | Path | Description |
|---|---|---|
GET | /keys | List your API keys |
POST | /keys | Create an API key |
DELETE | /keys/:keyId | Revoke an API key |
Quick Examples
List your maps
Every other endpoint needs a map ID, so this is usually the first call you make.
curl "https://mapiry.com/api/v1/maps?page=1&pageSize=50" \
-H "Authorization: Bearer mk_live_your_key_here"Response (200 OK):
{
"data": {
"maps": [
{
"id": "clxyz...",
"title": "Best Coffee in Tokyo",
"slug": "best-coffee-in-tokyo",
"createdBy": "user_xyz",
"public": false,
"_count": { "locations": 12, "likes": 3 },
"createdAt": "2026-03-01T00:00:00.000Z"
}
],
"pagination": { "total": 4, "page": 1, "pageSize": 50, "totalPages": 1 }
}
}Maps are returned newest first. pageSize defaults to 50 and is capped at 200 — a larger value is clamped, not rejected.
Create a map
curl -X POST https://mapiry.com/api/v1/maps \
-H "Authorization: Bearer mk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{"title": "Best Coffee in Tokyo", "description": "My favourite spots"}'Response (201 Created):
{
"data": {
"id": "clxyz...",
"title": "Best Coffee in Tokyo",
"slug": "best-coffee-in-tokyo",
"description": "My favourite spots",
"createdBy": "user_xyz",
"country": "",
"city": "",
"public": false,
"allowCloning": true,
"style": "streets-v12",
"centerLat": null,
"centerLng": null,
"zoom": null,
"brandingProfile": null,
"_count": { "locations": 0, "likes": 0 },
"createdAt": "2026-03-01T00:00:00.000Z",
"updatedAt": "2026-03-01T00:00:00.000Z"
}
}Bulk create locations
curl -X POST https://mapiry.com/api/v1/maps/clxyz.../locations/bulk \
-H "Authorization: Bearer mk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"locations": [
{"name": "Fuglen Tokyo", "lat": 35.6654, "lng": 139.7006},
{"name": "Bear Pond Espresso", "lat": 35.6585, "lng": 139.6968}
]
}'Response (201 Created):
{
"data": {
"created": [
{
"id": "loc1",
"name": "Fuglen Tokyo",
"lat": 35.6654,
"lng": 139.7006,
"layers": []
},
{
"id": "loc2",
"name": "Bear Pond Espresso",
"lat": 35.6585,
"lng": 139.6968,
"layers": []
}
],
"count": 2
}
}List locations with pagination
curl "https://mapiry.com/api/v1/maps/clxyz.../locations?page=1&pageSize=20" \
-H "Authorization: Bearer mk_live_your_key_here"Response:
{
"data": {
"locations": [
{
"id": "loc1",
"shortId": "a1b2c3d4",
"name": "Fuglen Tokyo",
"lat": 35.6654,
"lng": 139.7006,
"category": { "id": "cat1", "name": "Coffee", "color": "#ff6b35", "mapId": "clxyz..." },
"tags": [{ "id": "lt1", "tagId": "tag1", "tag": { "id": "tag1", "name": "wifi", "mapId": "clxyz..." } }],
"layers": [
{
"id": "layer_day1",
"name": "Day 1",
"color": "#6366F1",
"order": 0,
"routeEnabled": false,
"routeShowByDefault": false,
"mapId": "clxyz...",
"createdAt": "2026-03-01T00:00:00.000Z",
"updatedAt": "2026-03-01T00:00:00.000Z",
"locationLayerOrder": 0,
"layerId": "layer_day1",
"locationId": "loc1",
"layer": { "id": "layer_day1", "name": "Day 1", "mapId": "clxyz..." }
}
],
"createdAt": "2026-03-01T00:00:00.000Z",
"updatedAt": "2026-03-01T00:00:00.000Z"
}
],
"pagination": {
"total": 47,
"page": 1,
"pageSize": 20,
"totalPages": 3
}
}
}Layer entries carry the layer row's fields plus the membership extras (locationLayerOrder, layerId, layer, locationId), because the API embeds layer summaries directly on each location.
Get a map with its categories and tags
curl https://mapiry.com/api/v1/maps/clxyz... \
-H "Authorization: Bearer mk_live_your_key_here"The map detail endpoint embeds categories and tags alongside the _count object, ordered by name — so you can pick a category ID for a new location without a second request:
{
"data": {
"id": "clxyz...",
"title": "Best Coffee in Tokyo",
"createdBy": "user_xyz",
"_count": { "locations": 12, "likes": 3 },
"categories": [
{ "id": "cat_abc", "name": "Coffee", "color": "#ff6b35", "mapId": "clxyz..." }
],
"tags": [
{ "id": "tag_abc", "name": "wifi", "color": "#22c55e", "mapId": "clxyz..." }
]
}
}categories and tags appear on the map detail response only. The list, create, and update responses return the bare map row.
Assign tags to a location
Pass tag names — not IDs — and Mapiry matches them case-insensitively against the map's existing tags, creating any that do not exist yet:
curl -X POST https://mapiry.com/api/v1/maps/clxyz.../locations \
-H "Authorization: Bearer mk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{"name": "Fuglen Tokyo", "lat": 35.6654, "lng": 139.7006, "tags": ["wifi", "outdoor-seating"]}'Tag names work on create, update, and every item in a bulk request:
tags value | Behaviour |
|---|---|
| omitted | Existing tags are left untouched |
["wifi"] | Replaces the location's tags with just wifi |
[] | Clears all tags from the location |
Matching is case-insensitive, so sending "WiFi" when the map already has a wifi tag reuses that tag instead of creating a near-duplicate. Surrounding whitespace is trimmed, blank names are rejected, and a location can hold at most 50 tags.
This also makes the tagId filter on GET /maps/:mapId/locations usable — before this, tags could not be assigned through the API at all, so the filter had no way to match anything.
Assign a category by name
category takes a category name and works the same way — matched case-insensitively, created if absent:
curl -X POST https://mapiry.com/api/v1/maps/clxyz.../locations \
-H "Authorization: Bearer mk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{"name": "Fuglen Tokyo", "lat": 35.6654, "lng": 139.7006, "category": "Coffee", "tags": ["wifi"]}'Use categoryId instead when you already have the ID, for example from the map detail response. Supplying both is allowed — category (the name) wins, so you can pass a name without giving up the ID field.
Response Format
Every successful response returns JSON with a data key. The exact shape depends on whether the endpoint returns a single resource or a collection.
Single resource — always { "data": { ... } }:
{ "data": { "id": "clxyz...", "title": "Best Coffee in Tokyo", "...": "..." } }Paginated collection — data holds the array and a pagination object:
{
"data": {
"maps": [ { "id": "clxyz..." }, { "id": "clabc..." } ],
"pagination": { "page": 1, "pageSize": 50, "total": 4, "totalPages": 1 }
}
}The pagination object always has these four fields:
| Field | Meaning |
|---|---|
page | Current page number, 1-based |
pageSize | Page size actually applied — after clamping, which may differ from what you asked for |
total | Total number of matching records across all pages |
totalPages | Total number of pages |
Non-paginated collection — data holds the array under its plural name:
{ "data": { "categories": [ { "id": "cat_abc", "name": "Coffee" } ] } }GET /api/v1/keys is the one exception: it returns a bare array, { "data": [ ... ] }.
Deletes return 204 No Content with an empty body, not a JSON envelope.
Error Responses
All errors follow this shape:
{
"error": {
"code": "INVALID_API_KEY",
"message": "The API key is invalid or has been revoked."
}
}| Code | Status | Meaning |
|---|---|---|
MISSING_API_KEY | 401 | No Authorization header, or a header that is not a Bearer token |
INVALID_API_KEY | 401 | Key not found or revoked — these are deliberately indistinguishable |
API_KEY_EXPIRED | 401 | Key has passed its expiration date |
PRO_SUBSCRIPTION_REQUIRED | 403 | Account does not have an active Pro subscription |
INSUFFICIENT_SCOPE | 403 | Key scope doesn't allow this method (e.g. READ_ONLY used with POST) |
NOT_FOUND | 404 | Resource not found or not owned by you |
CONFLICT | 409 | Duplicate resource (e.g. tag name already exists on this map) |
VALIDATION_ERROR | 400 | Request body failed validation |
RATE_LIMIT_EXCEEDED | 429 | Rate limit hit — check Retry-After header |
Branch on code, not on message. The message is human-readable text meant for logs and support, and its wording can change; the code is the stable contract.
Notes
- The API is write-only scoped for v1 — you can read your own data but there is no public read access for other users' maps
- No geocoding endpoint — coordinates must be provided directly
- Bulk location creation is atomic: if any location fails validation, none are created
- Bulk create accepts a maximum of 100 locations per request
- Layer memberships are managed separately from location CRUD via
PUT /maps/:mapId/locations/:locId/layers - Location read responses include embedded
layerssummaries,tags, andcategoryso clients can render names, colours, and labels without extra lookups - Tags and categories can both be assigned by name and are created on demand — there is no need to create one before using it
- The map's owner is returned as
createdBy - All timestamps are ISO 8601 in UTC
What's Next
- Interactive API Reference — Full schema, live requests
- MCP Server — Call the same API from an MCP client
- Pro Features — What's included in Pro
- Limits & Quotas — Rate limit details
- Bulk Operations — Other bulk import options