{"openapi":"3.1.0","info":{"title":"Casa Digital Site API","version":"4.1.0","summary":"Content and lead intake for websites built on the Casa Digital platform.","description":"Every tenant (one client website) has its own API key, issued by Casa Digital when the site is provisioned; the key both authenticates the request and selects the tenant, so no site identifier is sent.\n\nThis describes the complete surface a client site has: read its content, create a lead, fetch an asset. Content editing, CRM, notifications and billing live in the backoffice and have no public API.\n\nThe narrative contract — environment, caching, the invalidation endpoint a site must expose, limits and error handling — is at https://www.casadigital.pt/api/v1/integration.md."},"servers":[{"url":"https://www.casadigital.pt"}],"security":[{"siteApiKey":[]}],"tags":[{"name":"Content","description":"Per-resource reads of the tenant's business data."},{"name":"Leads","description":"The only write a client site can perform."},{"name":"Assets","description":"Public delivery of files uploaded in the backoffice."}],"paths":{"/api/v1/settings":{"get":{"operationId":"getSettings","summary":"The business: identity, contacts, opening hours and social links.","description":"Scoped to the tenant that owns the API key. Always returns the latest content; an unauthored site returns a fully defaulted payload rather than an error.","tags":["Content"],"responses":{"200":{"description":"The resource, wrapped in the standard envelope.","headers":{"Cache-Control":{"description":"Always private and non-cacheable: freshness is owned by the consuming site.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"type":"object","required":["version","siteKey","settings"],"properties":{"version":{"type":"string","description":"Contract version served by the platform (semver).","examples":["4.1.0"]},"siteKey":{"type":"string","description":"Slug of the tenant the API key resolved to."},"settings":{"type":"object","properties":{"name":{"default":"","description":"Business name as displayed across the site.","type":"string"},"tagline":{"default":"","description":"One-line slogan shown near the logo/footer. Portuguese.","type":"string"},"description":{"default":"","description":"Short description of the business, 1-2 sentences. Portuguese.","type":"string"},"phone":{"default":"","description":"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.","type":"string"},"email":{"default":"","description":"Public contact email. Empty string if none.","type":"string"},"whatsapp":{"default":"","description":"WhatsApp chat URL (e.g. \"https://wa.me/351912345678\"). Empty string if none.","type":"string"},"appUrl":{"default":"","description":"Mobile app store URL if the business has an app. Empty string if none.","type":"string"},"schedule":{"default":[],"description":"Opening schedule rows, in display order.","type":"array","items":{"type":"object","properties":{"days":{"default":"","description":"Day range label (e.g. \"Seg – Sáb\"). Portuguese.","type":"string"},"hours":{"default":"","description":"Opening hours label (e.g. \"09h – 19h\" or \"Fechado\").","type":"string"}},"description":"One row of the opening schedule."}},"social":{"default":{},"type":"object","properties":{"facebook":{"default":"","description":"Facebook page URL. Empty string if none.","type":"string"},"instagram":{"default":"","description":"Instagram profile URL. Empty string if none.","type":"string"},"linkedin":{"default":"","description":"LinkedIn page URL. Empty string if none.","type":"string"},"x":{"default":"","description":"X/Twitter profile URL. Empty string if none.","type":"string"},"youtube":{"default":"","description":"YouTube channel URL. Empty string if none.","type":"string"}},"description":"Social media profile links."}},"description":"The business: identity, contacts, opening hours and social links. One per site."}}}}}},"401":{"description":"Missing, malformed or unknown API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"The platform could not load the resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/services":{"get":{"operationId":"getServices","summary":"Services offered by the business, in display order.","description":"Scoped to the tenant that owns the API key. Always returns the latest content; an unauthored site returns a fully defaulted payload rather than an error.","tags":["Content"],"responses":{"200":{"description":"The resource, wrapped in the standard envelope.","headers":{"Cache-Control":{"description":"Always private and non-cacheable: freshness is owned by the consuming site.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"type":"object","required":["version","siteKey","services"],"properties":{"version":{"type":"string","description":"Contract version served by the platform (semver).","examples":["4.1.0"]},"siteKey":{"type":"string","description":"Slug of the tenant the API key resolved to."},"services":{"type":"array","items":{"type":"object","properties":{"slug":{"type":"string","description":"URL-safe unique identifier, lowercase, hyphen-separated."},"title":{"default":"","description":"Service name. Portuguese.","type":"string"},"icon":{"type":"string","enum":["truck","wrench","packageCheck","hammer","boxes","warehouse","arrowDownToLine","zap","clock","shieldCheck"],"description":"Icon key from the fixed icon set (Lucide-style names)."},"image":{"type":"object","properties":{"assetId":{"description":"Asset document id, set by the backoffice after upload.","type":"string"},"pathname":{"default":"","description":"Blob storage pathname; client sites serve it through their own /api/blob/ proxy.","type":"string"},"alt":{"default":"","description":"Image alt text. Portuguese.","type":"string"},"width":{"description":"Image width in pixels.","type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"height":{"description":"Image height in pixels.","type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"description":"Illustrative image stored in Vercel Blob."},"tier":{"type":"string","enum":["primary","featured","secondary"],"description":"Display tier: \"primary\" for the main highlighted services, \"featured\" for the secondary highlights, \"secondary\" for the rest."},"short":{"default":"","description":"One-sentence summary shown on service cards. Portuguese.","type":"string"},"description":{"default":"","description":"Full description. Portuguese.","type":"string"},"bullets":{"default":[],"description":"Bullet-point highlights. Portuguese.","type":"array","items":{"type":"string"}}},"required":["slug","icon","tier"],"description":"One service offered by the business."},"description":"Services offered by the business, in display order."}}}}}},"401":{"description":"Missing, malformed or unknown API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"The platform could not load the resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/products":{"get":{"operationId":"getProducts","summary":"Products sold by the business, at a stated price, in display order.","description":"Scoped to the tenant that owns the API key. Always returns the latest content; an unauthored site returns a fully defaulted payload rather than an error.","tags":["Content"],"responses":{"200":{"description":"The resource, wrapped in the standard envelope.","headers":{"Cache-Control":{"description":"Always private and non-cacheable: freshness is owned by the consuming site.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"type":"object","required":["version","siteKey","products"],"properties":{"version":{"type":"string","description":"Contract version served by the platform (semver).","examples":["4.1.0"]},"siteKey":{"type":"string","description":"Slug of the tenant the API key resolved to."},"products":{"type":"array","items":{"type":"object","properties":{"slug":{"type":"string","description":"URL-safe unique identifier, lowercase, hyphen-separated."},"title":{"default":"","description":"Product name. Portuguese.","type":"string"},"icon":{"type":"string","enum":["truck","wrench","packageCheck","hammer","boxes","warehouse","arrowDownToLine","zap","clock","shieldCheck"],"description":"Icon key from the fixed icon set (Lucide-style names)."},"image":{"type":"object","properties":{"assetId":{"description":"Asset document id, set by the backoffice after upload.","type":"string"},"pathname":{"default":"","description":"Blob storage pathname; client sites serve it through their own /api/blob/ proxy.","type":"string"},"alt":{"default":"","description":"Image alt text. Portuguese.","type":"string"},"width":{"description":"Image width in pixels.","type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"height":{"description":"Image height in pixels.","type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"description":"Illustrative image stored in Vercel Blob."},"sku":{"default":"","description":"Internal reference or article number. Empty string if none.","type":"string"},"price":{"default":0,"description":"Price amount in the currency below. 0 means the price is not published.","type":"number","minimum":0},"currency":{"default":"EUR","description":"Currency of the price amount.","type":"string","enum":["EUR","USD","GBP"]},"pricePeriod":{"default":"once","description":"Billing period the price refers to: \"once\" for a one-off price, \"month\" or \"year\" for a subscription.","type":"string","enum":["once","month","year"]},"priceNote":{"default":"","description":"Short qualifier shown next to the price (e.g. \"IVA incluído\"). Portuguese.","type":"string"},"short":{"default":"","description":"One-sentence summary shown on product cards. Portuguese.","type":"string"},"description":{"default":"","description":"Full description. Portuguese.","type":"string"},"bullets":{"default":[],"description":"Bullet-point highlights. Portuguese.","type":"array","items":{"type":"string"}},"featured":{"default":false,"description":"True for a product the website should highlight.","type":"boolean"},"available":{"default":true,"description":"False for a product kept on record but not currently sold.","type":"boolean"}},"required":["slug","icon"],"description":"One product sold by the business, at a stated price."},"description":"Products sold by the business, in display order."}}}}}},"401":{"description":"Missing, malformed or unknown API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"The platform could not load the resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/caseStudies":{"get":{"operationId":"getCaseStudies","summary":"Case studies: work already delivered, with its links and attached files, in display order.","description":"Scoped to the tenant that owns the API key. Always returns the latest content; an unauthored site returns a fully defaulted payload rather than an error.","tags":["Content"],"responses":{"200":{"description":"The resource, wrapped in the standard envelope.","headers":{"Cache-Control":{"description":"Always private and non-cacheable: freshness is owned by the consuming site.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"type":"object","required":["version","siteKey","caseStudies"],"properties":{"version":{"type":"string","description":"Contract version served by the platform (semver).","examples":["4.1.0"]},"siteKey":{"type":"string","description":"Slug of the tenant the API key resolved to."},"caseStudies":{"type":"array","items":{"type":"object","properties":{"slug":{"type":"string","description":"URL-safe unique identifier, lowercase, hyphen-separated."},"client":{"default":"","description":"Name of the client the work was delivered for. Doubles as the case study's title.","type":"string"},"problem":{"default":"","description":"The situation the client was in. Portuguese.","type":"string"},"solution":{"default":"","description":"What the business did about it. Portuguese.","type":"string"},"results":{"default":"","description":"What the client got out of it. Portuguese.","type":"string"},"links":{"default":[],"description":"Related links, in display order.","type":"array","items":{"type":"object","properties":{"title":{"default":"","description":"Link label. Portuguese. May be empty — fall back to showing the URL.","type":"string"},"url":{"default":"","description":"Absolute URL, including the scheme.","type":"string"}},"description":"One outbound link related to the case study, such as the live website."}},"assets":{"default":[],"description":"Every attached file in one list, in display order. Group them by `kind` if your design shows media separately from documents.","type":"array","items":{"type":"object","properties":{"kind":{"type":"string","enum":["image","video","document"],"description":"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."},"pathname":{"type":"string","description":"Blob storage pathname; fetch the file from GET /api/v1/assets/{pathname}, or proxy it through your own route."},"contentType":{"default":"","description":"MIME type the file was stored with (e.g. \"image/webp\", \"application/pdf\").","type":"string"},"name":{"default":"","description":"Original filename at upload time. Use it as the download label for a document.","type":"string"},"alt":{"default":"","description":"Alt text, when one was written. Often empty — fall back to the surrounding context.","type":"string"},"size":{"description":"File size in bytes, when known.","type":"integer","minimum":0,"maximum":9007199254740991},"width":{"description":"Intrinsic width in pixels. Present for media, absent for documents.","type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"height":{"description":"Intrinsic height in pixels. Present for media, absent for documents.","type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"required":["kind","pathname"],"description":"One file uploaded through the backoffice and stored in Vercel Blob."}}},"required":["slug"],"description":"One piece of work the business has delivered, told as problem, solution and results."},"description":"Case studies published by the business, in display order."}}}}}},"401":{"description":"Missing, malformed or unknown API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"The platform could not load the resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/locations":{"get":{"operationId":"getLocations","summary":"Physical locations of the business, in display order.","description":"Scoped to the tenant that owns the API key. Always returns the latest content; an unauthored site returns a fully defaulted payload rather than an error.","tags":["Content"],"responses":{"200":{"description":"The resource, wrapped in the standard envelope.","headers":{"Cache-Control":{"description":"Always private and non-cacheable: freshness is owned by the consuming site.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"type":"object","required":["version","siteKey","locations"],"properties":{"version":{"type":"string","description":"Contract version served by the platform (semver).","examples":["4.1.0"]},"siteKey":{"type":"string","description":"Slug of the tenant the API key resolved to."},"locations":{"type":"array","items":{"type":"object","properties":{"slug":{"type":"string","description":"URL-safe unique identifier, lowercase, hyphen-separated."},"city":{"default":"","description":"City or locality name.","type":"string"},"lines":{"default":[],"description":"Address lines, in display order.","type":"array","items":{"type":"string"}},"mapsSearchUrl":{"default":"","description":"Google Maps search/share URL for this address. Empty string if none.","type":"string"},"mapEmbedUrl":{"default":"","description":"Google Maps embed URL (iframe src). Empty string if none.","type":"string"},"primary":{"default":false,"description":"True for the main location (at most one per site).","type":"boolean"}},"required":["slug"],"description":"One physical location of the business."},"description":"Physical locations of the business, in display order."}}}}}},"401":{"description":"Missing, malformed or unknown API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"The platform could not load the resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/values":{"get":{"operationId":"getValues","summary":"Company values, in display order.","description":"Scoped to the tenant that owns the API key. Always returns the latest content; an unauthored site returns a fully defaulted payload rather than an error.","tags":["Content"],"responses":{"200":{"description":"The resource, wrapped in the standard envelope.","headers":{"Cache-Control":{"description":"Always private and non-cacheable: freshness is owned by the consuming site.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"type":"object","required":["version","siteKey","values"],"properties":{"version":{"type":"string","description":"Contract version served by the platform (semver).","examples":["4.1.0"]},"siteKey":{"type":"string","description":"Slug of the tenant the API key resolved to."},"values":{"type":"array","items":{"type":"object","properties":{"slug":{"type":"string","description":"URL-safe unique identifier, lowercase, hyphen-separated."},"title":{"default":"","description":"Value name (e.g. Qualidade). Portuguese.","type":"string"},"description":{"default":"","description":"Short explanation of the value. Portuguese.","type":"string"}},"required":["slug"],"description":"One company value."},"description":"Company values, in display order."}}}}}},"401":{"description":"Missing, malformed or unknown API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"The platform could not load the resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/legal":{"get":{"operationId":"getLegal","summary":"Legal documents the client maintains, keyed by document.","description":"Scoped to the tenant that owns the API key. Always returns the latest content; an unauthored site returns a fully defaulted payload rather than an error.","tags":["Content"],"responses":{"200":{"description":"The resource, wrapped in the standard envelope.","headers":{"Cache-Control":{"description":"Always private and non-cacheable: freshness is owned by the consuming site.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"type":"object","required":["version","siteKey","legal"],"properties":{"version":{"type":"string","description":"Contract version served by the platform (semver).","examples":["4.1.0"]},"siteKey":{"type":"string","description":"Slug of the tenant the API key resolved to."},"legal":{"type":"object","propertyNames":{"type":"string","enum":["terms","privacy"]},"additionalProperties":{"type":"object","properties":{"sections":{"default":[],"description":"Ordered sections, as the document should read.","type":"array","items":{"type":"object","properties":{"title":{"default":"","description":"Section heading. Portuguese.","type":"string"},"body":{"default":"","description":"Section body prose; plain text, may contain multiple sentences. Portuguese.","type":"string"}}}}},"description":"One legal document."},"description":"Legal documents the client maintains, keyed by document. Only documents that have been written are present."}}}}}},"401":{"description":"Missing, malformed or unknown API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"The platform could not load the resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/leads":{"post":{"operationId":"createLead","summary":"Submit a lead","description":"Creates a lead for the tenant that owns the API key, as a website contact or quote form would. The lead enters the backoffice CRM with status \"new\". If the tenant has SMS notifications enabled and an active subscription they are notified; a notification failure never fails the request. A client site cannot read leads back.","tags":["Leads"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":2,"description":"Contact name. Required."},"email":{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$","description":"Contact email. Required."},"phone":{"description":"Contact phone, free format.","type":"string"},"formType":{"default":"quote","description":"Which website form produced the lead. Defaults to \"quote\" for older client sites that do not send it.","type":"string","enum":["quote","contact"]},"serviceSlug":{"description":"Slug of the service the visitor is interested in.","type":"string"},"originDestination":{"description":"Pickup / delivery description, for transport-style quote forms.","type":"string"},"message":{"type":"string","minLength":10,"description":"The visitor's message. Required."}},"required":["name","email","message"],"description":"A lead submitted through a client website form."}}}},"responses":{"201":{"description":"Lead created.","content":{"application/json":{"schema":{"type":"object","required":["success","id"],"properties":{"success":{"const":true},"id":{"type":"string","description":"Identifier of the created lead."}}}}}},"400":{"description":"Body was not valid JSON.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed or unknown API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Payload failed validation; issues are listed per field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}}}}},"/api/v1/assets/{pathname}":{"get":{"operationId":"getAsset","summary":"Asset delivery","description":"Streams a file uploaded through the backoffice. Pathnames come from services[].image.pathname, products[].image.pathname and caseStudies[].assets[].pathname. Public and heavily cached; client sites usually proxy it under their own route to keep same-origin URLs.","tags":["Assets"],"security":[],"parameters":[{"name":"pathname","in":"path","required":true,"description":"Blob pathname, exactly as delivered in the content payload.","schema":{"type":"string"}}],"responses":{"200":{"description":"The file.","content":{"*/*":{"schema":{"type":"string","format":"binary"}}}},"404":{"description":"No such pathname."}}}}},"components":{"securitySchemes":{"siteApiKey":{"type":"http","scheme":"bearer","description":"Site API key: `Authorization: Bearer sk_<48 hex chars>`. Server-side only; never expose it to a browser."}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string"}}},"ValidationError":{"type":"object","required":["error","issues"],"properties":{"error":{"type":"string"},"issues":{"type":"array","items":{"type":"object","required":["path","message"],"properties":{"path":{"type":"string","description":"Dot path of the offending field."},"message":{"type":"string"}}}}}}}}}