Docs

Site API reference

Version 6.3.0 · Markdown version at /api/v1/docs

casadigital.pt is the single owner of all customer data. Client websites — and any other app built on this platform — never touch the database directly: they read and write through this HTTP API.

Every tenant (a Site) has its own API key, issued when the site is provisioned. The key both authenticates the request and identifies the tenant, so no site identifier needs to be sent.

Building a site on the platform?

This page documents the endpoints. The full integration contract — environment, caching, the invalidation endpoint a client site should expose, limits and error handling — is at /api/v1/integration.md, described as OpenAPI 3.1 at /api/v1/openapi.json and indexed for agents at /llms.txt.

Authentication

All authenticated endpoints expect the key in the Authorization header: "Authorization: Bearer sk_<48 hex chars>".

Requests with a missing or unknown key receive 401. Keys are managed by Casa Digital; if a key leaks it can be regenerated, which immediately invalidates the old one.

Authorization: Bearer sk_0123abcd…

Content resources

Content is served one resource at a time so each consumer fetches only what it renders and caches it on its own schedule. There is no combined content document.

Every resource response is wrapped in the same envelope: { version, siteKey, <resource> }. Compare version against the contract version you built against and log loudly on a major mismatch.

An empty site is a valid site. Singleton resources come back fully defaulted (absent strings are empty strings) and collections come back as empty arrays, so a website whose content has not been authored yet still renders. Content problems are never signalled by a 404.

The platform serves design-agnostic business data only. Layout and section copy belong to each website's own design and are not part of these resources.

Endpoints

GET

/api/v1/settings

API key

Business name, tagline, description, contact details, opening schedule and social links for the authenticated site.

Response

200 with { version, siteKey, settings } — empty content included. 401 on bad key.

{
  "version": "6.3.0",
  "siteKey": "my-business",
  "settings": {
    "name": "…",
    "tagline": "…",
    "phone": "+351 912 345 678",
    "schedule": [{ "days": "Seg – Sáb", "hours": "09h – 19h" }],
    "social": { "instagram": "…", "facebook": "" }
  }
}
FieldTypeRequiredDescription
namestringnoBusiness name as displayed across the site.
taglinestringnoOne-line slogan shown near the logo/footer. Portuguese.
descriptionstringnoShort description of the business, 1-2 sentences. Portuguese.
phonestringnoDisplay 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.
emailstringnoPublic contact email. Empty string if none.
whatsappstringnoWhatsApp chat URL (e.g. "https://wa.me/351912345678"). Empty string if none.
appUrlstringnoMobile app store URL if the business has an app. Empty string if none.
schedulearray of objectnoOpening schedule rows, in display order.
schedule[].daysstringnoDay range label (e.g. "Seg – Sáb"). Portuguese.
schedule[].hoursstringnoOpening hours label (e.g. "09h – 19h" or "Fechado").
socialobjectnoSocial media profile links.
social.facebookstringnoFacebook page URL. Empty string if none.
social.instagramstringnoInstagram profile URL. Empty string if none.
social.linkedinstringnoLinkedIn page URL. Empty string if none.
social.xstringnoX/Twitter profile URL. Empty string if none.
social.youtubestringnoYouTube channel URL. Empty string if none.
GET

/api/v1/services

API key

Services for the authenticated site in display order, each with a slug, title, icon key, display tier, copy and an optional image.

An image arrives as an absolute `url` served from the blob store's CDN — usable as an image source as it stands, provided your image optimiser allows the host.

Response

200 with { version, siteKey, services } — empty content included. 401 on bad key.

