Skip to content

Levels, Areas, Markers ​

Manage map entities through the project API. All endpoints require authentication.

How It Works ​

Levels, areas, and markers are nested inside a project. You can:

The levels_data route is the same approach the Layota editor uses internally: it replaces the whole structure, so anything you leave out is deleted. Use the per-entity route when an integration needs to change one thing — availability, a price, opening hours — without resending the map.

Get Levels (with Areas & Markers) ​

http
GET /api/projects/{project_id}/levels/
Authorization: Bearer <token>

Response:

json
[
  {
    "id": "level-uuid",
    "name": "Floor 1",
    "key": "floor-1",
    "order": 0,
    "isHidden": false,
    "imageUrl": "https://...",
    "imageThumbnails": ["https://...small", "https://...medium"],
    "defaultZoom": 1.0,
    "minZoom": 0.75,
    "maxZoom": 4.0,
    "backgroundColor": null,
    "areas": [
      {
        "id": "area-uuid",
        "key": "coffee-shop",
        "data": [[100, 200], [300, 200], [300, 400], [100, 400]],
        "color": "#FF5733",
        "fillOpacity": 0.3,
        "strokeWidth": 2,
        "hoverOpacity": 0.5,
        "zoomOnSelect": true,
        "status": null,
        "address": null,
        "workingHours": null,
        "email": null,
        "webpage": null,
        "telephone": null,
        "rating": null,
        "price": null,
        "tags": [
          { "id": "tag-uuid-1", "name": "Food", "color": "#FF5733", "translations": { "ru": "Еда" } }
        ],
        "images": [
          {
            "id": "img-uuid",
            "url": "https://...",
            "thumbnails": ["https://...small", "https://...medium"],
            "order": 0
          }
        ],
        "translations": {
          "en": {
            "title": "Coffee Shop",
            "description": "Best coffee in town",
            "statusLabel": "",
            "features": [],
            "customButtonLabel": "Order online"
          },
          "ru": {
            "title": "Кофейня",
            "description": "Лучший кофе",
            "statusLabel": null,
            "features": [],
            "customButtonLabel": "Заказать онлайн"
          }
        }
      }
    ],
    "markers": [
      {
        "id": "marker-uuid",
        "key": "main-entrance",
        "position": { "x": 250, "y": 100 },
        "type": "classic",
        "icon": "door-open",
        "customIcon": null,
        "color": "#00AA00",
        "size": 10,
        "opacity": 1,
        "pinStyle": "icon",
        "markerNumber": 1,
        "showTooltip": true,
        "showCard": true,
        "zoomOnSelect": true,
        "status": null,
        "tags": [],
        "images": [],
        "translations": {
          "en": {
            "title": "Main Entrance",
            "description": null,
            "statusLabel": "",
            "features": [],
            "customButtonLabel": null
          }
        }
      }
    ]
  }
]

Where is title?

