openapi: 3.1.0
info:
  title: YachtMaster Adflow Feed API
  version: "1.0.0"
  description: >
    Public read-only feed of a company's active listings (boats, cars and other
    object types) for display on the company's own website.

    Two equivalent surfaces are available:

    - **Direct feed** (`app.yachtmaster.cloud`) — served straight from the
      YachtMaster app.
    - **Edge feed** (`ads.yachtmaster.cloud`) — a CORS-enabled, edge-cached
      mirror with an added `status` filter and a single-ad lookup.

    Both authenticate with the company feed token in the URL path (found in
    Settings → Adflow). Ad objects have an identical shape on both surfaces.

    New fields are added additively and are not treated as breaking; clients
    should ignore unknown fields. Breaking changes ship under a new version path.
servers:
  - url: https://app.yachtmaster.cloud
    description: Direct feed
  - url: https://ads.yachtmaster.cloud
    description: Edge feed

paths:
  /feed/{token}:
    servers:
      - url: https://app.yachtmaster.cloud
    get:
      summary: Direct feed — all active ads
      operationId: getDirectFeed
      parameters:
        - $ref: '#/components/parameters/Token'
        - $ref: '#/components/parameters/Filter'
        - $ref: '#/components/parameters/Since'
      responses:
        '200':
          description: Feed of matching ads.
          headers:
            Cache-Control:
              schema: { type: string, example: 'private, max-age=60' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DirectFeedResponse' }
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/ads/{token}:
    servers:
      - url: https://ads.yachtmaster.cloud
    get:
      summary: Edge feed — all active ads
      operationId: getEdgeFeed
      parameters:
        - $ref: '#/components/parameters/Token'
        - $ref: '#/components/parameters/Filter'
        - $ref: '#/components/parameters/Since'
        - $ref: '#/components/parameters/Status'
      responses:
        '200':
          description: Feed of matching ads.
          headers:
            Cache-Control:
              schema: { type: string, example: 'public, max-age=60' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EdgeFeedResponse' }

  /v1/ads/{token}/{id}:
    servers:
      - url: https://ads.yachtmaster.cloud
    get:
      summary: Edge feed — single ad by id
      operationId: getEdgeAd
      parameters:
        - $ref: '#/components/parameters/Token'
        - name: id
          in: path
          required: true
          description: Ad identifier.
          schema: { type: integer }
      responses:
        '200':
          description: The requested ad.
          headers:
            Cache-Control:
              schema: { type: string, example: 'public, max-age=60' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Ad' }
        '404':
          $ref: '#/components/responses/NotFound'

components:
  parameters:
    Token:
      name: token
      in: path
      required: true
      description: The company feed token (UUID). Treat it like a public API key.
      schema: { type: string, format: uuid }
    Filter:
      name: filter
      in: query
      required: false
      description: Only return ads tagged with this page value in `web_pages`.
      schema: { type: string }
      example: main
    Since:
      name: since
      in: query
      required: false
      description: Only return ads modified at or after this ISO 8601 timestamp.
      schema: { type: string, format: date-time }
      example: "2025-01-01T00:00:00Z"
    Status:
      name: status
      in: query
      required: false
      description: >
        Edge feed only. Narrow to one or more statuses (comma-separated).
        Values: `forsale`, `incoming`, `sold`.
      schema: { type: string }
      example: forsale,incoming

  responses:
    NotFound:
      description: Unknown/inactive token, or ad id not found for this token.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error: { type: string, example: Invalid feed token }

    DirectFeedResponse:
      type: object
      required: [feed_generated_at, ads]
      properties:
        feed_generated_at: { type: string, format: date-time }
        ads:
          type: array
          items: { $ref: '#/components/schemas/Ad' }

    EdgeFeedResponse:
      type: object
      required: [last_synced_at, ads]
      properties:
        last_synced_at:
          type: [string, "null"]
          format: date-time
        ads:
          type: array
          items: { $ref: '#/components/schemas/Ad' }

    LocalizedText:
      type: object
      description: Localized string keyed by language code (en, fi, sv, ...).
      additionalProperties: { type: string }
      example: { en: Diesel, fi: Diesel, sv: Diesel }

    Ad:
      type: object
      description: >
        A single listing. Boat-only and car-only fields are `null` for the other
        object type. Branch on `object_type` (slug), not the numeric id.
      required: [id, status, object_type_id, object_type, updated_at, web_pages, images, equipment]
      properties:
        id: { type: integer }
        status:
          type: string
          enum: [forsale, incoming, sold]
        object_type_id:
          type: integer
          description: "1 motorboat, 2 sailboat, 3 trailer, 4 car, 5 truck, 6 campervan, 7 motorbike, 8 work_machine, 9 snowmobile, 10 jetski, 11 equipment, 12 general."
        object_type:
          type: string
          enum: [motorboat, sailboat, trailer, car, truck, campervan, motorbike, work_machine, snowmobile, jetski, equipment, general]
        updated_at: { type: string, format: date-time }

        price: { type: [number, "null"], description: Asking price; null = price on request. }
        currency: { type: [string, "null"], description: ISO 4217 currency code. }
        vat: { type: [boolean, "null"] }
        vat_marginal: { type: [boolean, "null"] }
        year: { type: [integer, "null"] }
        make_name: { type: [string, "null"] }
        model: { type: [string, "null"] }
        distance: { type: [number, "null"], description: Engine hours (boats) or mileage in km (cars). }
        description: { oneOf: [{ $ref: '#/components/schemas/LocalizedText' }, { type: "null" }] }
        subtitle: { oneOf: [{ $ref: '#/components/schemas/LocalizedText' }, { type: "null" }] }
        country_code: { type: [string, "null"] }
        country_name: { oneOf: [{ $ref: '#/components/schemas/LocalizedText' }, { type: "null" }] }
        city_name: { oneOf: [{ $ref: '#/components/schemas/LocalizedText' }, { type: "null" }] }
        zip_code: { type: [string, "null"] }
        web_url: { type: [string, "null"], format: uri }
        web_pages:
          type: array
          items: { type: string }
        images:
          type: array
          items: { $ref: '#/components/schemas/Image' }
        equipment:
          type: array
          items: { $ref: '#/components/schemas/EquipmentGroup' }

        # Boat-only
        length: { type: [number, "null"], description: "Boat only — length overall (m)." }
        width: { type: [number, "null"], description: "Boat only — beam (m)." }
        draft: { type: [number, "null"], description: "Boat only — draft (m)." }
        weight: { type: [number, "null"], description: "Boat only — displacement (kg)." }
        cabins: { type: [integer, "null"], description: "Boat only." }
        berths: { type: [integer, "null"], description: "Boat only." }
        toilets: { type: [integer, "null"], description: "Boat only." }
        material_name:
          description: "Boat only — localized hull material."
          oneOf: [{ $ref: '#/components/schemas/LocalizedText' }, { type: "null" }]
        engine_qty: { type: [integer, "null"], description: "Boat only — number of engines." }
        engine_make_name: { type: [string, "null"], description: "Boat only." }
        engine_model: { type: [string, "null"], description: "Boat only." }
        engine_year: { type: [integer, "null"], description: "Boat only." }
        engine_type_name:
          description: "Boat only — localized engine type."
          oneOf: [{ $ref: '#/components/schemas/LocalizedText' }, { type: "null" }]
        engine_fuel_name:
          description: "Boat only — localized fuel type."
          oneOf: [{ $ref: '#/components/schemas/LocalizedText' }, { type: "null" }]

        # Car-only (canonical slugs)
        body_type: { type: [string, "null"], description: "Car only — e.g. suv, sedan, station_wagon." }
        transmission: { type: [string, "null"], enum: [manual, automatic, sequential, null] }
        fuel_type: { type: [string, "null"], enum: [gasoline, diesel, electric, hybrid, ethanol, gas, null] }
        drive_type: { type: [string, "null"], enum: [fwd, rwd, awd, null] }
        power_kw: { type: [integer, "null"], description: "Car only — engine power in kW." }
        engine_displacement: { type: [integer, "null"], description: "Car only — displacement in cm³." }
        color: { type: [string, "null"], description: "Car only — exterior colour slug." }
        number_of_seats: { type: [integer, "null"], description: "Car only." }
        number_of_doors: { type: [integer, "null"], description: "Car only." }
        first_registration_date: { type: [string, "null"], format: date, description: "Car only." }

    Image:
      type: object
      properties:
        id: { type: string }
        name: { type: [string, "null"] }
        width: { type: [integer, "null"] }
        height: { type: [integer, "null"] }
        type: { type: [string, "null"], description: File extension, e.g. jpg. }
        url: { type: string, format: uri }
        category: { type: [string, "null"], description: "e.g. exterior, interior, engine." }

    EquipmentGroup:
      type: object
      properties:
        equipment_group_name: { $ref: '#/components/schemas/LocalizedText' }
        group_sort: { type: integer }
        equipment_items:
          type: array
          items: { $ref: '#/components/schemas/EquipmentItem' }

    EquipmentItem:
      type: object
      properties:
        name: { $ref: '#/components/schemas/LocalizedText' }
        description: { type: [string, "null"], description: Optional free-text note for this item. }