{
  "version": "6.3.0",
  "siteKey": "my-business",
  "services": [
    {
      "slug": "mudancas",
      "title": "Mudanças",
      "icon": "truck",
      "tier": "primary",
      "short": "…",
      "description": "…",
      "bullets": ["…"]
    }
  ]
}
FieldTypeRequiredDescription
[].slugstringyesURL-safe unique identifier, lowercase, hyphen-separated.
[].titlestringnoService name. Portuguese.
[].icon"truck" | "wrench" | "packageCheck" | "hammer" | "boxes" | "warehouse" | "arrowDownToLine" | "zap" | "clock" | "shieldCheck"yesIcon key from the fixed icon set (Lucide-style names).
[].imageobjectnoIllustrative image stored in Vercel Blob.
[].image.assetIdstringnoAsset document id, set by the backoffice after upload.
[].image.urlstringnoAbsolute 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.altstringnoImage alt text. Portuguese.
[].image.widthintegernoImage width in pixels.
[].image.heightintegernoImage height in pixels.
[].tier"primary" | "featured" | "secondary"yesDisplay tier: "primary" for the main highlighted services, "featured" for the secondary highlights, "secondary" for the rest.
[].shortstringnoOne-sentence summary shown on service cards. Portuguese.
[].descriptionstringnoFull description. Portuguese.
[].bulletsarray of stringnoBullet-point highlights. Portuguese.
GET

/api/v1/products

API key

Products for the authenticated site in display order, each with a slug, title, icon key, copy, an optional image and a price.

The price is an amount plus its currency and billing period, so each website formats it in its own style. A price of 0 means it is not published, and `available: false` marks a product kept on record but not currently sold.

Response

200 with { version, siteKey, products } — empty content included. 401 on bad key.

{
  "version": "6.3.0",
  "siteKey": "my-business",
  "products": [
    {
      "slug": "caixa-cartao-60x40",
      "title": "Caixa de cartão 60x40",
      "icon": "boxes",
      "sku": "CX-6040",
      "price": 2.5,
      "currency": "EUR",
      "pricePeriod": "once",
      "priceNote": "IVA incluído",
      "featured": false,
      "available": true
    }
  ]
}
FieldTypeRequiredDescription
[].slugstringyesURL-safe unique identifier, lowercase, hyphen-separated.
[].titlestringnoProduct name. Portuguese.
[].icon"truck" | "wrench" | "packageCheck" | "hammer" | "boxes" | "warehouse" | "arrowDownToLine" | "zap" | "clock" | "shieldCheck"yesIcon key from the fixed icon set (Lucide-style names).
[].imageobjectnoIllustrative image stored in Vercel Blob.
[].image.assetIdstringnoAsset document id, set by the backoffice after upload.
[].image.urlstringnoAbsolute 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.altstringnoImage alt text. Portuguese.
[].image.widthintegernoImage width in pixels.
[].image.heightintegernoImage height in pixels.
[].skustringnoInternal reference or article number. Empty string if none.
[].pricenumbernoPrice amount in the currency below. 0 means the price is not published.
[].currency"EUR" | "USD" | "GBP"noCurrency of the price amount.
[].pricePeriod"once" | "month" | "year"noBilling period the price refers to: "once" for a one-off price, "month" or "year" for a subscription.
[].priceNotestringnoShort qualifier shown next to the price (e.g. "IVA incluído"). Portuguese.
[].shortstringnoOne-sentence summary shown on product cards. Portuguese.
[].descriptionstringnoFull description. Portuguese.
[].bulletsarray of stringnoBullet-point highlights. Portuguese.
[].featuredbooleannoTrue for a product the website should highlight.
[].availablebooleannoFalse for a product kept on record but not currently sold.
GET

/api/v1/projects

API key

Work the business has already delivered, in display order: the client it was for, then the problem, the solution and the results, plus related links.

Every attached file arrives in one `assets` array with its absolute `url`, `kind` (`image`, `video` or `document`), MIME type, original filename, byte size and, for media, its pixel dimensions. Group by `kind` if your design shows media separately from documents; the `url` is ready to use as a source or a download link.

Response

200 with { version, siteKey, projects } — empty content included. 401 on bad key.

