38 KiB
Backend API Endpoints Documentation
Complete reference for all API endpoints in the Tour Builder Platform backend.
Overview
- Base URL:
http://localhost:3000/apifor local development - Content-Type:
application/json - Authentication: JWT Bearer Token (except public endpoints)
- API Documentation UI:
http://localhost:3000/api-docs(Swagger, local development)
Standard VM note: the VM dev_stage backend listens on port 3000, while
the frontend listens on 3001 and Apache serves the public domain on port 80.
Use http://127.0.0.1:3000/api/... for direct VM backend checks. Protected
routes returning 401 Unauthorized without JWT are healthy. See
deployment-vm.md.
Table of Contents
- Authentication
- Health & System
- Users
- Roles
- Permissions
- Projects
- Project Memberships
- Tour Pages
- Assets
- Asset Variants
- Project Audio Tracks
- Project Transition Settings
- Global UI Controls
- Element Defaults
- Publishing
- File Management
- Search
- Access Logs
- Publish Events
- PWA Caches
- Presigned URL Requests
Common Patterns
Authentication Header
Authorization: Bearer <jwt_token>
Query Parameters (List Endpoints)
| Parameter | Type | Description |
|---|---|---|
page |
number | Page number (0-indexed) |
limit |
number | Items per page (default: 10) |
field |
string | Field to sort by |
sort |
string | Sort direction: asc or desc |
filetype |
string | Set to csv for CSV export |
<field> |
string | Text search filter (ILIKE) |
<field>Range |
string | Range filter: [min,max] |
Standard List Response
{
"rows": [...],
"count": 100
}
Rate Limits
| Endpoint Type | Limit | Window |
|---|---|---|
| Auth | 10 requests | 15 minutes |
| Signup | 5 requests | 1 hour |
| Password Reset | 5 requests | 1 hour |
| API (general) | 100 requests | 1 minute |
| Upload | 10 requests | 1 minute |
| Download | 200 requests | 1 minute |
| Search | 30 requests | 1 minute |
| AI | 20 requests | 1 minute |
Rate Limit Headers
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 99
X-RateLimit-Reset: 2026-03-30T12:00:00.000Z
Retry-After: 60 # (only on 429)
1. Authentication
POST /api/auth/signin/local
Sign in with email and password.
Authentication: None Rate Limit: 10/15min
Request:
{
"email": "user@example.com",
"password": "password123"
}
Response (200):
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
Errors:
400: Invalid credentials
Self-Registration
Self-registration is disabled. POST /api/auth/signup is not registered.
New users are created through the authenticated Users API/UI and receive an
invitation/setup link.
GET /api/auth/me
Get current authenticated user.
Authentication: Required
Response (200):
{
"id": "uuid",
"email": "user@example.com",
"firstName": "John",
"lastName": "Doe",
"phoneNumber": "+1234567890",
"emailVerified": true,
"disabled": false,
"app_role": {
"id": "uuid",
"name": "User",
"globalAccess": false
},
"custom_permissions": []
}
PUT /api/auth/password-reset
Reset password using token.
Authentication: None
Request:
{
"token": "reset-token-from-email",
"password": "newPassword123"
}
Response (200): true
POST /api/auth/send-password-reset-email
Send password reset email.
Authentication: None Rate Limit: 5/hour
Request:
{
"email": "user@example.com"
}
Response (200): true
PUT /api/auth/password-update
Update password for authenticated user.
Authentication: Required
Request:
{
"currentPassword": "oldPassword",
"newPassword": "newPassword123"
}
Response (200): true
PUT /api/auth/profile
Update user profile.
Authentication: Required
Request:
{
"profile": {
"firstName": "John",
"lastName": "Doe",
"phoneNumber": "+1234567890"
}
}
Response (200): true
PUT /api/auth/verify-email
Verify email address.
Authentication: None
Request:
{
"token": "verification-token-from-email"
}
Response (200): JWT token
GET /api/auth/email-configured
Check if email service is configured.
Authentication: None
Response (200): true or false
GET /api/auth/signin/google
Initiate Google OAuth flow.
Authentication: None
Query Parameters:
app: Optional state parameter
Response: Redirect to Google
GET /api/auth/signin/google/callback
Google OAuth callback.
Response: Redirect to frontend with token
GET /api/auth/signin/microsoft
Initiate Microsoft OAuth flow.
Authentication: None
Response: Redirect to Microsoft
GET /api/auth/signin/microsoft/callback
Microsoft OAuth callback.
Response: Redirect to frontend with token
2. Health & System
GET /api/health
Health check endpoint.
Authentication: None
Response (200):
{
"status": "ok",
"timestamp": "2026-03-30T12:00:00.000Z",
"uptime": 12345.678,
"environment": "production",
"database": "connected"
}
Response (503): (when database is down)
{
"status": "degraded",
"timestamp": "2026-03-30T12:00:00.000Z",
"uptime": 12345.678,
"environment": "production",
"database": "disconnected",
"databaseError": "Connection refused"
}
GET /api/runtime-context
Get current runtime context (environment detection).
Authentication: None
Response (200):
{
"mode": "workspace",
"projectSlug": "my-project",
"headerEnvironment": "production"
}
3. Users
Standard CRUD Endpoints
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/users |
Create user |
| GET | /api/users |
List users |
| GET | /api/users/count |
Count users |
| GET | /api/users/autocomplete |
Autocomplete search |
| GET | /api/users/:id |
Get user by ID |
| PUT | /api/users/:id |
Update user |
| DELETE | /api/users/:id |
Delete user |
| POST | /api/users/deleteByIds |
Bulk delete |
| POST | /api/users/bulk-import |
Bulk import |
Authentication: Required
Permissions: CREATE_USERS, READ_USERS, UPDATE_USERS, DELETE_USERS
POST /api/users
Request:
{
"data": {
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"phoneNumber": "+1234567890",
"app_role": "uuid-of-role",
"custom_permissions": ["uuid-1", "uuid-2"],
"password": "password123",
"disabled": false
}
}
GET /api/users
Query Parameters:
firstName: Filter by first namelastName: Filter by last nameemail: Filter by emailapp_role: Filter by role ID- Standard pagination params
Response:
{
"rows": [
{
"id": "uuid",
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"phoneNumber": "+1234567890",
"disabled": false,
"emailVerified": true,
"app_role": { "id": "uuid", "name": "User" }
}
],
"count": 1
}
4. Roles
Standard CRUD Endpoints
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/roles |
Create role |
| GET | /api/roles |
List roles |
| GET | /api/roles/count |
Count roles |
| GET | /api/roles/autocomplete |
Autocomplete search |
| GET | /api/roles/:id |
Get role by ID |
| PUT | /api/roles/:id |
Update role |
| DELETE | /api/roles/:id |
Delete role |
| POST | /api/roles/deleteByIds |
Bulk delete |
Permissions: CREATE_ROLES, READ_ROLES, UPDATE_ROLES, DELETE_ROLES
POST /api/roles
Request:
{
"data": {
"name": "Editor",
"globalAccess": false,
"permissions": ["uuid-1", "uuid-2"]
}
}
5. Permissions
Standard CRUD Endpoints
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/permissions |
Create permission |
| GET | /api/permissions |
List permissions |
| GET | /api/permissions/count |
Count permissions |
| GET | /api/permissions/autocomplete |
Autocomplete search |
| GET | /api/permissions/:id |
Get permission by ID |
| PUT | /api/permissions/:id |
Update permission |
| DELETE | /api/permissions/:id |
Delete permission |
| POST | /api/permissions/deleteByIds |
Bulk delete |
Permissions: CREATE_PERMISSIONS, READ_PERMISSIONS, UPDATE_PERMISSIONS, DELETE_PERMISSIONS
POST /api/permissions
Request:
{
"data": {
"name": "READ_CUSTOM_FEATURE"
}
}
6. Projects
Standard CRUD Endpoints
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/projects |
Create project |
| GET | /api/projects |
List projects |
| GET | /api/projects/count |
Count projects |
| GET | /api/projects/autocomplete |
Autocomplete search |
| GET | /api/projects/:id |
Get project by ID |
| PUT | /api/projects/:id |
Update project |
| DELETE | /api/projects/:id |
Delete project |
| POST | /api/projects/deleteByIds |
Bulk delete |
Permissions: CREATE_PROJECTS, READ_PROJECTS, UPDATE_PROJECTS, DELETE_PROJECTS
Runtime Public Access: GET endpoints accessible without auth in production mode.
POST /api/projects
Request:
{
"data": {
"name": "My Tour",
"slug": "my-tour",
"description": "A virtual tour of the museum",
"logo_url": "assets/logo.png",
"favicon_url": "assets/favicon.ico",
"og_image_url": "assets/og-image.jpg"
}
}
Fields:
| Field | Type | Description |
|---|---|---|
name |
string | Project display name |
slug |
string | URL-safe identifier (auto-generated if not provided) |
description |
string | Project description |
logo_url |
string | Path to project logo |
favicon_url |
string | Path to favicon |
og_image_url |
string | Path to Open Graph image |
POST /api/projects/:id/clone
Clone an existing project with all pages and settings.
Request: None (uses URL parameter)
Response:
{
"id": "new-project-uuid",
"name": "My Tour (Copy)",
"slug": "my-tour-copy"
}
GET /api/projects/:id/offline-manifest
Get PWA offline manifest for a project.
Query Parameters:
variant:mobileordesktop(default:desktop)
Response:
{
"projectId": "uuid",
"version": "2026-03-30T12:00:00.000Z",
"assets": [
{
"url": "assets/image.jpg",
"type": "image",
"size": 1024000
}
],
"pages": [
{
"slug": "home",
"name": "Home Page"
}
]
}
7. Project Memberships
Standard CRUD Endpoints
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/project_memberships |
Create membership |
| GET | /api/project_memberships |
List memberships |
| GET | /api/project_memberships/count |
Count memberships |
| GET | /api/project_memberships/:id |
Get membership by ID |
| PUT | /api/project_memberships/:id |
Update membership |
| DELETE | /api/project_memberships/:id |
Delete membership |
Permissions: CREATE_PROJECT_MEMBERSHIPS, READ_PROJECT_MEMBERSHIPS, etc.
POST /api/project_memberships
Request:
{
"data": {
"user": "user-uuid",
"project": "project-uuid",
"membershipRole": "editor"
}
}
Membership Roles:
owner- Full access including deleteeditor- Can edit contentviewer- Read-only access
8. Tour Pages
Standard CRUD Endpoints
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/tour_pages |
Create page |
| GET | /api/tour_pages |
List pages |
| GET | /api/tour_pages/count |
Count pages |
| GET | /api/tour_pages/:id |
Get page by ID |
| PUT | /api/tour_pages/:id |
Update page |
| DELETE | /api/tour_pages/:id |
Delete page |
| POST | /api/tour_pages/deleteByIds |
Bulk delete |
| POST | /api/tour_pages/reorder |
Reorder pages by updating sort_order only |
| POST | /api/tour_pages/:id/duplicate |
Duplicate a dev page as a new independent dev page |
Permissions: CREATE_TOUR_PAGES, READ_TOUR_PAGES, etc.
Runtime Public Access: GET endpoints accessible without auth in production mode.
POST /api/tour_pages/reorder
Reorders pages for a project in the constructor environment.
Auth: Required
Request:
{
"data": {
"projectId": "project-uuid",
"environment": "dev",
"orderedPageIds": ["page-uuid-1", "page-uuid-2", "page-uuid-3"]
}
}
Response:
[
{ "id": "page-uuid-1", "sort_order": 1 },
{ "id": "page-uuid-2", "sort_order": 2 },
{ "id": "page-uuid-3", "sort_order": 3 }
]
Rules:
- Only
environment: "dev"is accepted. Stage and production are updated by publishing, not by direct reorder writes. orderedPageIdsmust include every page in the project/dev environment exactly once.- The endpoint updates only
tour_pages.sort_order; it does not alterui_schema_json, slugs, backgrounds, media, navigation, or transitions. - The visible stage order changes after Save to Stage. The visible production order changes after Publish.
Validation failures:
- Missing
projectId - Empty or invalid
orderedPageIds - Duplicate page IDs
- Missing pages from the ordered list
- Unknown page IDs or page IDs outside the project/environment
- Any environment other than
dev
POST /api/tour_pages/:id/duplicate
Duplicates a constructor/dev page into a new independent dev page.
Auth: Required
Request:
{
"data": {
"projectId": "project-uuid",
"environment": "dev",
"name": "Lobby Copy",
"slug": "lobby-copy"
}
}
Rules:
- Only source pages in
environment: "dev"can be duplicated. - The requested target environment must also be
dev. - The source page must belong to the requested project.
- The new page is appended to constructor order with
sort_order = max(project dev sort_order) + 1. - The duplicate receives a new page ID, unique slug, and empty
source_key. - Page settings, background media fields, design dimensions,
requires_auth, andui_schema_jsonare copied. - Inline
ui_schema_json.elements[]IDs and nested gallery/carousel/info-panel item IDs are regenerated for independence. - Asset URLs, transition URLs, and navigation
targetPageSlugreferences are preserved. - Reverse-video processing uses the existing
TourPagesServicepath.
Validation failures:
- Invalid source page ID
- Source page not found
- Source page outside requested project
- Source or target environment other than
dev
POST /api/tour_pages
Request:
{
"data": {
"projectId": "project-uuid",
"environment": "dev",
"name": "Home Page",
"slug": "home",
"sort_order": 1,
"background_image_url": "assets/bg.jpg",
"background_video_url": null,
"background_audio_url": "assets/ambient.mp3",
"background_loop": true,
"requires_auth": false,
"ui_schema_json": {
"elements": [],
"canvasSettings": {}
}
}
}
ui_schema_json Structure:
{
"elements": [
{
"id": "element-uuid",
"type": "button",
"props": {
"x": 100,
"y": 200,
"width": 150,
"height": 50,
"label": "Click Me",
"targetPageSlug": "next-page",
"transitionVideo": "assets/transition.mp4"
}
}
],
"canvasSettings": {
"width": 1920,
"height": 1080
}
}
9. Assets
Standard CRUD Endpoints
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/assets |
Create asset record |
| GET | /api/assets |
List assets |
| GET | /api/assets/count |
Count assets |
| GET | /api/assets/autocomplete |
Autocomplete search |
| GET | /api/assets/:id |
Get asset by ID |
| PUT | /api/assets/:id |
Update asset |
| DELETE | /api/assets/:id |
Delete asset |
| POST | /api/assets/deleteByIds |
Bulk delete |
Permissions: CREATE_ASSETS, READ_ASSETS, etc.
POST /api/assets
Create an asset record with MIME type validation.
Request:
{
"data": {
"name": "Hero Image",
"asset_type": "image",
"type": "background",
"cdn_url": "https://cdn.example.com/assets/hero.jpg",
"storage_key": "assets/hero.jpg",
"mime_type": "image/jpeg",
"size_mb": 2.5,
"width_px": 1920,
"height_px": 1080,
"duration_sec": null,
"checksum": "abc123...",
"is_public": true,
"project": "project-uuid"
}
}
Asset Types: image, video, audio, document, other
Type (Usage): background, transition, element, general
MIME Type Validation:
The mime_type must match the asset_type:
| asset_type | Valid mime_type prefixes |
|---|---|
image |
image/ (jpeg, png, gif, webp, svg, etc.) |
video |
video/ (mp4, webm, mov, etc.) |
audio |
audio/ (mp3, wav, ogg, etc.) |
Other asset types (document, other, file) skip MIME validation.
Error Response (400):
{
"message": "Invalid file type for image. Expected image (jpeg, png, gif, webp, svg, etc.), got \"video/mp4\""
}
10. Asset Variants
Standard CRUD Endpoints
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/asset_variants |
Create variant |
| GET | /api/asset_variants |
List variants |
| GET | /api/asset_variants/:id |
Get variant by ID |
| PUT | /api/asset_variants/:id |
Update variant |
| DELETE | /api/asset_variants/:id |
Delete variant |
Permissions: CREATE_ASSET_VARIANTS, READ_ASSET_VARIANTS, etc.
POST /api/asset_variants
Request:
{
"data": {
"asset": "asset-uuid",
"variant_type": "mobile",
"cdn_url": "https://cdn.example.com/assets/hero-mobile.jpg",
"width_px": 768,
"height_px": 432,
"size_mb": 0.8
}
}
Variant Types: desktop, mobile, thumbnail
11. Project Audio Tracks
Standard CRUD Endpoints
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/project_audio_tracks |
Create audio track |
| GET | /api/project_audio_tracks |
List audio tracks |
| GET | /api/project_audio_tracks/:id |
Get audio track |
| PUT | /api/project_audio_tracks/:id |
Update audio track |
| DELETE | /api/project_audio_tracks/:id |
Delete audio track |
Runtime Public Access: GET endpoints accessible without auth in production mode.
POST /api/project_audio_tracks
Request:
{
"data": {
"projectId": "project-uuid",
"environment": "dev",
"name": "Background Music",
"slug": "background-music",
"url": "assets/music.mp3",
"loop": true,
"volume": 0.5,
"sort_order": 1,
"is_enabled": true
}
}
12. Project Transition Settings
Environment-aware CSS transition settings for page navigation.
Authentication Model
Uses URL-path-based public access - no headers required:
| Endpoint | Method | Environment | Auth Required |
|---|---|---|---|
/project/:id/env/production |
GET | production | No (public) |
/project/:id/env/dev |
GET | dev | JWT + READ_PAGE_ELEMENTS |
/project/:id/env/stage |
GET | stage | JWT + READ_PAGE_ELEMENTS |
/project/:id/env/* |
PUT/DELETE | any | JWT + UPDATE_PAGE_ELEMENTS |
| Standard CRUD | all | n/a | JWT + PAGE_ELEMENTS CRUD permission |
This allows public presentations (/p/[slug]) to fetch production transition settings without authentication in incognito mode.
Standard CRUD Endpoints (All Require Auth)
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/project-transition-settings |
Create settings |
| GET | /api/project-transition-settings |
List all settings |
| GET | /api/project-transition-settings/:id |
Get by ID |
| PUT | /api/project-transition-settings/:id |
Update by ID |
| DELETE | /api/project-transition-settings/:id |
Delete by ID |
Environment-Specific Endpoints
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /project/:projectId/env/production |
None | Get production settings (public) |
| GET | /project/:projectId/env/dev |
JWT | Get dev settings |
| GET | /project/:projectId/env/stage |
JWT | Get stage settings |
| PUT | /project/:projectId/env/:environment |
JWT + UPDATE_PAGE_ELEMENTS | Create or update (upsert) |
| DELETE | /project/:projectId/env/:environment |
JWT + UPDATE_PAGE_ELEMENTS | Reset settings to global defaults |
GET /api/project-transition-settings/project/:projectId/env/:environment
Parameters:
projectId(path): Project UUIDenvironment(path):dev,stage, orproduction
Authentication:
production: None required (public access)dev/stage:Authorization: Bearer {token}
Response (200):
{
"id": "uuid",
"projectId": "project-uuid",
"environment": "production",
"transition_type": "fade",
"duration_ms": 700,
"easing": "ease-in-out",
"overlay_color": "#000000",
"createdAt": "2026-05-01T12:00:00Z",
"updatedAt": "2026-05-01T12:00:00Z"
}
Response (200): null when no settings exist (use global/fallback defaults)
Response (401): Authentication required (for dev/stage without JWT)
PUT /api/project-transition-settings/project/:projectId/env/:environment
Creates or updates settings for a project/environment combination.
Authentication: Required (JWT + UPDATE_PAGE_ELEMENTS)
Request:
{
"transition_type": "fade",
"duration_ms": 1000,
"easing": "ease-out",
"overlay_color": "#1a1a1a"
}
Response (200): Created or updated settings object
13. Global UI Controls
Configurable fullscreen, global sound, and offline buttons. Settings cascade from global defaults to project/environment overrides and then page overrides.
Global Defaults
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/global-ui-control-defaults |
None | Get singleton global UI-control defaults |
| GET | /api/global-ui-control-defaults/:id |
None | Get defaults by ID |
| PUT | /api/global-ui-control-defaults/:id |
JWT + UPDATE_PAGE_ELEMENTS | Update singleton defaults |
Project/Environment Overrides
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/project-ui-control-settings/project/:projectId/env/production |
None or JWT for private production | Get production project overrides |
| GET | /api/project-ui-control-settings/project/:projectId/env/dev |
JWT | Get dev project overrides |
| GET | /api/project-ui-control-settings/project/:projectId/env/stage |
JWT | Get stage project overrides |
| PUT | /api/project-ui-control-settings/project/:projectId/env/:environment |
JWT + UPDATE_PAGE_ELEMENTS | Upsert project overrides |
| DELETE | /api/project-ui-control-settings/project/:projectId/env/:environment |
JWT + UPDATE_PAGE_ELEMENTS | Reset project overrides to global defaults |
production reads follow private-production presentation rules: public
presentations are public-readable, private presentations require JWT and an
allowlist/staff access check.
Settings JSON
{
"offline": {
"enabled": true,
"hidden": false,
"xPercent": 89.5,
"yPercent": 6,
"anchor": "center",
"buttonSizePercent": 2.6,
"iconSizePercent": 1.35,
"defaultIconUrl": "",
"activeIconUrl": "",
"defaultBackgroundColor": "#2563EB",
"activeBackgroundColor": "#059669",
"hoverBackgroundColor": "#1D4ED8",
"color": "#FFFFFF",
"defaultBorderColor": "#2563EB",
"activeBorderColor": "#059669",
"borderRadiusPercent": 0.42,
"opacity": 1,
"boxShadow": "",
"zIndex": 900,
"order": 1
}
}
14. Element Defaults
Global Transition Defaults
Platform-wide default transition settings (singleton).
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/global-transition-defaults |
None | Get defaults (public) |
| GET | /api/global-transition-defaults/:id |
None | Get by ID (public) |
| PUT | /api/global-transition-defaults/:id |
JWT + UPDATE_PAGE_ELEMENTS | Update defaults |
Authentication Model: GET is always public (for runtime presentations), PUT requires JWT.
GET /api/global-transition-defaults
GET /api/global-transition-defaults
# No authentication required
Response (200):
{
"id": "uuid",
"transition_type": "fade",
"duration_ms": 700,
"easing": "ease-in-out",
"overlay_color": "#000000"
}
PUT /api/global-transition-defaults/:id
PUT /api/global-transition-defaults/:id
Authorization: Bearer {token}
Content-Type: application/json
{
"data": {
"transition_type": "fade",
"duration_ms": 1000,
"easing": "ease-out"
}
}
Element Type Defaults (Global)
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/element-type-defaults |
List global defaults |
| GET | /api/element-type-defaults/:id |
Get default by ID |
| PUT | /api/element-type-defaults/:id |
Update default |
Alternative Path: /api/ui-elements (backwards compatibility)
Element Types (11 predefined):
button,hotspot,tooltip,gallerymedia_player,text_block,popuplogo,spot,hamburger_menu,image
Project Element Defaults
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/project-element-defaults |
List project defaults |
| GET | /api/project-element-defaults/:id |
Get default by ID |
| PUT | /api/project-element-defaults/:id |
Update default |
| POST | /api/project-element-defaults/:id/reset |
Reset to global |
| GET | /api/project-element-defaults/:id/diff |
Compare with global |
POST /api/project-element-defaults/:id/reset
Reset project element default to current global settings.
Response:
{
"id": "uuid",
"element_type": "button",
"projectId": "project-uuid",
"default_props_json": {...}
}
GET /api/project-element-defaults/:id/diff
Compare project default with global default.
Response:
{
"hasDifferences": true,
"projectDefault": {...},
"globalDefault": {...},
"differences": {
"backgroundColor": {
"project": "#FF0000",
"global": "#0000FF"
}
}
}
15. Publishing
POST /api/publish
Publish from stage to production.
Authentication: Required
Permissions: CREATE_PUBLISH_EVENTS
Request:
{
"projectId": "project-uuid",
"title": "Version 1.0 Release",
"description": "Initial public release with all pages"
}
Response:
{
"success": true,
"publishEventId": "event-uuid",
"summary": {
"pages_copied": 10,
"audios_copied": 3
}
}
POST /api/publish/save-to-stage
Copy dev content to stage environment.
Authentication: Required
Permissions: CREATE_PUBLISH_EVENTS
Request:
{
"projectId": "project-uuid"
}
Response:
{
"success": true,
"publishEventId": "event-uuid",
"summary": {
"pages_copied": 10,
"audios_copied": 3
}
}
Errors:
400: Publish already in progress404: Project not found
16. File Management
GET /api/file/download
Download a file from storage. Supports automatic client disconnect handling via AbortController.
Authentication: None (uses private URL) Rate Limit: 200/min
Query Parameters:
privateUrl: Storage key (e.g.,assets/image.jpg)
Response: File stream with appropriate Content-Type
Error Responses:
| HTTP Status | Condition | S3 Error Types |
|---|---|---|
| 400 | Missing privateUrl parameter | - |
| 401 | Expired credentials | ExpiredToken |
| 403 | Access denied | AccessDenied, InvalidAccessKeyId |
| 404 | File not found | NoSuchKey, NotFound, NoSuchBucket |
| 429 | Rate limited | ThrottlingException |
| 500 | Internal server error | InternalError |
| 503 | Service unavailable | NetworkingError, ServiceUnavailable |
| 504 | Gateway timeout | TimeoutError, RequestTimeout |
Error Response Format:
{
"message": "Could not download the file. NoSuchKey: The specified key does not exist."
}
Client Disconnect: When a client disconnects during download, the backend automatically aborts the S3 request to prevent wasted bandwidth.
POST /api/file/presign
Generate presigned URLs for direct S3 access. Includes path validation to prevent directory traversal attacks.
Authentication: None Rate Limit: 200/min
Request:
{
"urls": [
"assets/image1.jpg",
"assets/image2.jpg",
"assets/video.mp4"
]
}
Response:
{
"presignedUrls": {
"assets/image1.jpg": "https://bucket.s3.amazonaws.com/assets/image1.jpg?...",
"assets/image2.jpg": "https://bucket.s3.amazonaws.com/assets/image2.jpg?...",
"assets/video.mp4": "https://bucket.s3.amazonaws.com/assets/video.mp4?..."
}
}
Limits:
- Maximum 50 URLs per request
- Presigned URLs expire in 1 hour (configurable via
AWS_S3_PRESIGN_EXPIRY)
Path Validation: URLs are validated to prevent path traversal and ensure security:
- Must be non-empty strings
- Must not contain
..(parent directory traversal) - Must not start with
/(absolute paths) - Must not contain null bytes
Error Responses:
| HTTP Status | Condition |
|---|---|
| 400 | Missing urls array |
| 400 | urls array is empty |
| 400 | urls exceeds maximum of 50 |
| 400 | Invalid URL format (contains .., starts with /, etc.) |
| 500 | S3 presigning failed |
| 503 | S3 service unavailable |
Error Response Format:
{
"message": "Invalid URL format",
"code": "INVALID_PATH",
"details": { "invalidUrls": ["../etc/passwd", "/root/secret"] }
}
POST /api/file/upload/:table/:field
Upload a file (single request).
Authentication: Required
Rate Limit: 10/min
Content-Type: multipart/form-data
Form Fields:
file: Binary file datafilename: Target filename
Response:
{
"message": "Uploaded the file successfully: assets/hero.jpg",
"url": "https://bucket.s3.amazonaws.com/assets/hero.jpg"
}
Chunked Upload (Large Files)
For files larger than a few MB, use chunked upload:
1. Initialize Session
POST /api/file/upload-sessions/init
Request:
{
"folder": "assets",
"filename": "large-video.mp4",
"totalChunks": 10,
"size": 104857600,
"contentType": "video/mp4"
}
Response:
{
"sessionId": "session-uuid",
"uploadedChunks": [],
"totalChunks": 10
}
2. Upload Chunks
PUT /api/file/upload-sessions/:sessionId/chunks/:chunkIndex
Content-Type: application/octet-stream
Body: Raw binary chunk data
Response:
{
"sessionId": "session-uuid",
"chunkIndex": 0,
"uploadedChunks": 1,
"totalChunks": 10
}
3. Check Session Status
GET /api/file/upload-sessions/:sessionId
Response:
{
"sessionId": "session-uuid",
"totalChunks": 10,
"uploadedChunks": [0, 1, 2, 3, 4],
"status": "uploading"
}
4. Finalize Upload
POST /api/file/upload-sessions/:sessionId/finalize
Response:
{
"message": "Uploaded the file successfully: assets/large-video.mp4",
"privateUrl": "assets/large-video.mp4",
"url": "https://bucket.s3.amazonaws.com/assets/large-video.mp4"
}
17. Search
POST /api/search
Global full-text search across entities.
Authentication: Required
Rate Limit: 30/min
Permissions: READ_SEARCH (implicit via entity permissions)
Request:
{
"searchQuery": "museum"
}
Response:
[
{
"id": "uuid",
"name": "Museum Tour",
"tableName": "projects",
"matchAttribute": ["name", "description"]
},
{
"id": "uuid",
"slug": "museum-entrance",
"tableName": "tour_pages",
"matchAttribute": ["slug"]
}
]
Searchable Tables:
| Table | Text Fields | Numeric Fields |
|---|---|---|
users |
firstName, lastName, phoneNumber, email | - |
projects |
name, slug, description, logo_url, favicon_url, og_image_url | - |
assets |
name, cdn_url, storage_key, mime_type, checksum | size_mb, width_px, height_px, duration_sec |
asset_variants |
cdn_url | width_px, height_px, size_mb |
presigned_url_requests |
requested_key, mime_type, status | requested_size_mb |
tour_pages |
source_key, name, slug, background_image_url, background_video_url, background_audio_url, ui_schema_json | sort_order |
project_audio_tracks |
source_key, name, slug, url | volume, sort_order |
publish_events |
error_message | pages_copied, transitions_copied, audios_copied |
pwa_caches |
cache_version, manifest_json, asset_list_json | - |
access_logs |
path, ip_address, user_agent | - |
20. Access Logs
Standard CRUD Endpoints
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/access_logs |
List access logs |
| GET | /api/access_logs/count |
Count access logs |
| GET | /api/access_logs/:id |
Get access log by ID |
Permissions: READ_ACCESS_LOGS
GET /api/access_logs
Query Parameters:
path: Filter by pathip_address: Filter by IPuser_agent: Filter by user agent- Standard pagination params
Response:
{
"rows": [
{
"id": "uuid",
"path": "/p/my-tour/",
"ip_address": "192.168.1.1",
"user_agent": "Mozilla/5.0...",
"project": { "id": "uuid", "name": "My Tour" },
"createdAt": "2026-03-30T12:00:00.000Z"
}
],
"count": 100
}
21. Publish Events
Standard CRUD Endpoints
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/publish_events |
List publish events |
| GET | /api/publish_events/count |
Count publish events |
| GET | /api/publish_events/:id |
Get publish event by ID |
Permissions: READ_PUBLISH_EVENTS
GET /api/publish_events
Query Parameters:
project: Filter by project IDstatus: Filter by status (queued, running, success, failed)from_environment: Filter by source (dev, stage)to_environment: Filter by target (stage, production)- Standard pagination params
Response:
{
"rows": [
{
"id": "uuid",
"title": "Version 1.0",
"description": "Initial release",
"from_environment": "stage",
"to_environment": "production",
"status": "success",
"pages_copied": 10,
"audios_copied": 3,
"started_at": "2026-03-30T12:00:00.000Z",
"finished_at": "2026-03-30T12:00:05.000Z",
"project": { "id": "uuid", "name": "My Tour" },
"user": { "id": "uuid", "firstName": "John" }
}
],
"count": 5
}
22. PWA Caches
Standard CRUD Endpoints
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/pwa_caches |
Create PWA cache record |
| GET | /api/pwa_caches |
List PWA caches |
| GET | /api/pwa_caches/:id |
Get PWA cache by ID |
| PUT | /api/pwa_caches/:id |
Update PWA cache |
| DELETE | /api/pwa_caches/:id |
Delete PWA cache |
Permissions: CREATE_PWA_CACHES, READ_PWA_CACHES, etc.
POST /api/pwa_caches
Request:
{
"data": {
"project": "project-uuid",
"cache_version": "v1.0.0",
"manifest_json": {
"name": "My Tour",
"short_name": "Tour"
},
"asset_list_json": [
"assets/image1.jpg",
"assets/image2.jpg"
]
}
}
23. Presigned URL Requests
Standard CRUD Endpoints
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/presigned_url_requests |
Create presign request |
| GET | /api/presigned_url_requests |
List presign requests |
| GET | /api/presigned_url_requests/:id |
Get presign request |
| PUT | /api/presigned_url_requests/:id |
Update presign request |
| DELETE | /api/presigned_url_requests/:id |
Delete presign request |
Permissions: CREATE_PRESIGNED_URL_REQUESTS, READ_PRESIGNED_URL_REQUESTS, etc.
POST /api/presigned_url_requests
Request:
{
"data": {
"requested_key": "assets/new-upload.jpg",
"mime_type": "image/jpeg",
"requested_size_mb": 2.5,
"status": "pending",
"project": "project-uuid",
"user": "user-uuid"
}
}
Error Codes Reference
| Code | Message | Description |
|---|---|---|
| 400 | Bad Request | Invalid input data or validation error |
| 401 | Unauthorized | Missing or invalid JWT token |
| 403 | Forbidden | Insufficient permissions |
| 404 | Not Found | Resource does not exist |
| 409 | Conflict | Resource conflict (e.g., duplicate) |
| 422 | Unprocessable Entity | Validation failed |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Unexpected server error |
Headers Reference
Request Headers
| Header | Description | Required |
|---|---|---|
Authorization |
Bearer <token> |
For protected endpoints |
Content-Type |
application/json |
For JSON bodies |
X-Runtime-Environment |
dev, stage, or production |
For runtime context |
X-Request-Id |
UUID for request tracing | Optional |
Response Headers
| Header | Description |
|---|---|
X-Request-Id |
Request ID for tracing |
X-RateLimit-Limit |
Rate limit maximum |
X-RateLimit-Remaining |
Remaining requests |
X-RateLimit-Reset |
Reset timestamp |
Retry-After |
Seconds until rate limit resets (on 429) |
Cross-Origin-Resource-Policy |
cross-origin (on file downloads) |