openapi: 3.1.0
info:
  title: Aumakua Real Estate API
  version: "1.0.0"
  summary: Public read APIs and lead-capture for the Aumakua Algarve catalog.
  description: |
    Public REST API for the Aumakua Real Estate catalog (Algarve, Portugal).
    Designed for use as **OpenAI Custom GPT Actions** and other OpenAPI-driven
    assistant integrations. All endpoints are public (no auth) and return
    JSON. Multilingual content (`pt, en, fr, es, nl, de, it`) — pass `locale`
    where supported.

    ## Related discovery surfaces
    - `/llms.txt` — Markdown overview for LLM crawlers.
    - `/llms-full.txt` — Full catalog dump as Markdown.
    - `/.well-known/mcp.json` — MCP discovery manifest.
    - `/api/mcp` — MCP Streamable HTTP (JSON-RPC 2.0). Recommended for
      Claude Desktop, Cursor and other MCP-aware clients. Not part of this
      OpenAPI document (Custom GPT Actions does not consume MCP).

    ## Tone & approach (for Custom GPTs and agents)
    - Warm, calm, professional, understated. Boutique consultancy — no urgency tactics, no superlatives, no sales pressure.
    - Trusted local advisor for international buyers, relocators, second-home owners and investors in the Algarve.
    - Lead consultant: **Rosário Dantas**. Operator: **Futuro Persistente, Lda — AMI 21451**. When describing actions (responding, helping, scheduling visits, following up), always attribute them to **the Aumakua team** — never "Rosário responds / Rosário will get back to you". Rosário is named as the lead/contact person; the team is who acts.
    - Reply in the user's language (site: pt, en, fr, es, nl, de, it; human follow-up: pt, en, fr, es).
    - Quote prices, areas, bedrooms and locations **exactly** as returned by `searchProperties`. Never round or invent.
    - Portuguese typology convention: `T2`/`T3`/`T4` = number of bedrooms; explain on first use.
    - Always cite the canonical property URL (`sourceUrl` or `/{locale}/properties/{slug}`); link each listing individually.
    - Suggested next step is always to book a conversation with the Aumakua team (WhatsApp +351 963 060 824 or `/{locale}/contact`). Never promise prices, availability, financing or legal outcomes on Aumakua's behalf.
    - Scope: Algarve only (Loulé, Quarteira, Olhão, Faro, Almancil, Vilamoura). Redirect questions about other regions.
    - `createLead` requires explicit `consent: true` from the user — never submit without clear in-conversation approval.

    ## Licensing
    Aumakua authorises AI assistants and conversational search engines to
    read, summarise, quote and cite responses. Always link back to the
    canonical property URL returned in each payload.
  contact:
    name: Aumakua Real Estate & Investments
    email: comercial@aumakuarei.com
    url: https://aumakuarei.com
  license:
    name: Proprietary — AI-readable with attribution
    url: https://aumakuarei.com/llms.txt
externalDocs:
  description: MCP Streamable HTTP endpoint (for MCP-aware clients)
  url: https://aumakuarei.com/api/mcp
servers:
  - url: https://aumakuarei.com
    description: Production
  - url: https://aumakua-com.lovable.app
    description: Lovable published preview
tags:
  - name: Catalog
    description: Read-only property catalog.
  - name: Leads
    description: Lead-capture endpoint.
  - name: Health
    description: Operational probes.