{
  "version": "6.3.0",
  "siteKey": "my-business",
  "projects": [
    {
      "slug": "projeto-mudanca-armazem",
      "client": "Padaria Central",
      "problem": "…",
      "solution": "…",
      "results": "…",
      "links": [{ "title": "Website do cliente", "url": "https://exemplo.pt" }],
      "assets": [
        {
          "kind": "image",
          "url": "https://abc123xyz.public.blob.vercel-storage.com/sites/my-business/antes-abc123.webp",
          "contentType": "image/webp",
          "name": "antes.webp",
          "alt": "",
          "size": 184320,
          "width": 1600,
          "height": 1067
        },
        {
          "kind": "document",
          "url": "https://abc123xyz.public.blob.vercel-storage.com/sites/my-business/relatorio-def456.pdf",
          "contentType": "application/pdf",
          "name": "relatorio.pdf",
          "alt": "",
          "size": 240128
        }
      ]
    }
  ]
}
FieldTypeRequiredDescription
[].slugstringyesURL-safe unique identifier, lowercase, hyphen-separated.
[].titlestringnoName 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.
[].clientstringnoName 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.
[].problemstringnoThe situation the client was in. Portuguese.
[].solutionstringnoWhat the business did about it. Portuguese.
[].resultsstringnoWhat the client got out of it. Portuguese.
[].linksarray of objectnoRelated links, in display order.
[].links[].titlestringnoLink label. Portuguese. May be empty — fall back to showing the URL.
[].links[].urlstringnoAbsolute URL, including the scheme.
[].assetsarray of objectnoEvery 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"yesWhat 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[].urlstringyesAbsolute 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[].contentTypestringnoMIME type the file was stored with (e.g. "image/webp", "application/pdf").
[].assets[].namestringnoOriginal filename at upload time. Use it as the download label for a document.
[].assets[].altstringnoAlt text, when one was written. Often empty — fall back to the surrounding context.
[].assets[].sizeintegernoFile size in bytes, when known.
[].assets[].widthintegernoIntrinsic width in pixels. Present for media, absent for documents.
[].assets[].heightintegernoIntrinsic height in pixels. Present for media, absent for documents.
GET

/api/v1/posts

API key

News and articles the business writes itself, newest first by `publishedAt`. Only published posts are served: a draft is absent from this list entirely, so there is no state to check before rendering one.

`body` is an HTML fragment, unlike every other text on this API. It is sanitised when the tenant saves it, against a fixed allowlist — `p`, `h2`–`h4`, `strong`, `em`, `u`, `s`, `code`, `pre`, `blockquote`, `ul`, `ol`, `li`, `a`, `img`, `hr`, `br` — so that is the whole set of elements your stylesheet has to cover. Headings start at `h2`, because the `h1` of the page is the post's `title`.

An `img` in a body is always a file from this tenant's own blob store, with the same absolute `url` shape as every other file on this API. It carries `alt`, its pixel dimensions and `data-align` (`left`, `center` or `right`) — the writer's intent, for your design to interpret. There is no inline CSS and no class attribute.

`publishedAt` and `updatedOn` are calendar dates, not instants: format them in UTC or the day will read as the one before for anyone west of Greenwich.

Response

200 with { version, siteKey, posts } — empty content included. 401 on bad key.

{
  "version": "6.3.0",
  "siteKey": "my-business",
  "posts": [
    {
      "slug": "novo-armazem-em-leiria",
      "title": "Novo armazém em Leiria",
      "excerpt": "Duplicámos a capacidade de armazenamento na zona centro.",
      "body": "<p>A partir de <strong>setembro</strong> passamos a operar…</p><h2>O que muda para os clientes</h2><p>…</p><img src=\"https://abc123xyz.public.blob.vercel-storage.com/sites/my-business/armazem-abc123.webp\" alt=\"Interior do novo armazém\" width=\"1600\" height=\"1067\" data-align=\"center\" data-asset-id=\"6712f0a1b2c3d4e5f6a7b8c9\">",
      "coverImage": {
        "url": "https://abc123xyz.public.blob.vercel-storage.com/sites/my-business/capa-abc123.webp",
        "alt": "Fachada do novo armazém",
        "width": 1600,
        "height": 900
      },
      "publishedAt": "2026-08-24",
      "updatedOn": "",
      "tags": ["armazenamento", "leiria"],
      "author": "Ana Ferreira"
    }
  ]
}
FieldTypeRequiredDescription
[].slugstringyesURL-safe unique identifier, lowercase, hyphen-separated.
[].titlestringnoHeadline of the post. Portuguese.
[].excerptstringnoStandfirst: one or two sentences summarising the post. Plain text, no markup — it is written for a card and for a meta description. Portuguese.
[].bodystringnoThe 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") — style the alignment yourself, there is no inline CSS. Empty string for a post with no body yet.
[].coverImageobjectnoLead image for the post, shown on a card and at the top of the page.
[].coverImage.assetIdstringnoAsset document id, set by the backoffice after upload.
[].coverImage.urlstringnoAbsolute 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.altstringnoImage alt text. Portuguese.
[].coverImage.widthintegernoImage width in pixels.
[].coverImage.heightintegernoImage height in pixels.
[].publishedAtstringnoThe 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.
[].updatedOnstringnoThe 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.
[].tagsarray of stringnoFree-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.
[].authorstringnoWho wrote it, as it should be credited. Empty when the business does not by-line its posts.
GET

