openapi: 3.0.3
info:
  title: PricesAPI Developer API
  description: >-
    Price intelligence infrastructure for developers. Everything you need
    to build price intelligence. The Product Search API is a Product Search &
    Merchant Offers API: search by product name or retailer URL and get matched
    candidates with available merchant offers inline. Global coverage across
    supported markets; availability varies by product and market.


    Authenticate with an API key sent as a bearer token
    (`Authorization: Bearer <key>`). Search is synchronous; cold searches can
    take several minutes, so clients should use a long read timeout. Pricing
    models, included allowances, and usage limits for all Beta features may
    change before general availability. Existing Product Search pricing and
    Search-credit allowances remain unchanged unless separately announced.
  version: 1.0.0
  contact:
    name: Andrew at PricesAPI
    email: andrew@pricesapi.io
    url: https://pricesapi.io/docs
  termsOfService: https://pricesapi.io/terms
servers:
  - url: https://api.pricesapi.io
    description: Primary host
  - url: https://api.buywisely.com.au
    description: Alternate host (same API)
security:
  - bearerAuth: []
tags:
  - name: Products
    description: Product search and pricing.
  - name: Async Bulk Search
    description: Public Beta for asynchronous batches of Search Products requests, available to every valid API account. Beta pricing models, included allowances, and usage limits may change before general availability.
  - name: Schedules
    description: Caller-scoped recurring Search management. Schedules is in Beta. Limits and pricing are subject to change.
  - name: Price History
    description: Public Beta retained-data reads for sparse observed product prices, available to every valid API account. Availability and commercial terms may change during Beta.
