openapi: 3.1.0
info:
  title: Odaduu Public API
  version: "1.0.1"
  description: |
    Bearer-token HTTP API for B2B partners to pull booking status, browse
    reference catalogs, and submit new RFQs into the Odaduu sales pipeline.
    Bearer-authenticated, JSON in and JSON out, cursor-paginated where it
    matters.
  contact:
    name: Trawish API support
    email: info@flygoldfinch.com
    url: https://apidoc.trawish.com/odaduu/
  license:
    name: Proprietary
servers:
  - url: https://builder.trawish.com/api/public/v1
    description: Production
tags:
  - name: Health
    description: Verify authentication and echo tenant identity
  - name: Bookings
    description: Read booking pipeline and submit new RFQs
  - name: Reference
    description: Shared org-wide catalogs used to build RFQ bodies

security:
  - BearerAuth: []

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: odaduu_live_XXXX
  parameters:
    Limit:
      name: limit
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 25
    Cursor:
      name: cursor
      in: query
      required: false
      schema:
        type: string
      description: Opaque token from a prior response's meta.nextCursor
    City:
      name: city
      in: query
      required: false
      schema:
        type: string
      description: Case-insensitive city filter
    NameQuery:
      name: q
      in: query
      required: false
      schema:
        type: string
      description: Substring match on name

  headers:
    XRateLimitLimit:
      schema:
        type: integer
      description: Max requests per rolling hour for this key
    XRateLimitRemaining:
      schema:
        type: integer
      description: Requests remaining in the current rolling window

  schemas:
    Envelope:
      type: object
      required: [ok]
      properties:
        ok:
          type: boolean
        data:
          description: Payload on success
        error:
          $ref: "#/components/schemas/Error"
        meta:
          $ref: "#/components/schemas/Meta"

    Meta:
      type: object
      properties:
        count:
          type: integer
        nextCursor:
          type: [string, "null"]
          description: null when no more pages exist

    Error:
      type: object
      required: [code, message]
      properties:
        code:
          type: string
          enum:
            - unauthorized
            - forbidden
            - not_found
            - bad_request
            - rate_limited
            - conflict
            - internal_error
            - not_implemented
        message:
          type: string
        details:
          type: object
          additionalProperties: true

    Pax:
      type: object
      required: [adults, children, infants]
      properties:
        adults:
          type: integer
          minimum: 1
          maximum: 200
        children:
          type: integer
          minimum: 0
          maximum: 200
        infants:
          type: integer
          minimum: 0
          maximum: 200

    Destination:
      type: object
      required: [id, code, name, currency]
      properties:
        id:
          type: string
        code:
          type: string
          example: JP
        name:
          type: string
          example: Japan
        currency:
          type: string
          example: JPY

    Hotel:
      type: object
      required: [id, name]
      properties:
        id:
          type: string
        name:
          type: string
        city:
          type: [string, "null"]
        country:
          type: [string, "null"]
        address:
          type: [string, "null"]

    Sightseeing:
      type: object
      required: [id, name, category, kind, cities, timedEntry, mustSee]
      properties:
        id:
          type: string
        name:
          type: string
        category:
          type: string
          enum: [PAID, FREE]
        kind:
          type: string
          enum: [SIGHTSEEING, ACTIVITY, SHOPPING, WALKING]
        cities:
          type: array
          items:
            type: string
        description:
          type: [string, "null"]
        adultPrice:
          type: [number, "null"]
        childPrice:
          type: [number, "null"]
        childAgeFrom:
          type: [integer, "null"]
        childAgeTo:
          type: [integer, "null"]
        durationMinutes:
          type: [integer, "null"]
        openingHours:
          type: [object, "null"]
          properties:
            from:
              type: [string, "null"]
            to:
              type: [string, "null"]
        closedDaysOfWeek:
          type: array
          items:
            type: string
        timedEntry:
          type: boolean
        mustSee:
          type: boolean

    FulldayTour:
      type: object
      required: [id, name, cities, pricePerSeater]
      properties:
        id:
          type: string
        name:
          type: string
        cities:
          type: array
          items:
            type: string
        description:
          type: [string, "null"]
        pricePerSeater:
          type: object
          properties:
            seater6:
              type: [number, "null"]
            seater9:
              type: [number, "null"]
            seater13:
              type: [number, "null"]
            seater22:
              type: [number, "null"]
            seater29:
              type: [number, "null"]
            seater45:
              type: [number, "null"]

    BookingSummary:
      type: object
      required:
        - id
        - ref
        - status
        - pax
        - createdAt
        - updatedAt
      properties:
        id:
          type: string
        ref:
          type: string
          example: OD-2026-4132
        status:
          type: string
          enum:
            - draft_quote
            - quote_sent
            - client_confirmed
            - ops_in_progress
            - travel_completed
            - cancelled
        guestName:
          type: [string, "null"]
        arrivalDate:
          type: [string, "null"]
          format: date
        departureDate:
          type: [string, "null"]
          format: date
        pax:
          $ref: "#/components/schemas/Pax"
        destination:
          type: [object, "null"]
          properties:
            code:
              type: string
            name:
              type: string
        agentId:
          type: [string, "null"]
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    ActiveQuote:
      type: object
      properties:
        version:
          type: integer
        variant:
          type: string
        label:
          type: [string, "null"]
        totalSell:
          type: number
        sellCurrency:
          type: string
        marginPercent:
          type: number
        validUntil:
          type: [string, "null"]
          format: date-time
        sentAt:
          type: [string, "null"]
          format: date-time
        sentChannel:
          type: [string, "null"]

    BookingDetail:
      allOf:
        - $ref: "#/components/schemas/BookingSummary"
        - type: object
          properties:
            focPax:
              type: integer
            childAges:
              type: array
              items:
                type: integer
            leadSource:
              type: string
            agentRequirements:
              type: [string, "null"]
            activeQuote:
              oneOf:
                - $ref: "#/components/schemas/ActiveQuote"
                - type: "null"

    RfqRequest:
      type: object
      required: [destinationCode, arrivalDate, departureDate, pax]
      properties:
        destinationCode:
          type: string
          pattern: "^[A-Z]{2}$"
          example: JP
        guestName:
          type: [string, "null"]
          maxLength: 200
        arrivalDate:
          type: string
          format: date
        departureDate:
          type: string
          format: date
        pax:
          $ref: "#/components/schemas/Pax"
        childAges:
          type: array
          items:
            type: integer
            minimum: 0
            maximum: 17
        leadSource:
          type: string
          enum:
            - EMAIL
            - WHATSAPP
            - PERSONAL_CONNECTION
            - REP_COMPANY
            - MARKETING_EVENT
            - GROUND_VISIT
            - OTHER
          default: EMAIL
        agentRequirements:
          type: [string, "null"]
          maxLength: 4000
        agentName:
          type: [string, "null"]
          description: |
            Only honoured on org-scoped keys. Agent-scoped keys ignore any
            body-supplied agent identity and bind to their own.

  responses:
    Unauthorized:
      description: Missing, malformed, unknown, or expired API key
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Envelope"
    BadRequest:
      description: Validation failed
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Envelope"
    RateLimited:
      description: Hourly quota exceeded
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Envelope"
    NotFound:
      description: Resource does not exist or is out of scope
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Envelope"