/api/v1/testimonials

API key

What clients have said about the business, in display order. There is no draft state and nothing to filter: a testimonial the tenant no longer wants is deleted, and one created but not yet written is withheld, so everything here has words in it.

`rating` is a whole number of stars out of 5, and 0 means the testimonial came without one — show the words alone rather than an empty row of stars.

`author` may be empty (an anonymous testimonial) and `image`, the photograph of the person quoted, is usually absent. `date` is a calendar date: format it in UTC.

Response

200 with { version, siteKey, testimonials } — empty content included. 401 on bad key.

{
  "version": "6.3.0",
  "siteKey": "my-business",
  "testimonials": [
    {
      "slug": "padaria-central",
      "author": "Ana Ferreira",
      "role": "Gerente, Padaria Central",
      "quote": "Entregam sempre à hora combinada. Em dois anos nunca falhámos uma abertura.",
      "rating": 5,
      "date": "2026-06-12",
      "image": {
        "url": "https://abc123xyz.public.blob.vercel-storage.com/sites/my-business/ana-abc123.webp",
        "alt": "Ana Ferreira",
        "width": 400,
        "height": 400
      }
    }
  ]
}
FieldTypeRequiredDescription
[].slugstringyesURL-safe unique identifier, lowercase, hyphen-separated.
[].authorstringnoWho said it, as they should be credited. Empty for an anonymous testimonial.
[].rolestringnoWho the author is, in their own words (e.g. "Gerente, Padaria Central"). One line, meant to be read under the name. Portuguese.
[].quotestringnoWhat they said. Plain text, no markup. Portuguese.
[].ratingintegernoStars out of 5. 0 means the testimonial came without a rating — show the words alone rather than zero stars.
[].datestringnoThe day it was given, as "YYYY-MM-DD". Empty when it is not dated. A calendar date, not an instant: format it in UTC.
[].imageobjectnoPhotograph of the author, when there is one. Often absent.
[].image.assetIdstringnoAsset document id, set by the backoffice after upload.
[].image.urlstringnoAbsolute 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.altstringnoImage alt text. Portuguese.
[].image.widthintegernoImage width in pixels.
[].image.heightintegernoImage height in pixels.
GET

/api/v1/faqs

API key

The questions the business answers before they are asked, in display order, with the questions of a group together.

`category` is the tenant's own heading for a group and is free text, not a vocabulary: group on the exact string, and expect an ungrouped question (an empty `category`) beside grouped ones.

`answer` is plain text — split it on its line breaks to lay out paragraphs, as with a legal section's body. `slug` is stable, so it works as the anchor of a question you can link to.

Response

200 with { version, siteKey, faqs } — empty content included. 401 on bad key.

{
  "version": "6.3.0",
  "siteKey": "my-business",
  "faqs": [
    {
      "slug": "fazem-entregas-ao-sabado",
      "question": "Fazem entregas ao sábado?",
      "answer": "Sim, entre as 9h e as 13h na área metropolitana de Lisboa.\n\nFora dessa área, apenas em dias úteis.",
      "category": "Entregas"
    }
  ]
}
FieldTypeRequiredDescription
[].slugstringyesURL-safe unique identifier, lowercase, hyphen-separated. Stable, so it works as the anchor of a linkable question.
[].questionstringnoThe question, as a client would ask it. Portuguese.
[].answerstringnoThe answer. Plain text, no markup; split it on its line breaks to lay out paragraphs, like a legal section's body. Portuguese.
[].categorystringnoFree-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.
GET

