openapi: 3.1.0
info:
  title: Fly Frugal Public Travel API
  version: 1.1.0
  description: >-
    Read-only access to published itinerary previews, current observed flight
    fares, today's verified deals, route price history, supported airports,
    and typical weather. Private content and internal operations are excluded.
servers:
  - url: https://flyfrugal.fyi
security: []
paths:
  /api/itineraries:
    get:
      operationId: listItineraries
      summary: List published itinerary previews
      parameters:
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 50, default: 24 }
        - name: country
          in: query
          schema: { type: string, minLength: 2 }
      responses:
        "200":
          description: Published itinerary previews
          content:
            application/json:
              schema:
                type: object
                required: [items, count]
                properties:
                  items:
                    type: array
                    maxItems: 50
                    items: { $ref: "#/components/schemas/ItineraryPreview" }
                  count: { type: integer, minimum: 0, maximum: 50 }
  /api/itineraries/{slug}:
    get:
      operationId: getItinerary
      summary: Get one published itinerary preview
      parameters:
        - $ref: "#/components/parameters/Slug"
      responses:
        "200":
          description: Published itinerary preview
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ItineraryPreview" }
        "404": { $ref: "#/components/responses/NotFound" }
  /api/deals:
    get:
      operationId: listFlightDeals
      summary: List current observed flight deals
      parameters:
        - name: origin
          in: query
          schema: { type: string, pattern: "^[A-Za-z]{3}$" }
        - name: scope
          in: query
          description: Use `global` to search all supported departure airports.
          schema: { type: string, enum: [global] }
        - name: nights
          in: query
          schema: { type: integer, minimum: 1, maximum: 30, default: 7 }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 100 }
        - name: fresh
          in: query
          description: Restrict results to verified Google savings checked today in Pacific time.
          schema: { type: string, enum: [today] }
      responses:
        "200":
          description: Current observed deals
          content:
            application/json:
              schema:
                type: object
                required: [origin, nights, freshness, deals]
                properties:
                  origin: { type: string }
                  origin_details: { type: [object, "null"], additionalProperties: true }
                  nights: { type: integer }
                  freshness: { type: string }
                  deals:
                    type: array
                    maxItems: 100
                    items: { $ref: "#/components/schemas/FlightDeal" }
        "400": { $ref: "#/components/responses/BadRequest" }
  /api/calendar:
    get:
      operationId: getRouteCalendar
      summary: Compare future dates and price context for a route
      parameters:
        - { name: origin, in: query, required: true, schema: { type: string, pattern: "^[A-Za-z]{3}$" } }
        - { name: destination, in: query, required: true, schema: { type: string, pattern: "^[A-Za-z]{3}$" } }
        - { name: nights, in: query, schema: { type: integer, minimum: 1, maximum: 30, default: 7 } }
      responses:
        "200": { description: Route calendar and observed price context }
        "400": { $ref: "#/components/responses/BadRequest" }
  /api/history:
    get:
      operationId: getFareHistory
      summary: Get observed price history for one exact trip
      parameters:
        - { name: origin, in: query, required: true, schema: { type: string, pattern: "^[A-Za-z]{3}$" } }
        - { name: destination, in: query, required: true, schema: { type: string, pattern: "^[A-Za-z]{3}$" } }
        - { name: departure, in: query, required: true, schema: { type: string, format: date } }
        - { name: return, in: query, required: true, schema: { type: string, format: date } }
        - { name: nights, in: query, required: true, schema: { type: integer, minimum: 1, maximum: 30 } }
      responses:
        "200": { description: Up to 100 observations for the exact trip }
        "400": { $ref: "#/components/responses/BadRequest" }
  /api/catalog:
    get:
      operationId: getAirportCatalog
      summary: List supported departure and destination airports
      responses:
        "200": { description: Supported airport catalog }
  /api/weather:
    get:
      operationId: getTypicalWeather
      summary: Get historical seasonal weather context
      parameters:
        - { name: airport, in: query, required: true, schema: { type: string, pattern: "^[A-Za-z]{3}$" } }
        - { name: date, in: query, required: true, schema: { type: string, format: date } }
      responses:
        "200": { description: Typical historical weather for the destination and month }
        "400": { $ref: "#/components/responses/BadRequest" }
        "502": { description: Weather provider temporarily unavailable }
components:
  parameters:
    Slug:
      name: slug
      in: path
      required: true
      schema: { type: string, pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$" }
  schemas:
    ItineraryPreview:
      type: object
      required: [slug, country_name, tagline, days_count, current_revision, price_cents, currency, updated_at]
      properties:
        slug: { type: string }
        country_name: { type: string }
        tagline: { type: string }
        days_count: { type: integer }
        preview_json: { type: object, additionalProperties: true }
        cover_image_url: { type: [string, "null"] }
        first_day_image_url: { type: [string, "null"] }
        current_revision: { type: string }
        price_cents: { type: integer }
        currency: { type: string }
        updated_at: { type: string }
        pdf_ready: { type: boolean }
    FlightDeal:
      type: object
      required: [origin, destination_airport, departure_date, return_date, nights, price, currency, observed_at]
      properties:
        origin: { type: string }
        destination_airport: { type: string }
        label: { type: string }
        country: { type: string }
        departure_date: { type: string, format: date }
        return_date: { type: string, format: date }
        nights: { type: integer }
        price: { type: number }
        currency: { type: string }
        airlines: { type: [string, "null"] }
        longest_layover_minutes: { type: [integer, "null"] }
        google_price_signal: { type: [string, "null"] }
        google_usual_low: { type: [number, "null"] }
        google_usual_high: { type: [number, "null"] }
        google_savings: { type: [number, "null"] }
        google_signal_checked_at: { type: [string, "null"] }
        latest_checked_at: { type: string }
        fresh_today: { type: boolean }
        observation_count: { type: integer }
    Error:
      type: object
      required: [error]
      properties:
        error: { type: string }
  responses:
    BadRequest:
      description: Invalid public query parameters
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: Published resource not found