paths:
  /api/v1/products/search:
    get:
      operationId: searchProducts
      tags:
        - Products
      summary: Search products with offers inline
      description: >-
        The synchronous Product Search operation. Pass a product URL OR
        brand+model text as `q`. One call runs product discovery and offer
        lookup inline and returns up to `limit` ranked candidates, each carrying
        its seller offers.


        SYNCHRONOUS: cold searches can take several minutes. Set a long read
        timeout.


        An empty result (Google has nothing for the query) is a `200` with
        `data.products = []`, NOT a 404. If the offers stage fails but
        candidates are still found, the call returns `200` with
        `meta.degraded = true` and empty `offers` arrays.


        Billing: 1 credit only when a 200 Search Products response returns at
        least one product. Cache hits follow the same billing rule. Empty
        results and 4xx/5xx responses use no credit.
      parameters:
        - in: query
          name: q
          required: true
          description: >-
            Product URL OR brand+model text (e.g. `iphone 15 pro 256gb` or a
            retailer product URL). Missing -> 400 MISSING_QUERY.
          schema:
            type: string
          example: iphone 15 pro 256gb
        - in: query
          name: country
          required: false
          description: >-
            Lowercase ISO country code for global search coverage (for example,
            au, us, gb, nz, or ca).
            Use `gb` NOT `uk`. An unsupported code returns 400
            COUNTRY_NOT_SUPPORTED with the supported list.
          schema:
            type: string
            default: au
            example: au
        - in: query
          name: limit
          required: false
          description: >-
            Number of candidate products to return. Max 5 — a value above 5 is
            clamped to 5 (`meta.limit_capped = true`, `meta.limit_max = 5`).
            Runtime parses the leading base-10 integer; no numeric prefix or a
            parsed value below 1 returns 400 INVALID_LIMIT.
          schema:
            type: integer
            default: 3
            minimum: 1
            example: 3
        - in: query
          name: offers_limit
          required: false
          description: >-
            Max number of offers per candidate product, returned in each
            candidate's `offers[]` array. Default 15; values above 60 are
            clamped to 60.
            A smaller value keeps the first offers in their collected order;
            `offerCount` reflects the truncated length.
            Runtime parses the leading base-10 integer; no numeric prefix or a
            parsed value below 1 returns 400 INVALID_OFFERS_LIMIT.
          schema:
            type: integer
            default: 15
            minimum: 1
            example: 3
        - in: query
          name: max_age
          required: false
          description: >-
            Maximum acceptable age in seconds for scheduled stored data. When
            Schedules serving or fallback is enabled, this must contain only
            non-negative decimal digits; invalid values return 400
            INVALID_MAX_AGE. Supplying it is an explicit freshness requirement,
            so stale scheduled fallback is not used. When those features are
            disabled, the parameter is ignored.
          schema:
            type: integer
            minimum: 0
        - in: query
          name: no_cache
          required: false
          description: >-
            Values `1`, `true`, or `yes` (case-insensitive) bypass Redis,
            Postgres, and scheduled-store reads, force a live scrape, and still
            refresh caches. This is an explicit freshness requirement, so stale
            scheduled fallback is not used. Other values behave as false.
          schema:
            type: string
            example: 'true'
      responses:
        '200':
          description: >-
            Search succeeded. `data.products` may be empty (Google had nothing).
            `meta.degraded = true` indicates candidates were returned with empty
            offers because the offers stage failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SearchResponse'
        '400':
          description: >-
            Invalid request. `error.code` is one of MISSING_QUERY,
            COUNTRY_NOT_SUPPORTED, INVALID_LIMIT, INVALID_OFFERS_LIMIT, or
            INVALID_MAX_AGE (when Schedules serving or fallback is enabled).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: >-
            Authentication failed. `error.code` is MISSING_API_KEY,
            INVALID_API_KEY_FORMAT, or INVALID_API_KEY.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: >-
            The subscription is cancelled or its API credits are exhausted.
            `error.code` is SUBSCRIPTION_CANCELLED, CREDITS_EXCEEDED, or
            MONTHLY_LIMIT_EXCEEDED. The two quota spellings come from the
            pre-auth check and the atomic usage check respectively.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: >-
            Rate limit exceeded for this API key (per-minute limit depends on
            plan: Personal 6, Developer 20, Business 40, Enterprise 60).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: >-
            Server-side fault. `error.code` is DB_ERROR, SEARCH_FAILED, or
            VALIDATION_ERROR. Not the client's to retry.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          description: >-
            Scraper is transiently unavailable, busy, or its circuit is open.
            `error.code` is SCRAPER_UNAVAILABLE, SCRAPER_TIMEOUT, SCRAPER_BUSY,
            SCRAPER_CIRCUIT_OPEN, or SERVICE_UNAVAILABLE. Back off and retry
            after the `Retry-After` header (seconds).
          headers:
            Retry-After:
              description: Seconds to wait before retrying.
              schema:
                type: integer
                example: 30
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/v1/products:
    get:
      operationId: getProduct
      tags:
        - Products
      summary: Read a known product by an explicit typed ID
      description: >-
        Public Beta for every valid API account. PricesAPI and Google Shopping
        IDs use this same route and return the same canonical Product shape for
        one explicit market. This retained-data read does not run Search, call a
        scraper, or consume a Search credit. Pricing models, included allowances,
        and usage limits for all Beta features may change before general availability.
      parameters:
        - $ref: '#/components/parameters/ProductSnapshotId'
        - $ref: '#/components/parameters/ProductSnapshotIdType'
        - $ref: '#/components/parameters/ProductSnapshotMarket'
        - $ref: '#/components/parameters/ProductSnapshotOffersLimit'
        - $ref: '#/components/parameters/ProductSnapshotRetailer'
      responses:
        '200':
          description: Canonical product plus bounded latest retained merchant offers.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductSnapshotResponse'
        '400':
          description: Invalid identifier, identifier authority, market, offer limit, or retailer domain.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductSnapshotErrorResponse'
        '404':
          description: The exact product mapping was not found in this market.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductSnapshotErrorResponse'
        '405':
          description: Only GET is supported. No Product read or meter occurs.
          headers:
            Allow:
              schema:
                type: string
                enum: [GET]
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductSnapshotErrorResponse'
        '429':
          description: The account-scoped Product Snapshot rate limit was exceeded.
          headers:
            Retry-After:
              schema:
                type: integer
                minimum: 1
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductSnapshotErrorResponse'
        '503':
          description: Product Snapshot retained-data projection is temporarily unavailable.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductSnapshotErrorResponse'
  /api/v1/products/batch:
    post:
      operationId: getProducts
      tags:
        - Products
      summary: Read up to 100 known products in one ordered request
      description: >-
        Public Beta for every valid API account. Read 1 to 100 exact Product
        identities using the same typed ID and explicit-market contract as the
        single Product route. Results preserve input order; identical inputs keep
        every original position while sharing one physical read; PRODUCT_NOT_FOUND
        is a per-item result. This retained-data request does not run Search, call
        a scraper, or consume a Search credit. Pricing models, included allowances,
        and usage limits for all Beta features may change before general availability.
        Each requested item counts against the account's separate batch-read
        rate limit; the request does not count as only one read.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProductSnapshotBatchRequest'
      responses:
        '200':
          description: Ordered Product snapshots and explicit per-item misses.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductSnapshotBatchResponse'
        '400':
          description: Invalid JSON, batch shape, item, identifier, market, offer limit, or retailer domain.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductSnapshotErrorResponse'
        '405':
          description: Only POST is supported. No Product read or meter occurs.
          headers:
            Allow:
              schema:
                type: string
                enum: [POST]
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductSnapshotErrorResponse'
        '413':
          description: The JSON body exceeded the 100 KB request ceiling.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductSnapshotErrorResponse'
        '429':
          description: The account-scoped Product Snapshot rate limit was exceeded.
          headers:
            Retry-After:
              schema:
                type: integer
                minimum: 1
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductSnapshotErrorResponse'
        '503':
          description: Product Snapshot retained-data projection is temporarily unavailable.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductSnapshotErrorResponse'
  /api/v1/product-groups:
    get:
      operationId: listProductGroupMembers
      tags:
        - Products
      summary: Page the known products in a product group
      description: >-
        Public Beta for every valid API account. Returns deterministic known
        membership for one explicit Google Shopping product-group ID and market;
        it is not one product, Search ranking, or guaranteed exhaustive variants.
        This management read consumes no Search credit. Pricing models, included
        allowances, and usage limits for all Beta features may change before
        general availability.
      parameters:
        - $ref: '#/components/parameters/ProductGroupId'
        - $ref: '#/components/parameters/ProductGroupIdType'
        - $ref: '#/components/parameters/ProductSnapshotMarket'
        - $ref: '#/components/parameters/ProductGroupLimit'
        - $ref: '#/components/parameters/ProductGroupCursor'
      responses:
        '200':
          description: A bounded deterministic page of known products.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductGroupPageResponse'
        '400':
          description: Invalid product-group ID, authority, market, limit, or cursor.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductSnapshotErrorResponse'
        '404':
          description: The product group was not found in this market.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductSnapshotErrorResponse'
        '405':
          description: Only GET is supported. No group read or meter occurs.
          headers:
            Allow:
              schema:
                type: string
                enum: [GET]
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductSnapshotErrorResponse'
        '429':
          description: The account-scoped Product Snapshot rate limit was exceeded.
          headers:
            Retry-After:
              schema:
                type: integer
                minimum: 1
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductSnapshotErrorResponse'
        '503':
          description: Product Snapshot retained-data projection is temporarily unavailable.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductSnapshotErrorResponse'
  /api/v1/schedules:
    get:
      operationId: listSchedules
      tags: [Schedules]
      summary: List recurring searches and cadence health
      description: >-
        List only this API account's Schedules, including latest catalogue-discovery
        activity and caller-scoped cadence health. Optional account-wide analytics
        can be requested for 7, 30, or 90 days; if that supplemental read is
        unavailable, the schedule rows still return. This is a credit-free
        management operation and never runs Search. Schedules is in Beta. Limits
        and pricing are subject to change.
      parameters:
        - name: country
          in: query
          required: false
          description: Supported market code. Matching is case-insensitive.
          schema: { type: string }
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum: [starting, active, recovering, paused]
        - name: origin
          in: query
          required: false
          schema:
            type: string
            enum: [auto, manual, bulk]
        - name: q
          in: query
          required: false
          description: Normalized substring filter over caller-owned terms.
          schema: { type: string, minLength: 1, maxLength: 512 }
        - name: frequency
          in: query
          required: false
          schema:
            type: integer
            enum: [60, 360, 720, 1440, 10080]
        - name: activity_days
          in: query
          required: false
          schema:
            type: integer
            enum: [7, 30, 90]
            default: 7
        - name: cursor
          in: query
          required: false
          description: Exact opaque continuation value issued by this operation.
          schema: { type: string }
        - name: limit
          in: query
          required: false
          schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
      responses:
        '200':
          description: One caller-scoped schedule page and optional account-wide analytics.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleListResponse' }
        '400':
          description: Invalid filter, analytics window, cursor, market, or limit.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
        '401':
          description: API-key authentication failed.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
        '403':
          description: The subscription is cancelled.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
        '404':
          description: Schedules is unavailable in this deployment or the route is unknown.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
        '429':
          description: The separate Schedules management rate limit was exceeded.
          headers:
            Retry-After: { $ref: '#/components/headers/SchedulesRetryAfter' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
        '503':
          description: Schedules is temporarily unavailable.
          headers:
            Retry-After: { $ref: '#/components/headers/SchedulesRetryAfter' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
    post:
      operationId: createSchedules
      tags: [Schedules]
      summary: Add recurring searches
      description: >-
        Add one or more caller-owned recurring searches. Duplicate normalized
        term-and-market pairs report exists rather than creating a second schedule;
        each input returns its own outcome. The management operation is credit-free,
        and background scheduled refreshes use no Search credit. A later
        customer-initiated non-empty Search follows normal billing. Schedules is in
        Beta. Limits and pricing are subject to change.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ScheduleCreateRequest' }
      responses:
        '200':
          description: Per-input add, existing, cap, invalid, or retryable outcomes.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleCreateResponse' }
        '400':
          description: Invalid body or too many terms.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
        '401':
          description: API-key authentication failed.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
        '403':
          description: The subscription is cancelled or a reserved allocation is not permitted.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
        '404':
          description: Schedules is unavailable in this deployment or the route is unknown.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
        '413':
          description: The authenticated Schedules request body exceeded its configured limit.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
        '429':
          description: The separate Schedules management rate limit was exceeded.
          headers:
            Retry-After: { $ref: '#/components/headers/SchedulesRetryAfter' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
        '503':
          description: Schedule changes are paused or Schedules is temporarily unavailable.
          headers:
            Retry-After: { $ref: '#/components/headers/SchedulesRetryAfter' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
  /api/v1/schedules/history:
    get:
      operationId: listScheduleHistory
      tags: [Schedules]
      summary: List recent Search demand for adding to Schedules
      description: >-
        Page this API account's normalized Search history and caller-scoped schedule
        state. Optional demand analytics use the same 7, 30, or 90 day window and may
        be omitted without hiding history rows. This is a credit-free management read
        and never reruns Search. Schedules is in Beta. Limits and pricing are subject
        to change.
      parameters:
        - name: days
          in: query
          required: false
          schema: { type: integer, minimum: 1, maximum: 90, default: 90 }
        - name: country
          in: query
          required: false
          description: Supported market code. Matching is case-insensitive.
          schema: { type: string }
        - name: cursor
          in: query
          required: false
          description: Exact opaque continuation value issued by this operation.
          schema: { type: string }
        - name: limit
          in: query
          required: false
          schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
      responses:
        '200':
          description: One caller-scoped Search-history page with optional demand analytics.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleHistoryResponse' }
        '400':
          description: Invalid days, market, cursor, or limit.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
        '401':
          description: API-key authentication failed.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
        '403':
          description: The subscription is cancelled.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
        '404':
          description: Schedules is unavailable in this deployment or the route is unknown.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
        '429':
          description: The separate Schedules management rate limit was exceeded.
          headers:
            Retry-After: { $ref: '#/components/headers/SchedulesRetryAfter' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
        '503':
          description: Schedules is temporarily unavailable.
          headers:
            Retry-After: { $ref: '#/components/headers/SchedulesRetryAfter' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
  /api/v1/schedules/settings:
    get:
      operationId: getScheduleSettings
      tags: [Schedules]
      summary: Read account Schedule settings
      description: >-
        Read this API account's automatic-enrolment and default-cadence settings.
        This is a credit-free management operation and never runs Search. Schedules
        is in Beta. Limits and pricing are subject to change.
      responses:
        '200':
          description: Caller-owned Schedule settings.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleSettingsResponse' }
        '401':
          description: API-key authentication failed.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
        '403':
          description: The subscription is cancelled.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
        '404':
          description: Schedules is unavailable in this deployment or the route is unknown.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
        '429':
          description: The separate Schedules management rate limit was exceeded.
          headers:
            Retry-After: { $ref: '#/components/headers/SchedulesRetryAfter' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
        '503':
          description: Schedules is temporarily unavailable.
          headers:
            Retry-After: { $ref: '#/components/headers/SchedulesRetryAfter' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
    patch:
      operationId: updateScheduleSettings
      tags: [Schedules]
      summary: Update account Schedule settings
      description: >-
        Update automatic enrolment, its daily-or-weekly default, or the inactivity
        eviction window for this API account. This is a credit-free management
        operation; background refreshes use no Search credit. Schedules is in Beta.
        Limits and pricing are subject to change.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ScheduleSettingsUpdateRequest' }
      responses:
        '200':
          description: Updated caller-owned Schedule settings.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleSettingsResponse' }
        '400':
          description: Invalid body, cadence, or eviction window.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
        '401':
          description: API-key authentication failed.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
        '403':
          description: The subscription is cancelled.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
        '404':
          description: Schedules is unavailable in this deployment or the route is unknown.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
        '429':
          description: The separate Schedules management rate limit was exceeded.
          headers:
            Retry-After: { $ref: '#/components/headers/SchedulesRetryAfter' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
        '503':
          description: Schedule changes are paused or Schedules is temporarily unavailable.
          headers:
            Retry-After: { $ref: '#/components/headers/SchedulesRetryAfter' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
  /api/v1/schedules/{id}:
    get:
      operationId: getSchedule
      tags: [Schedules]
      summary: Read one caller-owned schedule
      description: >-
        Read one Schedule with latest discovery activity, cadence health, and the
        caller's available cadence choices. Another account's identifier returns the
        same 404 as an unknown identifier. This is a credit-free management operation
        and never runs Search. Schedules is in Beta. Limits and pricing are subject to
        change.
      parameters:
        - $ref: '#/components/parameters/ScheduleId'
      responses:
        '200':
          description: One caller-owned Schedule.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleDetailResponse' }
        '400':
          description: id is not a positive integer.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
        '401':
          description: API-key authentication failed.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
        '403':
          description: The subscription is cancelled.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
        '404':
          description: No such Schedule belongs to this account.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
        '429':
          description: The separate Schedules management rate limit was exceeded.
          headers:
            Retry-After: { $ref: '#/components/headers/SchedulesRetryAfter' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
        '503':
          description: Schedules is temporarily unavailable.
          headers:
            Retry-After: { $ref: '#/components/headers/SchedulesRetryAfter' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
    patch:
      operationId: updateSchedule
      tags: [Schedules]
      summary: Pause, resume, or change one schedule
      description: >-
        Change this caller's cadence and/or paused state. Resuming or choosing a
        faster cadence rechecks the account's schedule-slot cap. This credit-free
        management operation does not consume a Search credit. Schedules is in Beta.
        Limits and pricing are subject to change.
      parameters:
        - $ref: '#/components/parameters/ScheduleId'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ScheduleUpdateRequest' }
      responses:
        '200':
          description: Updated caller-owned Schedule.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleDetailResponse' }
        '400':
          description: Invalid id, body, cadence, or unavailable cadence.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
        '401':
          description: API-key authentication failed.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
        '403':
          description: The subscription is cancelled.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
        '404':
          description: No such Schedule belongs to this account.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
        '409':
          description: The requested change would exceed the account schedule-slot cap.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
        '429':
          description: The separate Schedules management rate limit was exceeded.
          headers:
            Retry-After: { $ref: '#/components/headers/SchedulesRetryAfter' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
        '503':
          description: Schedule changes are paused or Schedules is temporarily unavailable.
          headers:
            Retry-After: { $ref: '#/components/headers/SchedulesRetryAfter' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
    delete:
      operationId: deleteSchedule
      tags: [Schedules]
      summary: Remove one caller-owned schedule
      description: >-
        Idempotently remove this API account's subscription. An unknown or
        other-account identifier returns deleted false without revealing whether it
        exists. This is a credit-free management operation and consumes no Search
        credit. Schedules is in Beta. Limits and pricing are subject to change.
      parameters:
        - $ref: '#/components/parameters/ScheduleId'
      responses:
        '200':
          description: Removal result; deleted is false when nothing belonged to the caller.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleRemoveResponse' }
        '400':
          description: id is not a positive integer.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
        '401':
          description: API-key authentication failed.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
        '403':
          description: The subscription is cancelled.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
        '429':
          description: The separate Schedules management rate limit was exceeded.
          headers:
            Retry-After: { $ref: '#/components/headers/SchedulesRetryAfter' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
        '503':
          description: Schedule changes are paused or Schedules is temporarily unavailable.
          headers:
            Retry-After: { $ref: '#/components/headers/SchedulesRetryAfter' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduleErrorResponse' }
  /api/v1/search/jobs:
    post:
      operationId: submitSearchJob
      tags:
        - Async Bulk Search
      summary: Submit an asynchronous Search job
      description: >-
        Submit strict JSON Search requests as one account-scoped Beta job. The
        public Beta is available to every valid API account. The configured Beta
        ceiling is currently 100 items per job. The complete JSON body must not
        exceed 100 KB. Submission
        atomically reserves one Search credit per item or creates no job. A
        successful non-empty result consumes the reservation; empty, failed,
        and cancelled-before-start items are refunded. This management request
        uses no additional credit.
      parameters:
        - in: header
          name: Idempotency-Key
          required: true
          description: >-
            A caller-generated value from 1 to 255 characters. Reusing the key
            with the same normalized, ordered request returns the original job;
            reusing it with a different request returns 409.
          schema:
            type: string
            minLength: 1
            maxLength: 255
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BulkSearchSubmission'
      responses:
        '200':
          description: Job created, or the matching idempotent submission replayed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BulkSearchSubmitResponse'
        '400':
          description: >-
            Invalid idempotency key, body, item, country, or JSON. Codes include
            MISSING_IDEMPOTENCY_KEY, INVALID_IDEMPOTENCY_KEY, INVALID_BODY,
            INVALID_ITEM_COUNT, INVALID_ITEM, COUNTRY_NOT_SUPPORTED, and
            INVALID_JSON.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: API-key authentication failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: >-
            The subscription is cancelled or the account cannot reserve one
            Search credit for every item. Code is SUBSCRIPTION_CANCELLED or
            INSUFFICIENT_CREDITS. No partial job is created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: >-
            Async Bulk Search is unavailable in this deployment or the route is
            not found. The API retains its ordinary unknown-route posture
            (NOT_FOUND).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: The Idempotency-Key was already used with different normalized input.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '413':
          description: The JSON request body exceeds the 100 KB Bulk Search ceiling.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Too many Bulk Search management requests for this API key.
          headers:
            Retry-After:
              $ref: '#/components/headers/BulkSearchRetryAfter'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: An internal server or API-key validation fault.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          description: Bulk Search or API-key validation is temporarily unavailable.
          headers:
            Retry-After:
              description: Seconds to wait before retrying.
              schema:
                type: integer
                example: 5
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/v1/search/jobs/{id}:
    get:
      operationId: getSearchJob
      tags:
        - Async Bulk Search
      summary: Read Search job status
      description: >-
        Return caller-scoped item and credit counters. The job is complete when
        pending plus running equals zero. This management request uses no
        additional credit.
      parameters:
        - $ref: '#/components/parameters/SearchJobId'
      responses:
        '200':
          description: Current job counters.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BulkSearchJobResponse'
        '400':
          description: id is not a UUID (INVALID_JOB_ID).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: API-key authentication failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: The subscription is cancelled.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No such Search job belongs to this account (JOB_NOT_FOUND).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Too many Bulk Search management requests for this API key.
          headers:
            Retry-After:
              $ref: '#/components/headers/BulkSearchRetryAfter'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: An internal server or API-key validation fault.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          description: Bulk Search or API-key validation is temporarily unavailable.
          headers:
            Retry-After:
              description: Seconds to wait before retrying.
              schema:
                type: integer
                example: 5
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    delete:
      operationId: cancelSearchJob
      tags:
        - Async Bulk Search
      summary: Request Search job cancellation
      description: >-
        Idempotently cancel and refund pending items. Running items may still
        complete, and terminal items remain unchanged. This operation does not
        delete the job and uses no additional credit.
      parameters:
        - $ref: '#/components/parameters/SearchJobId'
      responses:
        '200':
          description: Updated job counters after requesting cancellation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BulkSearchJobResponse'
        '400':
          description: id is not a UUID (INVALID_JOB_ID).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: API-key authentication failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: The subscription is cancelled.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No such Search job belongs to this account (JOB_NOT_FOUND).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Too many Bulk Search management requests for this API key.
          headers:
            Retry-After:
              $ref: '#/components/headers/BulkSearchRetryAfter'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: An internal server or API-key validation fault.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          description: Bulk Search or API-key validation is temporarily unavailable.
          headers:
            Retry-After:
              description: Seconds to wait before retrying.
              schema:
                type: integer
                example: 5
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/v1/search/jobs/{id}/results:
    get:
      operationId: listSearchJobResults
      tags:
        - Async Bulk Search
      summary: Page through Search job items
      description: >-
        Return every item state in original input order. Results can be read
        while work is in progress. Pass next_cursor back unchanged; null marks
        the final page. Ordering and cursor boundaries are stable, but item
        states are live rather than a snapshot. After pending + running reaches
        zero, make a fresh pass from the first page to collect final outcomes.
        expires_at is the execution cutoff: unfinished pending
        items are cancelled and refunded, while already-running work may finish.
        Status and results remain readable after the cutoff in the current Beta,
        but are not durable storage; collect them promptly. This management
        request uses no additional credit.
      parameters:
        - $ref: '#/components/parameters/SearchJobId'
        - in: query
          name: limit
          required: false
          description: Number of items per page, from 1 to 200.
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
        - in: query
          name: cursor
          required: false
          description: Exact opaque next_cursor value issued for this job.
          schema:
            type: string
            maxLength: 200
      responses:
        '200':
          description: One input-ordered page with stable cursor boundaries and live item states, not a snapshot.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BulkSearchResultsResponse'
        '400':
          description: INVALID_JOB_ID, INVALID_LIMIT, or INVALID_CURSOR.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: API-key authentication failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: The subscription is cancelled.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No such Search job belongs to this account (JOB_NOT_FOUND).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Too many Bulk Search management requests for this API key.
          headers:
            Retry-After:
              $ref: '#/components/headers/BulkSearchRetryAfter'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: An internal server or API-key validation fault.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          description: Bulk Search or API-key validation is temporarily unavailable.
          headers:
            Retry-After:
              description: Seconds to wait before retrying.
              schema:
                type: integer
                example: 5
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/v1/products/history:
    get:
      operationId: getProductHistory
      tags:
        - Price History
      summary: Read sparse observed prices for one exact product
      description: >-
        Public Beta for every valid API account. Resolves either exact identifier to one canonical
        PricesAPI Product and returns only observed prices in an inclusive UTC
        window. Missing periods are omitted, never zero or fill-forward. The
        operation does not invoke Search or a scraper. Coverage is global across
        supported markets; Australia currently has the deepest retained-history
        cohort and other markets continue to accumulate data. During Beta this
        read uses a separate shadow meter and does not consume a Search credit.
      parameters:
        - name: id
          in: query
          required: true
          schema: { type: string, minLength: 1, maxLength: 64 }
        - name: id_type
          in: query
          required: true
          schema: { type: string, enum: [pricesapi, google_shopping] }
        - name: market
          in: query
          required: true
          schema: { type: string, pattern: '^[a-z]{2}$' }
        - name: from
          in: query
          required: true
          schema: { type: string, format: date }
        - name: to
          in: query
          required: true
          schema: { type: string, format: date }
        - name: group_by
          in: query
          required: true
          description: Aggregates observations that exist; it does not promise period coverage.
          schema: { type: string, enum: [day, month] }
      responses:
        '200':
          description: Sparse observed prices, including a valid empty known-product window
          headers:
            Cache-Control:
              schema: { type: string, enum: ['private, no-store'] }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/HistoryResponse' }
        '400':
          description: Invalid product, market, range, grouping, or PAGINATION_NOT_SUPPORTED
          content:
            application/json:
              schema: { $ref: '#/components/schemas/HistoryError' }
        '401':
          description: Missing, malformed, or unknown API key
          content:
            application/json:
              schema: { $ref: '#/components/schemas/HistoryAuthError' }
        '404':
          description: PRODUCT_NOT_FOUND in the requested market, or the Price History resource is currently unavailable
          content:
            application/json:
              schema: { $ref: '#/components/schemas/HistoryError' }
        '405':
          description: METHOD_NOT_ALLOWED
          content:
            application/json:
              schema: { $ref: '#/components/schemas/HistoryError' }
        '429':
          description: Price History account rate limit exceeded
          headers:
            Retry-After:
              schema: { type: integer, minimum: 1 }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/HistoryError' }
        '503':
          description: HISTORY_UNAVAILABLE or HISTORY_RATE_LIMIT_UNAVAILABLE
          headers:
            Retry-After:
              description: Present as 5 seconds when the History rate-limit authority is unavailable.
              schema: { type: integer, minimum: 1 }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/HistoryError' }

  /api/v1/products/{id}/offers:
    get:
      operationId: getProductOffers
      tags:
        - Products
      deprecated: true
      summary: (REMOVED) Fetch offers for a product
      description: >-
        REMOVED. This was the second call of the old two-step flow. Offers are
        now returned inline by `GET /api/v1/products/search`. This endpoint
        always responds `410 Gone` with `error.code = ENDPOINT_GONE`. See the
        migration guide in the API docs.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        '410':
          description: Endpoint removed. Use GET /api/v1/products/search instead.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
components:
  headers:
    BulkSearchRetryAfter:
      description: Seconds to wait before another Bulk Search management request.
      schema:
        type: integer
        minimum: 1
    SchedulesRetryAfter:
      description: Seconds to wait before another Schedules management request.
      schema:
        type: integer
        minimum: 1
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >-
        API key sent as a bearer token: `Authorization: Bearer <key>`
        (recommended). A `?api_key=<key>` query parameter is also accepted but
        is logged in request URLs — prefer the header.
  parameters:
    ScheduleId:
      in: path
      name: id
      required: true
      description: Positive integer Schedule identifier returned by create or list.
      schema:
        type: integer
        minimum: 1
    SearchJobId:
      in: path
      name: id
      required: true
      description: Search job UUID returned by submit.
      schema:
        type: string
        format: uuid
    ProductSnapshotId:
      name: id
      in: query
      required: true
      description: A PricesAPI decimal ID or opaque Google Shopping product ID, interpreted only by id_type.
      schema:
        type: string
        minLength: 1
        maxLength: 64
    ProductSnapshotIdType:
      name: id_type
      in: query
      required: true
      description: The authority of id; never inferred from its value.
      schema:
        type: string
        enum: [pricesapi, google_shopping]
    ProductGroupId:
      name: id
      in: query
      required: true
      description: Opaque Google Shopping product-group ID, 1–64 bytes.
      schema:
        type: string
        pattern: '^[A-Za-z0-9._~-]+$'
        minLength: 1
        maxLength: 64
    ProductGroupIdType:
      name: id_type
      in: query
      required: true
      description: Product groups currently accept this explicit external authority.
      schema:
        type: string
        enum: [google_shopping]
    ProductSnapshotMarket:
      name: market
      in: query
      required: true
      description: Supported lower-case market code; there is no default market.
      schema:
        type: string
        pattern: '^[a-z]{2}$'
    ProductSnapshotOffersLimit:
      name: offers_limit
      in: query
      required: false
      schema:
        type: integer
        minimum: 0
        maximum: 20
        default: 20
    ProductSnapshotRetailer:
      name: retailer
      in: query
      required: false
      style: form
      explode: true
      description: >-
        Repeatable hostname-only retailer domain. Up to 10 may be supplied. The
        API applies IDNA, lowercases, strips one leading www., preserves other
        subdomains, and matches exact canonical domains before offer limiting.
      schema:
        type: array
        minItems: 1
        maxItems: 10
        items:
          type: string
          minLength: 3
          maxLength: 253
    ProductGroupLimit:
      name: limit
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 100
    ProductGroupCursor:
      name: cursor
      in: query
      required: false
      description: Opaque API-issued continuation bound to this market and group.
      schema:
        type: string
  schemas:
    HistoryResponse:
      type: object
      additionalProperties: false
      required: [product, range, summary, prices]
      properties:
        product: { $ref: '#/components/schemas/HistoryProduct' }
        range: { $ref: '#/components/schemas/HistoryRange' }
        summary: { $ref: '#/components/schemas/HistorySummary' }
        prices:
          type: array
          maxItems: 1500
          items:
            oneOf:
              - { $ref: '#/components/schemas/DailyHistoryPrice' }
              - { $ref: '#/components/schemas/MonthlyHistoryPrice' }
    HistoryProduct:
      type: object
      additionalProperties: false
      required: [id, id_type, market, currency]
      properties:
        id: { type: string, pattern: '^[1-9][0-9]{0,9}$' }
        id_type: { type: string, enum: [pricesapi] }
        market: { type: string, pattern: '^[a-z]{2}$' }
        currency: { type: string, pattern: '^[A-Z]{3}$' }
    HistoryRange:
      type: object
      additionalProperties: false
      required: [from, to, group_by, available_from, available_to, requested_days, observed_days, missing_days]
      properties:
        from: { type: string, format: date }
        to: { type: string, format: date }
        group_by: { type: string, enum: [day, month] }
        available_from: { type: string, format: date, nullable: true }
        available_to: { type: string, format: date, nullable: true }
        requested_days: { type: integer, minimum: 1 }
        observed_days: { type: integer, minimum: 0 }
        missing_days: { type: integer, minimum: 0 }
    HistorySummary:
      type: object
      additionalProperties: false
      required: [first_observed_price, latest_observed_price, lowest_observed_price, highest_observed_price, change, change_percent]
      properties:
        first_observed_price: { type: number, nullable: true }
        latest_observed_price: { type: number, nullable: true }
        lowest_observed_price: { type: number, nullable: true }
        highest_observed_price: { type: number, nullable: true }
        change: { type: number, nullable: true }
        change_percent: { type: number, nullable: true }
    DailyHistoryPrice:
      type: object
      additionalProperties: false
      required: [date, lowest_price, median_merchant_price, highest_price, merchant_count, observation_count]
      properties:
        date: { type: string, format: date }
        lowest_price: { type: number, exclusiveMinimum: 0 }
        median_merchant_price: { type: number, exclusiveMinimum: 0 }
        highest_price: { type: number, exclusiveMinimum: 0 }
        merchant_count: { type: integer, minimum: 1 }
        observation_count: { type: integer, minimum: 1 }
    MonthlyHistoryPrice:
      type: object
      additionalProperties: false
      required: [month, opening_price, closing_price, lowest_price, median_merchant_price, highest_price, observed_days, merchant_day_count, max_daily_merchant_count, observation_count]
      properties:
        month: { type: string, pattern: '^\d{4}-(0[1-9]|1[0-2])$' }
        opening_price: { type: number, exclusiveMinimum: 0 }
        closing_price: { type: number, exclusiveMinimum: 0 }
        lowest_price: { type: number, exclusiveMinimum: 0 }
        median_merchant_price: { type: number, exclusiveMinimum: 0 }
        highest_price: { type: number, exclusiveMinimum: 0 }
        observed_days: { type: integer, minimum: 1 }
        merchant_day_count: { type: integer, minimum: 1 }
        max_daily_merchant_count: { type: integer, minimum: 1 }
        observation_count: { type: integer, minimum: 1 }
    HistoryError:
      type: object
      additionalProperties: false
      required: [success, error]
      properties:
        success: { type: boolean, enum: [false] }
        error:
          type: object
          additionalProperties: false
          required: [code, message]
          properties:
            code: { type: string }
            message: { type: string }
    HistoryAuthError:
      type: object
      additionalProperties: false
      required: [success, error]
      properties:
        success: { type: boolean, enum: [false] }
        error:
          type: object
          additionalProperties: false
          required: [code, message]
          properties:
            code: { type: string, enum: [AUTHENTICATION_REQUIRED, INVALID_API_KEY] }
            message: { type: string }
    ScheduleActivity:
      type: object
      additionalProperties: false
      required: [state, attempted_at, discovered_product_count]
      properties:
        state:
          type: string
          enum: [awaiting_first_discovery, discovery_complete, discovery_issue]
        attempted_at:
          type: string
          format: date-time
          nullable: true
        discovered_product_count:
          type: integer
          minimum: 0
          nullable: true
    ScheduleCadenceHealth:
      type: object
      additionalProperties: false
      required: [state, last_filled_at, next_due_at, late_by_seconds, missed_windows, latest_outcome, has_snapshot]
      properties:
        state:
          type: string
          enum: [starting, healthy, due, late, missed, paused]
        last_filled_at:
          type: string
          format: date-time
          nullable: true
        next_due_at:
          type: string
          format: date-time
        late_by_seconds: { type: integer, minimum: 0 }
        missed_windows: { type: integer, minimum: 0 }
        latest_outcome:
          type: string
          nullable: true
        has_snapshot: { type: boolean }
    Schedule:
      type: object
      additionalProperties: false
      required: [id, term, country, status, origin, frequency_minutes, slot_cost, paused, evictable, activity, cadence_health, added_at, last_queried_at, query_count]
      properties:
        id: { type: integer, minimum: 1 }
        term: { type: string, minLength: 1, maxLength: 512 }
        country: { type: string, pattern: '^[a-z]{2}$' }
        status:
          type: string
          enum: [starting, active, recovering, paused]
        origin:
          type: string
          enum: [auto, manual, bulk]
        frequency_minutes:
          type: integer
          enum: [60, 360, 720, 1440, 10080]
        slot_cost: { type: integer, minimum: 1 }
        paused: { type: boolean }
        evictable: { type: boolean }
        activity: { $ref: '#/components/schemas/ScheduleActivity' }
        cadence_health: { $ref: '#/components/schemas/ScheduleCadenceHealth' }
        added_at: { type: string, format: date-time }
        last_queried_at: { type: string, format: date-time }
        query_count: { type: integer, minimum: 0 }
    ScheduleFrequencyOption:
      type: object
      additionalProperties: false
      required: [minutes, slot_cost, available, requires_tier]
      properties:
        minutes:
          type: integer
          enum: [60, 360, 720, 1440, 10080]
        slot_cost: { type: integer, minimum: 1 }
        available: { type: boolean }
        requires_tier:
          type: string
          enum: [personal, developer, business, enterprise]
    ScheduleStatusCounts:
      type: object
      additionalProperties: false
      required: [active, starting, recovering, paused]
      properties:
        active: { type: integer, minimum: 0 }
        starting: { type: integer, minimum: 0 }
        recovering: { type: integer, minimum: 0 }
        paused: { type: integer, minimum: 0 }
    ScheduleCadenceCounts:
      type: object
      additionalProperties: false
      required: [healthy, due, late, missed, starting, paused]
      properties:
        healthy: { type: integer, minimum: 0 }
        due: { type: integer, minimum: 0 }
        late: { type: integer, minimum: 0 }
        missed: { type: integer, minimum: 0 }
        starting: { type: integer, minimum: 0 }
        paused: { type: integer, minimum: 0 }
    ScheduleActivityPoint:
      type: object
      additionalProperties: false
      required: [date, completed, issues, products_returned]
      properties:
        date: { type: string, format: date }
        completed: { type: integer, minimum: 0 }
        issues: { type: integer, minimum: 0 }
        products_returned: { type: integer, minimum: 0 }
    ScheduleActivitySeries:
      type: object
      additionalProperties: false
      required: [days, points]
      properties:
        days:
          type: integer
          enum: [7, 30, 90]
        points:
          type: array
          items: { $ref: '#/components/schemas/ScheduleActivityPoint' }
    ScheduleAccountMeta:
      type: object
      description: >-
        Caller-owned Schedules capacity and optional analytics. Schedules is in beta.
        Limits and pricing are subject to change.
      required: [tier, slots_cap, frequencies, beta]
      properties:
        tier:
          type: string
          enum: [personal, developer, business, enterprise]
        slots_used: { type: integer, minimum: 0 }
        slots_cap: { type: integer, minimum: 0 }
        slots_available: { type: integer, minimum: 0 }
        frequencies:
          type: array
          items: { $ref: '#/components/schemas/ScheduleFrequencyOption' }
        beta: { type: boolean, enum: [true] }
        notice:
          type: string
          nullable: true
          example: Schedules is in beta. Limits and pricing are subject to change.
        count: { type: integer, minimum: 0 }
        partial: { type: boolean }
        meta_unavailable: { type: boolean }
        status_counts: { $ref: '#/components/schemas/ScheduleStatusCounts' }
        cadence_counts: { $ref: '#/components/schemas/ScheduleCadenceCounts' }
        activity_series: { $ref: '#/components/schemas/ScheduleActivitySeries' }
    ScheduleListResponse:
      type: object
      required: [success, data, meta]
      properties:
        success: { type: boolean, enum: [true] }
        data:
          type: object
          additionalProperties: false
          required: [schedules, next_cursor]
          properties:
            schedules:
              type: array
              maxItems: 200
              items: { $ref: '#/components/schemas/Schedule' }
            next_cursor: { type: string, nullable: true }
        meta: { $ref: '#/components/schemas/ScheduleAccountMeta' }
    ScheduleDetailResponse:
      type: object
      required: [success, data, meta]
      properties:
        success: { type: boolean, enum: [true] }
        data:
          type: object
          additionalProperties: false
          required: [schedule]
          properties:
            schedule: { $ref: '#/components/schemas/Schedule' }
        meta: { $ref: '#/components/schemas/ScheduleAccountMeta' }
    ScheduleCreateTerm:
      type: object
      additionalProperties: false
      required: [term]
      properties:
        term: { type: string, minLength: 1, maxLength: 512 }
        country:
          type: string
          default: au
        frequency:
          type: integer
          enum: [60, 360, 720, 1440, 10080]
          default: 1440
    ScheduleCreateRequest:
      type: object
      additionalProperties: false
      required: [terms]
      properties:
        terms:
          type: array
          minItems: 1
          maxItems: 1000
          items:
            oneOf:
              - type: string
                minLength: 1
                maxLength: 512
              - $ref: '#/components/schemas/ScheduleCreateTerm'
        evictable:
          type: boolean
          default: true
          description: false is accepted only for an operator-reserved account allocation.
    ScheduleCreateResult:
      type: object
      additionalProperties: false
      required: [term, country, status]
      properties:
        term: { type: string, maxLength: 512 }
        country: { type: string }
        status:
          type: string
          enum: [added, exists, cap_reached, invalid, rate_limited]
        id: { type: integer, minimum: 1 }
        reason: { type: string }
    ScheduleCreateSummary:
      type: object
      additionalProperties: false
      required: [added, exists, cap_reached, invalid, rate_limited]
      properties:
        added: { type: integer, minimum: 0 }
        exists: { type: integer, minimum: 0 }
        cap_reached: { type: integer, minimum: 0 }
        invalid: { type: integer, minimum: 0 }
        rate_limited: { type: integer, minimum: 0 }
    ScheduleCreateResponse:
      type: object
      required: [success, data, meta]
      properties:
        success: { type: boolean, enum: [true] }
        data:
          type: object
          additionalProperties: false
          required: [results, summary]
          properties:
            results:
              type: array
              maxItems: 1000
              items: { $ref: '#/components/schemas/ScheduleCreateResult' }
            summary: { $ref: '#/components/schemas/ScheduleCreateSummary' }
        meta: { $ref: '#/components/schemas/ScheduleAccountMeta' }
    ScheduleUpdateRequest:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        frequency:
          type: integer
          enum: [60, 360, 720, 1440, 10080]
        frequency_minutes:
          type: integer
          enum: [60, 360, 720, 1440, 10080]
          deprecated: true
          description: Compatibility alias for frequency.
        paused: { type: boolean }
    ScheduleRemoveResponse:
      type: object
      required: [success, data, meta]
      properties:
        success: { type: boolean, enum: [true] }
        data:
          type: object
          additionalProperties: false
          required: [id, deleted]
          properties:
            id: { type: integer, minimum: 1 }
            deleted: { type: boolean }
        meta: { $ref: '#/components/schemas/ScheduleAccountMeta' }
    ScheduleSettings:
      type: object
      additionalProperties: false
      required: [auto_enrol, default_frequency_minutes, evict_days]
      properties:
        auto_enrol: { type: boolean }
        default_frequency_minutes:
          type: integer
          enum: [1440, 10080]
        evict_days: { type: integer, minimum: 7, maximum: 90 }
    ScheduleSettingsUpdateRequest:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        auto_enrol: { type: boolean }
        default_frequency_minutes:
          type: integer
          enum: [1440, 10080]
        evict_days: { type: integer, minimum: 7, maximum: 90 }
    ScheduleSettingsResponse:
      type: object
      required: [success, data, meta]
      properties:
        success: { type: boolean, enum: [true] }
        data:
          type: object
          additionalProperties: false
          required: [settings]
          properties:
            settings: { $ref: '#/components/schemas/ScheduleSettings' }
        meta: { $ref: '#/components/schemas/BetaMeta' }
    ScheduleHistoryRow:
      type: object
      additionalProperties: false
      required: [term, country, query_count, first_queried, last_queried, last_outcome, schedule_state, scheduled_search_id]
      properties:
        term: { type: string, maxLength: 512 }
        country: { type: string, pattern: '^[a-z]{2}$' }
        query_count: { type: integer, minimum: 0 }
        first_queried: { type: string, format: date-time, nullable: true }
        last_queried: { type: string, format: date-time, nullable: true }
        last_outcome:
          type: string
          enum: [clean, degraded, error, cached]
        schedule_state:
          type: string
          enum: [active, paused, cap_reached, not_scheduled]
        scheduled_search_id: { type: integer, minimum: 1, nullable: true }
    ScheduleDemandPoint:
      type: object
      additionalProperties: false
      required: [date, requests, successful_responses, errors]
      properties:
        date: { type: string, format: date }
        requests: { type: integer, minimum: 0 }
        successful_responses: { type: integer, minimum: 0 }
        errors: { type: integer, minimum: 0 }
    ScheduleDemandSeries:
      type: object
      additionalProperties: false
      required: [days, points]
      properties:
        days: { type: integer, minimum: 1, maximum: 90 }
        points:
          type: array
          items: { $ref: '#/components/schemas/ScheduleDemandPoint' }
    ScheduleHistoryMeta:
      type: object
      additionalProperties: false
      required: [count, days, beta]
      properties:
        count: { type: integer, minimum: 0 }
        days: { type: integer, minimum: 1, maximum: 90 }
        beta: { type: boolean, enum: [true] }
        demand_series: { $ref: '#/components/schemas/ScheduleDemandSeries' }
    ScheduleHistoryResponse:
      type: object
      required: [success, data, meta]
      properties:
        success: { type: boolean, enum: [true] }
        data:
          type: object
          additionalProperties: false
          required: [queries, next_cursor, truncated]
          properties:
            queries:
              type: array
              maxItems: 200
              items: { $ref: '#/components/schemas/ScheduleHistoryRow' }
            next_cursor: { type: string, nullable: true }
            truncated: { type: boolean }
        meta: { $ref: '#/components/schemas/ScheduleHistoryMeta' }
    ScheduleErrorResponse:
      type: object
      additionalProperties: false
      required: [success, error]
      properties:
        success: { type: boolean, enum: [false] }
        error:
          type: object
          additionalProperties: false
          required: [code, message]
          properties:
            code:
              type: string
              enum:
                - INVALID_BODY
                - INVALID_JSON
                - PAYLOAD_TOO_LARGE
                - TOO_MANY_TERMS
                - INVALID_CURSOR
                - INVALID_ID
                - INVALID_LIMIT
                - INVALID_DAYS
                - COUNTRY_NOT_SUPPORTED
                - INVALID_STATUS
                - INVALID_ORIGIN
                - INVALID_QUERY
                - INVALID_FREQUENCY
                - INVALID_ACTIVITY_DAYS
                - FREQUENCY_NOT_AVAILABLE
                - EVICTABLE_NOT_PERMITTED
                - CAP_REACHED
                - SCHEDULE_NOT_FOUND
                - SCHEDULE_ROUTE_NOT_FOUND
                - SCHEDULES_READONLY
                - SCHEDULES_UNAVAILABLE
                - RATE_LIMIT_EXCEEDED
                - MISSING_API_KEY
                - INVALID_API_KEY_FORMAT
                - INVALID_API_KEY
                - SUBSCRIPTION_CANCELLED
                - NOT_FOUND
            message: { type: string }
            details: { type: string, nullable: true }
    BetaMeta:
      type: object
      additionalProperties: false
      required:
        - beta
      properties:
        beta:
          type: boolean
          enum: [true]
    BulkSearchSubmission:
      type: object
      additionalProperties: false
      required:
        - searches
      properties:
        searches:
          type: array
          minItems: 1
          maxItems: 100
          items:
            $ref: '#/components/schemas/BulkSearchInput'
    BulkSearchInput:
      type: object
      additionalProperties: false
      required:
        - q
      properties:
        q:
          type: string
          minLength: 1
          description: >-
            Product name or retailer URL. The runtime trims it, rejects control
            characters, and limits the normalized value to 400 UTF-8 bytes.
          example: Sony WH-1000XM5
        country:
          type: string
          default: au
          description: >-
            Case-insensitive supported country code for global coverage. The
            effective request returns it lowercase.
          example: us
        limit:
          type: integer
          minimum: 1
          default: 3
          description: Values above 5 are valid and clamped to 5.
        offers_limit:
          type: integer
          minimum: 1
          default: 15
          description: Values above 60 are valid and clamped to 60.
        max_age:
          type: integer
          minimum: 0
          maximum: 9007199254740991
          description: Maximum acceptable cached or scheduled data age, in seconds.
        no_cache:
          type: boolean
          default: false
          description: true bypasses cache reads and requires a live Search.
    BulkSearchEffectiveRequest:
      allOf:
        - $ref: '#/components/schemas/BulkSearchInput'
        - type: object
          required:
            - q
            - country
            - limit
            - offers_limit
          properties:
            q:
              type: string
            country:
              type: string
            limit:
              type: integer
              minimum: 1
            offers_limit:
              type: integer
              minimum: 1
    BulkSearchJob:
      type: object
      additionalProperties: false
      required:
        - id
        - cancel_requested_at
        - expires_at
        - created_at
        - total
        - pending
        - running
        - succeeded
        - empty
        - failed
        - cancelled
        - reserved
        - consumed
        - refunded
      properties:
        id:
          type: string
          format: uuid
        cancel_requested_at:
          type: string
          format: date-time
          nullable: true
        expires_at:
          type: string
          format: date-time
          description: >-
            Execution cutoff. No pending item starts after this time; unfinished
            pending items are cancelled and refunded, while already-running work
            may finish. Status and results remain readable after the cutoff in the
            current Beta, but are not durable storage; collect them promptly.
        created_at:
          type: string
          format: date-time
        total:
          type: integer
          minimum: 0
        pending:
          type: integer
          minimum: 0
        running:
          type: integer
          minimum: 0
        succeeded:
          type: integer
          minimum: 0
        empty:
          type: integer
          minimum: 0
        failed:
          type: integer
          minimum: 0
        cancelled:
          type: integer
          minimum: 0
        reserved:
          type: integer
          minimum: 0
        consumed:
          type: integer
          minimum: 0
        refunded:
          type: integer
          minimum: 0
    BulkSearchItem:
      type: object
      additionalProperties: false
      required:
        - id
        - input_index
        - request
        - state
        - credit_state
        - attempt
        - response
        - error_code
        - started_at
        - completed_at
      properties:
        id:
          type: string
          description: Decimal item identifier.
        input_index:
          type: integer
          minimum: 0
          description: Zero-based position in the submitted searches array.
        request:
          $ref: '#/components/schemas/BulkSearchEffectiveRequest'
        state:
          type: string
          enum: [pending, running, succeeded, empty, failed, cancelled]
        credit_state:
          type: string
          enum: [reserved, consumed, refunded]
        attempt:
          type: integer
          minimum: 0
          description: Number of successful worker claims.
        response:
          type: object
          nullable: true
          description: >-
            Normal Search success or failure envelope for succeeded, empty, or
            failed items; null while pending or running and after cancellation.
          oneOf:
            - $ref: '#/components/schemas/SearchResponse'
            - $ref: '#/components/schemas/BulkSearchItemError'
        error_code:
          type: string
          nullable: true
          description: Machine-readable Search or worker code for a failed item.
        started_at:
          type: string
          format: date-time
          nullable: true
        completed_at:
          type: string
          format: date-time
          nullable: true
    BulkSearchItemError:
      type: object
      additionalProperties: false
      required:
        - success
        - error
        - meta
      properties:
        success:
          type: boolean
          enum: [false]
        error:
          $ref: '#/components/schemas/Error/properties/error'
        meta:
          description: >-
            Search failures retain their latency metadata. Failures synthesized
            by the asynchronous worker carry an empty metadata object.
          oneOf:
            - $ref: '#/components/schemas/FailureMeta'
            - type: object
              additionalProperties: false
              maxProperties: 0
    BulkSearchResultsPage:
      type: object
      additionalProperties: false
      required:
        - items
        - next_cursor
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/BulkSearchItem'
        next_cursor:
          type: string
          nullable: true
          description: Exact opaque cursor for the next page; null on the final page.
    BulkSearchSubmitResponse:
      type: object
      additionalProperties: false
      required: [success, data, meta]
      properties:
        success:
          type: boolean
          enum: [true]
        data:
          type: object
          additionalProperties: false
          required: [job, replayed]
          properties:
            job:
              $ref: '#/components/schemas/BulkSearchJob'
            replayed:
              type: boolean
        meta:
          $ref: '#/components/schemas/BetaMeta'
    BulkSearchJobResponse:
      type: object
      additionalProperties: false
      required: [success, data, meta]
      properties:
        success:
          type: boolean
          enum: [true]
        data:
          type: object
          additionalProperties: false
          required: [job]
          properties:
            job:
              $ref: '#/components/schemas/BulkSearchJob'
        meta:
          $ref: '#/components/schemas/BetaMeta'
    BulkSearchResultsResponse:
      type: object
      additionalProperties: false
      required: [success, data, meta]
      properties:
        success:
          type: boolean
          enum: [true]
        data:
          $ref: '#/components/schemas/BulkSearchResultsPage'
        meta:
          $ref: '#/components/schemas/BetaMeta'
    SearchResponse:
      type: object
      description: Successful search envelope.
      required:
        - success
        - data
        - meta
      properties:
        success:
          type: boolean
          enum: [true]
          description: Always true on a 200.
        data:
          $ref: '#/components/schemas/SearchData'
        meta:
          $ref: '#/components/schemas/Meta'
    SearchData:
      type: object
      required:
        - query
        - country
        - products
      properties:
        query:
          type: string
          description: The normalised query that was searched.
        country:
          type: string
          description: The resolved ISO country code (lowercase).
          example: au
        products:
          type: array
          description: >-
            Ranked candidate products. Empty when Google had nothing for the
            query (still a 200).
          items:
            $ref: '#/components/schemas/Candidate'
    Candidate:
      type: object
      description: >-
        One ranked candidate product, with its seller offers inline. Card-level
        signals (rating, reviews, delivery) live here, NOT on each offer.
      required:
        - position
        - pid
        - gpcid
        - gid
        - title
        - image
        - price
        - currency
        - condition
        - source
        - multi_store
        - rating
        - reviews
        - delivery
        - tags
        - nearby_distance_km
        - offers
        - offerCount
      properties:
        position:
          type: integer
          description: 1-based rank of this candidate in the result set.
        pid:
          type: integer
          nullable: true
          description: PricesAPI catalog product id (null if not catalogued).
        gpcid:
          type: string
          nullable: true
          description: Google product cluster id.
        gid:
          type: string
          nullable: true
          description: Google product id.
        title:
          type: string
          nullable: true
          description: Product title.
        image:
          type: string
          nullable: true
          description: Product image URL.
        price:
          type: number
          nullable: true
          description: Representative (card) price.
        currency:
          type: string
          nullable: true
          description: ISO currency code for the card price.
          example: AUD
        condition:
          type: string
          nullable: true
          description: Card-level condition (e.g. new, used).
        source:
          type: string
          nullable: true
          description: Source/store label shown on the card.
        multi_store:
          type: boolean
          description: True when the product is offered by multiple stores.
        rating:
          type: number
          nullable: true
          description: Average product rating (0-5).
        reviews:
          type: integer
          nullable: true
          description: Total review count.
        delivery:
          type: string
          nullable: true
          description: Card-level delivery text.
        tags:
          type: array
          description: Card badges/labels (e.g. sale, free delivery).
          items:
            type: string
        nearby_distance_km:
          type: number
          nullable: true
          description: Distance to a nearby physical store, when applicable.
        offers:
          type: array
          description: >-
            Seller offers for this candidate. Empty when the offers stage was
            degraded (see meta.degraded).
          items:
            $ref: '#/components/schemas/Offer'
        offerCount:
          type: integer
          description: >-
            Number of offers in `offers` (after truncation to `offers_limit`).
    Offer:
      type: object
      description: >-
        One seller offer, returned inline on each Candidate. Carries per-seller
        product_title, stock_status, and delivery_info. Per-product rating and
        reviews live on the Candidate, not the offer.
      required:
        - seller
        - seller_url
        - price
        - currency
        - product_title
        - stock_status
        - delivery_info
        - condition
        - url
      properties:
        seller:
          type: string
          description: Seller / store name.
        seller_url:
          type: string
          nullable: true
          description: Seller origin URL (homepage), parsed from the offer URL.
        price:
          type: number
          description: Offer price.
        currency:
          type: string
          nullable: true
          description: ISO currency code for the offer price.
          example: AUD
        product_title:
          type: string
          nullable: true
          description: The seller's listing title for this offer.
          example: Sony WH-1000XM5 Wireless Headphones
        stock_status:
          type: string
          nullable: true
          description: Availability text reported by the seller.
          example: In stock
        delivery_info:
          type: string
          nullable: true
          description: Delivery / shipping text reported by the seller (null if not stated).
          example: Free delivery
        condition:
          type: string
          nullable: true
          description: Offer condition (e.g. new, used).
        url:
          type: string
          description: Direct link to the seller's product page for this offer.
    Meta:
      type: object
      description: Per-call timing, provenance, and result-shape flags.
      required:
        - latency_ms
        - raw_count
        - gid_bearing_count
      properties:
        latency_ms:
          type: integer
          description: Total server-side latency for the call, in milliseconds.
        raw_count:
          type: integer
          description: Raw candidate count seen before filtering.
        gid_bearing_count:
          type: integer
          description: Candidates that carried a Google product id (gid).
        cache_source:
          type: string
          enum:
            - redis
            - db
            - miss
            - coalesced
            - scheduled
          description: >-
            Which tier served the result: `redis` (hot in-memory tier), `db`
            (durable Postgres tier), `miss` (cold live scrape), `coalesced`
            (shared an in-flight live scrape), or `scheduled` (scheduled store).
        cache_age_s:
          type: integer
          description: >-
            Age of the served durable entry, in seconds. Present ONLY on a `db`
            cache hit.
        discover_ms:
          type: integer
          description: Time spent in the discover stage (milliseconds).
        products_ms:
          type: integer
          description: Time spent in the offers/products stage (milliseconds).
        blocked:
          type: boolean
          description: Derived bimodal silent-fail signal from the discover stage.
        degraded:
          type: boolean
          description: >-
            True when discover succeeded but the offers stage failed — candidates
            are returned with empty `offers`.
        limit_capped:
          type: boolean
          description: True when the requested `limit` was clamped to `limit_max`.
        limit_max:
          type: integer
          description: The maximum allowed `limit` (5). Present when `limit_capped` is true.
          example: 5
        scraped_at:
          type: string
          format: date-time
          description: >-
            Time the returned data was scraped. Always present for scheduled
            store serves; optional on other sources while provenance rollout is
            gated, and absent when an older cache blob has no trustworthy time.
        next_refresh_at:
          type: string
          format: date-time
          description: >-
            Next scheduled cycle time. Present only on scheduled-store serves
            and may be in the past when a cycle is overdue.
        served_stale:
          type: boolean
          description: >-
            True when a live failure or degraded scrape fell back to scheduled
            data. Never present for an explicit `max_age` or truthy `no_cache`.
        schedule_state:
          type: string
          enum:
            - active
            - paused
            - cap_reached
            - not_scheduled
          description: >-
            Optional caller-scoped Schedules state. It is never a term-global
            signal and is absent when schedule peeking is disabled or unavailable.
    FailureMeta:
      type: object
      description: Timing metadata available on Search error envelopes.
      required:
        - latency_ms
      properties:
        latency_ms:
          type: integer
          description: Total server-side latency before the error, in milliseconds.
    Error:
      type: object
      description: Error envelope.
      required:
        - success
        - error
      properties:
        success:
          type: boolean
          enum: [false]
          description: Always false on an error.
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              description: >-
                Machine-readable error code. The enum covers synchronous Search,
                Async Bulk Search management, authentication, quota, and retired
                endpoint responses.
              enum:
                - MISSING_QUERY
                - COUNTRY_NOT_SUPPORTED
                - INVALID_LIMIT
                - INVALID_OFFERS_LIMIT
                - INVALID_MAX_AGE
                - MISSING_IDEMPOTENCY_KEY
                - INVALID_IDEMPOTENCY_KEY
                - INVALID_BODY
                - INVALID_ITEM_COUNT
                - INVALID_ITEM
                - INVALID_JSON
                - INVALID_JOB_ID
                - INVALID_CURSOR
                - PAYLOAD_TOO_LARGE
                - MISSING_API_KEY
                - INVALID_API_KEY_FORMAT
                - INVALID_API_KEY
                - SUBSCRIPTION_CANCELLED
                - CREDITS_EXCEEDED
                - MONTHLY_LIMIT_EXCEEDED
                - INSUFFICIENT_CREDITS
                - RATE_LIMIT_EXCEEDED
                - NOT_FOUND
                - JOB_NOT_FOUND
                - ENDPOINT_GONE
                - IDEMPOTENCY_CONFLICT
                - SCRAPER_UNAVAILABLE
                - SCRAPER_TIMEOUT
                - SCRAPER_BUSY
                - SCRAPER_CIRCUIT_OPEN
                - WORKER_EXCEPTION
                - WORKER_RETRY_EXHAUSTED
                - RESULT_TOO_LARGE
                - SERVICE_UNAVAILABLE
                - BULK_SEARCH_ADMISSION_PAUSED
                - BULK_SEARCH_UNAVAILABLE
                - DB_ERROR
                - SEARCH_FAILED
                - VALIDATION_ERROR
                - INTERNAL_ERROR
            message:
              type: string
              description: Human-readable error message.
            details:
              type: string
              nullable: true
              description: Optional extra context.
            supported:
              type: array
              description: >-
                On COUNTRY_NOT_SUPPORTED, the list of supported ISO country codes.
              items:
                type: string
            replacement:
              type: string
              description: On ENDPOINT_GONE, the replacement endpoint path.
        meta:
          $ref: '#/components/schemas/FailureMeta'
    ProductSnapshotErrorCode:
      type: string
      enum:
        - INVALID_PRODUCT_ID
        - INVALID_PRODUCT_GROUP_ID
        - INVALID_ID_TYPE
        - INVALID_MARKET
        - INVALID_OFFERS_LIMIT
        - INVALID_RETAILER
        - INVALID_GROUP_LIMIT
        - INVALID_GROUP_CURSOR
        - INVALID_BATCH
        - INVALID_BATCH_ITEM
        - INVALID_JSON
        - PAYLOAD_TOO_LARGE
        - PRODUCT_NOT_FOUND
        - PRODUCT_GROUP_NOT_FOUND
        - METHOD_NOT_ALLOWED
        - RATE_LIMIT_EXCEEDED
        - SNAPSHOT_RATE_LIMIT_UNAVAILABLE
        - SNAPSHOT_UNAVAILABLE
        - SNAPSHOT_DATA_INVALID
        - SNAPSHOT_RESPONSE_TOO_LARGE
    ProductSnapshotErrorResponse:
      type: object
      additionalProperties: false
      required: [success, error]
      properties:
        success:
          type: boolean
          enum: [false]
        error:
          type: object
          additionalProperties: false
          required: [code, message]
          properties:
            code:
              $ref: '#/components/schemas/ProductSnapshotErrorCode'
            message:
              type: string
            input_index:
              type: integer
              minimum: 0
              maximum: 99
              nullable: true
    ProductSnapshotAlias:
      type: object
      additionalProperties: false
      required: [id, id_type]
      properties:
        id:
          type: string
          pattern: '^[A-Za-z0-9._~-]+$'
          minLength: 1
          maxLength: 64
        id_type:
          type: string
          enum: [google_shopping]
    ProductSnapshotProduct:
      type: object
      additionalProperties: false
      required: [id, id_type, market, title, brand, image_url, category_id, aliases, status, offline_at]
      properties:
        id:
          type: string
          pattern: '^[1-9][0-9]{0,9}$'
          description: Canonical PricesAPI product ID from 1 to 2147483647.
        id_type:
          type: string
          enum: [pricesapi]
        market:
          type: string
          pattern: '^[a-z]{2}$'
        title:
          type: string
          nullable: true
        brand:
          type: string
          nullable: true
        image_url:
          type: string
          format: uri
          nullable: true
        category_id:
          type: integer
          nullable: true
        aliases:
          type: array
          minItems: 1
          maxItems: 1
          items:
            $ref: '#/components/schemas/ProductSnapshotAlias'
        status:
          type: string
          enum: [active, offline]
        offline_at:
          type: string
          format: date-time
          nullable: true
    ProductSnapshotObservation:
      type: object
      additionalProperties: false
      required: [state, reason, observed_at, age_seconds, stale_after_seconds, stale]
      properties:
        state:
          type: string
          enum: [observed, partial, unobserved]
        reason:
          type: string
          enum: [no_valid_offers, not_observed]
          nullable: true
        observed_at:
          type: string
          format: date-time
          nullable: true
        age_seconds:
          type: integer
          minimum: 0
          nullable: true
        stale_after_seconds:
          type: integer
          enum: [93600]
        stale:
          type: boolean
          nullable: true
    ProductSnapshotOffer:
      type: object
      additionalProperties: false
      required: [merchant, merchant_url, merchant_domain, price, currency, condition, availability, availability_raw, title, delivery_info, url, observed_at]
      properties:
        merchant:
          type: string
        merchant_url:
          type: string
          format: uri
          nullable: true
        merchant_domain:
          type: string
          nullable: true
          description: Canonical hostname derived from the retained offer URL; null when that URL is unavailable or unsafe.
        price:
          type: number
          minimum: 0
          exclusiveMinimum: true
        currency:
          type: string
          pattern: '^[A-Z]{3}$'
          description: ISO 4217 currency for the explicit market.
        condition:
          type: string
          enum: [new, used, refurbished]
          nullable: true
        availability:
          type: string
          enum: [in_stock, preorder, backorder, unknown, out_of_stock]
        availability_raw:
          type: string
          nullable: true
        title:
          type: string
          nullable: true
        delivery_info:
          type: string
          nullable: true
        url:
          type: string
          format: uri
          nullable: true
        observed_at:
          type: string
          format: date-time
    ProductSnapshotOffersPage:
      type: object
      additionalProperties: false
      required: [limit, returned, truncated]
      properties:
        limit:
          type: integer
          minimum: 0
          maximum: 20
        returned:
          type: integer
          minimum: 0
          maximum: 20
        truncated:
          type: boolean
    ProductSnapshotResponse:
      type: object
      additionalProperties: false
      required: [product, observation, offers, offers_page]
      properties:
        product:
          $ref: '#/components/schemas/ProductSnapshotProduct'
        observation:
          $ref: '#/components/schemas/ProductSnapshotObservation'
        offers:
          type: array
          maxItems: 20
          items:
            $ref: '#/components/schemas/ProductSnapshotOffer'
        offers_page:
          $ref: '#/components/schemas/ProductSnapshotOffersPage'
        retailer_filter:
          $ref: '#/components/schemas/ProductSnapshotRetailerFilter'
    ProductSnapshotRetailerFilter:
      type: object
      additionalProperties: false
      required: [requested, matched]
      properties:
        requested:
          type: array
          minItems: 1
          maxItems: 10
          items:
            type: string
            minLength: 3
            maxLength: 253
        matched:
          type: array
          maxItems: 10
          items:
            type: string
            minLength: 3
            maxLength: 253
    ProductSnapshotBatchItem:
      type: object
      additionalProperties: false
      required: [id, id_type, market]
      properties:
        id:
          type: string
          minLength: 1
          maxLength: 64
        id_type:
          type: string
          enum: [pricesapi, google_shopping]
        market:
          type: string
          pattern: '^[a-z]{2}$'
        offers_limit:
          type: integer
          minimum: 0
          maximum: 20
          default: 20
        retailers:
          type: array
          minItems: 1
          maxItems: 10
          items:
            type: string
            minLength: 3
            maxLength: 253
    ProductSnapshotBatchRequest:
      type: object
      additionalProperties: false
      required: [items]
      properties:
        items:
          type: array
          minItems: 1
          maxItems: 100
          items:
            $ref: '#/components/schemas/ProductSnapshotBatchItem'
    ProductSnapshotBatchFound:
      type: object
      additionalProperties: false
      required: [input_index, status, product, observation, offers, offers_page]
      properties:
        input_index:
          type: integer
          minimum: 0
          maximum: 99
        status:
          type: string
          enum: [found]
        product:
          $ref: '#/components/schemas/ProductSnapshotProduct'
        observation:
          $ref: '#/components/schemas/ProductSnapshotObservation'
        offers:
          type: array
          maxItems: 20
          items:
            $ref: '#/components/schemas/ProductSnapshotOffer'
        offers_page:
          $ref: '#/components/schemas/ProductSnapshotOffersPage'
        retailer_filter:
          $ref: '#/components/schemas/ProductSnapshotRetailerFilter'
    ProductSnapshotBatchNotFound:
      type: object
      additionalProperties: false
      required: [input_index, status, error]
      properties:
        input_index:
          type: integer
          minimum: 0
          maximum: 99
        status:
          type: string
          enum: [not_found]
        error:
          type: object
          additionalProperties: false
          required: [code, message]
          properties:
            code:
              type: string
              enum: [PRODUCT_NOT_FOUND]
            message:
              type: string
    ProductSnapshotBatchResponse:
      type: object
      additionalProperties: false
      required: [results, summary]
      properties:
        results:
          type: array
          minItems: 1
          maxItems: 100
          items:
            oneOf:
              - $ref: '#/components/schemas/ProductSnapshotBatchFound'
              - $ref: '#/components/schemas/ProductSnapshotBatchNotFound'
        summary:
          type: object
          additionalProperties: false
          required: [requested, distinct_lookups, found, not_found]
          properties:
            requested:
              type: integer
              minimum: 1
              maximum: 100
            distinct_lookups:
              type: integer
              minimum: 1
              maximum: 100
            found:
              type: integer
              minimum: 0
              maximum: 100
            not_found:
              type: integer
              minimum: 0
              maximum: 100
    ProductGroupMember:
      type: object
      additionalProperties: false
      required: [id, id_type, aliases, title, brand, image_url, status, offline_at]
      properties:
        id:
          type: string
          pattern: '^[1-9][0-9]{0,9}$'
          description: Canonical PricesAPI product ID from 1 to 2147483647.
        id_type:
          type: string
          enum: [pricesapi]
        aliases:
          type: array
          minItems: 1
          maxItems: 1
          items:
            $ref: '#/components/schemas/ProductSnapshotAlias'
        title:
          type: string
          nullable: true
        brand:
          type: string
          nullable: true
        image_url:
          type: string
          format: uri
          nullable: true
        status:
          type: string
          enum: [active, offline]
        offline_at:
          type: string
          format: date-time
          nullable: true
    ProductGroup:
      type: object
      additionalProperties: false
      required: [id, id_type, market]
      properties:
        id:
          type: string
          pattern: '^[A-Za-z0-9._~-]+$'
          minLength: 1
          maxLength: 64
        id_type:
          type: string
          enum: [google_shopping]
        market:
          type: string
          pattern: '^[a-z]{2}$'
    ProductGroupPage:
      type: object
      additionalProperties: false
      required: [limit, returned, next_cursor]
      properties:
        limit:
          type: integer
          minimum: 1
          maximum: 100
        returned:
          type: integer
          minimum: 0
          maximum: 100
        next_cursor:
          type: string
          nullable: true
    ProductGroupPageResponse:
      type: object
      additionalProperties: false
      required: [product_group, products, page]
      properties:
        product_group:
          $ref: '#/components/schemas/ProductGroup'
        products:
          type: array
          maxItems: 100
          items:
            $ref: '#/components/schemas/ProductGroupMember'
        page:
          $ref: '#/components/schemas/ProductGroupPage'
