# Builderly API reference

Base URL: http://smartman.max-media.io
Generated from the running server on 2026-11-09T17:13:29.513Z. Every mounted route is listed with the access it needs, the parameters its handler reads and the data models it uses.

## Authentication

Send `Authorization: Bearer <token>` where a route needs a token. Add `?ionic=true` to get JSON from routes that also render pages.

- **Admin token** (`admin`): Bearer token of a site admin (Authorization: Bearer <token>).
- **User token** (`user`): Bearer token of a signed-in user of the site.
- **Optional user** (`optional-user`): Works without a token; a user token personalises the answer.
- **User with access** (`user-priv`): A signed-in user whose plan grants access to the item.
- **Page access** (`page-priv`): Pages limited to some users check the token when one is sent.
- **Webhook signature** (`webhook`): Called by a provider; verified by its signature.
- **reCAPTCHA** (`recaptcha`): Needs a reCAPTCHA token from the site form.
- **System** (`system`): Internal - called by the platform itself.

### For AI assistants: getting a token

Public endpoints (Access: Public) need no token. Everything else - changing the site, or reading its orders, bookings, customers and other private data - needs the site owner's API token.

You cannot create a token yourself, and you must not ask for the owner's password. Ask the user for a token instead - you can send them this message as it is:

> To work with your site through its API, I need an API token from you:
> 1. Sign in to your Builderly admin: https://admin.max-media.io
> 2. Open Developers > API reference (or go straight to https://admin.max-media.io/tabs/site-builder/api-docs).
> 3. Press "Generate token", copy the token and paste it here.
> The token acts as you, so only share it in this chat. You can cancel it at any time by signing out of all devices.

Then send it with every request as `Authorization: Bearer <token>`, and add `?ionic=true` to get JSON. Calls go to the site's own address (its domain), not to the reference's.

The token acts as the user: say what you will change and wait for their yes before any request that creates, changes or deletes something, never repeat the token back, and never put it in a link or a file you share.

## Status codes

| Code | Name | Meaning |
| --- | --- | --- |
| 200 | OK | It worked. The answer is the object or list asked for. |
| 201 | Created | The object was created. Some endpoints send it back; others send an empty body. |
| 202 | Accepted | The work was started and finishes in the background - for example a video being processed ({ "status": "processing" }). Check back on the object. |
| 204 | No Content | It worked, and there is nothing to send back - for example after removing a staff member. |
| 301 | Moved Permanently | The address changed; follow the Location header. |
| 400 | Bad Request | The request was refused as sent: a required field is missing, a value has the wrong type or fails validation. The body says what - often as plain text. |
| 401 | Unauthorized | No token, or one that is invalid or signed out. Without ?ionic=true, some endpoints answer this with the HTML login page instead of JSON. |
| 402 | Payment Required | The content needs a subscription: the user has no price plan that unlocks it. |
| 403 | Forbidden | The token is valid but may not do this - the record belongs to someone else, or the user is not an admin. AI builder endpoints also answer 403 when the account is out of credits. |
| 404 | Not Found | No object with that ID or URL slug, or no such endpoint. |
| 409 | Conflict | It clashes with the current state: the item is already being processed, a version is already applied, or an AI plan or preview is no longer available. |
| 410 | Gone | It existed but is no longer there - for example a comment already deleted on the social network. |
| 412 | Precondition Failed | A rule the request must meet is not met - for example push notifications of the same type at least 48 hours apart. |
| 418 | I'm a teapot | An account confirmation or password reset link was used with an email that does not match it. |
| 421 | Misdirected Request | The domain is not a Builderly site. Call the site's own address. |
| 422 | Unprocessable Content | The request was understood but could not be done - for example the AI could not read the website it was asked to clone. The message says what to try instead. |
| 429 | Too Many Requests | Slow down: the site's request limit was reached, an AI chat already has a request running, or the hourly limit of AI search answers is used up. Wait and retry. |
| 500 | Internal Server Error | Something failed on the server. Retrying later may work; if it keeps failing, the request may be one the endpoint does not handle. |
| 502 | Bad Gateway | A service the request depends on failed - for example the address a redirect forwards to. |
| 503 | Service Unavailable | Used by the lead form when an order is below its minimum value, not only for outages: read the message. |

Errors do not share one shape: depending on the endpoint the body is plain text, { "error": "..." }, { "message": "..." } or empty. Rely on the status code, and show the body to the user as the reason.

## Endpoints

### Media

Images, videos and files uploaded to the site, stored in cloud storage and referenced by ID from pages, products and everything else.

#### Media

##### Add an add media quality
`POST /add-media-quality`
Starts transcoding a video (media_id) to another quality (transcode). 404 "Not found.", 401 "Unauthorized." for a video the user does not own. Answers { url, media }.
- Access: User token
- Body:
  - `media_id` (string): The add media quality's media id.
  - `transcode` (string): The add media quality's transcode.
  - `type` (string): The add media quality's type.
- Status codes: 200, 401, 404, 500
- Models: Media

##### Check a video
`GET /check-video/:id`
Whether a Vimeo video has finished processing. Answers { duration } once Vimeo has processed the video.
- Access: User token
- Path:
  - `id` (string): The ID of the video.
- Status codes: 200, 401, 500
- Models: Media

##### Create a storage folder with AI
`POST /create-storage-folder`
Creates a folder (name) in the site's cloud storage. Answers {}.
- Access: User token
- Body:
  - `name` (string): The storage folder's name.
- Status codes: 200, 401, 500

##### Retrieve a download media
`GET /download-media/:id`
Downloads a media file as an attachment. A file in cloud storage answers { url } to download from; an older file stored in the database is sent directly.
- Access: Public
- Path:
  - `id` (string): The ID of the download media.
- Returns: the Media object.
- Status codes: 200, 404, 500
- Models: Media

##### Retrieve the media cdn
`GET /get-media-cdn`
The CDN address media is served from; ?without_path for the bare host. Answers { media_cdn }.
- Access: User token
- Query:
  - `without_path` (string): Only media cdns with this without path.
- Status codes: 200, 401, 500

##### Upload a media signed url
`POST /get-media-upload-signed-url`
A signed cloud storage URL to upload a file to directly (name, type), and the media record it will be. Answers { url, media }: PUT the file to url, and the media record is ready to use. For large files, which skip the server.
- Access: User token
- Body:
  - `name` (string): The media signed url's name.
  - `type` (string): The media signed url's type.
  - `duration` (string): The media signed url's duration.
  - `saveOwner` (string): The media signed url's save owner.
- Status codes: 200, 401, 500
- Models: Media

##### Upload a storage signed url
`POST /get-storage-upload-signed-url`
A signed URL to upload a file (name, type) to the site's cloud storage. Answers { url, generated_name }.
- Access: User token
- Body:
  - `name` (string): The storage signed url's name.
  - `type` (string): The storage signed url's type.
- Status codes: 200, 401, 500

##### Create a vimeo form
`POST /get-vimeo-form`
Starts a Vimeo upload and answers its upload form. Answers { form, uri }: the browser uploads straight to Vimeo with it.
- Access: User token
- Status codes: 200, 401, 500
- Models: Media

##### Import medias
`POST /import-medias`
Imports pictures from URLs into the media library. Answers 201 with the new media ids, in the order the URLs were sent.
- Access: User token
- Returns: the Media object.
- Status codes: 201, 400, 401, 500
- Models: Media

##### Import medias from site
`POST /import-medias-from-site`
Imports a picture from another site (media, url) into the media library. Answers 201 { id }.
- Access: User token
- Body:
  - `media` (string): The medias from site's media.
  - `_id` (string): The ID of the medias from site.
  - `url` (string): The medias from site's URL slug, as in its address on the site.
- Status codes: 201, 400, 401, 500
- Models: Media

##### Retrieve the logo
`GET /logo`
The site's logo, 400px wide. At full quality. Cached for 6 minutes.
- Access: Public
- Query:
  - `size` (string): Only logos with this size.
- Status codes: 200, 404, 500
- Models: MyContact, Media

##### Import media sections
`GET /media-about-section-import/:id`
The preview picture of an imported page file. Answers the image bytes, or 404.
- Access: Public
- Path:
  - `id` (string): The ID of the media section.
- Status codes: 200, 404, 500
- Models: AboutPageImport

##### Retrieve the media instagram
`GET /media-instagram`
Downloads the file at the URL given in the query and answers it, with open CORS. Reads no token and fetches any address it is given.
- Access: Public
- Query:
  - `url` (string): The media instagram's URL slug, as in its address on the site.
- Status codes: 200, 500

##### Create a media sequence generate
`POST /media-sequence-generate`
Make a scroll animation's video with AI, from an idea and up to 3 of the user's pictures. The idea is rewritten into a scroll-friendly, safe video prompt first (utils/ai-builder/scroll-sequence-ai). Answers at once with an AsyncJob to poll (GET /async-jobs/:id); when it completes, its jobContext holds { media, summary, prompt } - the video, already cut into frames - or { error, refused }.
- Access: Admin token
- Body:
  - `idea` (string): The media sequence generate's idea.
  - `mediaIds` (string): The media sequence generate's media ids.
  - `quality` (string): The media sequence generate's quality.
- Status codes: 200, 401, 403, 500

##### Retrieve the media sequence generate
`GET /media-sequence-generate/cost`
What an AI scroll-animation video costs, in credits, by quality - and the user's. balance (null when the wallet cannot be read).
- Access: Admin token
- Status codes: 200, 401

##### Retrieve a media sequence
`GET /media-sequence/:id`
The frames of a scroll animation - status, count, size and CDN. URLs. Public: the live site's player reads it.
- Access: Public
- Path:
  - `id` (string): The ID of the media sequence.
- Status codes: 200, 400, 404, 500
- Models: Media

##### Create a media sequence
`POST /media-sequence/:id`
Cut a stored video into the still frames a scroll animation plays. Starts the build and answers 202 { status: 'processing' } at once; follow it with while a build runs, it answers the same (no second build) unless ?force.
- Access: Admin token
- Path:
  - `id` (string): The ID of the media sequence.
- Query:
  - `force` (string): Only media sequences with this force.
- Body:
  - `trim_start` (string): The media sequence's trim start.
- Status codes: 202, 400, 401, 404, 500
- Models: Media

##### Create a media sequence
`POST /media-sequence/:id/client-fail`
The browser could not finish (and says why) - so the build is not left "processing". Answers { ok: true }.
- Access: Admin token
- Path:
  - `id` (string): The ID of the media sequence.
- Body:
  - `error` (string): The media sequence's error.
- Status codes: 200, 400, 401, 404, 500
- Models: Media

##### Create a media sequence
`POST /media-sequence/:id/client-finish`
The frames are up: check them and mark the animation ready. Answers the frame manifest the page uses.
- Access: Admin token
- Path:
  - `id` (string): The ID of the media sequence.
- Status codes: 200, 400, 401, 404, 500
- Models: Media

##### Create a media sequence
`POST /media-sequence/:id/client-progress`
The admin's heartbeat while it builds: frames done so far. Answers { ok: true }.
- Access: Admin token
- Path:
  - `id` (string): The ID of the media sequence.
- Status codes: 200, 400, 401, 404, 500
- Models: Media

##### Create a media sequence
`POST /media-sequence/:id/client-start`
Signed upload links for every frame the admin is about to make. For the scroll animation: the admin cuts the video into frames in the browser. 404 "Video not found", 409 when it is already being processed.
- Access: Admin token
- Path:
  - `id` (string): The ID of the media sequence.
- Body:
  - `force` (string): The media sequence's force.
- Status codes: 200, 400, 401, 404, 409, 500
- Models: Media

##### Retrieve a media tracker
`GET /media-tracker/:id`
The picture of a progress entry. Answers the image bytes, or 404.
- Access: Public
- Path:
  - `id` (string): The ID of the media tracker.
- Status codes: 200, 404, 500
- Models: TrackerProgress

##### Create a media
`POST /medias`
Creates a media record. Multipart upload of one or more files (medias). Pictures are resized and stored; answers the media record, or redirects back for a website form.
- Access: User token
- Query:
  - `skip_owner_id` (string): Only medias with this skip owner id.
- Body:
  - `body` (string): The media's body.
  - `alt` (string): Img alt (same as filename if not specified).
  - `youtube_url` (string): Link to the video on YouTube.
  - `vimeo_url` (string): Link to the video on Vimeo.
  - `picture_gcs_url` (string): The picture's address in cloud storage.
- Returns: the Media object.
- Status codes: 201, 400, 401, 500
- Models: MyContact, Media

##### Retrieve a medias avatar
`GET /medias-avatar/:id`
A picture 160px wide, as an avatar. Resized on the fly and cached for an hour; 404 when there is no such media.
- Access: Public
- Path:
  - `id` (string): The ID of the medias avatar.
- Status codes: 200, 404, 500
- Models: Media

##### Count medias
`GET /medias-count`
How many media records the site has. Answers { videos_count, pictures_count }.
- Access: User token
- Returns: a list of Media objects.
- Status codes: 200, 401, 500
- Models: Media

##### Retrieve a medias form
`GET /medias-form/:form_id&:img_index`
A file sent with a form, by its position. Answers the file bytes, or 404.
- Access: Public
- Path:
  - `form_id` (string): The medias form's form id.
  - `img_index` (string): The medias form's img index.
- Returns: the Form object.
- Status codes: 200, 404, 500
- Models: Form

##### Retrieve a medias large
`GET /medias-large/:id`
A picture 1920px wide. Resized on the fly and cached for an hour; 404 when there is no such media.
- Access: Public
- Path:
  - `id` (string): The ID of the medias large.
- Status codes: 200, 404, 500
- Models: Media

##### Retrieve a medias marker
`GET /medias-marker/:id`
A picture for a map marker, resized (?size). Cached for an hour.
- Access: Public
- Path:
  - `id` (string): The ID of the medias marker.
- Query:
  - `size` (string): Only medias markers with this size.
- Status codes: 200, 404, 500
- Models: Media

##### Retrieve a medias medium
`GET /medias-medium/:id`
A picture 1280px wide. Resized on the fly and cached for an hour; 404 when there is no such media.
- Access: Public
- Path:
  - `id` (string): The ID of the medias medium.
- Status codes: 200, 404, 500
- Models: Media

##### Retrieve a medias object
`GET /medias-object/:id`
Get a Media object. The media record without its file. 404 "not found".
- Access: User token
- Path:
  - `id` (string): The ID of the medias object.
- Status codes: 200, 401, 404, 500
- Models: Media

##### Retrieve a medias og
`GET /medias-og/:id`
A picture as a JPEG sized for link previews (Open Graph). 1200×630; a GIF is sent as its first frame, since link previews do not play GIFs.
- Access: Public
- Path:
  - `id` (string): The ID of the medias og.
- Status codes: 200, 404, 500
- Models: Media

##### Retrieve a medias responsive
`GET /medias-responsive/:id`
A picture sized for its column. 1280px wide by default; ?size scales it to the column's width (up to 2560px). Resized on the fly and cached for an hour; 404 when there is no such media.
- Access: Public
- Path:
  - `id` (string): The ID of the medias responsive.
- Query:
  - `size` (string): Only medias responsives with this size.
  - `isFluid` (string): Only medias responsives with this is fluid.
- Status codes: 200, 404, 500
- Models: Media

##### Retrieve the medias storage
`GET /medias-storage`
How much storage the site's media uses. Answers the total size of the site's stored files.
- Access: User token
- Status codes: 200, 401, 500

##### Retrieve a medias thumbnail
`GET /medias-thumbnail/:id`
A picture 400px wide, as a thumbnail. Resized on the fly and cached for an hour; 404 when there is no such media.
- Access: Public
- Path:
  - `id` (string): The ID of the medias thumbnail.
- Status codes: 200, 404, 500
- Models: Media

##### Retrieve a medias xlarge
`GET /medias-xlarge/:id`
A picture 2560px wide. At quality 85. Product answers link their pictures here. Resized on the fly and cached for an hour; 404 when there is no such media.
- Access: Public
- Path:
  - `id` (string): The ID of the medias xlarge.
- Status codes: 200, 404, 500
- Models: Media

##### Retrieve a media
`GET /medias/:id`
A picture 860px wide. The default size for pictures in pages. Resized on the fly and cached for an hour; 404 when there is no such media.
- Access: Public
- Path:
  - `id` (string): The ID of the media.
- Returns: the Media object.
- Status codes: 200, 404, 500
- Models: Media

##### Update a media
`PATCH /medias/:id`
Updates a media record. 404 when there is no such media.
- Access: User token
- Path:
  - `id` (string): The ID of the media.
- Body:
  - `alt` (string): Img alt (same as filename if not specified).
  - `filename` (string): Name of the stored file.
  - `thumbnail` (string): Small preview of the file.
- Returns: the Media object.
- Status codes: 200, 401, 404, 500
- Models: Media

##### Delete a media
`DELETE /medias/:id`
Deletes a media record. Also deletes the stored file. 401 when the media belongs to another user. Records that still use it keep its id.
- Access: User token
- Path:
  - `id` (string): The ID of the media.
- Returns: an empty answer on success.
- Status codes: 200, 401, 404, 500
- Models: Media

##### Retrieve the app logo
`GET /pwa-logo`
The installable app's icon, resized (?size); ?media_id for another picture. Falls back to the site's logo when the app has no icon of its own. Cached for 6 minutes.
- Access: Public
- Query:
  - `media_id` (string): Only app logos with this media id.
  - `size` (string): Only app logos with this size.
- Status codes: 200, 404, 500
- Models: PwaSettings, MyContact, Media

##### Retrieve an app media
`GET /pwa-media/:id`
A picture resized for the installable app (?size_x, ?size_y). Answers the image bytes, or 404.
- Access: Public
- Path:
  - `id` (string): The ID of the app media.
- Query:
  - `size_x` (string): Only app medias with this size x.
  - `size_y` (string): Only app medias with this size y.
- Returns: the Media object.
- Status codes: 200, 404, 500
- Models: Media

##### Retrieve a QR code code media
`GET /qr-code-medias/:id`
A ticket's QR code as a PNG. 404 when there is no such ticket.
- Access: Public
- Path:
  - `id` (string): The ID of the QR code code media.
- Status codes: 200, 404, 500
- Models: Ticket

##### Remove a remove media quality
`PATCH /remove-media-quality`
Deletes one transcoded quality of a video (media_id, transcode). Answers { media }.
- Access: User token
- Body:
  - `media_id` (string): The remove media quality's media id.
  - `transcode` (string): The remove media quality's transcode.
- Status codes: 200, 401, 404, 500
- Models: Media

##### Update the storage file
`PATCH /storage-file`
Deletes a file (file) from the site's cloud storage. Only inside the site's own folder.
- Access: User token
- Body:
  - `file` (string): The storage file's file.
- Status codes: 200, 401, 500

##### List all storage files
`GET /storage-files`
Files and folders in the site's cloud storage under ?folder_path. Only the site's own folder: a path cannot reach another site's files.
- Access: Admin token
- Query:
  - `folder_path` (string): Only storage files with this folder path.
- Status codes: 200, 401, 500

#### File Route

##### List all file routes
`GET /file-routes`
The files served at the site's root (verification files and the like). Answers an array, newest first.
- Access: Admin token
- Returns: a list of FileRoute objects.
- Status codes: 200, 401, 500
- Models: FileRoute

##### Create a file route
`POST /file-routes`
Creates a root file route. The file must already be in the site's cloud storage (gcs_key) and be at most 2MB; url is the root path it is served at. Answers 201 { id }.
- Access: Admin token
- Body:
  - `url` (string): The file route's URL slug, as in its address on the site.
  - `content_type` (string): Optional content type override.
- Returns: the FileRoute object.
- Status codes: 201, 400, 401, 500
- Models: FileRoute

##### Update a file route
`PATCH /file-routes/:id`
Updates a root file route. The same checks as creating one. 404 when there is no such route.
- Access: Admin token
- Path:
  - `id` (string): The ID of the file route.
- Body:
  - `url` (string): The file route's URL slug, as in its address on the site.
  - `content_type` (string): Optional content type override.
- Returns: the FileRoute object.
- Status codes: 200, 400, 401, 404, 500
- Models: FileRoute

##### Delete a file route
`DELETE /file-routes/:id`
Deletes a root file route. Answers the deleted record, or 404. Records that point to it are not changed.
- Access: Admin token
- Path:
  - `id` (string): The ID of the file route.
- Returns: an empty answer on success.
- Status codes: 200, 401, 404, 500
- Models: FileRoute

#### Qr Code

##### Retrieve the QR code code
`GET /qr-code`
Ticket QR codes. Answers an array of every record.
- Access: Admin token
- Status codes: 200, 401, 500

##### Create a QR code code
`POST /qr-code`
Creates a ticket QR code. Answers 201 with no body.
- Access: Admin token
- Status codes: 201, 400, 401, 500
- Models: QrCode

##### Retrieve a QR code code
`GET /qr-code/:id`
One ticket QR code. 404 when there is no such record.
- Access: Admin token
- Path:
  - `id` (string): The ID of the QR code code.
- Status codes: 200, 401, 404, 500
- Models: QrCode

##### Update a QR code code
`PATCH /qr-code/:id`
Updates an alert. Fields outside the allowed list are silently ignored. Answers the updated record, or 404.
- Access: Admin token
- Path:
  - `id` (string): The ID of the QR code code.
- Status codes: 200, 400, 401, 404, 500
- Models: QrCode

#### Video

##### Retrieve a single video
`GET /single-video/:id`
One video's page with its comments and the rest of its playlist. 404 "Not found.". Answers { video, videos, collection } with ?ionic.
- Access: Optional user, Page access
- Path:
  - `id` (string): The ID of the single video.
- Returns: the Video object.
- Status codes: 200, 401, 402, 404, 500
- Models: Video, Comment, Collection

##### List all videos
`GET /videos`
The videos page, or the videos as JSON with ?ionic: filtered by playlist and published state, and sorted. Paged with ?skip, filtered by ?collection and ?published; ?sortBy sets the order.
- Access: User with access, Optional user
- Query:
  - `published` (string): Only videos with this published.
  - `sortBy` (string): The order of the results.
  - `collection` (string): Only results in this collection.
- Returns: a list of Video objects.
- Status codes: 200, 401, 402, 404, 500
- Models: Collection, Video, AboutPage

##### Create a video
`POST /videos`
Creates a video. Answers 201 and re-renders the home page.
- Access: User token
- Returns: the Video object.
- Status codes: 201, 400, 401, 500
- Models: Video

##### Delete multiple videos
`DELETE /videos-multiple/:delete_ids`
Deletes several videos, ids comma-separated. Answers the ids that were deleted.
- Access: User token
- Path:
  - `delete_ids` (string): The video's delete ids.
- Returns: an empty answer on success.
- Status codes: 200, 401, 500
- Models: Video

##### Update multiple videos
`PATCH /videos-multiple/:patch_ids`
Applies the same changes (updates) to several videos, ids comma-separated. Answers the ids that were changed.
- Access: User token
- Path:
  - `patch_ids` (string): The video's patch ids.
- Body:
  - `collection_id` (objectid): Collection.id.
  - `updates` (string): The video's updates.
- Returns: the Video object.
- Status codes: 200, 400, 401, 500
- Models: Video

##### Retrieve a video
`GET /videos/:id`
A playlist's page with its videos. 404 when there is no such playlist. JSON with ?ionic.
- Access: Optional user
- Path:
  - `id` (string): The ID of the video.
- Returns: the Video object.
- Status codes: 200, 404, 500
- Models: Collection, Video

##### Update a video
`PATCH /videos/:id`
Updates a video. Fields outside the allowed list are silently ignored. Answers the updated record, or 404.
- Access: User token
- Path:
  - `id` (string): The ID of the video.
- Returns: the Video object.
- Status codes: 200, 400, 401, 404, 500
- Models: Video

##### Delete a video
`DELETE /videos/:id`
Deletes a video. Answers the deleted record, or 404. Records that point to it are not changed.
- Access: User token
- Path:
  - `id` (string): The ID of the video.
- Returns: an empty answer on success.
- Status codes: 200, 401, 404, 500
- Models: Video

##### List all videos
`GET /videos/playlists`
The playlists page. 404 when there are no playlists.
- Access: Optional user
- Status codes: 200, 404, 500
- Models: Collection, Media

#### Video Interaction

##### Create a video interaction
`POST /video-interaction`
Creates a video watch record. Records a stretch of watching (start, end); it feeds the long-video insights. Answers 201.
- Access: User token
- Body:
  - `video_viewer` (objectid): User.id.
- Returns: the VideoInteraction object.
- Status codes: 201, 400, 401, 500
- Models: VideoInteraction

#### AI builder

##### Create an image with AI
`POST /create-new-image`
AI: generates a picture from the owner's prompt, stores it in the media library and charges its credits. Answers { action: 'JOB', async_job } at once; the admin polls GET /async-jobs/:id, and the result arrives in the chat as a message when the job ends. The AI work is charged to the account's credits.
- Access: Admin token
- Body:
  - `prompt` (string): What the AI should do with the image, in plain words - any language.
  - `chatId` (string): The AI chat this request belongs to; its history and language carry over. Leave it out to start a new chat.
  - `aiVersion` (string): Which version of the AI builder to use; leave it out for the current one.
- Status codes: 200, 401, 500
- Models: ChatMessage

##### Create a video with AI
`POST /create-new-video`
AI: generates a video from text, a picture or an earlier video, as a background job. Answers { action: 'JOB', async_job } at once; the admin polls GET /async-jobs/:id, and the result arrives in the chat as a message when the job ends. The AI work is charged to the account's credits.
- Access: Admin token
- Body:
  - `duration` (string): The video's duration.
  - `prompt` (string): What the AI should do with the video, in plain words - any language.
  - `mediaIds` (string): The video's media ids.
  - `chatId` (string): The AI chat this request belongs to; its history and language carry over. Leave it out to start a new chat.
  - `aspect_ratio` (string): The video's aspect ratio.
  - `aiVersion` (string): Which version of the AI builder to use; leave it out for the current one.
- Status codes: 200, 401, 500
- Models: ChatMessage, Media

##### Generate an image with AI
`POST /generate-image`
AI: routes a request for a new picture to create-new-image. Does no AI work itself: answers { action: 'REDIRECT', target_url, response } and the chat client calls target_url next with response as the prompt.
- Access: Admin token
- Body:
  - `prompt` (string): What the AI should do with the image, in plain words - any language.
  - `chatId` (string): The AI chat this request belongs to; its history and language carry over. Leave it out to start a new chat.
- Status codes: 200, 401, 500

##### Generate an image to video with AI
`POST /generate-image-to-video`
AI: routes a request for a video from a picture to create-new-video. Does no AI work itself: answers { action: 'REDIRECT', target_url, response } and the chat client calls target_url next with response as the prompt.
- Access: Admin token
- Body:
  - `prompt` (string): What the AI should do with the image to video, in plain words - any language.
  - `aspect_ratio` (string): The image to video's aspect ratio.
  - `chatId` (string): The AI chat this request belongs to; its history and language carry over. Leave it out to start a new chat.
- Status codes: 200, 401, 500

##### Generate a text to video with AI
`POST /generate-text-to-video`
AI: routes a request for a video from text to create-new-video, if the account has credits. Does no AI work itself: answers { action: 'REDIRECT', target_url, response } and the chat client calls target_url next with response as the prompt.
- Access: Admin token
- Body:
  - `prompt` (string): What the AI should do with the text to video, in plain words - any language.
  - `aspect_ratio` (string): The text to video's aspect ratio.
  - `chatId` (string): The AI chat this request belongs to; its history and language carry over. Leave it out to start a new chat.
- Status codes: 200, 401, 500

##### Generate a video to video with AI
`POST /generate-video-to-video`
AI: routes a change to a video to create-new-video. Does no AI work itself: answers { action: 'REDIRECT', target_url, response } and the chat client calls target_url next with response as the prompt.
- Access: Admin token
- Body:
  - `prompt` (string): What the AI should do with the video to video, in plain words - any language.
  - `aspect_ratio` (string): The video to video's aspect ratio.
  - `chatId` (string): The AI chat this request belongs to; its history and language carry over. Leave it out to start a new chat.
- Status codes: 200, 401, 500

##### Modify an AI image with AI
`POST /modify-ai-image`
AI: routes a change to a picture the AI made to update-image. Does no AI work itself: answers { action: 'REDIRECT', target_url, response } and the chat client calls target_url next with response as the prompt.
- Access: Admin token
- Body:
  - `prompt` (string): What the AI should do with the AI image, in plain words - any language.
  - `chatId` (string): The AI chat this request belongs to; its history and language carry over. Leave it out to start a new chat.
- Status codes: 200, 401, 500

##### Modify an AI user image with AI
`POST /modify-ai-user-image`
AI: routes a change to a picture to update-image. Does no AI work itself: answers { action: 'REDIRECT', target_url, response } and the chat client calls target_url next with response as the prompt.
- Access: Admin token
- Body:
  - `prompt` (string): What the AI should do with the AI user image, in plain words - any language.
  - `chatId` (string): The AI chat this request belongs to; its history and language carry over. Leave it out to start a new chat.
- Status codes: 200, 401, 500

##### Modify a user image with AI
`POST /modify-user-image`
AI: routes a change to one of the owner's pictures to update-image. Does no AI work itself: answers { action: 'REDIRECT', target_url, response } and the chat client calls target_url next with response as the prompt.
- Access: Admin token
- Body:
  - `prompt` (string): What the AI should do with the user image, in plain words - any language.
  - `chatId` (string): The AI chat this request belongs to; its history and language carry over. Leave it out to start a new chat.
- Status codes: 200, 401, 500

##### Update an image with AI
`POST /update-image`
AI: edits an existing picture as the owner asked and stores the result. Answers { action: 'JOB', async_job } at once; the admin polls GET /async-jobs/:id, and the result arrives in the chat as a message when the job ends. The AI work is charged to the account's credits.
- Access: Admin token
- Body:
  - `prompt` (string): What the AI should do with the image, in plain words - any language.
  - `chatId` (string): The AI chat this request belongs to; its history and language carry over. Leave it out to start a new chat.
  - `mediaIds` (string): The image's media ids.
  - `aiVersion` (string): Which version of the AI builder to use; leave it out for the current one.
- Status codes: 200, 401, 404, 500
- Models: Media, ChatMessage

## Objects

### AboutPage

A page built in the editor or by the AI builder. Its content is in containers: each container is a full-width section with columns, and each column has components, laid out on a 12-column grid by their size.

- `requiresLogin`: Boolean, default false - Only signed-in users can see the page.
- `requiresSubscription`: Boolean, default false - Only subscribers of one of price_plan_ids can see the page.
- `price_plan_ids`: String[] - The price plans whose subscribers can see the page, with requiresSubscription.
- `title`: String, required - The page's title, shown in the browser tab and in the admin.
- `url`: String, required - The page's address on the site: /<url>. Unique.
- `redirectUrl`: String - When set, visitors who open the page are sent to this address instead.
- `legal_kind`: String - For legal pages: which document it is, in English (Privacy Policy, Terms of Service...).
- `isPartial`: Boolean, default false - A part of other pages (a banner, a block inside a product page), not a page of its own.
- `showTo`: String, default "0" - Who sees the page: "0" everyone, "1" signed-in users only, "2" visitors who are not signed in.
- `showHeader`: Boolean, default true - Show the site's header on the page.
- `showFooter`: Boolean, default true - Show the site's footer on the page.
- `ai_context_v2`: Mixed - Legacy, no longer written: the AI context is built on demand. Old copies are not selected and are removed by npm run migrate:strip-about-page-ai-context.
- `ai_surfaces_v2`: Mixed - The editable regions the AI page modifier found on the page last time it read it; written by the AI builder.
- `ai_overview`: String - A short summary of the page the AI builder keeps for its own use.
- `ai_generation_metadata`: Mixed - How the AI builder generated the page (mode, prompt, timings); written by the AI builder.
- `containers`: Object[] - The page's content: sections, each with columns of components.
  - `isFluid`: Boolean, default false
  - `parallaxEnabled`: Boolean
  - `parallaxAmount`: Number
  - `parallaxZoom`: Number
  - `continuousEnabled`: Boolean, default false
  - `continuousEffect`: String, default "none"
  - `continuousDuration`: Number, default 2400
  - `continuousIntensity`: Number, default 4
  - `backgroundImage`: String
  - `backgroundSize`: String
  - `backgroundPosition`: String
  - `backgroundRepeat`: String
  - `backgroundVideo`: String
  - `backgroundColor`: String
  - `backgroundGradient.enabled`: Boolean, default false
  - `backgroundGradient.type`: String
  - `backgroundGradient.degrees`: Number
  - `backgroundGradient.radialShape`: String
  - `backgroundGradient.radialPosition`: String
  - `backgroundGradient.colors`: Object[]
    - `color`: String
    - `percentage`: Number
    - `_id`: ObjectId
  - `backgroundGradient.css`: String
  - `backgroundParallax`: Boolean, default false
  - `minHeight`: Number
  - `minHeightMobile`: Number
  - `padding_top.value`: String, default ""
  - `padding_top.unit`: String, default "px"
  - `padding_top.mobile_value`: String, default ""
  - `padding_top.mobile_unit`: String, default "px"
  - `padding_right.value`: String, default ""
  - `padding_right.unit`: String, default "px"
  - `padding_right.mobile_value`: String, default ""
  - `padding_right.mobile_unit`: String, default "px"
  - `padding_bottom.value`: String, default ""
  - `padding_bottom.unit`: String, default "px"
  - `padding_bottom.mobile_value`: String, default ""
  - `padding_bottom.mobile_unit`: String, default "px"
  - `padding_left.value`: String, default ""
  - `padding_left.unit`: String, default "px"
  - `padding_left.mobile_value`: String, default ""
  - `padding_left.mobile_unit`: String, default "px"
  - `allowOffset`: Boolean, default false
  - `offsetX`: Number, default 0
  - `offsetY`: Number, default 0
  - `offsetMobileX`: Number, default 0
  - `offsetMobileY`: Number, default 0
  - `zIndex`: Number, default 0
  - `wrap_reverse`: Boolean, default false
  - `borderRadius`: Number, default 0
  - `marginX`: Number, default 0
  - `editorName`: String
  - `horizontalScroll`: Boolean, default false
  - `horizontalScrollWidth`: Number, default 100
  - `overflowX`: String, default "hidden"
  - `columns`: Object[]
    - `size`: String
    - `hoverGroup`: Boolean
    - `parallaxEnabled`: Boolean
    - `parallaxAmount`: Number
    - `parallaxZoom`: Number
    - `continuousEnabled`: Boolean, default false
    - `continuousEffect`: String, default "none"
    - `continuousDuration`: Number, default 2400
    - `continuousIntensity`: Number, default 4
    - `sticky`: Boolean, default false
    - `justify`: String
    - `align`: String
    - `onHoverActions`: Boolean, default false
    - `onHoverTransition`: Number, default 0.3
    - `backgroundColor`: String
    - `backgroundGradient.enabled`: Boolean, default false
    - `backgroundGradient.type`: String
    - `backgroundGradient.degrees`: Number
    - `backgroundGradient.radialShape`: String
    - `backgroundGradient.radialPosition`: String
    - `backgroundGradient.colors`: Object[]
      - `color`: String
      - `percentage`: Number
      - `_id`: ObjectId
    - `backgroundGradient.css`: String
    - `backgroundColorOnHover`: String
    - `innerBackgroundColor`: String
    - `innerBackgroundGradient.enabled`: Boolean, default false
    - `innerBackgroundGradient.type`: String
    - `innerBackgroundGradient.degrees`: Number
    - `innerBackgroundGradient.radialShape`: String
    - `innerBackgroundGradient.radialPosition`: String
    - `innerBackgroundGradient.colors`: Object[]
      - `color`: String
      - `percentage`: Number
      - `_id`: ObjectId
    - `innerBackgroundGradient.css`: String
    - `innerBackgroundColorOnHover`: String
    - `backgroundImage`: String
    - `backgroundSize`: String
    - `backgroundPosition`: String
    - `backgroundRepeat`: String
    - `backgroundVideo`: String
    - `paddingX`: Number, default 0
    - `paddingXOnHover`: Number, default 0
    - `paddingY`: Number, default 0
    - `paddingYOnHover`: Number, default 0
    - `innerPaddingX`: Number, default 0
    - `innerPaddingXOnHover`: Number, default 0
    - `innerPaddingY`: Number, default 0
    - `allowOffset`: Boolean, default false
    - `offsetX`: Number, default 0
    - `offsetY`: Number, default 0
    - `offsetMobileX`: Number, default 0
    - `offsetMobileY`: Number, default 0
    - `zIndex`: Number, default 0
    - `innerPaddingYOnHover`: Number, default 0
    - `paddingOptions`: String
    - `paddingOptionsOnHover`: String
    - `innerPaddingOptions`: String
    - `borderOptions`: String
    - `borderOptionsOnHover`: String
    - `card`: Boolean, default false
    - `borderRadius`: Number, default 0
    - `borderRadiusOnHover`: Number, default 0
    - `borderType`: String, default "none"
    - `borderTypeOnHover`: String, default "none"
    - `borderWidth`: Number, default 0
    - `borderWidthOnHover`: Number, default 0
    - `borderColor`: String, default ""
    - `borderColorOnHover`: String, default ""
    - `components`: Object[]
      - `type`: String
      - `size`: String, default "12"
      - `sticky`: Boolean, default false
      - `paddingX`: Number, default 0
      - `paddingY`: Number, default 0
      - `allowOffset`: Boolean, default false
      - `offsetX`: Number, default 0
      - `offsetY`: Number, default 0
      - `offsetMobileX`: Number, default 0
      - `offsetMobileY`: Number, default 0
      - `zIndex`: Number, default 0
      - `position`: String, one of static | relative | absolute | fixed | sticky, default "static"
      - `top.value`: String, default "0"
      - `top.unit`: String, default "px"
      - `top.mobile_value`: String, default ""
      - `top.mobile_unit`: String, default "px"
      - `left.value`: String, default "0"
      - `left.unit`: String, default "px"
      - `left.mobile_value`: String, default ""
      - `left.mobile_unit`: String, default "px"
      - `right.value`: String, default "0"
      - `right.unit`: String, default "px"
      - `right.mobile_value`: String, default ""
      - `right.mobile_unit`: String, default "px"
      - `bottom.value`: String, default "0"
      - `bottom.unit`: String, default "px"
      - `bottom.mobile_value`: String, default ""
      - `bottom.mobile_unit`: String, default "px"
      - `custom_measurements`: Boolean, default false
      - `edit_type`: String, default "simple"
      - `custom_width.value`: Mixed
      - `custom_width.unit`: String, default "px"
      - `custom_width.mobile_value`: Mixed
      - `custom_width.mobile_unit`: String, default "px"
      - `custom_min_width.value`: Mixed
      - `custom_min_width.unit`: String, default "px"
      - `custom_min_width.mobile_value`: Mixed
      - `custom_min_width.mobile_unit`: String, default "px"
      - `custom_max_width.value`: Mixed
      - `custom_max_width.unit`: String, default "px"
      - `custom_max_width.mobile_value`: Mixed
      - `custom_max_width.mobile_unit`: String, default "px"
      - `custom_height.value`: Mixed
      - `custom_height.unit`: String, default "px"
      - `custom_height.mobile_value`: Mixed
      - `custom_height.mobile_unit`: String, default "px"
      - `custom_min_height.value`: Mixed
      - `custom_min_height.unit`: String, default "px"
      - `custom_min_height.mobile_value`: Mixed
      - `custom_min_height.mobile_unit`: String, default "px"
      - `custom_max_height.value`: Mixed
      - `custom_max_height.unit`: String, default "px"
      - `custom_max_height.mobile_value`: Mixed
      - `custom_max_height.mobile_unit`: String, default "px"
      - `object_fit`: String, default "none"
      - `aspect_ratio.custom`: Boolean, default false
      - `aspect_ratio.ratio`: String, default ""
      - `min_height`: Number, default 0
      - `margin_left.value`: Mixed
      - `margin_left.unit`: String, default "px"
      - `margin_left.mobile_value`: Mixed
      - `margin_left.mobile_unit`: String, default "px"
      - `margin_right.value`: Mixed
      - `margin_right.unit`: String, default "px"
      - `margin_right.mobile_value`: Mixed
      - `margin_right.mobile_unit`: String, default "px"
      - `margin_top.value`: Mixed
      - `margin_top.unit`: String, default "px"
      - `margin_top.mobile_value`: Mixed
      - `margin_top.mobile_unit`: String, default "px"
      - `margin_bottom.value`: Mixed
      - `margin_bottom.unit`: String, default "px"
      - `margin_bottom.mobile_value`: Mixed
      - `margin_bottom.mobile_unit`: String, default "px"
      - `padding_left.value`: Mixed
      - `padding_left.unit`: String, default "px"
      - `padding_left.mobile_value`: Mixed
      - `padding_left.mobile_unit`: String, default "px"
      - `padding_right.value`: Mixed
      - `padding_right.unit`: String, default "px"
      - `padding_right.mobile_value`: Mixed
      - `padding_right.mobile_unit`: String, default "px"
      - `padding_top.value`: Mixed
      - `padding_top.unit`: String, default "px"
      - `padding_top.mobile_value`: Mixed
      - `padding_top.mobile_unit`: String, default "px"
      - `padding_bottom.value`: Mixed
      - `padding_bottom.unit`: String, default "px"
      - `padding_bottom.mobile_value`: Mixed
      - `padding_bottom.mobile_unit`: String, default "px"
      - `border_left.color`: String
      - `border_left.width.value`: Mixed
      - `border_left.width.unit`: String, default "px"
      - `border_left.style`: String
      - `border_right.color`: String
      - `border_right.width.value`: Mixed
      - `border_right.width.unit`: String, default "px"
      - `border_right.style`: String
      - `border_top.color`: String
      - `border_top.width.value`: Mixed
      - `border_top.width.unit`: String, default "px"
      - `border_top.style`: String
      - `border_bottom.color`: String
      - `border_bottom.width.value`: Mixed
      - `border_bottom.width.unit`: String, default "px"
      - `border_bottom.style`: String
      - `border_top_left_radius.value`: Mixed
      - `border_top_left_radius.unit`: String, default "px"
      - `border_top_right_radius.value`: Mixed
      - `border_top_right_radius.unit`: String, default "px"
      - `border_bottom_left_radius.value`: Mixed
      - `border_bottom_left_radius.unit`: String, default "px"
      - `border_bottom_right_radius.value`: Mixed
      - `border_bottom_right_radius.unit`: String, default "px"
      - `custom_border_radius.value`: Number
      - `custom_border_radius.unit`: String, default "px"
      - `specific_border_radius`: Boolean, default false
      - `box_shadows`: Object[]
      - `mix_blend_mode`: String, one of normal | multiply | screen | overlay | darken | lighten | color-dodge | color-burn | hard-light | soft-light | difference | exclusion | hue | saturation | color | luminosity, default "normal"
      - `paddingOptions`: String
      - `backgroundColor`: String
      - `backgroundGradient.enabled`: Boolean, default false
      - `backgroundGradient.type`: String
      - `backgroundGradient.degrees`: Number
      - `backgroundGradient.radialShape`: String
      - `backgroundGradient.radialPosition`: String
      - `backgroundGradient.colors`: Object[]
      - `backgroundGradient.css`: String
      - `card`: Boolean, default false
      - `borderRadius`: Number, default 0
      - `borderType`: String, default "none"
      - `borderWidth`: Number, default 0
      - `borderColor`: String, default ""
      - `borderOptions`: String
      - `showOnMobile`: Boolean, default true
      - `showOnDesktop`: Boolean, default true
      - `icon`: String
      - `iconSettings.size`: Number, default 40
      - `iconSettings.color`: String, default "var(--ion-color-primary)"
      - `heading`: String
      - `subHeading`: String
      - `heading_alignment`: String
      - `innerHTML`: String
      - `innerCSS`: String
      - `innerJS`: String
      - `lineHeight`: String, default "normal"
      - `medias`: Object[]
      - `products`: ObjectId[], → Product
      - `product_gallery.design_layout`: String, default ""
      - `product_gallery.mode`: String, default "card"
      - `product_gallery.columns`: Number, default 3
      - `product_gallery.placeholderItems`: Object[]
      - `product_gallery.placeholderPlateColor`: String, default ""
      - `product_gallery.showName`: Boolean, default true
      - `product_gallery.showPrice`: Boolean, default true
      - `product_gallery.showDescription`: Boolean, default false
      - `product_gallery.showBrand`: Boolean, default false
      - `product_gallery.showRibbon`: Boolean, default false
      - `product_gallery.showRating`: Boolean, default false
      - `product_gallery.showCart`: Boolean, default false
      - `product_gallery.showLink`: Boolean, default false
      - `product_gallery.linkLabel`: String, default "View product"
      - `product_gallery.imageAspectRatio`: String, default "1/1"
      - `product_gallery.imageHeight`: Number, default 260
      - `product_gallery.imageRadius`: Number, default 20
      - `product_gallery.imageObjectFit`: String, default "cover"
      - `product_gallery.cardBackgroundColor`: String, default "var(--ion-card-background, #ffffff)"
      - `product_gallery.cardBorderRadius`: Number, default 28
      - `product_gallery.cardBorderWidth`: Number, default 0
      - `product_gallery.cardBorderColor`: String, default "transparent"
      - `product_gallery.cardPaddingX`: Number, default 20
      - `product_gallery.cardPaddingY`: Number, default 20
      - `product_gallery.gap`: Number, default 24
      - `product_gallery.boxShadowColor`: String, default "rgba(var(--ion-color-primary-rgb, 56,128,255), 0.22)"
      - `product_gallery.boxShadowOffsetY`: Number, default 20
      - `product_gallery.boxShadowBlurRadius`: Number, default 40
      - `product_gallery.boxShadowSpreadRadius`: Number, default -22
      - `product_gallery.fieldBackground`: String, default "transparent"
      - `product_gallery.hairlineColor`: String, default "transparent"
      - `product_gallery.hairlineWidth`: Number, default 0
      - `product_gallery.maxWidth`: Number, default 0
      - `product_gallery.scrimGradient`: String, default ""
      - `product_gallery.align`: String, default "left"
      - `product_gallery.accentColor`: String, default "var(--ion-color-primary, #3880ff)"
      - `product_gallery.nameColor`: String, default "var(--ion-text-color, #1e293b)"
      - `product_gallery.nameFontSize`: String, default "1em"
      - `product_gallery.nameFontWeight`: String, default "700"
      - `product_gallery.nameFontFamily`: String, default ""
      - `product_gallery.nameTransform`: String, default "none"
      - `product_gallery.priceColor`: String, default "var(--ion-color-primary, #3880ff)"
      - `product_gallery.priceFontSize`: String, default "0.94em"
      - `product_gallery.priceFontWeight`: String, default "600"
      - `product_gallery.priceFontFamily`: String, default ""
      - `product_gallery.priceFontStyle`: String, default "normal"
      - `product_gallery.metaColor`: String, default "rgba(var(--ion-text-color-rgb, 30,41,59), 0.66)"
      - `product_gallery.titlePosition`: String, default "bottom"
      - `product_gallery.imageSide`: String, default "left"
      - `product_gallery.imagePadding`: Number, default 0
      - `product_gallery.boxShadowOffsetX`: Number, default 0
      - `product_gallery.descFontSize`: String, default "13px"
      - `product_gallery.descFontWeight`: String, default "400"
      - `product_gallery.animation`: String, default "none"
      - `product_gallery.animationDuration`: Number, default 0
      - `product_gallery.animationDelay`: Number, default 0
      - `product_gallery.showCustomFields`: Boolean, default false
      - `product_gallery.cardDesign`: String, default ""
      - `product_gallery.carouselLoop`: Boolean, default false
      - `product_gallery.carouselControls`: Boolean, default false
      - `product_gallery.customCss`: String, default ""
      - `services`: ObjectId[], → Service
      - `service_gallery.design_layout`: String, default ""
      - `service_gallery.mode`: String, default "card"
      - `service_gallery.columns`: Number, default 3
      - `service_gallery.placeholderItems`: Object[]
      - `service_gallery.placeholderPlateColor`: String, default ""
      - `service_gallery.showTitle`: Boolean, default true
      - `service_gallery.showPrice`: Boolean, default true
      - `service_gallery.showDescription`: Boolean, default false
      - `service_gallery.showDuration`: Boolean, default false
      - `service_gallery.durationLabel`: String, default "min"
      - `service_gallery.showImage`: Boolean, default true
      - `service_gallery.showLink`: Boolean, default false
      - `service_gallery.linkLabel`: String, default "View service"
      - `service_gallery.imageAspectRatio`: String, default "1/1"
      - `service_gallery.imageHeight`: Number, default 260
      - `service_gallery.imageRadius`: Number, default 20
      - `service_gallery.imageObjectFit`: String, default "cover"
      - `service_gallery.cardBackgroundColor`: String, default "var(--ion-card-background, #ffffff)"
      - `service_gallery.cardBorderRadius`: Number, default 28
      - `service_gallery.cardBorderWidth`: Number, default 0
      - `service_gallery.cardBorderColor`: String, default "transparent"
      - `service_gallery.cardPaddingX`: Number, default 20
      - `service_gallery.cardPaddingY`: Number, default 20
      - `service_gallery.gap`: Number, default 24
      - `service_gallery.boxShadowColor`: String, default "rgba(var(--ion-color-primary-rgb, 56,128,255), 0.22)"
      - `service_gallery.boxShadowOffsetY`: Number, default 20
      - `service_gallery.boxShadowBlurRadius`: Number, default 40
      - `service_gallery.boxShadowSpreadRadius`: Number, default -22
      - `service_gallery.fieldBackground`: String, default "transparent"
      - `service_gallery.hairlineColor`: String, default "transparent"
      - `service_gallery.hairlineWidth`: Number, default 0
      - `service_gallery.rowRuleColor`: String, default "transparent"
      - `service_gallery.rowRuleWidth`: Number, default 0
      - `service_gallery.maxWidth`: Number, default 0
      - `service_gallery.scrimGradient`: String, default ""
      - `service_gallery.align`: String, default "left"
      - `service_gallery.accentColor`: String, default "var(--ion-color-primary, #3880ff)"
      - `service_gallery.titleColor`: String, default "var(--ion-text-color, #1e293b)"
      - `service_gallery.titleFontSize`: String, default "1em"
      - `service_gallery.titleFontWeight`: String, default "700"
      - `service_gallery.titleFontFamily`: String, default ""
      - `service_gallery.titleTransform`: String, default "none"
      - `service_gallery.priceColor`: String, default "var(--ion-color-primary, #3880ff)"
      - `service_gallery.priceFontSize`: String, default "0.94em"
      - `service_gallery.priceFontWeight`: String, default "600"
      - `service_gallery.priceFontFamily`: String, default ""
      - `service_gallery.priceFontStyle`: String, default "normal"
      - `service_gallery.metaColor`: String, default "rgba(var(--ion-text-color-rgb, 30,41,59), 0.66)"
      - `service_gallery.titlePosition`: String, default "bottom"
      - `service_gallery.imageSide`: String, default "left"
      - `service_gallery.imagePadding`: Number, default 0
      - `service_gallery.boxShadowOffsetX`: Number, default 0
      - `service_gallery.descFontSize`: String, default "13px"
      - `service_gallery.descFontWeight`: String, default "400"
      - `service_gallery.animation`: String, default "none"
      - `service_gallery.animationDuration`: Number, default 0
      - `service_gallery.animationDelay`: Number, default 0
      - `service_gallery.showCustomFields`: Boolean, default false
      - `service_gallery.cardDesign`: String, default ""
      - `service_gallery.carouselLoop`: Boolean, default false
      - `service_gallery.carouselControls`: Boolean, default false
      - `service_gallery.customCss`: String, default ""
      - `blog_gallery.design_layout`: String, default ""
      - `blog_gallery.mode`: String, default "card"
      - `blog_gallery.columns`: Number, default 3
      - `blog_gallery.leadLayout`: String, default "beside"
      - `blog_gallery.leadWidth`: Number, default 58
      - `blog_gallery.placeholderItems`: Object[]
      - `blog_gallery.placeholderPlateColor`: String, default ""
      - `blog_gallery.showTitle`: Boolean, default true
      - `blog_gallery.showExcerpt`: Boolean, default false
      - `blog_gallery.showDate`: Boolean, default true
      - `blog_gallery.showImage`: Boolean, default true
      - `blog_gallery.showLink`: Boolean, default false
      - `blog_gallery.linkLabel`: String, default "Read more"
      - `blog_gallery.showCustomFields`: Boolean, default false
      - `blog_gallery.imageAspectRatio`: String, default "1/1"
      - `blog_gallery.imageHeight`: Number, default 260
      - `blog_gallery.imageRadius`: Number, default 20
      - `blog_gallery.imageObjectFit`: String, default "cover"
      - `blog_gallery.imagePadding`: Number, default 0
      - `blog_gallery.imageSide`: String, default "left"
      - `blog_gallery.cardBackgroundColor`: String, default "var(--ion-card-background, #ffffff)"
      - `blog_gallery.cardBorderRadius`: Number, default 28
      - `blog_gallery.cardBorderWidth`: Number, default 0
      - `blog_gallery.cardBorderColor`: String, default "transparent"
      - `blog_gallery.cardPaddingX`: Number, default 20
      - `blog_gallery.cardPaddingY`: Number, default 20
      - `blog_gallery.gap`: Number, default 24
      - `blog_gallery.boxShadowColor`: String, default "rgba(var(--ion-color-primary-rgb, 56,128,255), 0.22)"
      - `blog_gallery.boxShadowOffsetX`: Number, default 0
      - `blog_gallery.boxShadowOffsetY`: Number, default 20
      - `blog_gallery.boxShadowBlurRadius`: Number, default 40
      - `blog_gallery.boxShadowSpreadRadius`: Number, default -22
      - `blog_gallery.fieldBackground`: String, default "transparent"
      - `blog_gallery.hairlineColor`: String, default "transparent"
      - `blog_gallery.hairlineWidth`: Number, default 0
      - `blog_gallery.rowRuleColor`: String, default "transparent"
      - `blog_gallery.rowRuleWidth`: Number, default 0
      - `blog_gallery.maxWidth`: Number, default 0
      - `blog_gallery.scrimGradient`: String, default ""
      - `blog_gallery.align`: String, default "left"
      - `blog_gallery.accentColor`: String, default "var(--ion-color-primary, #3880ff)"
      - `blog_gallery.titleColor`: String, default "var(--ion-text-color, #1e293b)"
      - `blog_gallery.titleFontSize`: String, default "1em"
      - `blog_gallery.titleFontWeight`: String, default "700"
      - `blog_gallery.titleFontFamily`: String, default ""
      - `blog_gallery.titleTransform`: String, default "none"
      - `blog_gallery.dateColor`: String, default "var(--ion-color-primary, #3880ff)"
      - `blog_gallery.dateFontSize`: String, default "0.81em"
      - `blog_gallery.dateFontWeight`: String, default "600"
      - `blog_gallery.dateFontFamily`: String, default ""
      - `blog_gallery.dateFontStyle`: String, default "normal"
      - `blog_gallery.metaColor`: String, default "rgba(var(--ion-text-color-rgb, 30,41,59), 0.66)"
      - `blog_gallery.excerptFontSize`: String, default "0.88em"
      - `blog_gallery.excerptFontWeight`: String, default "400"
      - `blog_gallery.excerptFontFamily`: String, default ""
      - `blog_gallery.titlePosition`: String, default "bottom"
      - `blog_gallery.animation`: String, default "none"
      - `blog_gallery.animationDuration`: Number, default 0
      - `blog_gallery.animationDelay`: Number, default 0
      - `blog_gallery.cardDesign`: String, default ""
      - `blog_gallery.customCss`: String, default ""
      - `multi_booking.layout`: String, default "chips"
      - `multi_booking.columns`: Number, default 3
      - `multi_booking.pickerBackground`: String, default "transparent"
      - `multi_booking.gap`: Number, default 10
      - `multi_booking.align`: String, default "left"
      - `multi_booking.showPrice`: Boolean, default false
      - `multi_booking.showDuration`: Boolean, default false
      - `multi_booking.showDescription`: Boolean, default false
      - `multi_booking.showImage`: Boolean, default false
      - `multi_booking.itemPaddingX`: Number, default 18
      - `multi_booking.itemPaddingY`: Number, default 10
      - `multi_booking.itemRadius`: Number, default 100
      - `multi_booking.itemBorderWidth`: Number, default 0
      - `multi_booking.itemBorderColor`: String, default "rgba(var(--ion-color-primary-rgb, 56,128,255), 0.12)"
      - `multi_booking.itemBackground`: String, default "rgba(var(--ion-color-primary-rgb, 56,128,255), 0.10)"
      - `multi_booking.itemTextColor`: String, default "var(--ion-text-color, inherit)"
      - `multi_booking.itemSubTextColor`: String, default "var(--ion-color-medium, rgba(0,0,0,0.55))"
      - `multi_booking.selectedBackground`: String, default "var(--ion-color-primary)"
      - `multi_booking.selectedTextColor`: String, default "var(--ion-color-primary-contrast, #ffffff)"
      - `multi_booking.selectedSubTextColor`: String, default "rgba(var(--ion-color-primary-contrast-rgb, 255,255,255), 0.75)"
      - `multi_booking.selectedBorderColor`: String, default "var(--ion-color-primary)"
      - `multi_booking.hoverBackground`: String, default "rgba(var(--ion-color-primary-rgb, 56,128,255), 0.18)"
      - `multi_booking.titleFontSize`: Number, default 14
      - `multi_booking.titleFontWeight`: String, default "500"
      - `multi_booking.subFontSize`: Number, default 13
      - `multi_booking.dividerColor`: String, default "rgba(var(--ion-color-primary-rgb, 56,128,255), 0.12)"
      - `multi_booking.dividerWidth`: Number, default 1
      - `multi_booking.autoScroll`: Boolean, default true
      - `multi_booking.cardBackground`: String, default "var(--ion-card-color, #ffffff)"
      - `multi_booking.cardRadius`: Number, default 28
      - `multi_booking.cardBorderWidth`: Number, default 0
      - `multi_booking.cardBorderColor`: String, default "rgba(var(--ion-color-primary-rgb, 56,128,255), 0.12)"
      - `multi_booking.cardMaxWidth`: Number, default 820
      - `multi_booking.cardLayout`: String, default "stack"
      - `multi_booking.splitWidth`: Number, default 340
      - `multi_booking.shadowColor`: String, default "rgba(var(--ion-color-primary-rgb, 56,128,255), 0.18)"
      - `multi_booking.shadowOffsetY`: Number, default 20
      - `multi_booking.shadowBlur`: Number, default 40
      - `multi_booking.shadowSpread`: Number, default -22
      - `multi_booking.sectionPaddingX`: Number, default 40
      - `multi_booking.sectionPaddingY`: Number, default 32
      - `multi_booking.sectionDividerColor`: String, default "rgba(var(--ion-color-primary-rgb, 56,128,255), 0.12)"
      - `multi_booking.sectionDividerWidth`: Number, default 1
      - `multi_booking.showStepLabels`: Boolean, default true
      - `multi_booking.serviceStepLabel`: String, default "Step 1 — Service"
      - `multi_booking.dayStepLabel`: String, default "Step 2 — Day"
      - `multi_booking.staffStepLabel`: String, default "Step 3 — Staff member"
      - `multi_booking.timeStepLabel`: String, default "Step 4 — Time"
      - `multi_booking.stepLabelColor`: String, default "var(--ion-color-medium, rgba(0,0,0,0.55))"
      - `multi_booking.stepLabelSize`: Number, default 13
      - `multi_booking.stepLabelWeight`: String, default "700"
      - `multi_booking.stepLabelSpacing`: Number, default 0.04
      - `multi_booking.stepLabelUppercase`: Boolean, default true
      - `multi_booking.dayCount`: Number, default 6
      - `multi_booking.dayLayout`: String, default "strip"
      - `multi_booking.dayBorderColor`: String, default "rgba(var(--ion-color-primary-rgb, 56,128,255), 0.15)"
      - `multi_booking.dayShape`: String, default "circle"
      - `multi_booking.daySize`: Number, default 64
      - `multi_booking.dayGap`: Number, default 10
      - `multi_booking.dayShowWeekday`: Boolean, default false
      - `multi_booking.dayBackground`: String, default "rgba(var(--ion-color-primary-rgb, 56,128,255), 0.10)"
      - `multi_booking.dayTextColor`: String, default "var(--ion-text-color, inherit)"
      - `multi_booking.daySelectedBackground`: String, default "var(--ion-color-primary)"
      - `multi_booking.daySelectedTextColor`: String, default "var(--ion-color-primary-contrast, #ffffff)"
      - `multi_booking.dayDisabledOpacity`: Number, default 0.35
      - `multi_booking.timeGap`: Number, default 10
      - `multi_booking.timeRadius`: Number, default 100
      - `multi_booking.timeBorderWidth`: Number, default 0
      - `multi_booking.timeBorderColor`: String, default "rgba(var(--ion-color-primary-rgb, 56,128,255), 0.15)"
      - `multi_booking.timeSelectedBorderColor`: String, default "var(--ion-color-primary)"
      - `multi_booking.timePaddingX`: Number, default 18
      - `multi_booking.timePaddingY`: Number, default 10
      - `multi_booking.timeBackground`: String, default "rgba(var(--ion-color-primary-rgb, 56,128,255), 0.10)"
      - `multi_booking.timeTextColor`: String, default "var(--ion-text-color, inherit)"
      - `multi_booking.timeSelectedBackground`: String, default "var(--ion-color-primary)"
      - `multi_booking.timeSelectedTextColor`: String, default "var(--ion-color-primary-contrast, #ffffff)"
      - `multi_booking.buttonText`: String, default "Confirm my booking"
      - `multi_booking.buttonBackground`: String, default "var(--ion-color-primary)"
      - `multi_booking.buttonTextColor`: String, default "var(--ion-color-primary-contrast, #ffffff)"
      - `multi_booking.buttonRadius`: Number, default 100
      - `multi_booking.buttonPaddingY`: Number, default 16
      - `multi_booking.buttonFontSize`: Number, default 15
      - `multi_booking.buttonFontWeight`: String, default "700"
      - `multi_booking.buttonFullWidth`: Boolean, default true
      - `blogposts`: ObjectId[], → BlogPost
      - `videos`: ObjectId[], → Video
      - `price_plans`: ObjectId[], → PricePlan
      - `href`: String, default "#"
      - `router_link`: String
      - `buttonOptions.fill`: String
      - `buttonOptions.expand`: String
      - `buttonOptions.size`: String
      - `buttonOptions.color`: String
      - `buttonOptions.class`: String
      - `buttonOptions.customColor`: String
      - `buttonOptions.customColorContrast`: String
      - `buttonOptions.customColorHover`: String
      - `buttonOptions.hoverRoll`: Boolean
      - `buttonOptions.fontWeight`: Number
      - `buttonOptions.letterSpacing`: Number
      - `buttonOptions.fontSize`: Number, default 14
      - `buttonOptions.borderRadius`: Number, default 6
      - `buttonOptions.customColorA`: String
      - `buttonOptions.customColorB`: String
      - `buttonOptions.customColorC`: String
      - `buttonOptions.custom_measurements`: Boolean, default false
      - `buttonOptions.height.value`: String
      - `buttonOptions.height.unit`: String
      - `buttonOptions.width.value`: String
      - `buttonOptions.width.unit`: String
      - `carousel.showArrows`: Boolean, default true
      - `carousel.showIndicators`: Boolean, default true
      - `carousel.height`: Number, default 90
      - `carousel.automaticHeight`: Boolean, default true
      - `carousel.carouselMedias`: Object[]
      - `map`: String
      - `contactForm.name`: String
      - `contactForm.email`: String
      - `contactForm.subject`: String
      - `contactForm.body`: String
      - `contactForm.button`: String
      - `contactForm.hideNewsletter`: Boolean
      - `contactForm.placeholders.name`: String
      - `contactForm.placeholders.email`: String
      - `contactForm.placeholders.subject`: String
      - `contactForm.placeholders.body`: String
      - `contactForm.style.backgroundColor`: String
      - `contactForm.style.textColor`: String
      - `contactForm.style.accentColor`: String
      - `contactForm.style.border.color`: String
      - `contactForm.style.border.width.value`: Mixed
      - `contactForm.style.border.width.unit`: String
      - `contactForm.style.border.style`: String
      - `contactForm.style.borderRadius.value`: Mixed
      - `contactForm.style.borderRadius.unit`: String
      - `contactForm.style.padding.value`: Mixed
      - `contactForm.style.padding.unit`: String
      - `contactForm.style.labelFontSize.value`: Mixed
      - `contactForm.style.labelFontSize.unit`: String
      - `contactForm.style.consentFontSize.value`: Mixed
      - `contactForm.style.consentFontSize.unit`: String
      - `contactForm.style.inputBackgroundColor`: String
      - `contactForm.style.inputTextColor`: String
      - `contactForm.style.placeholderColor`: String
      - `contactForm.style.inputFocusColor`: String
      - `contactForm.style.inputFontSize.value`: Mixed
      - `contactForm.style.inputFontSize.unit`: String
      - `contactForm.style.inputPadding.value`: Mixed
      - `contactForm.style.inputPadding.unit`: String
      - `contactForm.style.textareaMinHeight.value`: Mixed
      - `contactForm.style.textareaMinHeight.unit`: String
      - `contactForm.style.inputMargin.value`: Mixed
      - `contactForm.style.inputMargin.unit`: String
      - `contactForm.style.inputBorder.color`: String
      - `contactForm.style.inputBorder.width.value`: Mixed
      - `contactForm.style.inputBorder.width.unit`: String
      - `contactForm.style.inputBorder.style`: String
      - `contactForm.style.inputBorderRadius.value`: Mixed
      - `contactForm.style.inputBorderRadius.unit`: String
      - `contactForm.style.buttonBackgroundColor`: String
      - `contactForm.style.buttonTextColor`: String
      - `contactForm.style.buttonAlign`: String
      - `contactForm.style.buttonFontSize.value`: Mixed
      - `contactForm.style.buttonFontSize.unit`: String
      - `contactForm.style.buttonBorder.color`: String
      - `contactForm.style.buttonBorder.width.value`: Mixed
      - `contactForm.style.buttonBorder.width.unit`: String
      - `contactForm.style.buttonBorder.style`: String
      - `contactForm.style.buttonBorderRadius.value`: Mixed
      - `contactForm.style.buttonBorderRadius.unit`: String
      - `contactForm.style.buttonWidth.value`: Mixed
      - `contactForm.style.buttonWidth.unit`: String
      - `contactForm.additional`: Object[]
      - `instagram.columns`: Number
      - `instagram.first`: Number
      - `instagram.skip`: Number
      - `instagram.showInfo`: Boolean
      - `instagram.gridType`: Number
      - `instagram.profile`: String
      - `facebook.url`: String
      - `facebook.width`: String
      - `facebook.height`: String
      - `animation`: String, default "none"
      - `animationDuration`: Number, default 0
      - `animationStrength`: Number
      - `parallaxEnabled`: Boolean
      - `parallaxAmount`: Number
      - `parallaxZoom`: Number
      - `textReveal`: String
      - `animationDelay`: Number, default 0
      - `animationReplay`: String, default "once"
      - `animationEasing`: String, default "power3.out"
      - `animationBreakpoint`: String, default "all"
      - `scrollScrubEnabled`: Boolean, default false
      - `scrollScrubAxis`: String, default "y"
      - `scrollScrubDistance`: Number, default 80
      - `motionOrigin`: String, default "center center"
      - `hoverEnabled`: Boolean, default false
      - `hoverEffect`: String, default "none"
      - `hoverDuration`: Number, default 240
      - `hoverIntensity`: Number, default 4
      - `continuousEnabled`: Boolean, default false
      - `continuousEffect`: String, default "none"
      - `continuousDuration`: Number, default 2400
      - `continuousIntensity`: Number, default 4
      - `statefulEnabled`: Boolean, default false
      - `statefulEffect`: String, default "none"
      - `statefulDuration`: Number, default 260
      - `statefulStagger`: Number, default 60
      - `transform.enabled`: Boolean, default false
      - `transform.translateX.value`: Number, default 0
      - `transform.translateX.unit`: String, default "px"
      - `transform.translateY.value`: Number, default 0
      - `transform.translateY.unit`: String, default "px"
      - `transform.scaleX`: Number, default 1
      - `transform.scaleY`: Number, default 1
      - `transform.rotate`: Number, default 0
      - `transform.rotateX`: Number, default 0
      - `transform.rotateY`: Number, default 0
      - `transform.skewX`: Number, default 0
      - `transform.skewY`: Number, default 0
      - `transform.perspective`: Number, default 0
      - `transform.originX`: String, default "center"
      - `transform.originY`: String, default "center"
      - `opacity`: Mixed
      - `transitionDuration`: Mixed
      - `transitionEasing`: String
      - `states`: Mixed
      - `useIframe`: Boolean, default false
      - `popup.text`: String
      - `popup.backgroundColor`: String
      - `popup.fullScreen`: Boolean, default false
      - `popup.about_page`: ObjectId, → AboutPage
      - `custom_component.html`: String
      - `custom_component.compiled_html`: String
      - `custom_component.css`: String
      - `custom_component.compiled_css`: String
      - `custom_component.js`: String
      - `custom_component.iframe`: Boolean, default false
      - `custom_component.variables`: Object[]
      - `hoverable_media.before_hover_media.media`: ObjectId, → Media
      - `hoverable_media.before_hover_media.gcs_url`: String
      - `hoverable_media.before_hover_media.vimeo_uri`: String
      - `hoverable_media.before_hover_media.vimeo_picture`: String
      - `hoverable_media.before_hover_media.youtube_url`: String
      - `hoverable_media.before_hover_media.vimeo_url`: String
      - `hoverable_media.before_hover_media.href`: String
      - `hoverable_media.before_hover_media.router_link`: String
      - `hoverable_media.before_hover_media.thumbnail`: String
      - `hoverable_media.before_hover_media.autoplay`: Boolean, default false
      - `hoverable_media.before_hover_media.loop`: Boolean, default false
      - `hoverable_media.before_hover_media.show_controls`: Boolean, default true
      - `hoverable_media.after_hover_media.media`: ObjectId, → Media
      - `hoverable_media.after_hover_media.gcs_url`: String
      - `hoverable_media.after_hover_media.vimeo_uri`: String
      - `hoverable_media.after_hover_media.vimeo_picture`: String
      - `hoverable_media.after_hover_media.youtube_url`: String
      - `hoverable_media.after_hover_media.vimeo_url`: String
      - `hoverable_media.after_hover_media.href`: String
      - `hoverable_media.after_hover_media.router_link`: String
      - `hoverable_media.after_hover_media.thumbnail`: String
      - `hoverable_media.after_hover_media.autoplay`: Boolean, default false
      - `hoverable_media.after_hover_media.loop`: Boolean, default false
      - `hoverable_media.after_hover_media.show_controls`: Boolean, default true
      - `collapsible_container`: Object[]
      - `collapsible_container_settings.enable_customization`: Boolean, default false
      - `collapsible_container_settings.title_color`: String
      - `collapsible_container_settings.title_color_checked`: String
      - `collapsible_container_settings.title_background`: String
      - `collapsible_container_settings.title_background_checked`: String
      - `collapsible_container_settings.title_font_size`: String
      - `collapsible_container_settings.title_border_radius`: String
      - `collapsible_container_settings.title_border_color`: String
      - `collapsible_container_settings.title_border_width`: String
      - `collapsible_container_settings.body_color`: String
      - `collapsible_container_settings.body_background`: String
      - `collapsible_container_settings.body_border_radius`: String
      - `collapsible_container_settings.body_border_color`: String
      - `collapsible_container_settings.body_border_width`: String
      - `collapsible_container_settings.body_font_size`: String
      - `collapsible_container_settings.icon_color`: String
      - `collapsible_container_settings.icon_glyph_color`: String
      - `collapsible_container_settings.icon_background_color`: String
      - `quiz.title`: String
      - `quiz.header`: String
      - `quiz.questions`: Object[]
      - `quiz.score_results`: Object[]
      - `quiz.show_right_answers`: Boolean, default false
      - `quiz.mutliple_choice`: Boolean, default false
      - `partners.defaultCollection`: String, default ""
      - `partners.defaultView`: String, default "locations"
      - `scroll_sequence.media`: ObjectId, → Media
      - `scroll_sequence.scroll_length`: Number, default 3
      - `scroll_sequence.fit`: String, one of cover | contain, default "cover"
      - `scroll_sequence.background_color`: String, default "#000000"
      - `scroll_sequence.overlay_color`: String, default ""
      - `scroll_sequence.overlay_opacity`: Number, default 0
      - `scroll_sequence.rows_over`: Boolean, default true
      - `scroll_sequence.rows_effect`: String, one of slide | pop, default "slide"
      - `scroll_sequence.snap_points`: Number[]
      - `scroll_sequence.snap_scroll_speed`: Number, default 1.5
      - `scroll_sequence.snap_down`: Boolean, default true
      - `scroll_sequence.snap_up`: Boolean, default false
      - `scroll_sequence.stop_momentum`: Boolean, default false
      - `smart_search.placeholder`: String, default ""
      - `smart_search.kinds`: String[]
      - `smart_search.collections`: String[]
      - `smart_search.items`: Object[]
      - `smart_search.showKindFilter`: Boolean, default true
      - `smart_search.showPriceFilter`: Boolean, default false
      - `smart_search.showDateFilter`: Boolean, default false
      - `smart_search.showSort`: Boolean, default true
      - `smart_search.dateMode`: String, one of created | updated | availability, default "created"
      - `smart_search.layout`: String, one of grid | list, default "grid"
      - `smart_search.columns`: Number, default 3
      - `smart_search.perPage`: Number, default 12
      - `smart_search.instant`: Boolean, default true
      - `smart_search.showImages`: Boolean, default true
      - `smart_search.showPrices`: Boolean, default true
      - `smart_search.showInitial`: Boolean, default false
      - `smart_search.showAnswer`: Boolean, default false
      - `countdown.targetDate`: String, default ""
      - `countdown.expiredText`: String, default "Offer has ended"
      - `countdown.hideOnExpire`: Boolean, default false
      - `countdown.textColor`: String, default ""
      - `countdown.boxColor`: String, default ""
      - `countdown.align`: String, one of  | left | center | right, default ""
      - `swiper.slides`: Object[]
      - `swiper.autoplay`: Boolean, default false
      - `swiper.loop`: Boolean, default false
      - `swiper.show_controls`: Boolean, default true
      - `swiper.dynamic_bullets`: Boolean, default false
      - `swiper.pagination`: Boolean, default true
      - `dynamicField`: String
      - `useDynamicField`: Boolean, default false
      - `text_auto_slider.speed`: Number
      - `text_auto_slider.aspect_ratio`: String
      - `text_auto_slider.text_color`: String
      - `text_auto_slider.background_color`: String
      - `text_auto_slider.delimiter_color`: String
      - `text_auto_slider.direction`: String
      - `text_auto_slider.width`: Number
      - `text_auto_slider.font_size`: Number
      - `text_auto_slider.text_transform`: String, one of none | uppercase, default "none"
      - `text_auto_slider.texts`: Object[]
      - `image_auto_slider.speed`: Number
      - `image_auto_slider.aspect_ratio`: String
      - `image_auto_slider.direction`: String
      - `image_auto_slider.width`: Number
      - `image_auto_slider.medias`: Object[]
      - `image_dragger.aspect_ratio`: String
      - `image_dragger.media_1.media`: ObjectId, → Media
      - `image_dragger.media_1.gcs_url`: String
      - `image_dragger.media_1.vimeo_uri`: String
      - `image_dragger.media_1.vimeo_picture`: String
      - `image_dragger.media_1.vimeo_url`: String
      - `image_dragger.media_1.youtube_url`: String
      - `image_dragger.media_2.media`: ObjectId, → Media
      - `image_dragger.media_2.gcs_url`: String
      - `image_dragger.media_2.vimeo_uri`: String
      - `image_dragger.media_2.vimeo_picture`: String
      - `image_dragger.media_2.vimeo_url`: String
      - `image_dragger.media_2.youtube_url`: String
      - `image_dragger.media_2.title`: String
      - `image_dragger.media_2.body`: String
      - `image_dragger.media_2.textColor`: String
      - `image_dragger.media_2.href`: String
      - `image_dragger.media_2.router_link`: String
      - `_id`: ObjectId
    - `sections`: Object[]
      - `position`: String, one of static | relative | absolute | fixed | sticky, default "static"
      - `overflow`: String, one of visible | hidden | scroll | auto, default "visible"
      - `editorName`: String
      - `top.value`: String, default "0"
      - `top.unit`: String, default "px"
      - `top.mobile_value`: String, default ""
      - `top.mobile_unit`: String, default "px"
      - `left.value`: String, default "0"
      - `left.unit`: String, default "px"
      - `left.mobile_value`: String, default ""
      - `left.mobile_unit`: String, default "px"
      - `right.value`: String, default "0"
      - `right.unit`: String, default "px"
      - `right.mobile_value`: String, default ""
      - `right.mobile_unit`: String, default "px"
      - `bottom.value`: String, default "0"
      - `bottom.unit`: String, default "px"
      - `bottom.mobile_value`: String, default ""
      - `bottom.mobile_unit`: String, default "px"
      - `display`: String, one of block | inline | flex, default "block"
      - `flex_direction`: String, one of row | row-reverse | column | column-reverse, default "row"
      - `flex_direction_mobile`: String, default ""
      - `backdropFilter`: String, default ""
      - `justify`: String, one of start | end | center | space-between | space-around | stretch, default "start"
      - `justify_mobile`: String, default ""
      - `align`: String, one of start | end | center | space-between | space-around | stretch, default "start"
      - `align_mobile`: String, default ""
      - `animation`: String, default "none"
      - `animationDuration`: Number, default 0
      - `animationStrength`: Number
      - `parallaxEnabled`: Boolean
      - `parallaxAmount`: Number
      - `parallaxZoom`: Number
      - `animationDelay`: Number, default 0
      - `animationReplay`: String, default "once"
      - `animationEasing`: String, default "power3.out"
      - `animationBreakpoint`: String, default "all"
      - `scrollScrubEnabled`: Boolean, default false
      - `scrollScrubAxis`: String, default "y"
      - `scrollScrubDistance`: Number, default 80
      - `motionOrigin`: String, default "center center"
      - `hoverEnabled`: Boolean, default false
      - `hoverEffect`: String, default "none"
      - `hoverDuration`: Number, default 240
      - `hoverIntensity`: Number, default 4
      - `continuousEnabled`: Boolean, default false
      - `continuousEffect`: String, default "none"
      - `continuousDuration`: Number, default 2400
      - `continuousIntensity`: Number, default 4
      - `statefulEnabled`: Boolean, default false
      - `statefulEffect`: String, default "none"
      - `statefulDuration`: Number, default 260
      - `statefulStagger`: Number, default 60
      - `transform.enabled`: Boolean, default false
      - `transform.translateX.value`: Number, default 0
      - `transform.translateX.unit`: String, default "px"
      - `transform.translateY.value`: Number, default 0
      - `transform.translateY.unit`: String, default "px"
      - `transform.scaleX`: Number, default 1
      - `transform.scaleY`: Number, default 1
      - `transform.rotate`: Number, default 0
      - `transform.rotateX`: Number, default 0
      - `transform.rotateY`: Number, default 0
      - `transform.skewX`: Number, default 0
      - `transform.skewY`: Number, default 0
      - `transform.perspective`: Number, default 0
      - `transform.originX`: String, default "center"
      - `transform.originY`: String, default "center"
      - `opacity`: Mixed
      - `transitionDuration`: Mixed
      - `transitionEasing`: String
      - `states`: Mixed
      - `hoverGroup`: Boolean
      - `flex_wrap`: String, one of nowrap | wrap | wrap-reverse, default "wrap"
      - `flex_wrap_mobile`: String, default ""
      - `width.value`: String, default ""
      - `width.unit`: String, default "px"
      - `width.mobile_value`: String, default ""
      - `width.mobile_unit`: String, default "px"
      - `min_width.value`: String, default ""
      - `min_width.unit`: String, default "px"
      - `min_width.mobile_value`: String, default ""
      - `min_width.mobile_unit`: String, default "px"
      - `max_width.value`: String, default ""
      - `max_width.unit`: String, default "px"
      - `max_width.mobile_value`: String, default ""
      - `max_width.mobile_unit`: String, default "px"
      - `height.value`: String, default ""
      - `height.unit`: String, default "px"
      - `height.mobile_value`: String, default ""
      - `height.mobile_unit`: String, default "px"
      - `min_height.value`: String, default ""
      - `min_height.unit`: String, default "px"
      - `min_height.mobile_value`: String, default ""
      - `min_height.mobile_unit`: String, default "px"
      - `max_height.value`: String, default ""
      - `max_height.unit`: String, default "px"
      - `max_height.mobile_value`: String, default ""
      - `max_height.mobile_unit`: String, default "px"
      - `object_fit`: String
      - `aspect_ratio.custom`: Boolean, default false
      - `aspect_ratio.ratio`: String, default ""
      - `margin_left.value`: String, default "0"
      - `margin_left.unit`: String, default "px"
      - `margin_left.mobile_value`: String, default ""
      - `margin_left.mobile_unit`: String, default "px"
      - `margin_right.value`: String, default "0"
      - `margin_right.unit`: String, default "px"
      - `margin_right.mobile_value`: String, default ""
      - `margin_right.mobile_unit`: String, default "px"
      - `margin_top.value`: String, default "0"
      - `margin_top.unit`: String, default "px"
      - `margin_top.mobile_value`: String, default ""
      - `margin_top.mobile_unit`: String, default "px"
      - `margin_bottom.value`: String, default "0"
      - `margin_bottom.unit`: String, default "px"
      - `margin_bottom.mobile_value`: String, default ""
      - `margin_bottom.mobile_unit`: String, default "px"
      - `padding_left.value`: String, default "0"
      - `padding_left.unit`: String, default "px"
      - `padding_left.mobile_value`: String, default ""
      - `padding_left.mobile_unit`: String, default "px"
      - `padding_right.value`: String, default "0"
      - `padding_right.unit`: String, default "px"
      - `padding_right.mobile_value`: String, default ""
      - `padding_right.mobile_unit`: String, default "px"
      - `padding_top.value`: String, default "0"
      - `padding_top.unit`: String, default "px"
      - `padding_top.mobile_value`: String, default ""
      - `padding_top.mobile_unit`: String, default "px"
      - `padding_bottom.value`: String, default "0"
      - `padding_bottom.unit`: String, default "px"
      - `padding_bottom.mobile_value`: String, default ""
      - `padding_bottom.mobile_unit`: String, default "px"
      - `border_left.style`: String, default "none"
      - `border_left.color`: String
      - `border_left.width.value`: String, default "0"
      - `border_left.width.unit`: String, default "px"
      - `border_right.style`: String, default "none"
      - `border_right.color`: String
      - `border_right.width.value`: String, default "0"
      - `border_right.width.unit`: String, default "px"
      - `border_top.style`: String, default "none"
      - `border_top.color`: String
      - `border_top.width.value`: String, default "0"
      - `border_top.width.unit`: String, default "px"
      - `border_bottom.style`: String, default "none"
      - `border_bottom.color`: String
      - `border_bottom.width.value`: String, default "0"
      - `border_bottom.width.unit`: String, default "px"
      - `border_top_left_radius.value`: String, default "0"
      - `border_top_left_radius.unit`: String, default "px"
      - `border_top_right_radius.value`: String, default "0"
      - `border_top_right_radius.unit`: String, default "px"
      - `border_bottom_left_radius.value`: String, default "0"
      - `border_bottom_left_radius.unit`: String, default "px"
      - `border_bottom_right_radius.value`: String, default "0"
      - `border_bottom_right_radius.unit`: String, default "px"
      - `border_radius.value`: String, default "0"
      - `border_radius.unit`: String, default "px"
      - `specific_border_radius`: Boolean, default false
      - `box_shadows`: Object[]
      - `backgroundColor`: String
      - `backgroundGradient.enabled`: Boolean, default false
      - `backgroundGradient.type`: String
      - `backgroundGradient.degrees`: Number
      - `backgroundGradient.radialShape`: String
      - `backgroundGradient.radialPosition`: String
      - `backgroundGradient.colors`: Object[]
      - `backgroundGradient.css`: String
      - `backgroundVideo`: String
      - `backgroundImage`: String
      - `backgroundSize`: String
      - `backgroundPosition`: String
      - `backgroundRepeat`: String
      - `showOnMobile`: Boolean, default true
      - `showOnDesktop`: Boolean, default true
      - `components`: Object[]
      - `sections`: Object[]
      - `_id`: ObjectId
    - `_id`: ObjectId
  - `ai_meta`: Mixed
  - `_id`: ObjectId
- `suitability_price_plans`: ObjectId[], → PricePlan - PricePlan ids.
- `isSuitable`: Boolean - Set per request, not stored: whether the signed-in user's price plans match the page's suitability plans.
- `archived`: Boolean, default false - Hidden from the site and the admin's page list without being deleted.
- `archivedAt`: Date - When the page was archived.
- `_id`: ObjectId
- `updatedAt`: Date
- `createdAt`: Date

### AboutPageImport

- `title`: String, required - Name of the imported file.
- `url`: String, required - Address the import was read from, when it came from a link.
- `media`: Buffer, required - The uploaded file's bytes.
- `metadata`: String - Details read from the file (type, size, source) as JSON text.
- `_id`: ObjectId
- `updatedAt`: Date
- `createdAt`: Date

### ChatMessage

- `chat_id`: ObjectId, required, → ChatHistory - ChatHistory.id.
- `role`: String, one of user | model - One of: user, model.
- `text`: String - The message's text.
- `target_url`: String - Admin page the message links to.
- `action`: String, one of RESPONSE | REDIRECT | OBJECT_RESPONSE | ERROR | CLARIFY | PROPOSED_ACTION - One of: RESPONSE, REDIRECT, OBJECT_RESPONSE, ERROR, CLARIFY, PROPOSED_ACTION.
- `options`: String[] - Quick replies offered under the message.
- `data.object`: Mixed
- `data.object_type`: String
- `data.edit_summary`: Mixed
- `data.edit_diff`: Mixed
- `data.undo_record`: Mixed
- `data.hero_animation_offer`: Mixed
- `data.preview_state`: String
- `data.pending_credit_cost`: Number
- `data.target_restore`: Mixed
- `data.media_request`: Mixed
- `data.item_request`: Mixed
- `data.reference_request`: Mixed
- `data.section_options`: Mixed
- `data.section_blueprint`: Mixed
- `data.section_style_options`: Mixed
- `data.section_style_blueprint`: Mixed
- `data.section_role_picker`: Mixed
- `data.small_section_regeneration_options`: Mixed
- `data.small_section_regeneration_target`: Mixed
- `data.small_section_regeneration_prompt`: String
- `data.selected_small_section_action`: Mixed
- `data.component_regeneration_input`: Mixed
- `data.selected_section_role`: Mixed
- `data.selected_section_option`: Mixed
- `data.selected_section_style`: Mixed
- `data.section_options_target`: Mixed
- `data.section_options_prompt`: String
- `data.style_options_target`: Mixed
- `data.style_options_prompt`: String
- `data.previous_container`: Mixed
- `data.preserved_custom_components`: Mixed[]
- `data.selection_request`: Mixed
- `data.proposed_action`: Mixed
- `data.plan_result`: Mixed
- `data.parsed_intent`: Mixed
- `data.parsed_intent_version`: Number
- `data.parsed_intent_at`: Date
- `data.item_id`: ObjectId
- `data.design_archetype`: String
- `data.design_palette`: Mixed
- `data.out_of_credits`: Boolean
- `data.site_wide_edit`: Mixed
- `data.redesign`: Mixed
- `medias`: Object[] - Pictures attached to the message.
  - `media`: ObjectId, → Media
  - `gcs_url`: String
  - `gemini_uri`: String
  - `gemini_interaction_id`: String
  - `_id`: ObjectId
- `products`: ObjectId[], → Product - Product ids.
- `services`: ObjectId[], → Service - Service ids.
- `blogposts`: ObjectId[], → BlogPost - BlogPost ids.
- `programs`: ObjectId[], → Program - Program ids.
- `suggestions`: Object[] - Next steps suggested under the message.
  - `text`: String
  - `route`: String
  - `item_id`: ObjectId
  - `medias`: Mixed[]
  - `picture`: ObjectId
  - `_id`: ObjectId
- `is_system`: Boolean, default false - Written by the platform, not by the owner or the AI.
- `_id`: ObjectId
- `updatedAt`: Date
- `createdAt`: Date

### Collection

A group or filter of items; the prefix of its name is its kind (product/, grouping/, service/, blogpost/...).

- `name`: String, required - Written "<kind>/<Name>": the prefix is where it shows - product/ (a store category), service/ (a service category), blogpost/, program-category/, donation-category/; filters: grouping/ (in a product category), calendar-filter/ (on the booking page). "service/Deep cleaning". A name without a prefix shows nowhere.
- `parent_collection`: ObjectId, → Collection - Collection.id of the parent collection.
- `picture`: ObjectId, → Media - Media.id.
- `about_page`: ObjectId, → AboutPage - Reference to AboutPage.
- `match_to_option`: Boolean, default false - Matches all products which have an option with match_to_collection true.
- `is_color`: Boolean, default false - Used for products with color options.
- `color`: String - Used to visually show the color on the page when is_color is true.
- `position`: Number - Order among collections of the same kind; lower first.
- `items`: ObjectId[] - Array<Product.id> deprecated.
- `thumbnail`: ObjectId, → Media - Media.id, shown on the store cards instead of picture.
- `_id`: ObjectId

### Comment

- `user`: ObjectId, required, → User - User.id.
- `connected_account_user`: ObjectId, → User - User.id.
- `commented_on`: ObjectId, required - Id of the record commented on (a post, product, service...).
- `comment`: String, required - Comment message.
- `likes`: ObjectId[], → User - User ids.
- `replies`: Object[] - Message.
  - `comment`: String, required
  - `user`: ObjectId, required, → User
  - `likes`: ObjectId[], → User
  - `createdAt`: Date
  - `isHidden`: Boolean, default false
  - `_id`: ObjectId
- `mentions`: ObjectId[], → User - User ids.
- `isHidden`: Boolean, default false - Hidden by a moderator; kept but not shown.
- `medias`: Object[] - Pictures attached to the comment.
  - `media`: ObjectId, → Media
  - `_id`: ObjectId
- `_id`: ObjectId
- `updatedAt`: Date
- `createdAt`: Date

### FileRoute

- `url`: String, required - Root path the file is served at, e.g. 'google1a2b3c4d.html'.
- `content_type`: String - Optional content type override.
- `_id`: ObjectId
- `updatedAt`: Date
- `createdAt`: Date

### Form

- `name`: String - Name of sender.
- `email`: String - Email of sender.
- `subject`: String - Subject.
- `body`: String - Body of text.
- `medias`: Buffer[] - Files sent with the form.
- `additional`: Object[] - Additional fields - types - text field - 0, radio buttons - 1, single select - 2, multiple select - 3, media - 4.
  - `title`: String
  - `option`: String
  - `_id`: ObjectId
- `_id`: ObjectId
- `updatedAt`: Date
- `createdAt`: Date

### Media

An uploaded image, video or file.

- `file`: Buffer - (buffer) base64 image data.
- `file_path`: String - The bytes in Cloud Storage instead of `file` (utils/media-store.js): the object - named by the bytes' sha256, so identical pictures share one - and that hash.
- `alt`: String - Img alt (same as filename if not specified).
- `filename`: String - Name of the stored file.
- `filetype`: String - MIME type of the file.
- `vimeo_uri`: String - The video's Vimeo id, for videos hosted there.
- `vimeo_picture`: String - Thumbnail Vimeo made.
- `duration`: Number - Length of a video in seconds.
- `vimeo_url`: String - Link to the video on Vimeo.
- `youtube_url`: String - Link to the video on YouTube.
- `youtube_picture`: String - Thumbnail of the YouTube video.
- `thumbnail`: String - Small preview of the file.
- `gcs_url`: String - The file's address in cloud storage.
- `gcs_480_30_url`: String - The video transcoded to 480p.
- `gcs_720_30_url`: String - The video transcoded to 720p.
- `gcs_1080_30_url`: String - The video transcoded to 1080p.
- `picture_gcs_url`: String - The picture's address in cloud storage.
- `transcode_status`: String, default "not_started" - 'not_started' | 'pending' | 'error' | 'completed'.
- `owner_id`: ObjectId, → User - User id of owner.
- `sequence.status`: String
- `sequence.started_at`: Date
- `sequence.updated_at`: Date
- `sequence.step`: String
- `sequence.done`: Number
- `sequence.total`: Number
- `sequence.ext`: String
- `sequence.trim_start`: Number
- `sequence.trim_end`: Number
- `sequence.build_id`: String
- `sequence.previous_base`: String
- `sequence.frame_count`: Number
- `sequence.fps`: Number
- `sequence.width`: Number
- `sequence.height`: Number
- `sequence.desktop_path`: String
- `sequence.mobile_path`: String
- `sequence.portrait_path`: String
- `sequence.poster`: String
- `sequence.error`: String
- `source_url`: String - Site-clone real-asset reuse: the original URL this Media was downloaded from (asset-fetcher.js). Lets repeated/duplicate clone runs reuse the same stored asset instead of re-downloading it.
- `source_framing`: String - How a downloaded picture was prepared before it was stored (asset-fetcher.js FRAMING): a copy made before the frame its site baked in was taken off is not reused for one made after.
- `origin`: String - 'ai' for images the AI builder generated (validators.js). The site-readiness checklist counts the items still showing one, so people know which photos to replace with their own. Unset on uploads and on media created before it.
- `width`: Number - Displayed size of an image file (utils/image-size.js), so pages can give the <img> width/height and reserve its space before it loads. Set on save; older media by scripts/backfill-media-size.js.
- `height`: Number - Height of the picture in pixels.
- `_id`: ObjectId
- `updatedAt`: Date
- `createdAt`: Date

### MyContact

The site's name and contact details.

- `zip_code`: String, required - Postal/ZIP code.
- `city`: String, required - The city where the business is located.
- `address`: String, required - The street address.
- `contact`: String - Html format.
- `email`: String, required - The public contact email address.
- `google_app`: String - Link to the business's app on Google Play.
- `workhours`: String - Opening hours as text.
- `logo`: ObjectId - Media.id.
- `site_name`: String, required - The business/site name shown publicly.
- `domain`: String, required - The site's own domain.
- `stock_domain`: String - The platform subdomain the site always answers on.
- `isMarket`: Boolean, required, default false - The site is a marketplace.
- `forbidden_collections`: Object[] - Array of {collection_id}.
  - `collection_id`: ObjectId, → Collection
  - `_id`: ObjectId
- `isInit`: Boolean, default false - The site was set up from a template.
- `template`: String, default "0000" - The template the site was made from.
- `lang`: String - 2-letter language code (e.g. "en", "fr", "de").
- `allowedComponents.services`: Boolean, default true
- `allowedComponents.products`: Boolean, default true
- `allowedComponents.blogposts`: Boolean, default true
- `allowedComponents.programs`: Boolean, default true
- `favicon`: ObjectId - Media.id of the browser tab icon.
- `cacheEnabled`: Boolean - Pages are cached for faster loading.
- `has_completed_startup_prompt`: Boolean, default true - The owner went through the first AI prompt.
- `gmb_location`: String - The Google Business Profile location linked to the site.
- `ga_property`: String - The Google Analytics property linked to the site.
- `site_generated_at`: Date - When the AI builder first built this site (a plan that made the homepage). Written once (runPlan); starts the admin's 15-minute launch offer.
- `admin_first_opened_at`: Date - When the owner first opened the admin; with admin_last_opened_at it tells "never opened it" from "opened it and left".
- `admin_last_opened_at`: Date - When the owner last opened the admin.
- `admin_in_app_browser`: Boolean - The owner last opened the admin inside an app's browser.
- `domain_suggestions`: Mixed - Domain names suggested to the owner, kept so they are not suggested again.
- `_id`: ObjectId

### PwaSettings

- `name`: String - The app's name when installed.
- `short_name`: String - Name under the home screen icon.
- `logo`: ObjectId, → Media - Media.id.
- `background_color`: String - Splash screen colour.
- `description`: String - About the app.
- `screenshots`: Object[] - Screenshots shown in the install prompt.
  - `media`: ObjectId, → Media
  - `is_mobile_preview`: Boolean, default false
  - `_id`: ObjectId
- `_id`: ObjectId
- `updatedAt`: Date
- `createdAt`: Date

### QrCode

- `ticket`: ObjectId, required, → Ticket - Ticket.id.
- `status`: String, one of used | not-used, default "not-used" - One of: used, not-used.
- `_id`: ObjectId
- `updatedAt`: Date
- `createdAt`: Date

### Ticket

- `event`: ObjectId, required, → Event - Event.id.
- `date`: Number, required - Index of the event date.
- `section`: Number - Index of the section.
- `subsection`: Number - Index of the subsection.
- `row`: Number - Index of the row.
- `seat`: Number - Index of the seat.
- `row_index_formated`: Number - Row number as shown to the buyer.
- `seat_index_formated`: Number - Seat number as shown to the buyer.
- `lead_id`: ObjectId, → Lead - Lead.id.
- `socket_id`: String - The buyer's live connection, while the seat is held.
- `tier.name`: String
- `tier.price`: Number
- `status`: String, one of frozen | waiting-for-payment | paid | frozen-by-admin | bought-by-admin, default "frozen" - One of: frozen, waiting-for-payment, paid, frozen-by-admin, bought-by-admin.
- `expiration_time`: Date, required - When a held seat is released if not paid.
- `qr_code`: ObjectId, → QrCode - QrCode.id.
- `_id`: ObjectId
- `updatedAt`: Date
- `createdAt`: Date

### TrackerProgress

- `score`: String - The value entered.
- `user_id`: ObjectId, → User - User.id.
- `media`: Buffer - Picture added to the entry.
- `additional_fields`: Mixed - Values of the tracker's extra fields.
- `goal`: Mixed - The goal the entry counts towards.
- `_id`: ObjectId
- `updatedAt`: Date
- `createdAt`: Date

### Video

A video on the site - uploaded or generated - shown in galleries and programs.

- `name`: String, required - The video's title.
- `link`: String - Link to the video.
- `collection_id`: ObjectId, → Collection - Collection.id.
- `media_id`: ObjectId, → Media - Media.id.
- `selected`: Boolean, default false - Featured: the home page's highlighted videos block shows these first.
- `custom_1`: String - Custom defined field 1.
- `custom_2`: String - Custom defined field 2.
- `custom_3`: String - Custom defined field 3.
- `suitability_price_plans`: ObjectId[], → PricePlan - PricePlan ids.
- `isSuitable`: Boolean - Set per request, not stored: whether the signed-in user's price plans match the video's suitability plans.
- `views`: ObjectId[], → User - User ids.
- `total_views`: Number, default 0 - How many times it was opened.
- `times_showed`: Number, default 0 - How many times it was shown in a list.
- `owner_id`: ObjectId, → User - User.id.
- `requiresLogin`: Boolean, default false - Only signed-in users can watch it.
- `requiresSubscription`: Boolean, default false - Only subscribers of price_plan_ids can watch it.
- `price_plan_ids`: String[] - Price plans whose subscribers can watch it.
- `_id`: ObjectId
- `updatedAt`: Date
- `createdAt`: Date

### VideoInteraction

- `start`: Number, required - Second the viewer started watching from.
- `end`: Number, required - Second they stopped at.
- `watch_time`: Number, required - Seconds watched.
- `video_viewer`: ObjectId, required, → User - User.id.
- `video_id`: ObjectId, required, → Post - Post.id.
- `video_owner`: ObjectId, required, → User - User.id.
- `_id`: ObjectId
- `updatedAt`: Date
- `createdAt`: Date
