Content API reference
Schema version 6.9.0
casadigital.pt hosts a multi-tenant backoffice where customers manage the content of their own websites. Each customer website fetches its content through the authenticated Site API (documented at /api-docs); only casadigital.pt talks to the database.
This page documents the content formats: the normalized database collections and the per-resource shapes served by the Site API. It exposes field formats only, never content.
Endpoints
- GET /api/v1/integration.md — the full client-site integration contract (start here to build a site).
- GET /api/v1/openapi.json — OpenAPI 3.1 description of every Site API endpoint.
- GET /api/content-schema — versioned JSON Schemas of the Site API resources.
- GET /api/content-schema/entities — versioned JSON Schemas of the normalized collections.
- GET /api/content-schema/docs — this documentation as Markdown (best for AI agents).
- GET /api/v1/docs — the Site API reference as Markdown (human version at /api-docs).
- GET /llms.txt — index of everything an agent should read (/llms-full.txt for one fetch).
Tenant model
Every tenant is one document in the sites collection. Its key field (the siteKey) identifies the tenant; consumer apps authenticate with the site's API key, which resolves to this document.
All other content documents carry a siteId field referencing the owning sites document. companies and siteSettings are one-per-site; legalTexts is one-per-document-key; services, products, projects, posts, testimonials, faqs, locations and values are one-per-slug within a site; blocks are one-per-key within a site; assets are one-per-pathname.
All documents also carry MongoDB _id, createdAt and updatedAt fields managed by the database layer; they are omitted from the tables below.
Contract scope
The platform owns design-agnostic business data only: business identity and contacts, opening hours, the services performed, the products sold, the projects published, the posts written, the testimonials received, the questions answered, locations, company values, and the legal documents the client maintains. Every client website has its own bespoke design, so page structure, layout, section copy and metadata (band headings, stat figures, call-to-action labels, intro blocks) live in the client site's own repository and are deliberately absent from this contract.
Website blocks are the one opening in that rule, and it is the site's to make. A site may declare some of its own copy as blocks — named groups of typed fields (text, richText, number, image, link, list, collection) — in a manifest it syncs with PUT /api/v1/blocks/definitions. The platform then stores one blocks document per declared block, the owner edits its values in the backoffice, and the site reads them from GET /api/v1/blocks. The platform stores the fields and their values only, never the site's markup or styles; which blocks exist, and on which page, is decided by the site's manifest, not in the backoffice.
Each resource maps to one collection: settings to companies, services/products/projects/testimonials/faqs/locations/values to their per-slug collections (array position stored in each document's order field), posts to the posts collection (ordered by the publishedAt date each post carries rather than by a sort index), legal to one legalTexts document per document key, and blocks to one blocks document per declared block key (in the manifest's order, hidden blocks left out). Platform-only data never leaves: companies.availability is the schedule designer's working data (client sites read the schedule lines derived from it), posts.status decides whether a post is served at all rather than being served alongside it, and the whole siteSettings document holds backoffice preferences.
A service is work scoped per job and carries no price; a product is something sold at a stated price, stored as an amount plus its currency and billing period so each website can format it in its own style; a project is work already delivered, told as problem, solution and results; a post is something the business wrote, dated and published on its own decision; a testimonial is what somebody else said about the business, optionally rated and attributed; an FAQ is one question and its answer, grouped under a heading the tenant chose.
Resource responses omit siteId and every platform-only field, and they are fully defaulted: missing strings come back as empty strings and missing arrays as empty arrays. A site with no content yet returns valid empty resources rather than an error, so client sites can render an empty state. Block values are the exception: a block field with no value is absent from its block's values, so the site renders its own fallback copy.
Two kinds of field are markup rather than text: a post's body and a block's richText fields are HTML fragments. Each is sanitised against its own fixed element allowlist when the tenant saves it, so what the collection stores is already what may be rendered, and a consumer inserts it as HTML rather than escaping it. Every other text field on this contract is plain.
Two collections are served with a document withheld rather than flagged, so a consumer never has a state to check: an unpublished post, and a testimonial created but not yet written. Everything else in a collection is served as stored.
Content can be multilingual. A site has a main content language (sites.defaultContentLocale) and any number of extra ones (sites.contentLocales), from a fixed list. Main-language text lives in each document's ordinary fields; every other language lives beside it in translations.<locale>, holding only that type's translatable fields. Everything else — slugs, prices, URLs, dates, icons, addresses, the names of people and clients — is language-neutral and stored once. The Site API resolves a language at read time: each resource takes ?locale=, a field with no translation falls back to the main language and is named in the item's untranslated list, and the opening-hours lines are derived from availability in the language asked for. A legal document is served translated whole or not at all. Removing a language from a site hides it from the API and deletes nothing. The translations field itself is never served: a client site receives one language per response, in the resource's ordinary shape.
Every uploaded file lives in the assets collection, stored in Vercel Blob through the backoffice. The pathname is the storage key and stays internal: the Site API delivers each file as an absolute url on the store's CDN, which a client site uses directly. A service, a product, a testimonial or a post's cover embeds the pathname of its single image; a project references whole asset documents, so its files arrive with their kind, MIME type, name, size and pixel dimensions resolved. A post does both: the images inside its body carry their URLs in the markup, and the document lists the asset ids behind them so an image still in use is never retired.
Site API resources
GET /api/v1/settings
The business: identity, brand, contacts, opening hours and social links. One per site.
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | no | Business name as displayed across the site. |
| tagline | string | no | One-line slogan shown near the logo/footer. In the response language (`locale`). |
| description | string | no | Short description of the business, 1-2 sentences. In the response language (`locale`). |
| phone | string | no | Display phone number, formatted for reading (e.g. "+351 912 345 678"). Empty string if none. Derive a tel: URI from it by stripping everything but + and digits. |
| string | no | Public contact email. Empty string if none. | |
| string | no | WhatsApp chat URL (e.g. "https://wa.me/351912345678"). Empty string if none. | |
| appUrl | string | no | Mobile app store URL if the business has an app. Empty string if none. |
| schedule | array of object | no | Opening schedule rows, in display order. |
| schedule[].days | string | no | Day range label (e.g. "Segunda – Sábado"). In the response language (`locale`), the main one included: derived from the weekly hours the business drew on every read. |
| schedule[].hours | string | no | Opening hours label (e.g. "09h – 19h" or "Fechado"). |
| social | object | no | Social media profile links. |
| social.facebook | string | no | Facebook page URL. Empty string if none. |
| social.instagram | string | no | Instagram profile URL. Empty string if none. |
| social.linkedin | string | no | LinkedIn page URL. Empty string if none. |
| social.x | string | no | X/Twitter profile URL. Empty string if none. |
| social.youtube | string | no | YouTube channel URL. Empty string if none. |
| brand | object | no | The brand: logo and colour palette. Language-neutral: identical in every `locale`. |
| brand.logo | object | no | The company logo. |
| brand.logo.url | string | no | Absolute URL of the logo, an SVG file on the blob store's CDN. Empty string when the business has uploaded none. Sanitised and with its background removed on upload, so it is safe to inline and sits on any colour; served as `image/svg+xml`. |
| brand.logo.width | integer | no | Width of the logo's canvas (its viewBox), for its aspect ratio — an SVG has no pixel size. |
| brand.logo.height | integer | no | Height of the logo's canvas (its viewBox). |
| brand.palette | array of object | no | The brand's colours, in the order the business arranged them — any number, possibly none. The order is the business's own and carries no fixed meaning (primary, secondary…); use the names if it matters to your design. |
| brand.palette[].name | string | no | What the business calls the colour (e.g. "Azul principal"). Free text, possibly empty, the same in every language. |
| brand.palette[].hex | string | yes | The colour as six-digit lowercase hex, e.g. "#1a2b3c". Never has an alpha channel. |
| untranslated | array of string | no | Only when the response's `locale` is not the site's main language: the fields of this item that have no translation yet (e.g. "description", "image.alt"), served in the main language instead. A list (`bullets`, `tags`) is served whole in one language: a translation with fewer entries than the main list is not served, the main list is, and the field is named here. An untranslated `image.alt` may carry a stand-in already in the response language instead (see `alt`). Absent when the item is fully translated. |
GET /api/v1/services
Services offered by the business, in display order.
| Field | Type | Required | Description |
|---|---|---|---|
| [].slug | string | yes | URL-safe unique identifier, lowercase, hyphen-separated. |
| [].title | string | no | Service name. In the response language (`locale`). |
| [].icon | "truck" | "wrench" | "packageCheck" | "hammer" | "boxes" | "warehouse" | "arrowDownToLine" | "zap" | "clock" | "shieldCheck" | yes | Icon key from the fixed icon set (Lucide-style names). |
| [].image | object | no | Illustrative image stored in Vercel Blob. |
| [].image.assetId | string | no | Asset document id, set by the backoffice after upload. |
| [].image.url | string | no | Absolute URL of the image, served from the blob store's CDN. Use it directly as an image source; no API key is involved. Allow the host in your image optimiser. |
| [].image.alt | string | no | Image alt text. In the response language (`locale`). When the alt has no translation yet it is named in `untranslated` (`image.alt`, `coverImage.alt`) and, rather than the main-language alt, the item's translated title — or a testimonial author's name — is served where there is one. |
| [].image.width | integer | no | Image width in pixels. |
| [].image.height | integer | no | Image height in pixels. |
| [].tier | "primary" | "featured" | "secondary" | yes | Display tier: "primary" for the main highlighted services, "featured" for the secondary highlights, "secondary" for the rest. |
| [].short | string | no | One-sentence summary shown on service cards. In the response language (`locale`). |
| [].description | string | no | Full description. In the response language (`locale`). |
| [].bullets | array of string | no | Bullet-point highlights. In the response language (`locale`), whole: a translation with fewer entries than the main list is not served — the main list is, and `bullets` is named in `untranslated`. |
| [].untranslated | array of string | no | Only when the response's `locale` is not the site's main language: the fields of this item that have no translation yet (e.g. "description", "image.alt"), served in the main language instead. A list (`bullets`, `tags`) is served whole in one language: a translation with fewer entries than the main list is not served, the main list is, and the field is named here. An untranslated `image.alt` may carry a stand-in already in the response language instead (see `alt`). Absent when the item is fully translated. |
GET /api/v1/products
Products sold by the business, in display order.
| Field | Type | Required | Description |
|---|---|---|---|
| [].slug | string | yes | URL-safe unique identifier, lowercase, hyphen-separated. |
| [].title | string | no | Product name. In the response language (`locale`). |
| [].icon | "truck" | "wrench" | "packageCheck" | "hammer" | "boxes" | "warehouse" | "arrowDownToLine" | "zap" | "clock" | "shieldCheck" | yes | Icon key from the fixed icon set (Lucide-style names). |
| [].image | object | no | Illustrative image stored in Vercel Blob. |
| [].image.assetId | string | no | Asset document id, set by the backoffice after upload. |
| [].image.url | string | no | Absolute URL of the image, served from the blob store's CDN. Use it directly as an image source; no API key is involved. Allow the host in your image optimiser. |
| [].image.alt | string | no | Image alt text. In the response language (`locale`). When the alt has no translation yet it is named in `untranslated` (`image.alt`, `coverImage.alt`) and, rather than the main-language alt, the item's translated title — or a testimonial author's name — is served where there is one. |
| [].image.width | integer | no | Image width in pixels. |
| [].image.height | integer | no | Image height in pixels. |
| [].sku | string | no | Internal reference or article number. Empty string if none. |
| [].price | number | no | Price amount in the currency below. 0 means the price is not published. |
| [].currency | "EUR" | "USD" | "GBP" | no | Currency of the price amount. |
| [].pricePeriod | "once" | "month" | "year" | no | Billing period the price refers to: "once" for a one-off price, "month" or "year" for a subscription. |
| [].priceNote | string | no | Short qualifier shown next to the price (e.g. "IVA incluído"). In the response language (`locale`). |
| [].short | string | no | One-sentence summary shown on product cards. In the response language (`locale`). |
| [].description | string | no | Full description. In the response language (`locale`). |
| [].bullets | array of string | no | Bullet-point highlights. In the response language (`locale`), whole: a translation with fewer entries than the main list is not served — the main list is, and `bullets` is named in `untranslated`. |
| [].featured | boolean | no | True for a product the website should highlight. |
| [].available | boolean | no | False for a product kept on record but not currently sold. |
| [].untranslated | array of string | no | Only when the response's `locale` is not the site's main language: the fields of this item that have no translation yet (e.g. "description", "image.alt"), served in the main language instead. A list (`bullets`, `tags`) is served whole in one language: a translation with fewer entries than the main list is not served, the main list is, and the field is named here. An untranslated `image.alt` may carry a stand-in already in the response language instead (see `alt`). Absent when the item is fully translated. |
GET /api/v1/projects
Projects published by the business, in display order.
| Field | Type | Required | Description |
|---|---|---|---|
| [].slug | string | yes | URL-safe unique identifier, lowercase, hyphen-separated. |
| [].title | string | no | Name of the project, as the website should title it. Read this as the heading; a project stored before 6.1.0 may carry only `client`, so fall back to that when this is empty. |
| [].client | string | no | Name of the client the work was delivered for. Empty when there is no client to name — the business's own product, or work it may not attribute. |
| [].problem | string | no | The situation the client was in. In the response language (`locale`). |
| [].solution | string | no | What the business did about it. In the response language (`locale`). |
| [].results | string | no | What the client got out of it. In the response language (`locale`). |
| [].links | array of object | no | Related links, in display order. |
| [].links[].title | string | no | Link label. In the response language (`locale`). May be empty — fall back to showing the URL. |
| [].links[].url | string | no | Absolute URL, including the scheme. |
| [].assets | array of object | no | Every attached file in one list, in display order. Group them by `kind` if your design shows media separately from documents. |
| [].assets[].kind | "image" | "video" | "document" | yes | What the file is, derived from its MIME type: "image" and "video" are media, everything else is a "document". Split a list on this rather than on the file extension. |
| [].assets[].url | string | yes | Absolute URL of the file, served from the blob store's CDN. Use it directly — as an image source, a video source, or the href of a download link — with no API key involved. |
| [].assets[].contentType | string | no | MIME type the file was stored with (e.g. "image/webp", "application/pdf"). |
| [].assets[].name | string | no | Original filename at upload time. Use it as the download label for a document. |
| [].assets[].alt | string | no | Alt text, when one was written. Often empty — fall back to the surrounding context. |
| [].assets[].size | integer | no | File size in bytes, when known. |
| [].assets[].width | integer | no | Intrinsic width in pixels. Present for media, absent for documents. |
| [].assets[].height | integer | no | Intrinsic height in pixels. Present for media, absent for documents. |
| [].untranslated | array of string | no | Only when the response's `locale` is not the site's main language: the fields of this item that have no translation yet (e.g. "description", "image.alt"), served in the main language instead. A list (`bullets`, `tags`) is served whole in one language: a translation with fewer entries than the main list is not served, the main list is, and the field is named here. An untranslated `image.alt` may carry a stand-in already in the response language instead (see `alt`). Absent when the item is fully translated. |
GET /api/v1/posts
Published posts, newest first by publishedAt. Drafts are absent, so the list is exactly what may be shown.
| Field | Type | Required | Description |
|---|---|---|---|
| [].slug | string | yes | URL-safe unique identifier, lowercase, hyphen-separated. |
| [].title | string | no | Headline of the post. In the response language (`locale`). |
| [].excerpt | string | no | Standfirst: one or two sentences summarising the post. Plain text, no markup — it is written for a card and for a meta description. In the response language (`locale`). |
| [].body | string | no | The post itself, as an HTML fragment (no <html> or <body> wrapper). Render it as HTML; it is sanitised on save against a fixed allowlist, so the only elements it can contain are p, h2, h3, h4, strong, em, u, s, code, pre, blockquote, ul, ol, li, a, img, hr, br. Headings start at h2, because the page's h1 is the title above. An <a> carries href, and one leaving the site also carries target and rel. An <img> is always a file from this tenant's own blob store and carries src, alt, width, height and data-align ("left", "center" or "right"); a <p>, <h2>, <h3> or <h4> the writer centred or right-aligned carries data-align ("center" or "right"), and a left-aligned one nothing — style the alignment yourself, there is no inline CSS. Empty string for a post with no body yet. |
| [].coverImage | object | no | Lead image for the post, shown on a card and at the top of the page. |
| [].coverImage.assetId | string | no | Asset document id, set by the backoffice after upload. |
| [].coverImage.url | string | no | Absolute URL of the image, served from the blob store's CDN. Use it directly as an image source; no API key is involved. Allow the host in your image optimiser. |
| [].coverImage.alt | string | no | Image alt text. In the response language (`locale`). When the alt has no translation yet it is named in `untranslated` (`image.alt`, `coverImage.alt`) and, rather than the main-language alt, the item's translated title — or a testimonial author's name — is served where there is one. |
| [].coverImage.width | integer | no | Image width in pixels. |
| [].coverImage.height | integer | no | Image height in pixels. |
| [].publishedAt | string | no | The day the post is dated, as "YYYY-MM-DD". Treat it as a calendar date, not an instant: format it in UTC or the day will read as the one before west of Greenwich. Posts arrive newest first. |
| [].updatedOn | string | no | The day the post was last revised, as "YYYY-MM-DD". Empty when it has not been revised since publishing — useful as dateModified in structured data. |
| [].tags | array of string | no | Free-text labels the business groups its posts by. Display order. Not a fixed vocabulary and not slugs — slugify them yourself if you route by them. In the response language (`locale`), whole: a translation with fewer tags than the main list is not served — the main list is, and `tags` is named in `untranslated`. |
| [].author | string | no | Who wrote it, as it should be credited. Empty when the business does not by-line its posts. |
| [].untranslated | array of string | no | Only when the response's `locale` is not the site's main language: the fields of this item that have no translation yet (e.g. "description", "image.alt"), served in the main language instead. A list (`bullets`, `tags`) is served whole in one language: a translation with fewer entries than the main list is not served, the main list is, and the field is named here. An untranslated `image.alt` may carry a stand-in already in the response language instead (see `alt`). Absent when the item is fully translated. |
GET /api/v1/testimonials
Testimonials the business publishes, in display order.
| Field | Type | Required | Description |
|---|---|---|---|
| [].slug | string | yes | URL-safe unique identifier, lowercase, hyphen-separated. |
| [].author | string | no | Who said it, as they should be credited. Empty for an anonymous testimonial. |
| [].role | string | no | Who the author is, in their own words (e.g. "Gerente, Padaria Central"). One line, meant to be read under the name. In the response language (`locale`). |
| [].quote | string | no | What they said. Plain text, no markup. In the response language (`locale`). |
| [].rating | integer | no | Stars out of 5. 0 means the testimonial came without a rating — show the words alone rather than zero stars. |
| [].date | string | no | The day it was given, as "YYYY-MM-DD". Empty when it is not dated. A calendar date, not an instant: format it in UTC. |
| [].image | object | no | Photograph of the author, when there is one. Often absent. |
| [].image.assetId | string | no | Asset document id, set by the backoffice after upload. |
| [].image.url | string | no | Absolute URL of the image, served from the blob store's CDN. Use it directly as an image source; no API key is involved. Allow the host in your image optimiser. |
| [].image.alt | string | no | Image alt text. In the response language (`locale`). When the alt has no translation yet it is named in `untranslated` (`image.alt`, `coverImage.alt`) and, rather than the main-language alt, the item's translated title — or a testimonial author's name — is served where there is one. |
| [].image.width | integer | no | Image width in pixels. |
| [].image.height | integer | no | Image height in pixels. |
| [].untranslated | array of string | no | Only when the response's `locale` is not the site's main language: the fields of this item that have no translation yet (e.g. "description", "image.alt"), served in the main language instead. A list (`bullets`, `tags`) is served whole in one language: a translation with fewer entries than the main list is not served, the main list is, and the field is named here. An untranslated `image.alt` may carry a stand-in already in the response language instead (see `alt`). Absent when the item is fully translated. |
GET /api/v1/faqs
Frequently asked questions, in display order. Questions sharing a category arrive together, in that order.
| Field | Type | Required | Description |
|---|---|---|---|
| [].slug | string | yes | URL-safe unique identifier, lowercase, hyphen-separated. Stable, so it works as the anchor of a linkable question. |
| [].question | string | no | The question, as a client would ask it. In the response language (`locale`). |
| [].answer | string | no | The answer. Plain text, no markup; split it on its line breaks to lay out paragraphs, like a legal section's body. In the response language (`locale`). |
| [].category | string | no | Free-text heading this question is grouped under (e.g. "Entregas"). Empty for an ungrouped question. Not a fixed vocabulary and not a slug — group by the exact string, and expect an empty one alongside filled ones. In the response language (`locale`), falling back per question: one question whose category is untranslated arrives with the main-language heading (and `category` in its `untranslated`), so grouping by the exact string puts it under a heading of its own. |
| [].untranslated | array of string | no | Only when the response's `locale` is not the site's main language: the fields of this item that have no translation yet (e.g. "description", "image.alt"), served in the main language instead. A list (`bullets`, `tags`) is served whole in one language: a translation with fewer entries than the main list is not served, the main list is, and the field is named here. An untranslated `image.alt` may carry a stand-in already in the response language instead (see `alt`). Absent when the item is fully translated. |
GET /api/v1/locations
Physical locations of the business, in display order.
| Field | Type | Required | Description |
|---|---|---|---|
| [].slug | string | yes | URL-safe unique identifier, lowercase, hyphen-separated. |
| [].city | string | no | City or locality name. |
| [].lines | array of string | no | Address lines, in display order. |
| [].mapsSearchUrl | string | no | Google Maps search/share URL for this address. Empty string if none. |
| [].mapEmbedUrl | string | no | Google Maps embed URL (iframe src). Empty string if none. |
| [].primary | boolean | no | True for the main location (at most one per site). |
GET /api/v1/values
Company values, in display order.
| Field | Type | Required | Description |
|---|---|---|---|
| [].slug | string | yes | URL-safe unique identifier, lowercase, hyphen-separated. |
| [].title | string | no | Value name (e.g. Qualidade). In the response language (`locale`). |
| [].description | string | no | Short explanation of the value. In the response language (`locale`). |
| [].untranslated | array of string | no | Only when the response's `locale` is not the site's main language: the fields of this item that have no translation yet (e.g. "description", "image.alt"), served in the main language instead. A list (`bullets`, `tags`) is served whole in one language: a translation with fewer entries than the main list is not served, the main list is, and the field is named here. An untranslated `image.alt` may carry a stand-in already in the response language instead (see `alt`). Absent when the item is fully translated. |
GET /api/v1/legal
Legal documents the client maintains, keyed by document. Only documents that have been written are present.
| Field | Type | Required | Description |
|---|---|---|---|
| <key> | object | no | One legal document. |
| <key>.sections | array of object | no | Ordered sections, as the document should read. |
| <key>.sections[].title | string | no | Section heading. In the response language (`locale`). |
| <key>.sections[].body | string | no | Section body prose; plain text, may contain multiple sentences. In the response language (`locale`). |
| <key>.untranslated | array of string | no | Only when the response's `locale` is not the site's main language: the fields of this item that have no translation yet (e.g. "description", "image.alt"), served in the main language instead. A list (`bullets`, `tags`) is served whole in one language: a translation with fewer entries than the main list is not served, the main list is, and the field is named here. An untranslated `image.alt` may carry a stand-in already in the response language instead (see `alt`). Absent when the item is fully translated. |
GET /api/v1/blocks
GET /api/v1/blocks: one entry per block the site's manifest declares, in the manifest's order, including blocks that hold only defaults or nothing yet. Blocks dropped from the manifest are not served.
| Field | Type | Required | Description |
|---|---|---|---|
| [].key | string | yes | The block's key, as the manifest declares it. |
| [].values | object | yes | The block's values by field id: what the owner saved, or the manifest's defaults where nothing was saved. A field with no value (never set, cleared by the owner, or a collection) is absent: render your own fallback. |
| [].values.<key> | unknown | no | |
| [].untranslated | array of string | no | Only when the response's `locale` is not the site's main language and something fell back: the paths of the values served in the main language for want of a translation (`title`, `cta.label`, `photo.alt`, `steps.2.label`, or a list's id when the whole list fell back). |
Normalized collections
sites
sites collection: one document per tenant (client website). Sites have no lifecycle state — a client site always serves the latest content.
| Field | Type | Required | Description |
|---|---|---|---|
| key | string | yes | Unique tenant slug (siteKey). Echoed in every Site API response envelope; the tenant itself is selected by the API key, not by this value. |
| name | string | yes | Business name. |
| domain | string | no | Production domain of the client site. The backoffice's Website editor previews https://<domain>, so it must be the host the site is actually served on (www.example.pt when the bare domain redirects there). |
| defaultContentLocale | "pt" | "en" | "es" | "fr" | "de" | "it" | "nl" | no | The main content language: the one every document's ordinary fields are written in. Absent means "pt". Served as `defaultLocale` in every Site API envelope. |
| contentLocales | array of "pt" | "en" | "es" | "fr" | "de" | "it" | "nl" | no | The extra languages the site publishes, besides the main one. Removing one hides it from the Site API and deletes no translation. Served, main language first, as `locales` in every Site API envelope. |
companies
companies collection: the business itself, one document per site (unique siteId).
| Field | Type | Required | Description |
|---|---|---|---|
| siteId | string | yes | Tenant scope: ObjectId of the owning document in the sites collection. |
| name | string | yes | Business name as displayed across the site. |
| tagline | string | no | One-line slogan. In the site's main language; other languages in `translations`. |
| description | string | no | Short business description. In the site's main language; other languages in `translations`. |
| phone | string | no | Display phone number, formatted for reading. |
| string | no | Public contact email. | |
| string | no | WhatsApp chat URL. | |
| appUrl | string | no | Mobile app store URL. |
| social | object | no | Social media profile links. |
| social.facebook | string | no | Facebook page URL. |
| social.instagram | string | no | Instagram profile URL. |
| social.linkedin | string | no | LinkedIn page URL. |
| social.x | string | no | X/Twitter profile URL. |
| social.youtube | string | no | YouTube channel URL. |
| availability | array of object | no | Structured weekly opening hours from the backoffice designer. Never served to client sites — they read the derived schedule below. |
| availability[].id | string | yes | Stable slot identifier. |
| availability[].week_day | integer | yes | Day of week, 0 = Sunday. |
| availability[].start_time | string | yes | Start time as "HH:mm". |
| availability[].end_time | string | yes | End time as "HH:mm". |
| availability[].active | boolean | no | False for a slot kept but not in effect. |
| schedule | array of object | no | Display lines derived from availability on save, in the site's main language and in display order. Other languages are derived from availability when they are read. |
| schedule[].days | string | yes | Day range label (e.g. "Segunda – Sábado"). |
| schedule[].hours | string | yes | Opening hours label (e.g. "09:00 – 19:00"). |
| brand | object | no | How the business looks. Language-neutral, so never in `translations`. |
| brand.logo | object | no | The company logo. Absent when none was uploaded. |
| brand.logo.assetId | string | no | The assets document of the logo: an SVG, sanitised on upload. |
| brand.logo.pathname | string | no | Storage pathname of the logo in the public blob store. |
| brand.logo.width | number | no | Width of the SVG's canvas (viewBox). |
| brand.logo.height | number | no | Height of the SVG's canvas (viewBox). |
| brand.palette | array of object | no | Brand colours, in display order. |
| brand.palette[].name | string | no | The tenant's own name for the colour. |
| brand.palette[].hex | string | yes | "#rrggbb", lowercase. |
| translations | object keyed by "pt" | "en" | "es" | "fr" | "de" | "it" | "nl" | no | The document's text in the site's other content languages, keyed by language. Each entry holds only the fields written in a language; a field left out or empty is served in the main language, and an entry for a language the site no longer publishes is kept, not deleted. |
| translations.<key> | object | no | Translatable fields of the company (settings). |
| translations.<key>.tagline | string | no | One-line slogan. |
| translations.<key>.description | string | no | Short description of the business. |
siteSettings
siteSettings collection: backoffice preferences, one document per site (unique siteId).
| Field | Type | Required | Description |
|---|---|---|---|
| siteId | string | yes | Tenant scope: ObjectId of the owning document in the sites collection. |
| crm | object | no | Backoffice notification preferences. Never served to client sites. |
| crm.smsOnNewLead | boolean | no | Send an SMS when a lead arrives. |
| crm.notificationsPhone | string | no | Number to notify. |
| crm.telegramOnNewLead | boolean | no | Send a Telegram message when a lead arrives. |
| crm.telegramBotToken | string | no | Token of the tenant's own Telegram bot, from BotFather. |
| crm.telegramChatId | string | no | Chat to post into: a person, a group or a channel. |
| crm.autoReplyOnNewLead | boolean | no | Email the person who filled the form a confirmation. Absent means on. |
| crm.autoReplyWindow | string | no | How soon the business answers, in its own words ("24 horas úteis"). |
services
services collection: one document per service (unique siteId+slug).
| Field | Type | Required | Description |
|---|---|---|---|
| siteId | string | yes | Tenant scope: ObjectId of the owning document in the sites collection. |
| slug | string | yes | URL-safe identifier, unique per site. |
| title | string | yes | Service name. In the site's main language; other languages in `translations`. |
| icon | "truck" | "wrench" | "packageCheck" | "hammer" | "boxes" | "warehouse" | "arrowDownToLine" | "zap" | "clock" | "shieldCheck" | yes | Icon key from the fixed icon set. |
| image | object | no | Optional illustrative image stored in Vercel Blob. |
| image.assetId | string | no | Reference to the assets collection. |
| image.pathname | string | no | Blob storage pathname; the Site API delivers it to client sites as an absolute url. |
| image.alt | string | no | Image alt text. In the site's main language; other languages in `translations` (as `imageAlt`, or `coverAlt` on a post). |
| image.width | integer | no | Image width in pixels. |
| image.height | integer | no | Image height in pixels. |
| tier | "primary" | "featured" | "secondary" | yes | Display tier: "primary", "featured", or "secondary". |
| short | string | no | One-sentence summary. In the site's main language; other languages in `translations`. |
| description | string | no | Full description. In the site's main language; other languages in `translations`. |
| bullets | array of string | no | Bullet-point highlights. In the site's main language; other languages in `translations`. |
| order | integer | no | Sort index, ascending. |
| translations | object keyed by "pt" | "en" | "es" | "fr" | "de" | "it" | "nl" | no | The document's text in the site's other content languages, keyed by language. Each entry holds only the fields written in a language; a field left out or empty is served in the main language, and an entry for a language the site no longer publishes is kept, not deleted. |
| translations.<key> | object | no | Translatable fields of a service. |
| translations.<key>.title | string | no | Service name. |
| translations.<key>.short | string | no | One-line summary. |
| translations.<key>.description | string | no | Full description. |
| translations.<key>.bullets | array of string | no | Highlights, in the same order as the main language's. Served whole, and only once it has as many entries as the main list. |
| translations.<key>.imageAlt | string | no | Alt text of the service image. |
products
products collection: one document per product (unique siteId+slug).
| Field | Type | Required | Description |
|---|---|---|---|
| siteId | string | yes | Tenant scope: ObjectId of the owning document in the sites collection. |
| slug | string | yes | URL-safe identifier, unique per site. |
| title | string | yes | Product name. In the site's main language; other languages in `translations`. |
| icon | "truck" | "wrench" | "packageCheck" | "hammer" | "boxes" | "warehouse" | "arrowDownToLine" | "zap" | "clock" | "shieldCheck" | yes | Icon key from the fixed icon set. |
| image | object | no | Optional illustrative image stored in Vercel Blob. |
| image.assetId | string | no | Reference to the assets collection. |
| image.pathname | string | no | Blob storage pathname; the Site API delivers it to client sites as an absolute url. |
| image.alt | string | no | Image alt text. In the site's main language; other languages in `translations` (as `imageAlt`, or `coverAlt` on a post). |
| image.width | integer | no | Image width in pixels. |
| image.height | integer | no | Image height in pixels. |
| sku | string | no | Internal reference or article number. |
| price | number | no | Price amount; 0 when not published. |
| currency | "EUR" | "USD" | "GBP" | yes | Currency of the price amount. |
| pricePeriod | "once" | "month" | "year" | yes | Billing period: "once", "month" or "year". |
| priceNote | string | no | Short qualifier shown next to the price. In the site's main language; other languages in `translations`. |
| short | string | no | One-sentence summary. In the site's main language; other languages in `translations`. |
| description | string | no | Full description. In the site's main language; other languages in `translations`. |
| bullets | array of string | no | Bullet-point highlights. In the site's main language; other languages in `translations`. |
| featured | boolean | no | True for a product the website should highlight. |
| available | boolean | no | False for a product not currently sold. |
| order | integer | no | Sort index, ascending. |
| translations | object keyed by "pt" | "en" | "es" | "fr" | "de" | "it" | "nl" | no | The document's text in the site's other content languages, keyed by language. Each entry holds only the fields written in a language; a field left out or empty is served in the main language, and an entry for a language the site no longer publishes is kept, not deleted. |
| translations.<key> | object | no | Translatable fields of a product. |
| translations.<key>.title | string | no | Product name. |
| translations.<key>.short | string | no | One-line summary. |
| translations.<key>.description | string | no | Full description. |
| translations.<key>.bullets | array of string | no | Highlights. Served whole, and only once it has as many entries as the main list. |
| translations.<key>.priceNote | string | no | Note shown beside the price. |
| translations.<key>.imageAlt | string | no | Alt text of the product image. |
projects
projects collection: one document per project (unique siteId+slug).
| Field | Type | Required | Description |
|---|---|---|---|
| siteId | string | yes | Tenant scope: ObjectId of the owning document in the sites collection. |
| slug | string | yes | URL-safe identifier, unique per site. |
| title | string | no | Name of the project. The heading a website shows. In the site's main language; other languages in `translations`. |
| client | string | no | Name of the client the work was delivered for. Absent for the business's own work. |
| problem | string | no | The situation the client was in. In the site's main language; other languages in `translations`. |
| solution | string | no | What the business did about it. In the site's main language; other languages in `translations`. |
| results | string | no | What the client got out of it. In the site's main language; other languages in `translations`. |
| links | array of object | no | Related links, in display order. |
| links[].title | string | no | Link label. In the site's main language; other languages in `translations`. |
| links[].url | string | no | Absolute URL. |
| assets | array of string | no | References into the assets collection, in display order. One list for media and documents alike; the Site API resolves each reference and reports its kind. |
| order | integer | no | Sort index, ascending. |
| translations | object keyed by "pt" | "en" | "es" | "fr" | "de" | "it" | "nl" | no | The document's text in the site's other content languages, keyed by language. Each entry holds only the fields written in a language; a field left out or empty is served in the main language, and an entry for a language the site no longer publishes is kept, not deleted. |
| translations.<key> | object | no | Translatable fields of a project. |
| translations.<key>.title | string | no | Project title. |
| translations.<key>.problem | string | no | The problem the project solved. |
| translations.<key>.solution | string | no | What was delivered. |
| translations.<key>.results | string | no | What it achieved. |
| translations.<key>.linkTitles | object | no | Link titles, keyed by the link's URL. |
| translations.<key>.linkTitles.<key> | string | no |
posts
posts collection: one document per post (unique siteId+slug). Ordered by publishedAt rather than by a stored sort index, because a post is dated and a service is arranged.
| Field | Type | Required | Description |
|---|---|---|---|
| siteId | string | yes | Tenant scope: ObjectId of the owning document in the sites collection. |
| slug | string | yes | URL-safe identifier, unique per site. |
| title | string | yes | Headline of the post. In the site's main language; other languages in `translations`. |
| excerpt | string | no | One- or two-sentence summary, plain text. In the site's main language; other languages in `translations`. |
| body | string | no | The post itself, as an HTML fragment. Sanitised on save against a fixed allowlist of elements and attributes, so what is stored is already what may be rendered. In the site's main language; a translated body in `translations` is sanitised the same way. |
| coverImage | object | no | Optional lead image stored in Vercel Blob. |
| coverImage.assetId | string | no | Reference to the assets collection. |
| coverImage.pathname | string | no | Blob storage pathname; the Site API delivers it to client sites as an absolute url. |
| coverImage.alt | string | no | Image alt text. In the site's main language; other languages in `translations` (as `imageAlt`, or `coverAlt` on a post). |
| coverImage.width | integer | no | Image width in pixels. |
| coverImage.height | integer | no | Image height in pixels. |
| status | "draft" | "published" | yes | Whether the post is published. Platform-only: the Site API serves "published" posts and omits drafts entirely, so no client site ever sees this field. |
| publishedAt | string | no | The day the post is dated, as "YYYY-MM-DD". Posts are read newest first. |
| updatedOn | string | no | The day the post was last revised, as "YYYY-MM-DD". Absent until it is revised. |
| tags | array of string | no | Free-text labels, in display order. In the site's main language; other languages in `translations`. |
| author | string | no | Who the post is credited to. |
| assets | array of string | no | Every asset the post uses: its cover image and each image placed in the body, in any language. Derived from the bodies on save, not edited directly — it is the record of which files the post uses. |
| translations | object keyed by "pt" | "en" | "es" | "fr" | "de" | "it" | "nl" | no | The document's text in the site's other content languages, keyed by language. Each entry holds only the fields written in a language; a field left out or empty is served in the main language, and an entry for a language the site no longer publishes is kept, not deleted. |
| translations.<key> | object | no | Translatable fields of a post. |
| translations.<key>.title | string | no | Post title. |
| translations.<key>.excerpt | string | no | Summary shown in lists. |
| translations.<key>.body | string | no | Post body, an HTML fragment. |
| translations.<key>.tags | array of string | no | Tags. Served whole, and only once it has as many entries as the main list. |
| translations.<key>.coverAlt | string | no | Alt text of the cover image. |
testimonials
testimonials collection: one document per testimonial (unique siteId+slug).
| Field | Type | Required | Description |
|---|---|---|---|
| siteId | string | yes | Tenant scope: ObjectId of the owning document in the sites collection. |
| slug | string | yes | URL-safe identifier, unique per site. |
| author | string | no | Who said it. Absent for an anonymous testimonial. |
| role | string | no | Who the author is, in one line. In the site's main language; other languages in `translations`. |
| quote | string | no | What they said, plain text. Absent only for a testimonial created and not yet written; the Site API withholds those. In the site's main language; other languages in `translations`. |
| rating | integer | no | Stars out of 5. 0 or absent means the testimonial came without a rating. |
| date | string | no | The day it was given, as "YYYY-MM-DD". |
| image | object | no | Optional photograph of the author, stored in Vercel Blob. |
| image.assetId | string | no | Reference to the assets collection. |
| image.pathname | string | no | Blob storage pathname; the Site API delivers it to client sites as an absolute url. |
| image.alt | string | no | Image alt text. In the site's main language; other languages in `translations` (as `imageAlt`, or `coverAlt` on a post). |
| image.width | integer | no | Image width in pixels. |
| image.height | integer | no | Image height in pixels. |
| order | integer | no | Sort index, ascending. |
| translations | object keyed by "pt" | "en" | "es" | "fr" | "de" | "it" | "nl" | no | The document's text in the site's other content languages, keyed by language. Each entry holds only the fields written in a language; a field left out or empty is served in the main language, and an entry for a language the site no longer publishes is kept, not deleted. |
| translations.<key> | object | no | Translatable fields of a testimonial. |
| translations.<key>.quote | string | no | The quotation, translated. |
| translations.<key>.role | string | no | The author's role or company description. |
| translations.<key>.imageAlt | string | no | Alt text of the author's photograph. |
faqs
faqs collection: one document per question (unique siteId+slug).
| Field | Type | Required | Description |
|---|---|---|---|
| siteId | string | yes | Tenant scope: ObjectId of the owning document in the sites collection. |
| slug | string | yes | URL-safe identifier, unique per site. Usable as a link anchor. |
| question | string | yes | The question. In the site's main language; other languages in `translations`. |
| answer | string | no | The answer, plain text. In the site's main language; other languages in `translations`. |
| category | string | no | Free-text heading this question is grouped under. Absent for an ungrouped question. In the site's main language; other languages in `translations`. |
| order | integer | no | Sort index, ascending. |
| translations | object keyed by "pt" | "en" | "es" | "fr" | "de" | "it" | "nl" | no | The document's text in the site's other content languages, keyed by language. Each entry holds only the fields written in a language; a field left out or empty is served in the main language, and an entry for a language the site no longer publishes is kept, not deleted. |
| translations.<key> | object | no | Translatable fields of a question. |
| translations.<key>.question | string | no | The question. |
| translations.<key>.answer | string | no | The answer. |
| translations.<key>.category | string | no | The group heading. Translate it the same way across the group: each question falls back on its own, so an untranslated one is grouped apart. |
locations
locations collection: one document per location (unique siteId+slug).
| Field | Type | Required | Description |
|---|---|---|---|
| siteId | string | yes | Tenant scope: ObjectId of the owning document in the sites collection. |
| slug | string | yes | URL-safe identifier, unique per site. |
| city | string | yes | City or locality name. |
| lines | array of string | yes | Address lines, in display order. |
| mapsSearchUrl | string | no | Google Maps search/share URL. |
| mapEmbedUrl | string | no | Google Maps embed URL (iframe src). |
| primary | boolean | no | True for the main location. At most one per site (enforced by a partial unique index). |
| order | integer | no | Sort index, ascending. |
values
values collection: one document per company value (unique siteId+slug).
| Field | Type | Required | Description |
|---|---|---|---|
| siteId | string | yes | Tenant scope: ObjectId of the owning document in the sites collection. |
| slug | string | yes | URL-safe identifier, unique per site. |
| title | string | yes | Value name. In the site's main language; other languages in `translations`. |
| description | string | no | Short explanation. In the site's main language; other languages in `translations`. |
| order | integer | no | Sort index, ascending. |
| translations | object keyed by "pt" | "en" | "es" | "fr" | "de" | "it" | "nl" | no | The document's text in the site's other content languages, keyed by language. Each entry holds only the fields written in a language; a field left out or empty is served in the main language, and an entry for a language the site no longer publishes is kept, not deleted. |
| translations.<key> | object | no | Translatable fields of a company value. |
| translations.<key>.title | string | no | Value name. |
| translations.<key>.description | string | no | What it means. |
legalTexts
legalTexts collection: one document per site per legal document (unique siteId+key). The only page-shaped content the platform stores.
| Field | Type | Required | Description |
|---|---|---|---|
| siteId | string | yes | Tenant scope: ObjectId of the owning document in the sites collection. |
| key | "terms" | "privacy" | yes | Which legal document. Unique per site. |
| sections | array of object | no | Ordered sections, as the document should read. In the site's main language; other languages in `translations`. |
| sections[].title | string | no | Section heading. |
| sections[].body | string | no | Section body prose, plain text. |
| translations | object keyed by "pt" | "en" | "es" | "fr" | "de" | "it" | "nl" | no | The document's text in the site's other content languages, keyed by language. Each entry holds only the fields written in a language; a field left out or empty is served in the main language, and an entry for a language the site no longer publishes is kept, not deleted. |
| translations.<key> | object | no | Translatable fields of a legal document. |
| translations.<key>.sections | array of object | no | The whole document in this language. A translated legal text is served whole or not at all — only once it has as many filled sections as the main document — and is never mixed section by section with the main language. |
| translations.<key>.sections[].title | string | no | |
| translations.<key>.sections[].body | string | no |
blocks
blocks collection: one document per block a tenant's website declares (unique siteId+key). Created and reshaped only by the site's manifest sync (PUT /api/v1/blocks/definitions); its values are written only by the owner in the backoffice. Served by GET /api/v1/blocks, hidden blocks left out.
| Field | Type | Required | Description |
|---|---|---|---|
| siteId | string | yes | Tenant scope: ObjectId of the owning document in the sites collection. |
| key | string | yes | The block's key from the site's manifest, unique per site (e.g. "home.hero"). |
| label | string | yes | What the owner sees in the backoffice's Website editor. From the manifest. |
| page | string | yes | Path of the site page the block is on; the backoffice preview opens it. From the manifest. |
| order | integer | yes | Position in the manifest, ascending. The order of the editor's list and of GET /api/v1/blocks. |
| fields | array of object | yes | The field declarations from the last sync, in form order. Replaced whole by every sync. |
| fields[].id | string | yes | Field id: what the site reads the value by and the markup attribute points at. |
| fields[].type | "text" | "richText" | "number" | "image" | "link" | "list" | "collection" | yes | Field type. |
| fields[].label | string | yes | What the owner sees above the input. |
| fields[].fields | array of unknown | no | A list's item fields, in the same form. |
| fields[].resource | "services" | "products" | "projects" | "posts" | "testimonials" | "faqs" | "locations" | "values" | no | A collection's resource. |
| content | object | yes | Values by field id, in the site's main language: what the owner saved, or a manifest default where nothing was saved. A field the owner cleared is stored as null and served as absent. Values of fields no longer declared, or declared with another type, are kept here and never served. |
| content.<key> | unknown | no | |
| translations | object keyed by "pt" | "en" | "es" | "fr" | "de" | "it" | "nl" | no | The block's text in the site's other content languages, keyed by language: for each field only what is written in a language (a text or rich text, a link's label, an image's alt, and the same inside each list item). Numbers, link targets, images and their framing are the main language's. |
| translations.<key> | object | no | |
| translations.<key>.<key> | unknown | no | |
| hiddenAt | string | null | yes | When a sync found the block missing from the manifest (ISO date), or null while declared. A hidden block keeps its content and returns when the manifest declares it again. |
assets
assets collection: files uploaded through the backoffice to Vercel Blob (unique siteId+pathname). Services, products, testimonials and a post's cover embed the pathname of their single image; projects reference whole documents by _id, which is how they can carry files of mixed kinds with their type and dimensions. A post does both: its body carries the URL of each image it shows and the document lists the ids behind them. One asset may be referenced by several records (a file chosen again from the media library); an image or video is never retired (deletedAt) for being detached — only the media library deletes it, and not while anything uses it — and a document is retired only when nothing references it.
| Field | Type | Required | Description |
|---|---|---|---|
| siteId | string | yes | Tenant scope: ObjectId of the owning document in the sites collection. |
| pathname | string | yes | Blob storage pathname, unique per site. The storage key: the Site API delivers an absolute url built from it, and never the pathname itself. |
| url | string | no | Absolute Blob URL, as returned when the file was uploaded. |
| originalName | string | no | Original filename at upload time. |
| contentType | string | no | MIME type. |
| size | integer | no | File size in bytes. |
| width | integer | no | Image width in pixels. |
| height | integer | no | Image height in pixels. |
| blurDataURL | string | no | Base64 blur placeholder for images. |
| alt | string | no | Default alt text. |
| deletedAt | string | null | no | Soft-delete timestamp (ISO date), or null while active. |
media
media collection: the backoffice media library ("Galeria"), one document per image or video a tenant has, whatever uploaded it (unique siteId+pathname). A layer over assets, never a replacement: content keeps referencing the asset row or the URL, and this document adds the measured dimensions, a name and an archived state. Not served by the Site API — a client site reads the file URLs from the content, as before.
| Field | Type | Required | Description |
|---|---|---|---|
| siteId | string | yes | Tenant scope: ObjectId of the owning document in the sites collection. |
| assetId | string | null | no | The assets document behind the file (1:1), or null for a file referenced by URL with no asset row. |
| pathname | string | yes | Blob storage pathname, unique per site — the same as the asset's. Never leaves the platform. |
| url | string | yes | Absolute Blob URL of the file: the same URL the content already carries, never rewritten. |
| mediaType | "image" | "video" | yes | `image` or `video`. |
| contentType | string | yes | MIME type. |
| size | integer | no | File size in bytes. |
| width | integer | no | Intrinsic width in pixels, when known. |
| height | integer | no | Intrinsic height in pixels, when known. |
| duration | number | no | A video's length in seconds, when its container declares it. |
| dimensionsSource | "file" | "browser" | "none" | yes | Where width and height came from: `file` (read from the file's own header on the server), `browser` (measured by the uploader's browser, or kept from the asset row), `none` (could not be determined). |
| name | string | yes | The tenant's name for the file; the uploaded file name until renamed. |
| originalName | string | no | The file name it was uploaded with. |
| archivedAt | string | null | no | When the tenant archived it (ISO date), or null. Archiving hides it in the backoffice library only. |