paths:
  /health:
    get:
      tags: [Health]
      summary: Verify auth and echo tenant identity
      operationId: getHealth
      responses:
        "200":
          description: OK
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  data:
                    type: object
                    properties:
                      status:
                        type: string
                      orgId:
                        type: string
                      agentId:
                        type: [string, "null"]
                      rateLimitPerHour:
                        type: [integer, "null"]
                      apiVersion:
                        type: string
                      serverTime:
                        type: string
                        format: date-time
        "401":
          $ref: "#/components/responses/Unauthorized"

  /destinations:
    get:
      tags: [Reference]
      summary: List active destinations for RFQ submission
      operationId: listDestinations
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Destination"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /hotels:
    get:
      tags: [Reference]
      summary: List hotels with optional city/name filter
      operationId: listHotels
      parameters:
        - $ref: "#/components/parameters/City"
        - $ref: "#/components/parameters/NameQuery"
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 100
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Hotel"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /sightseeings:
    get:
      tags: [Reference]
      summary: List paid activities and free stops
      operationId: listSightseeings
      parameters:
        - $ref: "#/components/parameters/City"
        - name: category
          in: query
          required: false
          schema:
            type: string
            enum: [PAID, FREE]
        - name: kind
          in: query
          required: false
          schema:
            type: string
            enum: [SIGHTSEEING, ACTIVITY, SHOPPING, WALKING]
        - $ref: "#/components/parameters/NameQuery"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Sightseeing"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /fullday-tours:
    get:
      tags: [Reference]
      summary: List private full-day tour catalog
      operationId: listFulldayTours
      parameters:
        - $ref: "#/components/parameters/City"
        - $ref: "#/components/parameters/NameQuery"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/FulldayTour"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /bookings:
    get:
      tags: [Bookings]
      summary: List bookings visible to your key
      operationId: listBookings
      parameters:
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum:
              - draft_quote
              - quote_sent
              - client_confirmed
              - ops_in_progress
              - travel_completed
              - cancelled
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/BookingSummary"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /bookings/{id}:
    get:
      tags: [Bookings]
      summary: Full detail for one booking including active quote
      operationId: getBooking
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  data:
                    $ref: "#/components/schemas/BookingDetail"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /quotes:
    post:
      tags: [Bookings]
      summary: Submit an RFQ; creates a DRAFT_QUOTE booking
      operationId: submitRfq
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RfqRequest"
      responses:
        "200":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  data:
                    $ref: "#/components/schemas/BookingDetail"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
