openapi: 3.0.3
info:
  title: ScraperCompany Hotel Rate API
  description: |
    Self-hosted hotel rate collection API. Three layers: Google SERP, direct OTA, and official booking engines.
    
    ## Authentication
    All endpoints require the `x-api-key` header with your API key (starts with `sk_`).
    
    ## Key Features
    - **Google Calendar**: 90-night horizon in one ~30KB request
    - **Per-OTA Breakdown**: Booking, Hotels.com, Expedia, Agoda with room detail
    - **Official Sites**: Auto-detected booking engines (29+ supported)
    - **Stored Rates**: Indexed current rates with freshness metadata
    - **Pay for Success**: Failed requests are free
    
    ## Base URL
    Production: `https://api.scrapercompany.com`
  version: 1.0.0
  contact:
    name: ScraperCompany Support
    url: https://scrapercompany.com/contact
  termsOfService: https://scrapercompany.com/terms

servers:
  - url: https://api.scrapercompany.com
    description: Production

security:
  - ApiKeyAuth: []

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: API key starting with `sk_`. Get yours from the dashboard.

  schemas:
    SearchRequest:
      type: object
      required:
        - name
      properties:
        name:
          type: string
          description: Property name as a guest would type it
          example: "Hilton Chicago"
        city:
          type: string
          description: Strongly recommended to avoid wrong-city matches
          example: "Chicago"
        market:
          type: string
          description: ISO country code (US, MX, GB, AU, etc.)
          example: "US"
        fallback_markets:
          type: array
          items:
            type: string
          description: Markets to try if primary fails
          example: ["US"]
        limit:
          type: integer
          default: 5
          description: Max candidates to return
        verify:
          type: boolean
          default: true
          description: Verify each candidate prices and matches name

    CalendarRequest:
      type: object
      required:
        - token
      properties:
        token:
          type: string
          description: Google property token from /v1/search
          example: "ChUIoben2Mv6-CYaCi9tLzA3czVwbjQQAQ"
        days:
          type: integer
          default: 90
          minimum: 1
          maximum: 180
          description: Number of nights to price
        start:
          type: string
          format: date
          description: First stay date (defaults to tomorrow)
          example: "2026-09-14"
        currency:
          type: string
          description: ISO 4217 currency code
          example: "USD"
        market:
          type: string
          description: Market/country (US, MX, AU, GB, etc.)
          example: "US"
        adults:
          type: integer
          default: 2
          minimum: 1
          maximum: 8
          description: Occupancy
        los:
          type: integer
          default: 1
          minimum: 1
          maximum: 30
          description: Length of stay per quote
        basis:
          type: string
          enum: [cheapest, mainstream]
          default: cheapest
          description: "cheapest: fast single RPC. mainstream: samples multi-MB offers pages"

    OffersRequest:
      type: object
      required:
        - token
        - check_in
      properties:
        token:
          type: string
          description: Google property token
          example: "ChUIoben2Mv6-CYaCi9tLzA3czVwbjQQAQ"
        check_in:
          type: string
          format: date
          example: "2026-09-14"
        nights:
          type: integer
          default: 1
          minimum: 1
          maximum: 30
        check_out:
          type: string
          format: date
          description: Alternative to nights (takes precedence)
        currency:
          type: string
          example: "USD"
        market:
          type: string
          example: "US"
        adults:
          type: integer
          default: 2
          minimum: 1
          maximum: 8
        device:
          type: string
          enum: [iphone, android, desktop]
          default: iphone
          description: Render profile (iPhone finds more offers)
        coverage:
          type: integer
          default: 1
          minimum: 1
          maximum: 3
          description: Number of device profiles to merge

    BookingSearchRequest:
      type: object
      required:
        - name
      properties:
        name:
          type: string
          example: "Moxy Boston Downtown"
        city:
          type: string
          example: "Boston"
        limit:
          type: integer
          default: 5

    HotelsSearchRequest:
      type: object
      required:
        - name
      properties:
        name:
          type: string
          example: "Moxy Boston Downtown"
        city:
          type: string
          example: "Boston"
        market:
          type: string
          example: "US"
        limit:
          type: integer
          default: 5

    AgodaSearchRequest:
      type: object
      required:
        - name
      properties:
        name:
          type: string
          example: "Moxy Boston Downtown"
        city:
          type: string
          example: "Boston"
        origin:
          type: string
          example: "US"
        limit:
          type: integer
          default: 5

    BookingCalendarRequest:
      type: object
      required:
        - pagename
        - country
      properties:
        pagename:
          type: string
          description: Booking.com URL slug (from search)
          example: "moxy-boston-downtown"
        country:
          type: string
          example: "us"
        days:
          type: integer
          default: 365
          description: Up to 365 nights in one GraphQL call
        start:
          type: string
          format: date
        currency:
          type: string
          example: "USD"

    HotelsRateRequest:
      type: object
      required:
        - property_id
        - check_in
      properties:
        property_id:
          type: string
          description: Hotels.com numeric ID (from search)
          example: "38766175"
        check_in:
          type: string
          format: date
          example: "2026-09-14"
        nights:
          type: integer
          default: 1
        market:
          type: string
          default: "US"
        currency:
          type: string
          example: "USD"

    AgodaRoomsRequest:
      type: object
      required:
        - property_id
        - check_in
      properties:
        property_id:
          type: string
          description: Agoda numeric ID (from search)
          example: "8795952"
        check_in:
          type: string
          format: date
          example: "2026-09-14"
        nights:
          type: integer
          default: 1
        currency:
          type: string
          example: "USD"

    OfficialRequest:
      type: object
      required:
        - url
        - check_in
      properties:
        url:
          type: string
          format: uri
          description: Hotel's booking engine URL
          example: "https://bookings.example.com/hotel"
        check_in:
          type: string
          format: date
          example: "2026-09-14"
        nights:
          type: integer
          default: 1

    JobRequest:
      type: object
      required:
        - items
      properties:
        items:
          type: array
          minItems: 1
          maxItems: 50
          items:
            type: object
            required:
              - token
            properties:
              token:
                type: string
              label:
                type: string
              days:
                type: integer
                default: 90
              currency:
                type: string
              market:
                type: string
        priority:
          type: integer
          default: 5
          minimum: 0
          maximum: 9
          description: Queue priority (0=lowest, 9=highest)
        callback_url:
          type: string
          format: uri
          description: HTTPS webhook for job completion

    ErrorResponse:
      type: object
      properties:
        detail:
          type: string
          description: Human-readable error message
        status_code:
          type: integer
        error_code:
          type: string

