39948-vm/backend/docs/api-endpoints.md
2026-07-03 16:11:24 +02:00

38 KiB

Backend API Endpoints Documentation

Complete reference for all API endpoints in the Tour Builder Platform backend.

Overview

  • Base URL: http://localhost:3000/api for 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

  1. Authentication
  2. Health & System
  3. Users
  4. Roles
  5. Permissions
  6. Projects
  7. Project Memberships
  8. Tour Pages
  9. Assets
  10. Asset Variants
  11. Project Audio Tracks
  12. Project Transition Settings
  13. Global UI Controls
  14. Element Defaults
  15. Publishing
  16. File Management
  17. Search
  18. Access Logs
  19. Publish Events
  20. PWA Caches
  21. 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 name
  • lastName: Filter by last name
  • email: Filter by email
  • app_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: mobile or desktop (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 delete
  • editor - Can edit content
  • viewer - 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.
  • orderedPageIds must include every page in the project/dev environment exactly once.
  • The endpoint updates only tour_pages.sort_order; it does not alter ui_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, and ui_schema_json are 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 targetPageSlug references are preserved.
  • Reverse-video processing uses the existing TourPagesService path.

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 UUID
  • environment (path): dev, stage, or production

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, gallery
  • media_player, text_block, popup
  • logo, 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 progress
  • 404: 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 data
  • filename: 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"
}

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 path
  • ip_address: Filter by IP
  • user_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 ID
  • status: 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)