paths:
  /api/properties:
    get:
      tags: [Catalog]
      operationId: searchProperties
      summary: List, search or fetch a single property from the catalog.
      description: |
        Three modes, selected by query parameters:
        - **List + featured** (default): omit `slug` and `q` to receive a
          filtered list plus a curated `featured` array.
        - **Search**: pass `q` for full-text search across all locales.
        - **Single property**: pass `slug` to receive
          `{kind: "catalog", property: PropertyDetail}` for one listing.
      parameters:
        - in: query
          name: slug
          schema: { type: string }
          description: URL slug. When set, returns one property and ignores other filters.
        - in: query
          name: q
          schema: { type: string }
          description: Free-text query (any of the 7 site languages).
        - in: query
          name: locale
          schema:
            type: string
            enum: [pt, en, fr, es, nl, de, it]
            default: en
        - in: query
          name: status
          schema:
            type: string
            enum: [for_sale, for_rent, sold, reserved, archived]
        - in: query
          name: district
          schema: { type: string }
          example: Faro
        - in: query
          name: municipality
          schema: { type: string }
          example: Loulé
        - in: query
          name: min_price
          schema: { type: number, format: double }
        - in: query
          name: max_price
          schema: { type: number, format: double }
        - in: query
          name: min_bedrooms
          schema: { type: integer, minimum: 0 }
        - in: query
          name: tier
          schema: { type: string }
          description: Internal curation tier.
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 100, default: 24 }
        - in: query
          name: offset
          schema: { type: integer, minimum: 0, default: 0 }
      responses:
        "200":
          description: List response, search response, or single-property envelope.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/PropertyListResponse"
                  - $ref: "#/components/schemas/SinglePropertyResponse"
        "404":
          description: Slug not found.
          content:
            text/plain:
              schema: { type: string, example: "Not Found" }
        "503":
          description: Catalog backend unavailable.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /api/properties/featured:
    get:
      tags: [Catalog]
      operationId: listFeaturedProperties
      summary: List Aumakua's curated featured properties.
      description: |
        GET-friendly endpoint for AI assistants that cannot make MCP POST
        JSON-RPC calls (ChatGPT free-tier browse, Perplexity, You.com).
        Returns a top-level array of featured properties — no need to parse
        `featured` out of `/api/properties`.
      parameters:
        - in: query
          name: locale
          schema:
            type: string
            enum: [pt, en, fr, es, nl, de, it, sv, no, da, ar]
            default: en
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 24, default: 12 }
      responses:
        "200":
          description: Featured properties.
          content:
            application/json:
              schema:
                type: object
                properties:
                  locale: { $ref: "#/components/schemas/Locale" }
                  count: { type: integer }
                  properties:
                    type: array
                    items: { $ref: "#/components/schemas/PropertySummary" }
        "503":
          description: Catalog backend unavailable.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /api/properties/recent:
    get:
      tags: [Catalog]
      operationId: listRecentProperties
      summary: List the most recently synced catalog properties.
      description: |
        GET-friendly endpoint returning the newest units as a top-level
        array. Same shape as `/api/properties/featured`.
      parameters:
        - in: query
          name: locale
          schema:
            type: string
            enum: [pt, en, fr, es, nl, de, it, sv, no, da, ar]
            default: en
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 24, default: 12 }
      responses:
        "200":
          description: Recent properties.
          content:
            application/json:
              schema:
                type: object
                properties:
                  locale: { $ref: "#/components/schemas/Locale" }
                  count: { type: integer }
                  properties:
                    type: array
                    items: { $ref: "#/components/schemas/PropertySummary" }
        "503":
          description: Catalog backend unavailable.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /api/leads:
    post:
      tags: [Leads]
      operationId: createLead
      summary: Submit a new lead.
      description: |
        Public, write-only endpoint. Required fields: `name`, `email`,
        `consent: true`. All other fields are optional and feed the lead
        scoring model (0-100).
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/LeadInput" }
      responses:
        "200":
          description: Lead persisted.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/LeadResponse" }
        "400":
          description: Validation error.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "502":
          description: Lead insert failed (database error).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /api/chat-health:
    get:
      tags: [Health]
      operationId: getChatHealth
      summary: Readiness probe for the conversational assistant.
      responses:
        "200":
          description: Provider configured.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ChatHealth" }
        "503":
          description: No chat provider configured.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ChatHealth" }