paths:
  /v1/search:
    post:
      summary: Find Property Token
      description: |
        Search for a Google property token by name and city. This is the starting point:
        you need a token to call any rate endpoint.
        
        Returns scored candidates with verification that each property prices and matches the name.
      operationId: searchProperty
      tags:
        - Discovery
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SearchRequest'
      responses:
        '200':
          description: Candidates found
          content:
            application/json:
              schema:
                type: object
                properties:
                  recommended_match:
                    type: object
                    description: Top verified match (null if ambiguous or not found)
                    properties:
                      token:
                        type: string
                      name:
                        type: string
                      name_score:
                        type: number
                  matches:
                    type: array
                    items:
                      type: object
                  search_status:
                    type: string
                    enum: [matched, ambiguous, not_found]
        '422':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/calendar:
    post:
      summary: 90-Night Calendar Rates
      description: |
        Get a forward horizon of nightly rates in one ~30KB request. The cheapest 
        endpoint by far: one Google batchexecute RPC covers the whole window.
        
        **Cost**: 1 credit per request (regardless of nights)
      operationId: getCalendar
      tags:
        - Google SERP
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CalendarRequest'
      responses:
        '200':
          description: Calendar rates
          content:
            application/json:
              schema:
                type: object
                properties:
                  priced:
                    type: array
                    items:
                      type: object
                      properties:
                        stay_date:
                          type: string
                          format: date
                        rate_base:
                          type: number
                        tax:
                          type: number
                        fees:
                          type: number
                        rate_total:
                          type: number
                        min_length_of_stay:
                          type: integer
                  coverage_pct:
                    type: number
                  meta:
                    type: object

  /v1/stay:
    post:
      summary: Explicit Check-In/Check-Out Window
      description: |
        Get rates for a specific stay window. Alternative to /v1/calendar when 
        you have exact dates.
      operationId: getStayRates
      tags:
        - Google SERP
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - token
                - check_in
                - check_out
              properties:
                token:
                  type: string
                  example: "ChUIoben2Mv6-CYaCi9tLzA3czVwbjQQAQ"
                check_in:
                  type: string
                  format: date
                  example: "2026-09-14"
                check_out:
                  type: string
                  format: date
                  example: "2026-09-17"
                currency:
                  type: string
                  example: "USD"
                market:
                  type: string
                  example: "US"
                adults:
                  type: integer
                  default: 2
      responses:
        '200':
          description: Stay rates
          content:
            application/json:
              schema:
                type: object

  /v1/offers:
    post:
      summary: Per-OTA Breakdown with Rooms
      description: |
        Get offers from each major OTA (Booking, Hotels.com, Expedia, Agoda) with 
        per-room detail and price ladders.
        
        **Cost**: ~5 credits (samples multi-MB pages)
      operationId: getOffers
      tags:
        - Google SERP
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OffersRequest'
      responses:
        '200':
          description: Per-OTA offers
          content:
            application/json:
              schema:
                type: object
                properties:
                  offers:
                    type: array
                    items:
                      type: object
                      properties:
                        source:
                          type: string
                        rate_base:
                          type: number
                        rate_total:
                          type: number
                        rooms:
                          type: array
                          items:
                            type: object

  /v1/rooms:
    post:
      summary: Room Types by OTA Matrix
      description: |
        Pivot room types across major OTAs, aligned by normalized signature.
      operationId: getRoomMatrix
      tags:
        - Google SERP
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - token
                - check_in
              properties:
                token:
                  type: string
                check_in:
                  type: string
                  format: date
                nights:
                  type: integer
                  default: 1
                currency:
                  type: string
                market:
                  type: string
      responses:
        '200':
          description: Room matrix
          content:
            application/json:
              schema:
                type: object

  /v1/serp/google_hotels_property:
    post:
      summary: Google Hotels Property Request
      description: |
        Get per-OTA offers for a specific property and date. Returns structured 
        rate data with consistent JSON envelope.
      operationId: serpProperty
      tags:
        - Google SERP
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
      responses:
        '200':
          description: Property offers with OTA breakdown
          content:
            application/json:
              schema:
                type: object

  /v1/serp/google_hotels_calendar:
    post:
      summary: Google Hotels Calendar Request
      description: |
        Get calendar rates in structured JSON envelope. Returns 90-night horizon 
        in a single request.
      operationId: serpCalendar
      tags:
        - Google SERP
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
      responses:
        '200':
          description: Calendar rates in JSON envelope
          content:
            application/json:
              schema:
                type: object

  /v1/serp/airbnb:
    post:
      summary: Airbnb Destination Search
      description: |
        Search Airbnb destinations with listings, prices, filters, and pagination. 
        Returns structured JSON with booking links, GPS, images, and reviews.
      operationId: serpAirbnb
      tags:
        - Airbnb
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                q:
                  type: string
                  example: "Toronto"
                airbnb_domain:
                  type: string
                  example: "airbnb.ca"
                check_in_date:
                  type: string
                  format: date
                check_out_date:
                  type: string
                  format: date
                adults:
                  type: integer
                currency:
                  type: string
      responses:
        '200':
          description: Airbnb listings
          content:
            application/json:
              schema:
                type: object

  /api/v1/search:
    get:
      summary: Airbnb Search (Query String)
      description: |
        Query-string variant of Airbnb search endpoint. Alternative to POST /v1/serp/airbnb 
        for integrations that prefer GET requests.
      operationId: airbnbSearchAlias
      tags:
        - Airbnb
      parameters:
        - name: engine
          in: query
          required: true
          schema:
            type: string
            enum: [airbnb]
        - name: q
          in: query
          schema:
            type: string
        - name: check_in_date
          in: query
          schema:
            type: string
            format: date
        - name: check_out_date
          in: query
          schema:
            type: string
            format: date
      responses:
        '200':
          description: Airbnb listings
          content:
            application/json:
              schema:
                type: object

  /v1/ota/booking/search:
    post:
      summary: Booking.com Property Search
      description: |
        Find Booking.com pagename and country by hotel name and city.
      operationId: searchBooking
      tags:
        - OTA Direct
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BookingSearchRequest'
      responses:
        '200':
          description: Booking.com candidates
          content:
            application/json:
              schema:
                type: object

  /v1/ota/hotels/search:
    post:
      summary: Hotels.com Property Search
      description: |
        Find Hotels.com numeric property ID by hotel name and city.
      operationId: searchHotels
      tags:
        - OTA Direct
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/HotelsSearchRequest'
      responses:
        '200':
          description: Hotels.com candidates
          content:
            application/json:
              schema:
                type: object

  /v1/ota/agoda/search:
    post:
      summary: Agoda Property Search
      description: |
        Find Agoda numeric property ID by hotel name and city.
      operationId: searchAgoda
      tags:
        - OTA Direct
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AgodaSearchRequest'
      responses:
        '200':
          description: Agoda candidates
          content:
            application/json:
              schema:
                type: object

  /v1/ota/booking:
    post:
      summary: Booking.com 61-Date Calendar
      description: |
        Get a 61-date calendar from Booking.com GraphQL in ~33KB. One of the 
        cleanest direct OTA endpoints.
        
        **Requires**: Browser-minted token (expensive ~15s first call, then cached)
      operationId: bookingCalendar
      tags:
        - OTA Direct
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BookingCalendarRequest'
      responses:
        '200':
          description: Booking.com calendar
          content:
            application/json:
              schema:
                type: object

  /v1/ota/hotels:
    post:
      summary: Hotels.com Single-Date Rate
      description: |
        Get Hotels.com rate for one stay date. Unlike Booking, Hotels.com has 
        no calendar endpoint (one call per date).
        
        **Requires**: Browser-minted token
      operationId: hotelsRate
      tags:
        - OTA Direct
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/HotelsRateRequest'
      responses:
        '200':
          description: Hotels.com rate
          content:
            application/json:
              schema:
                type: object

  /v1/ota/agoda:
    post:
      summary: Agoda Rooms with Rate Plans
      description: |
        Get every Agoda room type with rate plans. The cleanest OTA endpoint: 
        no anti-bot gate, per-room ladder included.
        
        **Cost**: ~0.3s, plain curl_cffi (no browser needed)
      operationId: agodaRooms
      tags:
        - OTA Direct
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AgodaRoomsRequest'
      responses:
        '200':
          description: Agoda room types
          content:
            application/json:
              schema:
                type: object

  /v1/ota/compare:
    post:
      summary: Fan-Out Across OTAs (Fail-Soft)
      description: |
        Run Booking, Hotels.com, and Agoda concurrently. Fail-soft per source: 
        one blocked OTA returns 200 with that source marked error and others answered.
      operationId: compareOTAs
      tags:
        - OTA Direct
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                check_in:
                  type: string
                  format: date
                currency:
                  type: string
                agoda_property_id:
                  type: string
                hotels_property_id:
                  type: string
                booking_pagename:
                  type: string
      responses:
        '200':
          description: Multi-source comparison
          content:
            application/json:
              schema:
                type: object

  /v1/ota/sources:
    get:
      summary: OTA Capability Matrix
      description: |
        Which OTA engines are available on this deployment, their call patterns, 
        and browser requirements.
      operationId: otaSources
      tags:
        - OTA Direct
      responses:
        '200':
          description: Source capabilities
          content:
            application/json:
              schema:
                type: object
                properties:
                  sources:
                    type: array
                    items:
                      type: object
                      properties:
                        name:
                          type: string
                        dates_per_call:
                          type: integer
                        per_room_detail:
                          type: boolean
                        search_browser:
                          type: boolean
                        rate_browser:
                          type: boolean

  /v1/official:
    post:
      summary: Official Site (Auto-Detected Engine)
      description: |
        Fetch rates from the hotel's own booking engine. Engine is auto-detected 
        from the URL (29+ extractors: SiteMinder, Seekda, iHotelier, SynXis, 
        Cloudbeds, DerbySoft, IHG, Hilton, etc.).
        
        Returns public and member/loyalty rates where exposed.
      operationId: officialRates
      tags:
        - Official Site
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OfficialRequest'
      responses:
        '200':
          description: Official site rates
          content:
            application/json:
              schema:
                type: object
                properties:
                  engine:
                    type: string
                  cheapest_rate:
                    type: number
                  cheapest_member_rate:
                    type: number
                  rooms:
                    type: array
                    items:
                      type: object
        '501':
          description: Engine not available on this deployment

  /v1/official/engines:
    get:
      summary: Supported Booking Engines
      description: |
        List of official-site booking engines that can be extracted, their 
        detection patterns, and cost per query.
      operationId: officialEngines
      tags:
        - Official Site
      responses:
        '200':
          description: Engine registry
          content:
            application/json:
              schema:
                type: object
                properties:
                  engines:
                    type: array
                    items:
                      type: object
                      properties:
                        name:
                          type: string
                        cost_seconds:
                          type: number
                        browser_required:
                          type: string
                          enum: [never, once_per_session, every_query]

  /v1/rates/stored:
    get:
      summary: Indexed Current Rates
      description: |
        Query stored rates by property/date with freshness metadata. Does not 
        scrape upstream; reads from PostgreSQL current-rates table.
      operationId: getStoredRates
      tags:
        - Storage & Usage
      parameters:
        - name: token
          in: query
          schema:
            type: string
        - name: date
          in: query
          schema:
            type: string
            format: date
      responses:
        '200':
          description: Stored rates with freshness
          content:
            application/json:
              schema:
                type: object

  /v1/storage/status:
    get:
      summary: Storage Health
      description: |
        Database and R2 archive health: safe table counts, retention windows, 
        and operational status.
      operationId: storageStatus
      tags:
        - Storage & Usage
      responses:
        '200':
          description: Storage metrics
          content:
            application/json:
              schema:
                type: object

  /v1/usage:
    get:
      summary: API Usage by Key
      description: |
        Request counts, errors, bytes, and latency aggregated by endpoint and day.
      operationId: getUsage
      tags:
        - Storage & Usage
      parameters:
        - name: period
          in: query
          schema:
            type: string
            enum: [24h, 7d, 30d, 90d]
            default: 30d
      responses:
        '200':
          description: Usage stats
          content:
            application/json:
              schema:
                type: object

  /v1/requests:
    get:
      summary: Durable Request History
      description: |
        Filterable request ledger with forward/back paging. Headers and response 
        bodies are never stored; request params are sanitized.
      operationId: getRequests
      tags:
        - Storage & Usage
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
        - name: status
          in: query
          schema:
            type: integer
        - name: next_cursor
          in: query
          schema:
            type: string
      responses:
        '200':
          description: Request history
          content:
            application/json:
              schema:
                type: object
                properties:
                  requests:
                    type: array
                    items:
                      type: object
                  next_cursor:
                    type: string

  /v1/requests/{request_id}:
    get:
      summary: One Request Record
      description: |
        Detailed metadata for one request by its opaque ID (returned in 
        `x-request-id` header).
      operationId: getRequest
      tags:
        - Storage & Usage
      parameters:
        - name: request_id
          in: path
          required: true
          schema:
            type: string
            pattern: '^req_'
      responses:
        '200':
          description: Request detail
          content:
            application/json:
              schema:
                type: object

  /v1/jobs:
    post:
      summary: Queue Async Calendar Batch
      description: |
        Submit a durable, idempotent batch of up to 50 calendar requests. Postgres-backed 
        queue with per-property status and optional webhook.
        
        **Returns**: 202 with job_id
      operationId: createJob
      tags:
        - Async Jobs
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/JobRequest'
      responses:
        '202':
          description: Job accepted
          content:
            application/json:
              schema:
                type: object
                properties:
                  job_id:
                    type: string
                  status:
                    type: string
                    enum: [queued]

  /v1/jobs/{job_id}:
    get:
      summary: Job Status + Results
      description: |
        Poll job status. Once succeeded/partial/failed, results are available.
      operationId: getJob
      tags:
        - Async Jobs
      parameters:
        - name: job_id
          in: path
          required: true
          schema:
            type: string
        - name: include_results
          in: query
          schema:
            type: boolean
            default: false
      responses:
        '200':
          description: Job status
          content:
            application/json:
              schema:
                type: object
                properties:
                  job_id:
                    type: string
                  status:
                    type: string
                    enum: [queued, running, succeeded, partial, failed]
                  items:
                    type: array
                    items:
                      type: object

tags:
  - name: Discovery
    description: Find property tokens by name
  - name: Google SERP
    description: Calendar, offers, and rooms from Google Travel
  - name: Airbnb
    description: Airbnb destination search
  - name: OTA Direct
    description: Booking.com, Hotels.com, Agoda direct GraphQL/API
  - name: Official Site
    description: Hotel's own booking engine (29+ extractors)
  - name: Storage & Usage
    description: Stored rates, usage stats, request history
  - name: Async Jobs
    description: Durable calendar batches with webhooks