Responses have no top-level title on areas and markers — the display name always comes from translations (the project's default language entry is always present). On write you send a top-level title, which becomes the default-language title.

Public Read-Only Endpoints ​

These endpoints require no authentication and work for published projects. They also support API key auth (Authorization: Bearer sk_... — read or write key), which bypasses the allowed domains check.

Lookup by UUID or by slug key (e.g. floor-1, coffee-shop).

List Levels (metadata only) ​

http
GET /api/p/{project_id}/levels/

Returns a lightweight list without nested areas/markers:

json
[
  {
    "id": "level-uuid",
    "name": "Floor 1",
    "key": "floor-1",
    "imageUrl": "https://...",
    "imageThumbnails": ["https://..."],
    "isHidden": false,
    "defaultZoom": 1.0,
    "minZoom": 0.75,
    "maxZoom": 4.0,
    "backgroundColor": null,
    "order": 0,
    "areaCount": 12,
    "markerCount": 5
  }
]

Get Level Detail ​

http
GET /api/p/{project_id}/levels/{level_id_or_key}/

Returns full level with all areas and markers (same format as GET /api/projects/{id}/levels/ but for a single level).

Get Area Detail ​

http
GET /api/p/{project_id}/areas/{area_id_or_key}/

Returns a single area with images, tags, and translations.

Get Marker Detail ​

http
GET /api/p/{project_id}/markers/{marker_id_or_key}/

Returns a single marker with images, tags, and translations.

Update Project (with Levels, Areas, Markers) ​

Send levels_data as part of a project update. Each level can contain areas and markers arrays. Include id for existing entities (to update) or omit it (to create). Entities not included in the array are deleted.

http
PATCH /api/projects/{project_id}/
Authorization: Bearer <token>
Content-Type: application/json

{
  "levels_data": [
    {
      "id": "existing-level-uuid",
      "name": "Ground Floor",
      "key": "ground-floor",
      "order": 0,
      "isHidden": false,
      "areas": [
        {
          "id": "existing-area-uuid",
          "title": "Updated Coffee Shop",
          "key": "coffee-shop",
          "data": [[100, 200], [300, 200], [300, 400], [100, 400]],
          "color": "#3366FF",
          "fillOpacity": 0.3,
          "strokeWidth": 2,
          "tags": ["tag-uuid"]
        },
        {
          "title": "New Store",
          "key": "new-store",
          "data": [[400, 200], [600, 200], [600, 400], [400, 400]],
          "color": "#FF5733"
        }
      ],
      "markers": [
        {
          "title": "Main Entrance",
          "key": "main-entrance",
          "position": { "x": 250, "y": 100 },
          "type": "classic",
          "icon": "door-open",
          "color": "#00AA00"
        }
      ]
    }
  ]
}

With Image Upload ​

When uploading level images, use multipart/form-data with the data as a JSON string in the data field:

http
PATCH /api/projects/{project_id}/
Authorization: Bearer <token>
Content-Type: multipart/form-data

data={"levels_data": [...]}
level_image_0=<file>

Update a Single Area or Marker ​

Change one entity without resending the map. Unlike levels_data, this is a true partial update: only the fields you send change, and nothing is deleted for being absent. Requires a write API key (or a signed-in editor); read keys get 403.

http
PATCH /api/projects/{project_id}/markers/{id_or_key}/
PATCH /api/projects/{project_id}/areas/{id_or_key}/
Authorization: Bearer sk_...
Content-Type: application/json

{
  "status": { "type": "warning", "label": "Booked until 18:00" },
  "price": "€45 / hour"
}

The entity is addressed by its UUID or its key slug, exactly like the public read endpoints — so an integration can use its own identifiers:

bash
curl -X PATCH https://api.layota.app/api/projects/$PROJECT_ID/markers/lane-3/ \
  -H "Authorization: Bearer $LAYOTA_WRITE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": {"type": "success", "label": "Free now"}}'

Returns the full updated entity (same shape as GET /api/p/{project_id}/markers/{id}/).

Patchable fields ​

FieldNotes
status{ type, label }. type is success, warning, error, info, or null to clear. The label is also written to the default-language translation.
priceFree text, e.g. "€45 / hour"
occupancyOccupancy schedule object
workingHoursOpening-hours object
customButton{ enabled, icon, label, url } — label is the default-language text; other languages go in translations.<lang>.customButtonLabel
isHiddenBoolean
tagsReplaces the entity's tags
ratingNumber
translationsPer-language title / description / statusLabel / features / customButtonLabel

Anything else is rejected with 400, including position, data and images — geometry and photos belong to the editor. Sending an unknown field fails loudly rather than being silently ignored.

Published maps update immediately ​

/api/p/ serves a published snapshot, so most writes to a published project only become visible after POST /api/projects/{id}/update-live/. This endpoint is the exception: it patches the live snapshot in place, so the change is served right away — no update-live call, no full-map rebuild.

TIP

For anything this endpoint does not cover (geometry, images, creating or deleting entities), use levels_data and then call update-live if the project is published.