components:
  schemas:
    Locale:
      type: string
      enum: [pt, en, fr, es, nl, de, it]

    Price:
      type: object
      properties:
        amount:
          type: [number, "null"]
          description: Numeric amount in `currency`.
        currency:
          type: string
          example: EUR
        display:
          type: [string, "null"]
          description: Pre-formatted price string for display.

    PropertySummary:
      type: object
      description: Catalog summary row returned by list and search.
      properties:
        egoId: { type: string }
        reference: { type: [string, "null"] }
        slug: { type: string }
        sourceUrl: { type: string, format: uri }
        status:
          type: string
          enum: [for_sale, for_rent, sold, reserved, archived]
        tier: { type: [string, "null"] }
        price: { $ref: "#/components/schemas/Price" }
        typology: { type: [string, "null"], example: "T3" }
        bedrooms: { type: [integer, "null"] }
        bathrooms: { type: [integer, "null"] }
        grossAreaM2: { type: [number, "null"] }
        landAreaM2: { type: [number, "null"] }
        district: { type: [string, "null"] }
        municipality: { type: [string, "null"] }
        parish: { type: [string, "null"] }
        publicLocation: { type: [string, "null"] }
        primaryImageUrl: { type: [string, "null"], format: uri }
        title: { type: [string, "null"] }
        subtitle: { type: [string, "null"] }
        description: { type: [string, "null"] }
        featureKeys:
          type: array
          items: { type: string }
      required: [egoId, slug, sourceUrl, status, price]

    FeaturedSummary:
      type: object
      properties:
        egoId: { type: string }
        slug: { type: string }
        reference: { type: [string, "null"] }
        title: { type: [string, "null"] }
        subtitle: { type: [string, "null"] }
        price: { $ref: "#/components/schemas/Price" }
        location:
          type: object
          properties:
            district: { type: [string, "null"] }
            municipality: { type: [string, "null"] }
            parish: { type: [string, "null"] }
            publicLocation: { type: [string, "null"] }
        primaryImageUrl: { type: [string, "null"], format: uri }

    PropertyDetail:
      allOf:
        - $ref: "#/components/schemas/PropertySummary"
        - type: object
          properties:
            energyCertificate: { type: [string, "null"] }
            latitude: { type: [number, "null"] }
            longitude: { type: [number, "null"] }
            layout: { type: [string, "null"] }
            outdoor: { type: [string, "null"] }
            locationCopy: { type: [string, "null"] }
            seo:
              type: object
              properties:
                title: { type: [string, "null"] }
                description: { type: [string, "null"] }
                ogTitle: { type: [string, "null"] }
                ogDescription: { type: [string, "null"] }
            media:
              type: object
              properties:
                photos: { type: array, items: { type: object } }
                panoramics: { type: array, items: { type: object } }
                floorPlans: { type: array, items: { type: object } }
                brochures: { type: array, items: { type: object } }
                videos: { type: array, items: { type: object } }
                externalTours: { type: array, items: { type: object } }
            features:
              type: array
              items: { type: object }

    PropertyListResponse:
      type: object
      properties:
        locale: { $ref: "#/components/schemas/Locale" }
        query: { type: [string, "null"] }
        filters: { type: object, additionalProperties: true }
        featured:
          type: array
          items: { $ref: "#/components/schemas/FeaturedSummary" }
        catalog:
          type: array
          items: { $ref: "#/components/schemas/PropertySummary" }
      required: [locale, catalog, featured]

    SinglePropertyResponse:
      type: object
      properties:
        kind:
          type: string
          enum: [catalog]
        property: { $ref: "#/components/schemas/PropertyDetail" }
      required: [kind, property]

    LeadInput:
      type: object
      required: [name, email, consent]
      properties:
        name: { type: string, minLength: 1 }
        email: { type: string, format: email }
        consent:
          type: boolean
          description: Must be `true`. GDPR consent flag.
        phone: { type: string }
        country: { type: string }
        preferredLanguage:
          oneOf:
            - $ref: "#/components/schemas/Locale"
            - type: string
              enum: [other]
        locale: { $ref: "#/components/schemas/Locale" }
        source:
          type: string
          enum: [property_page, chat_widget, catalog, manual]
          default: property_page
        property_id: { type: [string, "null"] }
        property_slug: { type: [string, "null"] }
        purpose:
          type: string
          enum: [ownHome, secondHome, relocation, investment, multigenerational]
          default: ownHome
        budget:
          type: string
          enum: [under1m, r1to15, r15to2, over2m]
          default: r1to15
        financing:
          type: string
          enum: [cash, preapproved, exploring, unsure]
          default: exploring
        timeline:
          type: string
          enum: [now, m1to3, m3to6, m6plus]
          default: m1to3
        visitType:
          type: string
          enum: [inPerson, video, either]
          default: either
        message: { type: string }

    LeadResponse:
      type: object
      properties:
        ok: { type: boolean, example: true }
        id: { type: string, description: Lead UUID }
        score:
          type: integer
          minimum: 0
          maximum: 100
          description: Computed lead score.
      required: [ok, id, score]

    ChatHealth:
      type: object
      properties:
        status:
          type: string
          enum: [ok, not_ready]
        ready: { type: boolean }
        provider:
          type: [string, "null"]
          enum: [lovable, anthropic, null]
        model: { type: [string, "null"] }
        reason: { type: string }
      required: [status, ready]

    Error:
      type: object
      properties:
        error: { type: string }
        fields:
          type: array
          items: { type: string }
      required: [error]
