Appearance
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:
- Read them via
GET /api/projects/{project_id}/levels/ - Create / Update / Delete them via
PATCH /api/projects/{project_id}/withlevels_data - Update a single area or marker via
PATCH /api/projects/{project_id}/markers/{id_or_key}/
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
| Field | Notes |
|---|---|
status | { type, label }. type is success, warning, error, info, or null to clear. The label is also written to the default-language translation. |
price | Free text, e.g. "€45 / hour" |
occupancy | Occupancy schedule object |
workingHours | Opening-hours object |
customButton | { enabled, icon, label, url } — label is the default-language text; other languages go in translations.<lang>.customButtonLabel |
isHidden | Boolean |
tags | Replaces the entity's tags |
rating | Number |
translations | Per-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
| Type | Description |
|---|---|
classic | Standard pin (default) |
pulse | Animated pulsing dot |
drop-pin | Map-style pin with configurable pin style |
rounded-square | Square pin with rounded corners |
numbered | Pin with a number inside |
label-hover | Text label on hover |
Area & Marker Fields Reference
Area Fields
| Field | Type | Description |
|---|---|---|
title | string | Display name (write-only at top level; read it from translations) |
key | string | URL-friendly slug |
data | array | Polygon coordinates [[x, y], ...] |
color | string | Hex color |
fillOpacity | number | Fill transparency (0–1) |
strokeWidth | number | Border thickness |
hoverOpacity | number | Opacity on hover |
zoomOnSelect | boolean | Zoom on click |
status | object | { "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 |
address | string | Physical address |
workingHours | object | Schedule per day |
email | string | Email address |
webpage | string | Website URL |
telephone | string | Phone number |
rating | number | Rating (e.g. 0–5) |
price | string | Free-text price |
features | array | List of feature strings (write-only at top level; read per-language in translations.features) |
tags | array | Tag 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
| Field | Type | Description |
|---|---|---|
title | string | Display name (write-only at top level; read it from translations) |
key | string | URL-friendly slug |
position | object | { "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 |
type | string | Marker type (see table above) |
icon | string | Built-in icon name (Lucide) or custom icon URL |
customIcon | string | Custom icon UUID (set when using an uploaded icon) |
color | string | Hex color |
size | number | Size in pixels |
opacity | number | Opacity (0–1) |
pinStyle | string | icon, dot, plain (drop-pin only) |
markerNumber | number | Number for numbered type |
showTooltip | boolean | Show tooltip on hover |
showCard | boolean | Show info card on click |
zoomOnSelect | boolean | Zoom on click |
status | object | { "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 |
address | string | Physical address |
workingHours | object | Schedule per day |
email | string | Email address |
webpage | string | Website URL |
telephone | string | Phone number |
rating | number | Rating (e.g. 0–5) |
price | string | Free-text price |
features | array | List of feature strings (write-only at top level; read per-language in translations.features) |
tags | array | Tag 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.