Marker Types ​

TypeDescription
classicStandard pin (default)
pulseAnimated pulsing dot
drop-pinMap-style pin with configurable pin style
rounded-squareSquare pin with rounded corners
numberedPin with a number inside
label-hoverText label on hover

Area & Marker Fields Reference ​

Area Fields ​

FieldTypeDescription
titlestringDisplay name (write-only at top level; read it from translations)
keystringURL-friendly slug
dataarrayPolygon coordinates [[x, y], ...]
colorstringHex color
fillOpacitynumberFill transparency (0–1)
strokeWidthnumberBorder thickness
hoverOpacitynumberOpacity on hover
zoomOnSelectbooleanZoom on click
statusobject{ "type": "...", "label": "..." } or null; type is one of success, warning, error, info. Responses contain only type — the label is returned per-language in translations.statusLabel
addressstringPhysical address
workingHoursobjectSchedule per day
emailstringEmail address
webpagestringWebsite URL
telephonestringPhone number
ratingnumberRating (e.g. 0–5)
pricestringFree-text price
featuresarrayList of feature strings (write-only at top level; read per-language in translations.features)
tagsarrayTag objects { "id", "name", "color" } in responses. On write, pass existing tag UUIDs (or objects with id); objects with name/color and no id create new tags

Marker Fields ​

FieldTypeDescription
titlestringDisplay name (write-only at top level; read it from translations)
keystringURL-friendly slug
positionobject{ "x": number, "y": number } — finite numbers only; NaN/Infinity (as strings or out-of-range literals) are rejected with 400, as for every numeric field
typestringMarker type (see table above)
iconstringBuilt-in icon name (Lucide) or custom icon URL
customIconstringCustom icon UUID (set when using an uploaded icon)
colorstringHex color
sizenumberSize in pixels
opacitynumberOpacity (0–1)
pinStylestringicon, dot, plain (drop-pin only)
markerNumbernumberNumber for numbered type
showTooltipbooleanShow tooltip on hover
showCardbooleanShow info card on click
zoomOnSelectbooleanZoom on click
statusobject{ "type": "...", "label": "..." } or null; type is one of success, warning, error, info. Responses contain only type — the label is returned per-language in translations.statusLabel
addressstringPhysical address
workingHoursobjectSchedule per day
emailstringEmail address
webpagestringWebsite URL
telephonestringPhone number
ratingnumberRating (e.g. 0–5)
pricestringFree-text price
featuresarrayList of feature strings (write-only at top level; read per-language in translations.features)
tagsarrayTag objects { "id", "name", "color" } in responses. On write, pass existing tag UUIDs (or objects with id); objects with name/color and no id create new tags

Translations ​

Translations are included in the entity response and can be set via the project update:

json
{
  "levels_data": [
    {
      "id": "level-uuid",
      "areas": [
        {
          "id": "area-uuid",
          "translations": {
            "ru": {
              "title": "Кофейня",
              "description": "Лучший кофе",
              "statusLabel": "Открыто",
              "features": ["Wi-Fi", "Розетки"]
            }
          }
        }
      ]
    }
  ]
}

Area Images ​

Upload Image ​

http
POST /api/projects/{project_id}/upload-image/
Authorization: Bearer <token>
Content-Type: multipart/form-data

file=<file>

Images can also be uploaded as part of the project update using multipart/form-data (fields level_{i}_image, level_{i}_area_{j}_image_{k}, level_{i}_marker_{j}_image_{k}) or as a base64 data: URI in a level's imageUrl.

Every path applies the same rules. The image type is detected from the file content, not its name: JPEG, PNG, GIF, WebP and SVG are accepted, anything else (including an .html renamed to .png) is rejected with 400 and the whole save is rolled back. The stored extension follows the detected type, so a JPEG posted as plan.png is served as .jpg. SVGs are sanitized (scripts, event handlers and non-image hrefs are stripped) and must have an <svg> root. Maximum size is 5 MB per file.

Layota Documentation