/api/v1/locations

API key

Locations for the authenticated site in display order, with address lines and optional Google Maps URLs. At most one is flagged primary.

Response

200 with { version, siteKey, locations } — empty content included. 401 on bad key.

{
  "version": "6.3.0",
  "siteKey": "my-business",
  "locations": [
    {
      "slug": "lisboa",
      "city": "Lisboa",
      "lines": ["Rua Exemplo 12", "1000-001 Lisboa"],
      "primary": true
    }
  ]
}
FieldTypeRequiredDescription
[].slugstringyesURL-safe unique identifier, lowercase, hyphen-separated.
[].citystringnoCity or locality name.
[].linesarray of stringnoAddress lines, in display order.
[].mapsSearchUrlstringnoGoogle Maps search/share URL for this address. Empty string if none.
[].mapEmbedUrlstringnoGoogle Maps embed URL (iframe src). Empty string if none.
[].primarybooleannoTrue for the main location (at most one per site).
GET

/api/v1/values

API key

Company values for the authenticated site in display order.

Response

200 with { version, siteKey, values } — empty content included. 401 on bad key.

{
  "version": "6.3.0",
  "siteKey": "my-business",
  "values": [{ "slug": "qualidade", "title": "Qualidade", "description": "…" }]
}
FieldTypeRequiredDescription
[].slugstringyesURL-safe unique identifier, lowercase, hyphen-separated.
[].titlestringnoValue name (e.g. Qualidade). Portuguese.
[].descriptionstringnoShort explanation of the value. Portuguese.
GET

/api/v1/legal

API key

Terms and privacy text the client maintains in the backoffice, keyed by document, each an ordered list of title and body sections.

This is the only page-shaped content the platform holds. Which pages a website has, what they say and how they are titled belongs to the website itself.

Response

200 with { version, siteKey, legal } — empty content included. 401 on bad key.

{
  "version": "6.3.0",
  "siteKey": "my-business",
  "legal": {
    "terms": { "sections": [{ "title": "1. Objeto", "body": "…" }] },
    "privacy": { "sections": [{ "title": "1. Dados recolhidos", "body": "…" }] }
  }
}
FieldTypeRequiredDescription
<key>objectnoOne legal document.
<key>.sectionsarray of objectnoOrdered sections, as the document should read.
<key>.sections[].titlestringnoSection heading. Portuguese.
<key>.sections[].bodystringnoSection body prose; plain text, may contain multiple sentences. Portuguese.
POST

/api/v1/leads

API key

Creates a lead for the authenticated site — typically a quote or contact form submission. The lead enters the backoffice CRM pipeline with status "new".

If the tenant enabled SMS or Telegram notifications in the backoffice, they are notified. Unless they turned it off, the platform also emails the person who filled the form to confirm it arrived — so your site should not send a confirmation of its own. Notification failures never fail the request.

Request body

FieldTypeRequiredDescription
namestringyesContact name. Required.
emailstring (email)yesContact email. Required.
phonestringnoContact phone, free format.
formType"quote" | "contact"noWhich website form produced the lead. Defaults to "quote" for older client sites that do not send it.
serviceSlugstringnoSlug of the service the visitor is interested in.
originDestinationstringnoPickup / delivery description, for transport-style quote forms.
messagestringyesThe visitor's message. Required.

Response

201 with { success: true, id }. 422 with field issues when the payload is invalid. 401 on bad key.

{
  "success": true,
  "id": "665f1c2ab8d3e0a1f0c4d5e6"
}

Content formats

The shape of each resource is documented separately: JSON Schema at /api/content-schema, human-readable reference at /docs/content-api, Markdown for AI agents at /api/content-schema/docs. Open the content format reference.