# Builderly API reference

Base URL: http://hristovtintdetailng.com
Generated from the running server on 2026-11-09T17:13:29.278Z. 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

### Collections

Collections group and filter the site's items. The prefix of the name is its kind: product/ is a top-level product category, grouping/ a filter inside one, service/ a service category, calendar-filter/ a filter on the booking page, blogpost/ a blog category, and so on. Collections can nest and can show their own page.

##### List all collections
`GET /collections`
Categories and filters; ?type for one kind (product, service, blogpost...). Ordered by position, then name.
- Access: Public
- Query:
  - `type` (string): Only collections with this type.
- Returns: a list of Collection objects.
- Status codes: 200, 500
- Models: Collection

##### Create a collection
`POST /collections`
Creates a category or filter, placed last in its kind's order; createNav also adds it to the navigation. The name's prefix says where it shows (product/, service/, blogpost/...; see the Collection model). Answers 201 with the category.
- Access: Admin token
- Body:
  - `createNav` (string): The collection's create nav.
- Returns: the Collection object.
- Status codes: 201, 400, 401, 500
- Models: Collection, SiteMenu

##### Reorder collections
`PATCH /collections-reorder`
Persist a new display order for a set of collections. Send items: [{ _id, position }]. 400 "No items" for an empty list. Answers 200.
- Access: Admin token
- Body:
  - `items` (string): The collection's items.
- Status codes: 200, 400, 401, 500

##### Retrieve a collection
`GET /collections/:id`
One category, with the grouping filters inside it. With the grouping filters that sit in it or in no category. 404 when there is no such category.
- Access: Public
- Path:
  - `id` (string): The ID of the collection.
- Returns: the Collection object.
- Status codes: 200, 404, 500
- Models: Collection

##### Update a collection
`PATCH /collections/:id`
Updates a category. Fields outside the allowed list are ignored. A rename also renames the navigation entry that links to it. Answers the category, or 404.
- Access: Admin token
- Path:
  - `id` (string): The ID of the collection.
- Returns: the Collection object.
- Status codes: 200, 400, 401, 404, 500
- Models: Collection, SiteMenu

##### Delete a collection
`DELETE /collections/:id`
Deletes a category. Answers the deleted category. Items in it keep its id as collection_id, and a navigation entry linking to it stays: remove those separately.
- Access: Admin token
- Path:
  - `id` (string): The ID of the collection.
- Returns: an empty answer on success.
- Status codes: 200, 401, 404, 500
- Models: Collection

##### Retrieve a collection
`GET /collections/:id/media`
A category's picture. Answers the picture's media id, or 404.
- Access: Public
- Path:
  - `id` (string): The ID of the collection.
- Returns: the Collection object.
- Status codes: 200, 404, 500
- Models: Collection

##### Create a collection
`POST /collections/:id/media`
Uploads a category's picture (media, form field). Multipart upload; becomes the category's picture.
- Access: Admin token
- Path:
  - `id` (string): The ID of the collection.
- Status codes: 200, 400, 401

##### Delete a collection
`DELETE /collections/:id/media`
Removes a category's picture. Deletes the media record too.
- Access: Admin token
- Path:
  - `id` (string): The ID of the collection.
- Returns: an empty answer on success.
- Status codes: 200, 401
- Models: Collection, Media

## Objects

### 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

### 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

### SiteMenu

A link in the site's menu.

- `title`: String - The menu link label shown to users, required.
- `url`: String - Where it leads. The site's own pages as "../<path>": "../about-pages/<page url>" for a custom page, "../products", "../services?collection=service/<name>", "../contacts"; '' for the homepage; a link elsewhere (https://, mailto:) as it is.
- `position`: Number - Display order starting from 0, optional.
- `submenus`: Object[] - The dropdown's links under this entry, each { title, url, position }; url in the same "../<path>" form.
  - `title`: String
  - `url`: String
  - `position`: Number
  - `badge`: String
  - `badge_background`: String
  - `badge_color`: String
  - `badge_radius`: Number
  - `_id`: ObjectId
- `customDropdown`: Boolean, default false - Has custom page attached to this menu.
- `customDropdownId`: ObjectId, → AboutPage - Id of the about page.
- `sideMenu`: Boolean, default false - Shown in the side menu instead of the top bar.
- `sidemenus`: Object[] - Groups of links shown in the side menu, when sideMenu is true.
  - `title`: String
  - `submenus`: Object[]
    - `title`: String
    - `url`: String
    - `position`: Number
    - `icon`: String
    - `img`: ObjectId, → Media
    - `badge`: String
    - `badge_background`: String
    - `badge_color`: String
    - `badge_radius`: Number
    - `submenus`: Object[]
      - `title`: String
      - `url`: String
      - `position`: Number
      - `icon`: String
      - `img`: ObjectId, → Media
      - `badge`: String
      - `badge_background`: String
      - `badge_color`: String
      - `badge_radius`: Number
      - `_id`: ObjectId
    - `_id`: ObjectId
  - `_id`: ObjectId
- `icon`: String - FontAwesome 5 class (e.g. "fas fa-home"), optional.
- `_id`: ObjectId
