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_here

API keys are scoped:

ScopeAllowed Methods
READ_ONLYGET
READ_WRITEGET, POST, PUT, DELETE

Creating an API Key

  1. Go to Account Settings → API Keys
  2. Click Create API Key
  3. Choose a name, scope, and optional expiration
  4. 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/v1

Rate Limits

Rate limits are per account (shared across all your keys):

BucketLimit
General1,000 requests / hour
Map creation (POST /maps)50 requests / hour
Bulk location creation20 requests / hour

Every response includes rate limit headers:

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 997
X-RateLimit-Reset: 1740000000

When a limit is exceeded you receive 429 Too Many Requests with a Retry-After header.


Endpoints Overview

Maps

MethodPathDescription
GET/mapsList your maps
POST/mapsCreate a map
GET/maps/:mapIdGet a map
PUT/maps/:mapIdUpdate a map
DELETE/maps/:mapIdDelete a map

Locations

MethodPathDescription
GET/maps/:mapId/locationsList locations
POST/maps/:mapId/locationsCreate a location
POST/maps/:mapId/locations/bulkBulk create (up to 100)
GET/maps/:mapId/locations/:locIdGet a location
PUT/maps/:mapId/locations/:locIdUpdate a location
DELETE/maps/:mapId/locations/:locIdDelete a location

Categories

MethodPathDescription
GET/maps/:mapId/categoriesList categories
POST/maps/:mapId/categoriesCreate a category
PUT/maps/:mapId/categories/:catIdUpdate a category
DELETE/maps/:mapId/categories/:catIdDelete a category

Tags

MethodPathDescription
GET/maps/:mapId/tagsList tags
POST/maps/:mapId/tagsCreate a tag
PUT/maps/:mapId/tags/:tagIdUpdate a tag
DELETE/maps/:mapId/tags/:tagIdDelete a tag

Layers

MethodPathDescription
GET/maps/:mapId/layersList layers
POST/maps/:mapId/layersCreate a layer
PUT/maps/:mapId/layers/:layerIdUpdate a layer
DELETE/maps/:mapId/layers/:layerIdDelete a layer
PUT/maps/:mapId/locations/:locId/layersReplace a location's layer memberships

API Keys

MethodPathDescription
GET/keysList your API keys
POST/keysCreate an API key
DELETE/keys/:keyIdRevoke 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 valueBehaviour
omittedExisting 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:

FieldMeaning
pageCurrent page number, 1-based
pageSizePage size actually applied — after clamping, which may differ from what you asked for
totalTotal number of matching records across all pages
totalPagesTotal 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."
  }
}
CodeStatusMeaning
MISSING_API_KEY401No Authorization header, or a header that is not a Bearer token
INVALID_API_KEY401Key not found or revoked — these are deliberately indistinguishable
API_KEY_EXPIRED401Key has passed its expiration date
PRO_SUBSCRIPTION_REQUIRED403Account does not have an active Pro subscription
INSUFFICIENT_SCOPE403Key scope doesn't allow this method (e.g. READ_ONLY used with POST)
NOT_FOUND404Resource not found or not owned by you
CONFLICT409Duplicate resource (e.g. tag name already exists on this map)
VALIDATION_ERROR400Request body failed validation
RATE_LIMIT_EXCEEDED429Rate 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 layers summaries, tags, and category so 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

Related