# Casa Digital Site API reference

Version: 4.1.0

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.

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

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

## GET /api/v1/settings

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

Authentication: required (Bearer API key)

### Response

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

| 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. Portuguese. |
| `description` | string | no | Short description of the business, 1-2 sentences. Portuguese. |
| `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. |
| `email` | string | no | Public contact email. Empty string if none. |
| `whatsapp` | 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. "Seg – Sáb"). Portuguese. |
| `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. |

```json
{
  "version": "4.1.0",
  "siteKey": "my-business",
  "settings": {
    "name": "…",
    "tagline": "…",
    "phone": "+351 912 345 678",
    "schedule": [{ "days": "Seg – Sáb", "hours": "09h – 19h" }],
    "social": { "instagram": "…", "facebook": "" }
  }
}
```

## GET /api/v1/services

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

Image pathnames refer to GET /api/v1/assets/{pathname}; client sites usually proxy that under their own route.

Authentication: required (Bearer API key)

### Response

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

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `[].slug` | string | yes | URL-safe unique identifier, lowercase, hyphen-separated. |
| `[].title` | string | no | Service name. Portuguese. |
| `[].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.pathname` | string | no | Blob storage pathname; client sites serve it through their own /api/blob/ proxy. |
| `[].image.alt` | string | no | Image alt text. Portuguese. |
| `[].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. Portuguese. |
| `[].description` | string | no | Full description. Portuguese. |
| `[].bullets` | array of string | no | Bullet-point highlights. Portuguese. |

```json
{
  "version": "4.1.0",
  "siteKey": "my-business",
  "services": [
    {
      "slug": "mudancas",
      "title": "Mudanças",
      "icon": "truck",
      "tier": "primary",
      "short": "…",
      "description": "…",
      "bullets": ["…"]
    }
  ]
}
```

## GET /api/v1/products

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.

Authentication: required (Bearer API key)

### Response

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

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `[].slug` | string | yes | URL-safe unique identifier, lowercase, hyphen-separated. |
| `[].title` | string | no | Product name. Portuguese. |
| `[].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.pathname` | string | no | Blob storage pathname; client sites serve it through their own /api/blob/ proxy. |
| `[].image.alt` | string | no | Image alt text. Portuguese. |
| `[].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"). Portuguese. |
| `[].short` | string | no | One-sentence summary shown on product cards. Portuguese. |
| `[].description` | string | no | Full description. Portuguese. |
| `[].bullets` | array of string | no | Bullet-point highlights. Portuguese. |
| `[].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. |

```json
{
  "version": "4.1.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
    }
  ]
}
```

## GET /api/v1/caseStudies

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 `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; fetch each file from `GET /api/v1/assets/{pathname}`.

Authentication: required (Bearer API key)

### Response

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

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `[].slug` | string | yes | URL-safe unique identifier, lowercase, hyphen-separated. |
| `[].client` | string | no | Name of the client the work was delivered for. Doubles as the case study's title. |
| `[].problem` | string | no | The situation the client was in. Portuguese. |
| `[].solution` | string | no | What the business did about it. Portuguese. |
| `[].results` | string | no | What the client got out of it. Portuguese. |
| `[].links` | array of object | no | Related links, in display order. |
| `[].links[].title` | string | no | Link label. Portuguese. 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[].pathname` | string | yes | Blob storage pathname; fetch the file from GET /api/v1/assets/{pathname}, or proxy it through your own route. |
| `[].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. |

```json
{
  "version": "4.1.0",
  "siteKey": "my-business",
  "caseStudies": [
    {
      "slug": "caso-mudanca-armazem",
      "client": "Padaria Central",
      "problem": "…",
      "solution": "…",
      "results": "…",
      "links": [{ "title": "Website do cliente", "url": "https://exemplo.pt" }],
      "assets": [
        {
          "kind": "image",
          "pathname": "sites/my-business/antes-abc123.webp",
          "contentType": "image/webp",
          "name": "antes.webp",
          "alt": "",
          "size": 184320,
          "width": 1600,
          "height": 1067
        },
        {
          "kind": "document",
          "pathname": "sites/my-business/relatorio-def456.pdf",
          "contentType": "application/pdf",
          "name": "relatorio.pdf",
          "alt": "",
          "size": 240128
        }
      ]
    }
  ]
}
```

## GET /api/v1/locations

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

Authentication: required (Bearer API key)

### Response

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

| 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). |

```json
{
  "version": "4.1.0",
  "siteKey": "my-business",
  "locations": [
    {
      "slug": "lisboa",
      "city": "Lisboa",
      "lines": ["Rua Exemplo 12", "1000-001 Lisboa"],
      "primary": true
    }
  ]
}
```

## GET /api/v1/values

Company values for the authenticated site in display order.

Authentication: required (Bearer API key)

### Response

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

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `[].slug` | string | yes | URL-safe unique identifier, lowercase, hyphen-separated. |
| `[].title` | string | no | Value name (e.g. Qualidade). Portuguese. |
| `[].description` | string | no | Short explanation of the value. Portuguese. |

```json
{
  "version": "4.1.0",
  "siteKey": "my-business",
  "values": [{ "slug": "qualidade", "title": "Qualidade", "description": "…" }]
}
```

## GET /api/v1/legal

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.

Authentication: required (Bearer API key)

### Response

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

| 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. Portuguese. |
| `<key>.sections[].body` | string | no | Section body prose; plain text, may contain multiple sentences. Portuguese. |

```json
{
  "version": "4.1.0",
  "siteKey": "my-business",
  "legal": {
    "terms": { "sections": [{ "title": "1. Objeto", "body": "…" }] },
    "privacy": { "sections": [{ "title": "1. Dados recolhidos", "body": "…" }] }
  }
}
```

## POST /api/v1/leads

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 notifications in the backoffice, an SMS is sent to them. Notification failures never fail the request.

Authentication: required (Bearer API key)

### Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes | Contact name. Required. |
| `email` | string (email) | yes | Contact email. Required. |
| `phone` | string | no | Contact phone, free format. |
| `formType` | "quote" \| "contact" | no | Which website form produced the lead. Defaults to "quote" for older client sites that do not send it. |
| `serviceSlug` | string | no | Slug of the service the visitor is interested in. |
| `originDestination` | string | no | Pickup / delivery description, for transport-style quote forms. |
| `message` | string | yes | The visitor's message. Required. |

### Response

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

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

## GET /api/v1/assets/{pathname}

Streams a file from the tenant asset store (anything uploaded through the backoffice). Pathnames come from the content itself: services[].image.pathname, products[].image.pathname and caseStudies[].assets[].pathname.

Public and heavily cached; client sites usually proxy it under their own /api/blob/ route to keep same-origin URLs. Serve a document from a download link rather than inline.

Authentication: none

### Response

200 with the file body and its Content-Type. 404 when the pathname does not exist.

```json
(binary file body)
```

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