openapi: 3.1.0
info:
  title: Eventory API
  version: "1.0"
  description: |
    Every HTTP endpoint of the Eventory platform, reached through the single public
    gateway at `https://api.eventory.ai`. The Notifications API is a
    WebSocket and is documented separately under **Notifications API**.

    Authenticate every request with the `apikey` header. Each endpoint lists its
    credit cost; see **Credits & access** for how billing works.
  contact:
    name: Eventory
    url: https://eventory.ai
servers:
  - url: https://api.eventory.ai
    description: Production
security:
  - apiKey: []
tags:
  - name: Market Data
    description: |
      Historical and aggregated marketplace data for events and artists, plus
      full-text search over the Eventory catalog. Polling API.
      **1 credit per request** (sales list scales with rows requested).
  - name: Live Availability
    description: |
      On-demand scrape of the current ticket availability for one event on one of
      the twelve supported platforms. **5 credits per request.**
  - name: Watchlist
    description: |
      Manage the events your account monitors. The watchlist drives the
      Notifications API. **Free.**
  - name: Account
    description: Your plan and credit usage, live from billing. **Free.**

paths:
  # ---------------------------------------------------------------- Market Data
  /eventory/data/ping:
    get:
      tags: [Market Data]
      summary: Ping
      operationId: dataPing
      x-mint:
        metadata:
          title: Market Data API ping
          sidebarTitle: Ping
      description: |
        Liveness probe for the Market Data API.

        **Cost:** 1 credit. Prefer `GET /usage` if you only want to test your key,
        which is free.
      responses:
        "200":
          description: Service is up.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Status"
              example: { status: ok }
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "402":
          $ref: "#/components/responses/PaymentRequired"

  /eventory/data/viagogo/{event_id}:
    get:
      tags: [Market Data]
      summary: Viagogo event
      operationId: getViagogoEvent
      x-mint:
        metadata:
          title: viagogo event prices and daily stats
          sidebarTitle: Viagogo event
      description: |
        Returns Viagogo event metadata, the latest aggregate summary, the full
        daily statistics history, and a price distribution histogram.

        **Cost:** 1 credit.

        Decimal amounts are returned as JSON strings to preserve precision.
      parameters:
        - name: event_id
          in: path
          required: true
          description: Viagogo event identifier including the `E-` prefix.
          schema:
            type: string
            pattern: "^E-\\d+$"
            example: E-160973997
      responses:
        "200":
          description: Event data.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ViagogoResponse"
        "400":
          description: The path id does not match `E-<digits>`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataApiError"
              example: { status: error, code: INVALID_VIAGOGO_ID, message: "Invalid viagogo id '160973997': expected format E-<digits>." }
        "404":
          description: No tracked event matches the id.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataApiError"
              example: { status: error, code: EVENT_NOT_FOUND, message: "No viagogo event with id E-1." }
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "502":
          $ref: "#/components/responses/DataUpstream"

  /eventory/data/sales/viagogo/{event_id}:
    get:
      tags: [Market Data]
      summary: Viagogo recent sales
      operationId: getViagogoSales
      x-mint:
        metadata:
          title: Recent viagogo ticket sales
          sidebarTitle: Viagogo recent sales
      description: |
        Returns the most recent individual Viagogo sales for an event, newest first.

        **Cost:** 1 credit per 20 rows requested, i.e. `ceil(sales_limit / 20)`.
        Omitting `sales_limit` returns 20 rows for 1 credit; `sales_limit=200`
        costs 10 credits. A value outside `1..200` is rejected with
        `400 invalid_parameter` before anything is billed.
      parameters:
        - name: event_id
          in: path
          required: true
          description: Viagogo event identifier including the `E-` prefix.
          schema:
            type: string
            pattern: "^E-\\d+$"
            example: E-160973997
        - name: section
          in: query
          required: false
          description: Only return sales in this section label (exact match).
          schema:
            type: string
            example: Floor
        - name: sales_limit
          in: query
          required: false
          description: Number of rows to return. Default 20. Billed at 1 credit per 20 rows.
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 20
      responses:
        "200":
          description: Recent sales.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SalesResponse"
        "400":
          description: Invalid event id, or `sales_limit` out of range.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/DataApiError"
                  - $ref: "#/components/schemas/GatewayError"
              examples:
                bad_limit:
                  summary: sales_limit out of range (rejected by the gateway)
                  value: { error: { code: invalid_parameter, message: "sales_limit must be an integer between 1 and 200." } }
                bad_id:
                  summary: Malformed event id
                  value: { status: error, code: INVALID_VIAGOGO_ID, message: "Invalid viagogo id '160973997': expected format E-<digits>." }
        "404":
          description: No tracked event matches the id.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataApiError"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "502":
          $ref: "#/components/responses/DataUpstream"

  /eventory/data/ticketmaster/{tm_event_id}:
    get:
      tags: [Market Data]
      summary: Ticketmaster event
      operationId: getTicketmasterEvent
      x-mint:
        metadata:
          title: Ticketmaster event inventory and prices
          sidebarTitle: Ticketmaster event
      description: |
        Returns Ticketmaster event data (scrape runs, per-section snapshot stats,
        statistics history), the latest aggregate stats, and the dashboard chart
        payload. The three blocks are fetched in parallel.

        `scrapeRuns` and `statisticsHistory` are sorted newest first and trimmed
        to `limit`. `sectionStats` has one row per section, from the most recent
        scrape run only. `offerStats` is currently always empty.

        The inner shapes are stable but additive: new keys may appear without notice.

        Every price is a number in the venue's local currency, as Ticketmaster lists
        it. The `currency` on each row or object names it, and it is the same across
        the whole response. Percentages are strings such as `"5.25"`, except in
        `dashboard_v2`, where they are numbers.

        **Cost:** 1 credit.
      parameters:
        - name: tm_event_id
          in: path
          required: true
          description: Ticketmaster event id as it appears in the event URL.
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,64}$"
            example: 0200642CF18CAA24
        - name: limit
          in: query
          required: false
          description: Maximum entries in `scrapeRuns`, `statisticsHistory`, and the dashboard chart series.
          schema:
            type: integer
            minimum: 1
            maximum: 1000
            default: 100
      responses:
        "200":
          description: Ticketmaster event data.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TicketmasterResponse"
        "400":
          description: "The path id is not 1 to 64 letters, digits, `-` or `_`."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataApiError"
              example: { status: error, code: INVALID_TICKETMASTER_ID, message: "Invalid ticketmaster id 'abc.def': expected 1-64 letters, digits, '-' or '_'." }
        "404":
          description: The id is not tracked by Eventory, or it is linked to an event Eventory has no Ticketmaster data for yet.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataApiError"
              example: { status: error, code: EVENT_NOT_FOUND, message: "No tracked ticketmaster event with id '0200642CF18CAA24'." }
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "502":
          $ref: "#/components/responses/DataUpstream"

  /eventory/data/vividseats/{production_id}:
    get:
      tags: [Market Data]
      summary: VividSeats event
      operationId: getVividseatsEvent
      x-mint:
        metadata:
          title: Vivid Seats event listings and stats
          sidebarTitle: VividSeats event
      description: |
        Returns VividSeats event metadata, the latest summary of listed
        inventory, a daily statistics history, and a per-section breakdown of
        the latest snapshot.

        VividSeats data covers **listed inventory only** — there are no sales
        fields. Prices are all-in (fees included) and in the venue market's
        currency (`event.currency`). A "price point" is a (section, all-in price)
        bucket, not an individual seller listing.

        Every summary metric `X` has `X_24h` (signed change versus the most
        recent snapshot at least 24 hours older) and `X_24h_percentage`. Both
        are `0` until a snapshot that old exists. Check `summary.stats_date`
        for freshness.

        Decimal amounts are returned as JSON strings to preserve precision.

        **Cost:** 1 credit.
      parameters:
        - name: production_id
          in: path
          required: true
          description: VividSeats production id — the number after `/production/` in the event URL.
          schema:
            type: string
            pattern: "^\\d+$"
            example: "6686410"
      responses:
        "200":
          description: Event data.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VividseatsResponse"
        "400":
          description: The path id is not all digits.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataApiError"
              example: { status: error, code: INVALID_VIVIDSEATS_ID, message: "Invalid vividseats id 'abc': expected the numeric production id." }
        "404":
          description: No VividSeats data for the production. Parking-pass productions also return 404.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataApiError"
              example: { status: error, code: EVENT_NOT_FOUND, message: "No vividseats data for production 1." }
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"

  /eventory/data/artist:
    get:
      tags: [Market Data]
      summary: Artist
      operationId: getArtist
      x-mint:
        metadata:
          title: Artist social metrics
          sidebarTitle: Artist
      description: |
        Resolves an artist by its exact marketplace URL and returns social
        metrics, the latest snapshot, and a rolling window of platform snapshots.

        The match on `url` is exact. Whitespace is trimmed but nothing else is
        normalised, so pass the URL exactly as it appears in `event.artist.url`
        of a Viagogo event response.

        **Cost:** 1 credit.
      parameters:
        - name: url
          in: query
          required: true
          description: Exact artist URL on record.
          schema:
            type: string
            format: uri
            example: https://www.viagogo.com/Concert-Tickets/R-B/Contemporary-R-B/Teddy-Swims-Tickets
        - name: snapshots_limit
          in: query
          required: false
          description: Maximum entries in `snapshots_history`.
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 30
      responses:
        "200":
          description: Artist data.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ArtistResponse"
        "400":
          description: "`url` is missing (`INVALID_REQUEST`) or empty (`MISSING_URL`)."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataApiError"
              examples:
                missing:
                  value: { status: error, code: INVALID_REQUEST, message: "query.url: Field required" }
                empty:
                  value: { status: error, code: MISSING_URL, message: "Query parameter 'url' is required." }
        "404":
          description: No artist on file with that exact URL.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataApiError"
              example: { status: error, code: ARTIST_NOT_FOUND, message: "No artist found for url 'https://www.viagogo.com/Concert-Tickets/Unknown-Artist-Tickets'." }
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "502":
          $ref: "#/components/responses/DataUpstream"

  /eventory/data/search:
    post:
      tags: [Market Data]
      summary: Search events
      operationId: searchEvents
      x-mint:
        metadata:
          title: Search the event catalog
          sidebarTitle: Search events
      description: |
        Full-text search across the Eventory event catalog with optional filters
        and sort orders. See the **Search filters** guide for the
        `filterExpression` syntax and the filterable attributes.

        **Cost:** 1 credit.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SearchRequest"
            example:
              query: drake
              page: 1
              hitsPerPage: 24
              sortBy: tickets_sold.desc
              filterExpression: "venue_country_code:US"
      responses:
        "200":
          description: Search results.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SearchResponse"
        "400":
          description: Validation failed (bad type, out-of-range value, expression too long), or the `filterExpression` is malformed (`INVALID_FILTER`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataApiError"
              examples:
                invalid_request:
                  value: { status: error, code: INVALID_REQUEST, message: "hitsPerPage: Input should be less than or equal to 100" }
                invalid_filter:
                  value: { status: error, code: INVALID_FILTER, message: "Invalid filterExpression: Unexpected token 'AND' expected filter at col 26" }
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "502":
          $ref: "#/components/responses/DataUpstream"

  /eventory/data/events/add:
    post:
      tags: [Market Data]
      summary: Add event to tracking
      operationId: addEvent
      description: |
        Registers an event so that Eventory starts collecting market data for it.
        Supply exactly one source: a Viagogo pair (`event_url` + `viagogo_event_id`)
        or a Ticketmaster pair (`ticketmaster_event_url` + `ticketmaster_event_id`).

        Data for a newly added event accumulates over time; the first calls to the
        event endpoints may return empty statistics.

        **Cost:** 1 credit.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EventsAddRequest"
            examples:
              viagogo:
                summary: Viagogo event
                value:
                  event_url: https://www.viagogo.com/Concert-Tickets/Rock-and-Pop/Artist-Tickets/E-160973997
                  viagogo_event_id: "160973997"
              ticketmaster:
                summary: Ticketmaster event
                value:
                  ticketmaster_event_url: https://www.ticketmaster.com/event/0200642CF18CAA24
                  ticketmaster_event_id: 0200642CF18CAA24
      responses:
        "200":
          description: Upstream acknowledgement. Extra keys may be present.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventsAddResponse"
              example: { status: success, data: { event_id: "5b1c6c3e-2f5c-4c1e-9a4e-1c2d3e4f5a6b" } }
        "400":
          description: Validation failed (both or neither source supplied, non-numeric Viagogo id, bad URL, non-UUID `event_id`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataApiError"
              example: { status: error, code: INVALID_REQUEST, message: "Value error, provide a viagogo or ticketmaster event" }
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "502":
          $ref: "#/components/responses/DataUpstream"

  # ---------------------------------------------------------- Live Availability
  /events/platforms:
    get:
      tags: [Live Availability]
      summary: List platforms
      operationId: listPlatforms
      x-mint:
        metadata:
          title: List live availability platforms
          sidebarTitle: List platforms
      description: |
        Returns the twelve supported platform keys with their accepted regions, the
        shape of the `sections` they usually return, and an example event id. Use it to confirm
        the exact `platform` and `event_id` format before calling `POST /events/scrape`.
        Several platforms take the full event URL as `event_id`.

        **Cost:** 5 credits. Cache the result; the list changes rarely.
      responses:
        "200":
          description: Platform registry.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlatformsResponse"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

  /events/scrape:
    post:
      tags: [Live Availability]
      summary: Scrape live availability
      operationId: scrapeEvent
      x-mint:
        metadata:
          title: Live ticket availability for an event
          sidebarTitle: Scrape live availability
      description: |
        Fetches the current ticket availability for a single event on one of the
        supported platforms (see `GET /events/platforms`).
        The scrape runs synchronously and can take up to 60 seconds; set your client
        timeout to at least 65 seconds.

        A successful result is reused for 60 seconds: an identical request (same
        `platform`, `event_id` and `region`) in that window returns it again, with
        its original `scrape_timestamp`, and is billed like any other call.

        The shape of `sections` depends on `sections_type`, which can differ between
        events of the same platform. See the **Section types** guide.

        **Cost:** 5 credits.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ScrapeRequest"
            example:
              platform: dice_fm
              event_id: https://link.dice.fm/Q3dd78515650
              region: FR
      responses:
        "200":
          description: Current availability.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ScrapeResponse"
        "400":
          description: Unknown platform, a region the platform does not list, or an `event_id` it cannot take, such as a bare Eventim id.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DetailError"
              examples:
                platform:
                  value: { detail: "Unsupported platform: dicefm. Supported: ['axs', 'dice_fm', 'eventim', 'fansale', 'seatgeek', 'stubhub', 'ticketek', 'ticketportal', 'tickpick', 'tm', 'viagogo', 'vividseats']" }
                region:
                  value: { detail: "Unsupported region: fr for platform fansale. Supported: ['at', 'ch', 'de', 'dk', 'es', 'fi', 'it', 'nl', 'no', 'pl', 'uk']" }
                event_id:
                  value: { detail: "Eventim needs the full event URL, e.g. https://www.eventim.de/en/event/artist-venue-12345/" }
        "404":
          description: Event not found on the platform (sold out, expired, removed, or wrong id).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DetailError"
              example: { detail: "Event not found" }
        "408":
          description: The platform took longer than 60 seconds. Retry once; if it persists the event id is probably wrong.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DetailError"
              example: { detail: "Scrape timed out after 60s" }
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/ServiceUnavailable"
        "502":
          $ref: "#/components/responses/ServiceUnavailable"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /events/ping:
    get:
      tags: [Live Availability]
      summary: Ping
      operationId: scrapePing
      x-mint:
        metadata:
          title: Live Availability API ping
          sidebarTitle: Ping
      description: |
        Liveness probe for the Live Availability API.

        **Cost:** 5 credits, like every call on this route. Prefer `GET /usage`
        to test your key.
      responses:
        "200":
          description: Service is up.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Status"
              example: { status: ok }
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

  # ------------------------------------------------------------------ Watchlist
  /watchlist:
    get:
      tags: [Watchlist]
      summary: List watchlist items
      operationId: listWatchlist
      description: |
        Returns the watchlist items owned by your account, newest first. `items` is an
        empty array if you have none.

        Omitting `limit` returns the entire watchlist in one response. Large watchlists
        are easier to handle page by page: pass `limit` and walk `offset` forward until
        `has_more` is `false`.

        Ordering is stable across pages — creation time descending, then `id`
        descending — so paging never repeats or skips an item, even when many items
        share a creation timestamp because they were added in bulk.

        **Cost:** free.
      parameters:
        - name: limit
          in: query
          required: false
          description: |
            Maximum number of items to return. Omit to return the entire watchlist.
            A value outside `1..1000` is rejected with `400`.
          schema:
            type: integer
            minimum: 1
            maximum: 1000
            example: 500
        - name: offset
          in: query
          required: false
          description: |
            Number of items to skip before collecting the page. Defaults to 0.
            Items are ordered by creation time, newest first.
          schema:
            type: integer
            minimum: 0
            default: 0
            example: 1000
      responses:
        "200":
          description: One page of watchlist items, with the totals needed to page through the rest.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WatchlistPage"
        "400":
          description: "`limit` or `offset` is not an integer, or is out of range."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WatchlistError"
              examples:
                limit:
                  value: { error: "\"limit\" must be an integer between 1 and 1000" }
                offset:
                  value: { error: "\"offset\" must be an integer >= 0" }
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "502":
          $ref: "#/components/responses/ServiceUnavailable"
    post:
      tags: [Watchlist]
      summary: Create watchlist item
      operationId: createWatchlistItem
      description: |
        Creates a watchlist item. `website`, `event`, and `region` are required;
        every other field is optional and takes the server default when omitted.
        Sending an explicit `null` stores `NULL`, except on boolean fields and `priority`,
        where it is ignored.

        Keep the returned `id`: you need it to read, update, or delete the item.
        The Notifications API picks up new items within 60 seconds.

        `event` may carry several ids separated by commas, which creates one item per
        id. Validation is all-or-nothing: if any one id fails the format check for that
        `website` and `region`, the whole request is rejected and nothing is created.

        Active items count toward your plan's limit of active events. A request that
        would go over it is rejected with `403` (`Event limit reached: …`), and nothing
        is created.

        **Unknown keys are rejected with `400`, not ignored.** The read-only fields
        returned by `GET` (`id`, `client_id`, `created_at`, `updated_at`, `deleted`,
        `website_id`) are accepted and ignored.

        A filter that the target `website` / `region` does not support is dropped and
        listed in `ignored_fields` — the request still succeeds. See
        [Watchlist platforms](/guides/watchlist-platforms).

        **Cost:** free.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WatchlistItemCreate"
            example:
              website: ticketmaster
              event: 0200642CF18CAA24
              region: US
              seat_quantity: 2
              max_price_filter: 250
      responses:
        "201":
          description: |
            Item created. `item` is the first created item and `items` carries all of
            them — the two differ only when `event` held several comma-separated ids.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WatchlistCreateResponse"
        "400":
          description: Invalid input, or the body is not a JSON object. See [Errors](/errors#watchlist-errors) for the full message catalogue.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/WatchlistError"
                  - $ref: "#/components/schemas/DetailError"
              examples:
                bad_event_id:
                  value: { error: "One or more IDs are invalid for ticketmaster. Example: 0200642CF18CAA24" }
                unknown_website:
                  value: { error: "Unknown website: \"vividseat\". Use one of the supported website values listed at https://docs.eventory.ai/guides/watchlist-platforms.", website: vividseat }
                missing_field:
                  value: { error: "Website is required" }
                unknown_field:
                  value: { error: "Unknown field(s): max_price", unknown_fields: [max_price] }
                not_json:
                  value: { detail: "request body is not valid JSON" }
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "502":
          $ref: "#/components/responses/ServiceUnavailable"

  /watchlist/{id}:
    parameters:
      - name: id
        in: path
        required: true
        description: Watchlist item id, as returned on create. `0` or a negative id is rejected with `400`.
        schema:
          type: integer
          example: 123
    get:
      tags: [Watchlist]
      summary: Get watchlist item
      operationId: getWatchlistItem
      description: |
        Returns a single item by id.

        **Cost:** free.
      responses:
        "200":
          description: The item, wrapped in an `item` key.
          content:
            application/json:
              schema:
                type: object
                required: [item]
                properties:
                  item:
                    $ref: "#/components/schemas/WatchlistItem"
        "404":
          $ref: "#/components/responses/WatchlistNotFound"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
    patch:
      tags: [Watchlist]
      summary: Update watchlist item
      operationId: updateWatchlistItem
      description: |
        Partial update. Send only the fields you want to change; omitted fields are
        left untouched. To clear a nullable field, send it explicitly as `null`;
        `null` on a boolean field or on `priority` is ignored.

        Unknown keys are rejected with `400` rather than ignored. The read-only fields
        returned by `GET` (`id`, `client_id`, `created_at`, `updated_at`, `deleted`,
        `website_id`) are accepted and ignored, so you can send back an item you read.

        A filter that the target `website` / `region` does not support is dropped and
        listed in `ignored_fields`; the update still succeeds.

        An update that would take your account past your plan's limit of active events,
        such as setting `task_active` to `true` or moving an active item to another
        event, is rejected with `403`.

        Changes, including `change_type`, reach active stream connections within
        60 seconds without a reconnect.

        **Cost:** free.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WatchlistItemUpdate"
            example:
              seat_quantity: 4
              task_active: true
      responses:
        "200":
          description: |
            The full updated item under `item`, plus `ignored_fields` and `warning` when
            a filter was reset.
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    required: [item]
                    properties:
                      item:
                        $ref: "#/components/schemas/WatchlistItem"
                  - $ref: "#/components/schemas/WatchlistIgnoredFields"
        "400":
          description: Invalid input, or the body is not a JSON object. See [Errors](/errors#watchlist-errors) for the full message catalogue.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/WatchlistError"
                  - $ref: "#/components/schemas/DetailError"
              examples:
                unknown_field:
                  value: { error: "Unknown field(s): max_price", unknown_fields: [max_price] }
                bad_threshold:
                  value: { error: "min_stock_increase_filter must be a whole number greater than 0, or null to disable it" }
                not_json:
                  value: { detail: "request body is not valid JSON" }
        "404":
          $ref: "#/components/responses/WatchlistNotFound"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
    delete:
      tags: [Watchlist]
      summary: Delete watchlist item
      operationId: deleteWatchlistItem
      description: |
        Deletes an item permanently. The stream stops delivering notifications for it within 60 seconds.

        **Cost:** free.
      responses:
        "200":
          description: Deleted. `deletedIds` lists the ids that were removed.
          content:
            application/json:
              schema:
                type: object
                required: [deletedIds]
                properties:
                  deletedIds:
                    type: array
                    items: { type: integer }
              example: { deletedIds: [123] }
        "404":
          $ref: "#/components/responses/WatchlistNotFound"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"

  /watchlist/health:
    get:
      tags: [Watchlist]
      summary: Health
      operationId: watchlistHealth
      x-mint:
        metadata:
          title: Watchlist API health check
          sidebarTitle: Health
      description: |
        Liveness probe for the Watchlist API. Authentication is required like any other call,
        and a key without watchlist access gets `403`.

        **Cost:** free.
      responses:
        "200":
          description: Service is up.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Status"
              example: { status: ok }
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"

  # -------------------------------------------------------------------- Account
  /usage:
    get:
      tags: [Account]
      summary: Usage
      operationId: getUsage
      x-mint:
        metadata:
          title: Credit usage and plan
          sidebarTitle: Usage
      description: |
        Returns your plan, billing period, and credit usage, fetched live from
        billing on every call. `credits.used` can trail your most recent requests
        by a minute or so. Any valid key may call this endpoint, whatever
        routes it is granted, so it is also the safest way to test a key.

        `credits.included` is the quota bundled into your plan; crossing it bills
        overage rather than blocking. `credits.hard_cap` is where the gateway stops
        serving with `402 limit_reached`. `usage_status` is a single field to
        switch on.

        **Cost:** free.
      responses:
        "200":
          description: Usage report.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UsageResponse"
              examples:
                active:
                  summary: Active subscription
                  value:
                    customer_id: cus_abc123
                    plan: basic
                    plan_ids: { subscription_id: sub_123, price_id: price_123, product_id: prod_123 }
                    status: active
                    usage_status: ok
                    cancel_at_period_end: false
                    canceled_at: null
                    trial_end: null
                    unit: credits
                    as_of: "2026-06-12T09:15:00Z"
                    period: { start: "2026-06-01T00:00:00Z", end: "2026-07-01T00:00:00Z", days_remaining: 19, elapsed_fraction: 0.3771 }
                    credits: { included: 5000, used: 1234, remaining: 3766, overage: 0, percent_used: 24.68, projected_used: 3272, hard_cap: 6000, in_overage: false }
                none:
                  summary: No active subscription
                  value:
                    customer_id: cus_abc123
                    plan: null
                    plan_ids: null
                    status: none
                    usage_status: none
                    cancel_at_period_end: false
                    canceled_at: null
                    trial_end: null
                    unit: null
                    as_of: "2026-06-12T09:15:00Z"
                    period: null
                    credits: { included: 0, used: 0, remaining: 0, overage: 0, percent_used: 0.0, projected_used: null, hard_cap: 0, in_overage: false }
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "409":
          description: Your account has no billing account yet. Contact Eventory.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DetailError"
              example: { detail: no_stripe_customer }
        "502":
          description: Billing provider unreachable, or the plan is misconfigured. Comes back in the gateway envelope. Retry with backoff; if it persists, contact support.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GatewayError"
              example: { error: { code: service_unavailable, message: "The service is temporarily unavailable." } }

components:
  securitySchemes:
    apiKey:
      type: apiKey
      in: header
      name: apikey
      description: Your Eventory API key. Sent as the `apikey` request header.

  responses:
    Unauthenticated:
      description: No credential was sent, or the key was rejected.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/GatewayError"
          examples:
            missing:
              value: { error: { code: unauthenticated, message: "Authentication is required to access this resource." } }
            invalid:
              value: { error: { code: invalid_credentials, message: "Your credentials are invalid or expired." } }
    Forbidden:
      description: The key is valid but not granted this route. Contact support to enable it.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/GatewayError"
          example: { error: { code: forbidden, message: "You do not have access to this service." } }
    PaymentRequired:
      description: Billing denied the request. See `code`.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/GatewayError"
          examples:
            limit:
              value: { error: { code: limit_reached, message: "You have reached your usage limit." } }
            subscription:
              value: { error: { code: no_subscription, message: "Your account has no active subscription." } }
    RateLimited:
      description: Live Availability only. More than 30 requests per minute from your account. Back off.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/GatewayError"
          example: { error: { code: rate_limited, message: "Too many requests. Please slow down." } }
    ServiceUnavailable:
      description: The upstream service failed or is unreachable. Retry with backoff.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/GatewayError"
          example: { error: { code: service_unavailable, message: "The service is temporarily unavailable." } }
    DataUpstream:
      description: The Market Data API failed or timed out. The gateway keeps the status (`500`, `502`, `503` or `504`) and answers with its own envelope; the underlying error is not forwarded. Retry with backoff.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/GatewayError"
          example: { error: { code: service_unavailable, message: "The service is temporarily unavailable." } }
    WatchlistNotFound:
      description: |
        No item with that id belongs to your account. Also returned instead of `403`
        when the item belongs to someone else — ids are never confirmed to exist across
        accounts.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/WatchlistError"
          example: { error: "Watchlist monitor not found" }

  schemas:
    Status:
      type: object
      properties:
        status:
          type: string
          example: ok

    GatewayError:
      type: object
      description: |
        Envelope for every failure decided by the gateway (auth, ACL, billing,
        rate limit, unknown route, upstream 5xx). Key on `code`; `message` is prose
        and may change.
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              enum:
                - unauthenticated
                - invalid_credentials
                - forbidden
                - no_subscription
                - limit_reached
                - no_billing_account
                - plan_unavailable
                - payment_required
                - invalid_parameter
                - rate_limited
                - not_found
                - service_unavailable
            message:
              type: string

    DataApiError:
      type: object
      description: Error envelope of the Market Data API.
      required: [status, code, message]
      properties:
        status:
          type: string
          const: error
        code:
          type: string
          enum:
            - INVALID_VIAGOGO_ID
            - INVALID_VIVIDSEATS_ID
            - INVALID_TICKETMASTER_ID
            - INVALID_REQUEST
            - INVALID_FILTER
            - MISSING_URL
            - EVENT_NOT_FOUND
            - ARTIST_NOT_FOUND
        message:
          type: string

    DetailError:
      type: object
      required: [detail]
      properties:
        detail:
          type: string

    WatchlistError:
      type: object
      required: [error]
      properties:
        error:
          type: string

    # ------------------------------------------------------------- Market Data
    Venue:
      type: object
      properties:
        name: { type: [string, "null"] }
        city: { type: [string, "null"] }
        country: { type: [string, "null"] }
        country_code: { type: [string, "null"], description: ISO-3166 alpha-2. }
        svg_map_url: { type: [string, "null"] }
        timezone: { type: [string, "null"], description: "IANA time zone of the venue, e.g. `Europe/Vienna`. Convert `event_date` to it for the local date and time. `null` when unknown." }

    ArtistRef:
      type: object
      properties:
        name: { type: [string, "null"] }
        url: { type: [string, "null"], description: Pass this value to `GET /eventory/data/artist`. }
        image_url: { type: [string, "null"] }

    ExternalUrls:
      type: object
      description: Cross-marketplace identifiers. Fields are `null` when not on file.
      properties:
        ticketmaster_event_id: { type: [string, "null"] }
        ticketmaster_event_url: { type: [string, "null"] }
        vividseats_event_id: { type: [string, "null"] }
        vividseats_event_url: { type: [string, "null"] }

    ViagogoEvent:
      type: object
      properties:
        viagogo_event_id: { type: integer, description: Numeric portion of the event id. }
        viagogo_event_url: { type: [string, "null"] }
        event_name: { type: [string, "null"] }
        event_date: { type: [string, "null"], format: date-time, description: "RFC3339, UTC, so its date can differ from the show's local date: convert it to `venue.timezone`." }
        event_time_tba: { type: [boolean, "null"], description: "`true` when the start time is not confirmed yet: the time in `event_date` is provisional, often a placeholder such as 15:00 local, and only the local day in `venue.timezone` is reliable. `null` when unknown." }
        event_date_tba: { type: [boolean, "null"], description: "`true` when the date is not confirmed either. `null` when unknown." }
        event_image: { type: [string, "null"] }
        category_name: { type: [string, "null"] }
        currency: { type: string, description: Dominant sale currency (ISO-4217). `USD` when no sales exist yet. }
        venue:
          oneOf:
            - $ref: "#/components/schemas/Venue"
            - type: "null"
        artist:
          oneOf:
            - $ref: "#/components/schemas/ArtistRef"
            - type: "null"
        external_urls:
          $ref: "#/components/schemas/ExternalUrls"

    Decimal:
      type: [string, "null"]
      description: Decimal amount encoded as a string.
      example: "129.50"

    ViagogoSummary:
      type: object
      description: Latest aggregate snapshot. Every numeric field can be `null` before the first stats row exists.
      properties:
        is_general_admission_only: { type: [boolean, "null"] }
        stats_date: { type: [string, "null"], format: date-time }
        tickets_sold: { type: [integer, "null"] }
        tickets_sold_24h: { type: [integer, "null"] }
        tickets_sold_24h_percentage: { $ref: "#/components/schemas/Decimal" }
        listings_sold: { type: [integer, "null"] }
        listings_sold_24h: { type: [integer, "null"] }
        listings_sold_24h_percentage: { $ref: "#/components/schemas/Decimal" }
        listings_available: { type: [integer, "null"] }
        listings_available_24h: { type: [integer, "null"] }
        listings_available_24h_percentage: { $ref: "#/components/schemas/Decimal" }
        available_tickets: { type: [integer, "null"] }
        available_tickets_24h: { type: [integer, "null"] }
        available_tickets_24h_percentage: { $ref: "#/components/schemas/Decimal" }
        average_ticket_price_sold: { $ref: "#/components/schemas/Decimal" }
        average_ticket_price_sold_24h: { $ref: "#/components/schemas/Decimal" }
        average_ticket_price_sold_24h_percentage: { $ref: "#/components/schemas/Decimal" }
        average_ticket_price_available: { $ref: "#/components/schemas/Decimal" }
        average_ticket_price_available_24h: { $ref: "#/components/schemas/Decimal" }
        average_ticket_price_available_24h_percentage: { $ref: "#/components/schemas/Decimal" }
        highest_sale_price: { $ref: "#/components/schemas/Decimal" }
        highest_sale_price_24h: { $ref: "#/components/schemas/Decimal" }
        highest_sale_price_24h_percentage: { $ref: "#/components/schemas/Decimal" }
        get_in_price: { $ref: "#/components/schemas/Decimal", description: "Cheapest available ticket. `null` when nothing is listed." }
        get_in_price_24h: { $ref: "#/components/schemas/Decimal", description: "`null` when nothing is listed, or when there is no comparable value from 24 hours earlier." }
        get_in_price_24h_percentage: { $ref: "#/components/schemas/Decimal", description: "`null` in the same cases as `get_in_price_24h`." }
        total_volume: { $ref: "#/components/schemas/Decimal", description: Total sales volume in `currency`. }
        currency: { type: string }

    ViagogoDailyStatistic:
      type: object
      properties:
        stat_date: { type: string, format: date, description: The calendar day the entry covers, in UTC. }
        tickets_sold: { type: [integer, "null"] }
        tickets_sold_24h: { type: [integer, "null"], description: "Tickets sold during that day: the 24 hours up to the day's last update, normally the end of the UTC day." }
        available_tickets: { type: [integer, "null"] }
        listings_available: { type: [integer, "null"] }
        average_ticket_price_sold: { $ref: "#/components/schemas/Decimal" }
        average_ticket_price_available: { $ref: "#/components/schemas/Decimal" }
        highest_sale_price: { $ref: "#/components/schemas/Decimal" }

    PriceHistogram:
      type: object
      description: When there are no sales yet, `min` and `max` are `"0"` and `data` is empty.
      properties:
        bins: { type: integer }
        min: { type: string, example: "0" }
        max: { type: string, example: "500" }
        currency: { type: string }
        data:
          type: array
          items:
            type: object
            properties:
              start: { type: string, description: Inclusive lower bound. }
              end: { type: string, description: Upper bound. }
              tickets_sold: { type: integer }

    ViagogoResponse:
      type: object
      properties:
        event:
          $ref: "#/components/schemas/ViagogoEvent"
        summary:
          $ref: "#/components/schemas/ViagogoSummary"
        daily_statistics:
          type: array
          description: One entry per calendar day (UTC) with data since the event was first tracked, chronological. Days without data are missing.
          items:
            $ref: "#/components/schemas/ViagogoDailyStatistic"
        price_histogram:
          $ref: "#/components/schemas/PriceHistogram"
      example:
        event:
          viagogo_event_id: 160973997
          viagogo_event_url: https://www.viagogo.com/Concert-Tickets/Rock-and-Pop/Artist-Tickets/E-160973997
          event_name: Artist World Tour
          event_date: "2026-06-15T20:00:00Z"
          event_time_tba: false
          event_date_tba: false
          event_image: https://images.example.com/cover.jpg
          category_name: Concerts
          currency: USD
          venue: { name: Madison Square Garden, city: New York, country: United States, country_code: US, svg_map_url: null, timezone: America/New_York }
          artist: { name: Artist, url: https://www.viagogo.com/Concert-Tickets/Rock-and-Pop/Artist-Tickets, image_url: null }
          external_urls: { ticketmaster_event_id: 0200642CF18CAA24, ticketmaster_event_url: https://www.ticketmaster.com/event/0200642CF18CAA24, vividseats_event_id: null, vividseats_event_url: null }
        summary:
          is_general_admission_only: false
          stats_date: "2026-06-12T06:00:00Z"
          tickets_sold: 1840
          tickets_sold_24h: 62
          tickets_sold_24h_percentage: "3.49"
          listings_available: 410
          available_tickets: 1275
          average_ticket_price_sold: "162.40"
          average_ticket_price_available: "201.10"
          highest_sale_price: "980.00"
          get_in_price: "74.00"
          total_volume: "298816.00"
          currency: USD
        daily_statistics:
          - { stat_date: "2026-06-11", tickets_sold: 1778, tickets_sold_24h: 51, available_tickets: 1320, listings_available: 422, average_ticket_price_sold: "160.90", average_ticket_price_available: "199.30", highest_sale_price: "980.00" }
          - { stat_date: "2026-06-12", tickets_sold: 1840, tickets_sold_24h: 62, available_tickets: 1275, listings_available: 410, average_ticket_price_sold: "162.40", average_ticket_price_available: "201.10", highest_sale_price: "980.00" }
        price_histogram:
          bins: 10
          min: "0"
          max: "1000"
          currency: USD
          data:
            - { start: "0", end: "100", tickets_sold: 412 }
            - { start: "100", end: "200", tickets_sold: 903 }

    VividseatsEvent:
      type: object
      properties:
        vividseats_event_id: { type: string, description: The production id requested. }
        vividseats_event_url: { type: [string, "null"] }
        event_name: { type: [string, "null"] }
        event_date: { type: [string, "null"], format: date-time, description: "RFC3339, UTC, so its date can differ from the show's local date: convert it to `venue.timezone`." }
        event_time_tba: { type: [boolean, "null"], description: "`true` when the start time is not confirmed yet: the time in `event_date` is provisional, often a placeholder such as 15:00 local, and only the local day in `venue.timezone` is reliable. `null` when unknown." }
        event_date_tba: { type: [boolean, "null"], description: "`true` when the date is not confirmed either. `null` when unknown." }
        event_image: { type: [string, "null"] }
        category_name: { type: [string, "null"] }
        currency: { type: string, description: "ISO-4217 code of the venue market (`USD`, `CAD`, `GBP`, `EUR`, ...). All prices use it." }
        venue:
          oneOf:
            - $ref: "#/components/schemas/Venue"
            - type: "null"
        artist:
          oneOf:
            - $ref: "#/components/schemas/ArtistRef"
            - type: "null"
        external_urls:
          $ref: "#/components/schemas/ExternalUrls"

    VividseatsSummary:
      type: object
      description: Latest snapshot. Each metric has a `_24h` signed change and a `_24h_percentage`.
      properties:
        stats_date: { type: string, format: date-time, description: When the snapshot was taken. }
        currency: { type: string }
        available_tickets: { type: [integer, "null"], description: Tickets currently listed. }
        available_tickets_24h: { type: [integer, "null"] }
        available_tickets_24h_percentage: { $ref: "#/components/schemas/Decimal" }
        price_points: { type: [integer, "null"], description: "Distinct (section, price) buckets listed." }
        price_points_24h: { type: [integer, "null"] }
        price_points_24h_percentage: { $ref: "#/components/schemas/Decimal" }
        average_ticket_price_available: { $ref: "#/components/schemas/Decimal", description: Average all-in ask price per ticket. }
        average_ticket_price_available_24h: { $ref: "#/components/schemas/Decimal" }
        average_ticket_price_available_24h_percentage: { $ref: "#/components/schemas/Decimal" }
        median_price: { $ref: "#/components/schemas/Decimal", description: Median all-in ask price per ticket. }
        median_price_24h: { $ref: "#/components/schemas/Decimal" }
        median_price_24h_percentage: { $ref: "#/components/schemas/Decimal" }
        get_in_price: { $ref: "#/components/schemas/Decimal", description: Cheapest listed ticket. }
        get_in_price_24h: { $ref: "#/components/schemas/Decimal" }
        get_in_price_24h_percentage: { $ref: "#/components/schemas/Decimal" }
        max_price: { $ref: "#/components/schemas/Decimal", description: Most expensive listed ticket. }
        max_price_24h: { $ref: "#/components/schemas/Decimal" }
        max_price_24h_percentage: { $ref: "#/components/schemas/Decimal" }
        listed_inventory_value: { $ref: "#/components/schemas/Decimal", description: Sum of price x tickets across everything listed. }
        listed_inventory_value_24h: { $ref: "#/components/schemas/Decimal" }
        listed_inventory_value_24h_percentage: { $ref: "#/components/schemas/Decimal" }

    VividseatsDailyStatistic:
      type: object
      properties:
        stat_date: { type: string, format: date, description: Calendar day (UTC). }
        available_tickets: { type: [integer, "null"] }
        price_points: { type: [integer, "null"] }
        average_ticket_price_available: { $ref: "#/components/schemas/Decimal" }
        median_price: { $ref: "#/components/schemas/Decimal" }
        get_in_price: { $ref: "#/components/schemas/Decimal" }
        max_price: { $ref: "#/components/schemas/Decimal" }
        listed_inventory_value: { $ref: "#/components/schemas/Decimal" }

    VividseatsSection:
      type: object
      properties:
        section_name: { type: string }
        zone: { type: [string, "null"] }
        is_general_admission: { type: [boolean, "null"] }
        price_points: { type: [integer, "null"] }
        available_tickets: { type: [integer, "null"] }
        average_price: { $ref: "#/components/schemas/Decimal" }
        min_price: { $ref: "#/components/schemas/Decimal" }
        max_price: { $ref: "#/components/schemas/Decimal" }

    VividseatsResponse:
      type: object
      properties:
        event:
          $ref: "#/components/schemas/VividseatsEvent"
        summary:
          $ref: "#/components/schemas/VividseatsSummary"
        daily_statistics:
          type: array
          description: One entry per scraped day (the day's last snapshot), chronological. Days without a scrape are absent.
          items:
            $ref: "#/components/schemas/VividseatsDailyStatistic"
        sections:
          type: array
          description: Per-section breakdown of the latest snapshot, most tickets first.
          items:
            $ref: "#/components/schemas/VividseatsSection"
      example:
        event:
          vividseats_event_id: "6686410"
          vividseats_event_url: https://www.vividseats.com/hamilton-tickets-albuquerque-popejoy-hall-9-12-2027/production/6686410
          event_name: Hamilton
          event_date: "2027-09-12T19:00:00Z"
          event_time_tba: false
          event_date_tba: false
          event_image: https://images.example.com/cover.jpg
          category_name: Hamilton
          currency: USD
          venue: { name: Popejoy Hall, city: Albuquerque, country: USA, country_code: US, svg_map_url: null, timezone: America/Denver }
          artist: { name: Hamilton, url: https://www.viagogo.com/Theater-Tickets/Cast-Recordings/Musicals/Hamilton-Tickets, image_url: null }
          external_urls: { ticketmaster_event_id: null, ticketmaster_event_url: null, vividseats_event_id: "6686410", vividseats_event_url: https://www.vividseats.com/hamilton-tickets-albuquerque-popejoy-hall-9-12-2027/production/6686410 }
        summary:
          stats_date: "2026-10-20T12:27:29Z"
          currency: USD
          available_tickets: 178
          available_tickets_24h: -6
          available_tickets_24h_percentage: "-3.26"
          price_points: 14
          price_points_24h: 0
          price_points_24h_percentage: "0.00"
          average_ticket_price_available: "681.16"
          median_price: "583.00"
          get_in_price: "403.00"
          get_in_price_24h: "12.00"
          get_in_price_24h_percentage: "3.07"
          max_price: "1180.00"
          listed_inventory_value: "121247.00"
        daily_statistics:
          - { stat_date: "2026-10-19", available_tickets: 184, price_points: 14, average_ticket_price_available: "672.40", median_price: "580.00", get_in_price: "391.00", max_price: "1180.00", listed_inventory_value: "123722.00" }
          - { stat_date: "2026-10-20", available_tickets: 178, price_points: 14, average_ticket_price_available: "681.16", median_price: "583.00", get_in_price: "403.00", max_price: "1180.00", listed_inventory_value: "121247.00" }
        sections:
          - { section_name: BALC LEFT, zone: Balcony, is_general_admission: false, price_points: 2, available_tickets: 32, average_price: "686.00", min_price: "438.00", max_price: "934.00" }
          - { section_name: ORCH CTR, zone: Orchestra, is_general_admission: false, price_points: 2, available_tickets: 32, average_price: "985.00", min_price: "825.00", max_price: "1145.00" }

    Sale:
      type: object
      properties:
        listing_id:
          oneOf:
            - type: string
            - type: integer
            - type: "null"
        section: { type: [string, "null"] }
        row: { type: [string, "null"] }
        seat: { type: [string, "null"] }
        sold_tickets: { type: integer }
        raw_price: { $ref: "#/components/schemas/Decimal", description: "Per-ticket price, in `currency`." }
        currency: { type: [string, "null"], description: ISO-4217 currency of this sale. }
        created_datetime: { type: [string, "null"], format: date-time }

    SalesResponse:
      type: object
      properties:
        sales:
          type: array
          items:
            $ref: "#/components/schemas/Sale"
      example:
        sales:
          - { listing_id: "9876543", section: Floor, row: C, seat: "12", sold_tickets: 2, raw_price: "210.00", currency: USD, created_datetime: "2026-04-27T18:32:11Z" }
          - { listing_id: "9876212", section: Floor, row: F, seat: null, sold_tickets: 1, raw_price: "185.00", currency: USD, created_datetime: "2026-04-27T17:05:40Z" }

    TicketmasterResponse:
      type: object
      properties:
        ticketmaster_event:
          type: object
          properties:
            scrapeRuns:
              type: array
              description: Newest first, trimmed to `limit`.
              items:
                type: object
                additionalProperties: true
                properties:
                  region: { type: string }
                  created_date_time: { type: string, format: date-time }
            sectionStats:
              type: array
              description: One row per section, from the most recent scrape run only.
              items:
                type: object
                additionalProperties: true
                properties:
                  section_name: { type: string }
                  seatmap_id: { type: [string, "null"] }
                  capacity: { type: [integer, "null"] }
                  is_general_admission: { type: [boolean, "null"] }
                  created_date_time: { type: string, format: date-time }
                  currency: { type: string }
                  offers:
                    type: array
                    description: |
                      One entry per offer type in the section, with that offer type's
                      `available_stock` and `sold_amount`. The average prices, fees
                      included, are the section's and the same on every entry; `null`
                      when nothing in the section is priced.
                    items:
                      type: object
                      additionalProperties: true
                      properties:
                        offer_type: { type: [string, "null"] }
                        available_stock: { type: string }
                        sold_amount: { type: string }
                        average_price_total: { type: [number, "null"] }
                        average_price_resale: { type: [number, "null"] }
                        average_price_primary: { type: [number, "null"] }
                        currency: { type: string }
                        created_date_time: { type: string, format: date-time }
            offerStats:
              type: array
              description: Currently always empty.
              items:
                type: object
                additionalProperties: true
                properties:
                  offer_name: { type: string }
                  offer_stock: { type: integer }
                  average_price_fees: { type: number }
                  average_price_no_fees: { type: number }
                  offer_type: { type: integer }
                  currency: { type: string }
                  created_date_time: { type: string, format: date-time }
            statisticsHistory:
              type: array
              description: Newest first, trimmed to `limit`.
              items:
                type: object
                additionalProperties: true
                properties:
                  tickets_sold: { type: integer }
                  tickets_sold_24h: { type: integer }
                  tickets_sold_24h_percentage: { type: string }
                  available_tickets: { type: integer }
                  created_date_time: { type: string, format: date-time }
                  highest_price: { type: number }
                  lowest_price: { type: number }
                  average_ticket_price_available: { type: number }
                  average_ticket_price_standard: { type: number }
                  average_ticket_price_resell: { type: number }
                  average_ticket_price_platinum: { type: number }
                  capacity: { type: integer }
                  currency: { type: string }
        latest_stats:
          type: object
          additionalProperties: true
          properties:
            tickets_sold: { type: integer }
            available_tickets: { type: integer }
            currency: { type: string }
        dashboard_v2:
          type: object
          additionalProperties: true
          properties:
            currency: { type: string }
            current_values:
              type: object
              additionalProperties: true
              properties:
                average_sales_price: { type: number, description: "Average price of the standard tickets on sale, not of tickets sold." }
                highest_sale_price: { type: number, description: "Highest ticket price on sale, not a sale price." }
                listing_available: { type: number, description: "Resale tickets on sale, counted in tickets rather than listings." }
                average_tickets_per_sale: { type: number, description: "Not computed; always `0`." }
            charts:
              type: object
              description: Parallel arrays indexed by `timestamps`.
              additionalProperties: true
              properties:
                timestamps:
                  type: array
                  items: { type: string }
      example:
        ticketmaster_event:
          scrapeRuns:
            - { region: US, created_date_time: "2026-06-12T05:00:00Z" }
          sectionStats:
            - section_name: "101"
              seatmap_id: manifest_section_0
              capacity: 250
              is_general_admission: false
              created_date_time: "2026-06-12T05:00:00Z"
              currency: USD
              offers:
                - { offer_type: "Standard:Admission_101", available_stock: "33", sold_amount: "217", average_price_total: 104.5, average_price_resale: 120, average_price_primary: 95, currency: USD, created_date_time: "2026-06-12T05:00:00Z" }
          offerStats: []
          statisticsHistory:
            - { tickets_sold: 200, tickets_sold_24h: 15, tickets_sold_24h_percentage: "8.11", available_tickets: 320, created_date_time: "2026-06-12T05:00:00Z", highest_price: 180, lowest_price: 45, average_ticket_price_available: 97, average_ticket_price_standard: 88, average_ticket_price_resell: 120, average_ticket_price_platinum: 150, capacity: 520, currency: USD }
        latest_stats: { tickets_sold: 1000, available_tickets: 550, currency: USD }
        dashboard_v2:
          currency: USD
          current_values: { total_tickets_sold: 1000, tickets_sold_last_24h: 40, tickets_sold_last_24h_percentage: 4.1, available_tickets: 550, available_tickets_delta_percentage: -1.3, listing_available: 120, average_sales_price: 98, average_listing_price: 105, get_in_price: 48 }
          charts:
            timestamps: ["2026-06-11", "2026-06-12"]
            total_tickets_sold: [960, 1000]
            tickets_sold_last_24h: [32, 40]
            available_tickets: [610, 550]
            listing_available: [150, 120]
            average_listing_price: [99, 105]
            get_in_price: [44, 48]

    ArtistResponse:
      type: object
      properties:
        artist:
          type: object
          properties:
            artist_url: { type: string }
            artist_name: { type: [string, "null"] }
            artist_image_url: { type: [string, "null"] }
        social_metrics:
          type: [object, "null"]
          description: "`null` when the artist has no social profile on file."
          additionalProperties: true
          properties:
            artist_name: { type: [string, "null"] }
            country_code: { type: [string, "null"], description: Empty string when unknown. }
            city: { type: [string, "null"], description: Empty string when unknown. }
            bio: { type: [string, "null"], description: Empty string when unknown. }
            level: { type: [string, "null"], description: "Career stage: `long_tail`, `mid_level`, `mainstream` or `superstar`." }
            gender: { type: [string, "null"], description: Empty string when unknown. }
            birth_date: { type: [string, "null"], format: date-time }
            main_genre: { type: [string, "null"] }
            subgenres: { type: array, items: { type: string } }
            created_at: { type: [string, "null"], format: date-time }
            updated_at: { type: [string, "null"], format: date-time }
        latest_snapshot:
          type: [object, "null"]
          description: "`null` when there is no social profile. `demographics`, `scores`, and `releases` are free-form and may evolve; treat them as opaque."
          additionalProperties: true
          properties:
            snapshot_date: { type: string, format: date }
            platforms:
              type: object
              description: Keyed by platform (`spotify`, `youtube`, …), each mapping metric name to value.
              additionalProperties: true
            demographics: { type: [object, "null"], additionalProperties: true }
            scores: { type: [object, "null"], additionalProperties: true }
            releases: { type: [object, "null"], additionalProperties: true }
        snapshots_history:
          type: array
          description: Empty when there is no social profile.
          items:
            type: object
            additionalProperties: true
            properties:
              snapshot_date: { type: string, format: date }
              platforms: { type: object, additionalProperties: true }
      example:
        artist:
          artist_url: https://www.viagogo.com/Concert-Tickets/R-B/Contemporary-R-B/Teddy-Swims-Tickets
          artist_name: Teddy Swims
          artist_image_url: https://images.example.com/teddy.jpg
        social_metrics: { artist_name: Teddy Swims, country_code: US, city: Conyers, bio: "Singer-songwriter from Georgia.", level: mainstream, gender: male, birth_date: "1992-09-25T00:00:00.000Z", main_genre: pop, subgenres: [soul, r&b], created_at: "2026-03-02T10:15:00.000Z", updated_at: "2026-06-12T06:00:00.000Z" }
        latest_snapshot:
          snapshot_date: "2026-06-12"
          platforms: { spotify: { followers: 12345678, monthly_listeners: 45678901 }, youtube: { subscribers: 2345678 } }
          demographics: {}
          scores: {}
          releases: {}
        snapshots_history:
          - { snapshot_date: "2026-06-11", platforms: { spotify: { followers: 12340000 } } }
          - { snapshot_date: "2026-06-12", platforms: { spotify: { followers: 12345678 } } }

    SearchRequest:
      type: object
      properties:
        query:
          type: string
          maxLength: 200
          default: ""
          description: Free-text query.
        page:
          type: integer
          minimum: 1
          maximum: 1000
          default: 1
          description: 1-based page index. At most the first 1,000 results of a query can be paged; past that, `hits` is empty.
        hitsPerPage:
          type: integer
          minimum: 1
          maximum: 100
          default: 24
        sortBy:
          type: string
          default: recommended
          description: |
            Only `recommended`, `tickets_sold.desc`, `tickets_sold_24h.desc`,
            `average_ticket_price_sold.desc` and `capacity.desc` order the whole result
            set. Every other value sorts only the first `page × hitsPerPage` results of
            the relevance order. See the **Search filters** guide.
          enum:
            - recommended
            - event_date
            - event_date.desc
            - event_name
            - event_name.desc
            - average_ticket_price_sold
            - average_ticket_price_sold.desc
            - tickets_sold
            - tickets_sold.desc
            - tickets_sold_24h
            - tickets_sold_24h.desc
            - available_tickets
            - available_tickets.desc
            - capacity
            - capacity.desc
        filterExpression:
          type: string
          maxLength: 6000
          default: ""
          description: Filter expression. See the **Search filters** guide.
          example: "venue_country_code:US AND tickets_sold > 1000"

    SearchResponse:
      type: object
      properties:
        hits:
          type: array
          description: |
            Numbers in hits are JSON numbers, not strings. Hits may carry other fields
            than the ones listed here; those can change without notice. For an event's
            details, call `GET /eventory/data/viagogo/E-{viagogo_event_id}` or
            `GET /eventory/data/ticketmaster/{ticketmaster_event_id}`.
          items:
            type: object
            additionalProperties: true
            properties:
              objectID: { type: string, description: Same value as `event_id`. }
              event_id: { type: string, format: uuid, description: Eventory event id. }
              event_name: { type: string }
              event_date: { type: [string, "null"], format: date-time }
              event_timestamp: { type: integer, description: Event start as a Unix timestamp (seconds). Use it for date filters. }
              category_name: { type: string }
              artist_id: { type: integer }
              artist_name: { type: string }
              venue_id: { type: integer }
              venue_name: { type: string }
              venue_city: { type: string }
              venue_country: { type: string }
              venue_country_code: { type: string }
              tickets_sold: { type: integer }
              tickets_sold_24h: { type: integer }
              available_tickets: { type: integer }
              listings_available: { type: [integer, "null"] }
              average_ticket_price_sold: { type: number }
              average_ticket_price_available: { type: [number, "null"] }
              get_in_price: { type: [number, "null"] }
              highest_sale_price: { type: [number, "null"] }
              has_ticketmaster_data: { type: boolean }
              ticketmaster_event_id: { type: [string, "null"], description: "Pass it to `GET /eventory/data/ticketmaster/{tm_event_id}`. `null` when no Ticketmaster event is linked." }
              viagogo_event_id: { type: integer, description: "Numeric viagogo event id: call `GET /eventory/data/viagogo/E-{viagogo_event_id}`. Absent when no viagogo event is linked." }
              viagogo_event_url: { type: string, description: The event URL `viagogo_event_id` was taken from. }
        page: { type: integer }
        hitsPerPage: { type: integer }
        nbHits: { type: integer, description: Total matching events. }
        nbPages: { type: integer }
        processingTimeMS: { type: integer }
      example:
        hits:
          - { objectID: "8f2d4c1a-6b3e-4f7a-9c2d-1e5b7a9c3d40", event_id: "8f2d4c1a-6b3e-4f7a-9c2d-1e5b7a9c3d40", event_name: "Drake — World Tour", event_date: "2026-06-12T20:00:00+00:00", event_timestamp: 1781294400, category_name: Drake, artist_id: 981, artist_name: Drake, venue_id: 77, venue_name: O2 Arena, venue_city: London, venue_country: United Kingdom, venue_country_code: GB, tickets_sold: 8000, tickets_sold_24h: 210, available_tickets: 1400, listings_available: 320, average_ticket_price_sold: 145.2, average_ticket_price_available: 188.4, get_in_price: 96.5, highest_sale_price: 610.0, has_ticketmaster_data: true, ticketmaster_event_id: 0200642CF18CAA24, viagogo_event_id: 160973997, viagogo_event_url: https://www.viagogo.com/E-160973997 }
        page: 1
        hitsPerPage: 24
        nbHits: 1
        nbPages: 1
        processingTimeMS: 7

    EventsAddRequest:
      type: object
      description: Exactly one source mode. Never both, never neither.
      properties:
        event_url:
          type: string
          format: uri
          description: |
            Viagogo event URL. Required with `viagogo_event_id`. It is not stored: the
            event keeps its canonical URL, `https://www.viagogo.com/E-<viagogo_event_id>`.
        viagogo_event_id:
          type: string
          pattern: "^\\d+$"
          description: Numeric Viagogo event id (without the `E-` prefix). Required with `event_url`.
        ticketmaster_event_url:
          type: string
          format: uri
          description: Ticketmaster event URL. Required with `ticketmaster_event_id`.
        ticketmaster_event_id:
          type: string
          description: Ticketmaster event id. Required with `ticketmaster_event_url`.
        event_id:
          type: string
          format: uuid
          description: Optional existing Eventory event UUID to attach the source to. Anything that is not a UUID is rejected with `400`.

    EventsAddResponse:
      type: object
      additionalProperties: true
      properties:
        status: { type: [string, "null"] }
        data: { type: [object, "null"], additionalProperties: true }

    # -------------------------------------------------------- Live Availability
    PlatformsResponse:
      type: object
      properties:
        platforms:
          type: object
          description: Keyed by platform identifier.
          additionalProperties:
            type: object
            properties:
              description:
                type: [string, "null"]
                description: What this platform monitors.
              sections_type:
                type: string
                enum: [OfferSections, SeatMapSections]
                description: The shape of `sections` this platform usually returns. Dispatch on the `sections_type` of each scrape response.
              regions:
                type: [array, "null"]
                description: Accepted region codes; any other region is rejected with `400`. `null` means any value is accepted or ignored.
                items: { type: string }
              event_id_example:
                type: string
                description: A realistic `event_id` for this platform.
      example:
        platforms:
          axs:
            description: null
            sections_type: SeatMapSections
            regions: [uk, us, nu, de, fr, au, nz, sg]
            event_id_example: https://www.axs.com/e/artist-name-tickets-venue/551681
          dice_fm:
            description: null
            sections_type: OfferSections
            regions: null
            event_id_example: https://link.dice.fm/Q3dd78515650
          eventim:
            description: "Eventim — monitors ticket availability across Europe and Brazil. The correct scraping strategy (HTML or performance API) is selected automatically based on the event region."
            sections_type: OfferSections
            regions: [de, it, uk, fi, fr, nl, no, sk, es, ch, se, pl, dk, at, si, hr, ro, hu, br]
            event_id_example: https://www.eventim.de/en/tickets/artist-name-venue-12345/
          fansale:
            description: null
            sections_type: OfferSections
            regions: [it, uk, de, es, ch, at, nl, fi, dk, pl, no]
            event_id_example: "532635"
          seatgeek:
            description: null
            sections_type: SeatMapSections
            regions: null
            event_id_example: "18010329"
          stubhub:
            description: "StubHub — monitors resale ticket availability worldwide. UK events (stubhub.co.uk) are routed to the dedicated UK monitor automatically; all other regions use the global StubHub monitor."
            sections_type: SeatMapSections
            regions: null
            event_id_example: https://www.stubhub.com/artist-name-venue-tickets/event/1234567
          ticketek:
            description: "Ticketek — monitors ticket availability for events in Australia, New Zealand, Singapore, Malaysia, UK, and the Philippines. Region is inferred from the event URL domain."
            sections_type: OfferSections
            regions: [au, nz, sg, my, uk, ph]
            event_id_example: https://premier.ticketek.com.au/events/ONKKETEC26/venues/SSD/performances/ESSD2026866EC/tickets
          ticketportal:
            description: "Ticketportal — monitors ticket availability for events"
            sections_type: OfferSections
            regions: [cz, sk]
            event_id_example: "1462276"
          tickpick:
            description: null
            sections_type: OfferSections
            regions: null
            event_id_example: "1234567"
          tm:
            description: "Ticketmaster — monitors primary and resale ticket availability for events worldwide."
            sections_type: SeatMapSections
            regions: [us, ca, uk, ie, au, nz, mx, de, es, nl, at, dk, be, no, ch, se, fi, pl, ae, cz, fr, it, za]
            event_id_example: 0200642CF18CAA24
          viagogo:
            description: null
            sections_type: SeatMapSections
            regions: null
            event_id_example: https://www.viagogo.com/Concert-Tickets/E-1234567
          vividseats:
            description: "VividSeats — monitors resale ticket availability worldwide. Listings are priced in the currency of the event's own market (venue country), not the region of the request."
            sections_type: SeatMapSections
            regions: null
            event_id_example: https://www.vividseats.com/artist-name-venue-tickets-city-1-1-2027/production/1234567

    ScrapeRequest:
      type: object
      required: [platform, event_id, region]
      properties:
        platform:
          type: string
          description: Platform key from `GET /events/platforms`.
          enum: [tm, axs, dice_fm, eventim, fansale, seatgeek, stubhub, ticketek, ticketportal, tickpick, viagogo, vividseats]
          example: dice_fm
        event_id:
          type: string
          description: The event's id on that platform. Some platforms take a bare id, others the full event URL; copy the format of `event_id_example` from `GET /events/platforms`. A bare Eventim id is rejected with `400`.
          example: https://link.dice.fm/Q3dd78515650
        region:
          type: string
          description: Country or region code (`US`, `UK`, `FR`, `IT`, …). Case-insensitive. Must be one of the platform's `regions` in `GET /events/platforms` unless those are `null`; any other value is rejected with `400`.
          example: FR

    ScrapeResponse:
      type: object
      properties:
        success: { type: boolean, description: "`true` if the event was found and data was returned." }
        platform: { type: string }
        event_id: { type: string }
        region: { type: string }
        scrape_timestamp: { type: string, format: date-time }
        sections_type:
          type: [string, "null"]
          enum: [OfferSections, SeatMapSections, null]
          description: The shape of `sections` in this response. It can differ from the platform's usual `sections_type` in `GET /events/platforms`, so dispatch on this one.
        total_stock:
          type: [integer, "null"]
          description: Total tickets available across all sections. `0` if sold out or not tracked.
        sections:
          type: object
          description: See the **Section types** guide.
          additionalProperties: true
        event_info:
          oneOf:
            - $ref: "#/components/schemas/EventInfo"
            - type: "null"
        degraded:
          type: [string, "null"]
          description: Present on Ticketmaster scrapes when one data arm was unavailable. `v1` means resale/GA truth is missing and counts may be approximate; `v2` means seat-level detail is missing.
        metadata:
          type: [object, "null"]
          additionalProperties: true
          description: Platform-specific extras, e.g. `venue_map` on viagogo and StubHub. Treat as opaque.
        error:
          type: [string, "null"]
      example:
        success: true
        platform: dice_fm
        event_id: https://link.dice.fm/Q3dd78515650
        region: FR
        scrape_timestamp: "2026-06-12T10:00:00.000000+00:00"
        sections_type: OfferSections
        total_stock: 0
        sections:
          "FINAL RELEASE - Entrance Before 5pm":
            offer_name: "FINAL RELEASE - Entrance Before 5pm"
            available: true
            price: 21.4
            price_label: "EUR 21.40"
            stock: null
            general_admission: null
            atc: null
          "REGULAR FINAL RELEASE":
            offer_name: "REGULAR FINAL RELEASE"
            available: true
            price: 32.1
            price_label: "EUR 32.10"
            stock: null
            general_admission: null
            atc: null
        event_info:
          event_name: Croccante
          event_date: "2025-10-18T14:00:00+02:00"
          event_time: null
          event_venue: Balagan Roma
          event_venue_url: null
          event_artist_name: null
          event_location: "Via Aldo Palazzeschi 125, 00137 Rome, Italy"
          event_url: https://link.dice.fm/Q3dd78515650
          event_image: https://dice-media.imgix.net/example.jpg
        degraded: null
        error: null

    EventInfo:
      type: object
      description: Event metadata. Every field may be `null` depending on the platform.
      properties:
        event_name: { type: [string, "null"] }
        event_date: { type: [string, "null"], description: "ISO 8601 datetime, or a date alone when `event_time_tba` is `true`." }
        event_timezone: { type: [string, "null"], description: "IANA time zone of the venue, e.g. `Europe/Madrid`, when known." }
        event_time_tba: { type: [boolean, "null"], description: "`true` when only the day is known." }
        event_time_local: { type: [boolean, "null"], description: "`true` when `event_date` is the venue's local time without a UTC offset." }
        event_time: { type: [string, "null"], description: Human-readable time, if provided separately. }
        event_venue: { type: [string, "null"] }
        event_venue_url: { type: [string, "null"] }
        event_artist_name: { type: [string, "null"] }
        event_location: { type: [string, "null"] }
        event_url: { type: [string, "null"] }
        event_image: { type: [string, "null"] }

    # ---------------------------------------------------------------- Watchlist
    WatchlistFilters:
      type: object
      description: |
        Optional filters and options. Omit a field to keep the server default.
        Boolean fields and `priority` are never `null`; sending `null` for one is ignored.
      properties:
        min_price_filter: { type: [number, "null"], description: Ignore tickets priced below this. }
        max_price_filter: { type: [number, "null"], description: Ignore tickets priced above this. }
        min_avg_price_threshold: { type: [number, "null"] }
        max_avg_price_threshold: { type: [number, "null"] }
        stock_threshold_filter: { type: [integer, "null"], description: Minimum available stock to notify on. }
        stock_threshold_filter_max: { type: [integer, "null"], description: Maximum available stock to notify on. }
        min_stock_increase_filter:
          type: [integer, "null"]
          description: |
            Only notify when the stock increase is at least this many tickets.
            Must be a positive integer; `null` turns the filter off. `0` or a negative
            value is rejected with `400`.
          example: 4
        min_stock_decrease_filter:
          type: [integer, "null"]
          description: |
            Only notify when the stock decrease is at least this many tickets.
            Must be a positive integer; `null` turns the filter off. `0` or a negative
            value is rejected with `400`.
          example: 4
        vip_tickets: { type: boolean, description: Include VIP tickets. }
        seat_quantity: { type: [integer, "null"], description: Number of contiguous seats wanted. }
        priority: { type: [integer, "null"] }
        section: { type: [string, "null"], description: Restrict to a section name. }
        keywords: { type: [string, "null"] }
        date_filter: { type: [string, "null"] }
        row_filter: { type: [string, "null"] }
        change_type:
          type: [string, "null"]
          description: |
            Which change triggers a notification. The stream enforces this server-side.
            Any value outside this list is rejected with `400`.
          enum: [stock_increase, stock_decrease, price_decrease, price_increase, null]
          default: stock_increase
        standard_tickets: { type: boolean }
        resale_tickets: { type: boolean }
        general_admission_tickets: { type: boolean }
        platinum_tickets: { type: boolean }
        no_restricted_view: { type: boolean }
        no_accessibility: { type: boolean }
        no_presale: { type: boolean }
        auto_add_to_watchlist: { type: boolean }
        task_active: { type: boolean, description: Whether monitoring for this item is active. }
        muted_until: { type: [string, "null"], format: date-time, description: Suppress notifications until this time. }
        notes: { type: [string, "null"] }
        tm_did: { type: [string, "null"], description: Ticketmaster device id. }
        roles_json:
          description: Pass-through JSON; treat as opaque.
          oneOf:
            - type: object
              additionalProperties: true
            - type: array
              items: {}
            - type: "null"
        webhook_setting_id: { type: [integer, "null"] }
        role_id: { type: [integer, "null"] }
        website_id: { type: [integer, "null"], readOnly: true, description: Read-only. Accepted and ignored on write. }

    WatchlistIgnoredFields:
      type: object
      properties:
        ignored_fields:
          type: array
          description: |
            Filters that were reset because the target `website` / `region` does not
            support them. Only `seat_quantity`, `row_filter`, `tm_did`,
            `platinum_tickets` and `vip_tickets` are checked. The request still
            succeeds; the stored item has those filters switched off: `null`, or `true`
            for `platinum_tickets` and `vip_tickets`. Absent when nothing was reset.
          items:
            type: string
          example: [platinum_tickets, row_filter]
        warning:
          type: string
          description: Human-readable summary of `ignored_fields`. Present only when `ignored_fields` is.
          example: "Not supported on this website/region and reset: platinum_tickets, row_filter"

    WatchlistPage:
      type: object
      required: [items, total, offset, limit, has_more]
      properties:
        items:
          type: array
          description: This page of watchlist items, newest first.
          items:
            $ref: "#/components/schemas/WatchlistItem"
        total:
          type: integer
          description: Total items on the account, independent of `limit` and `offset`.
          example: 6788
        offset:
          type: integer
          description: The offset that was applied.
          example: 1000
        limit:
          type: [integer, "null"]
          description: The limit that was applied; `null` when none was passed.
          example: 1000
        has_more:
          type: boolean
          description: "`true` when items remain after this page."
          example: true

    WatchlistCreateResponse:
      allOf:
        - type: object
          required: [item, items, createdIds]
          properties:
            item:
              $ref: "#/components/schemas/WatchlistItem"
            items:
              type: array
              description: Every item created by this request — more than one when `event` held several comma-separated ids.
              items:
                $ref: "#/components/schemas/WatchlistItem"
            createdIds:
              type: array
              description: The ids of the created items, in the same order as `items`.
              items: { type: integer }
              example: [123]
        - $ref: "#/components/schemas/WatchlistIgnoredFields"

    WatchlistIdentity:
      type: object
      required: [website, event, region]
      properties:
        website:
          type: string
          description: |
            Platform to monitor, e.g. `ticketmaster`. An unknown value is rejected with
            `400` (`Unknown website: "<website>". …`). For the platforms with a known id
            format, `event` is checked against that platform's (and often that region's)
            format. Some platforms are available on request: until one is enabled for
            your account, using it returns `403`
            (`Website "<website>" is not available on this account`). See
            [Watchlist platforms](/guides/watchlist-platforms) for the recognised keys,
            the retired aliases, and the discovery monitors.
          example: ticketmaster
        event:
          type: string
          description: |
            Event identifier on that platform — a bare id or a full event URL depending
            on the platform. On create, several ids may be sent comma-separated to make
            one item each. For a discovery monitor this is the artist or venue URL
            rather than an event id.
          example: 0200642CF18CAA24
        region:
          type: string
          description: Region or market, e.g. `US`.
          example: US

    WatchlistItemCreate:
      allOf:
        - $ref: "#/components/schemas/WatchlistIdentity"
        - $ref: "#/components/schemas/WatchlistFilters"

    WatchlistItemUpdate:
      allOf:
        - type: object
          properties:
            website: { type: string }
            event: { type: string }
            region: { type: string }
        - $ref: "#/components/schemas/WatchlistFilters"

    WatchlistItem:
      allOf:
        - type: object
          required: [id, client_id, created_at, updated_at, deleted]
          properties:
            id: { type: integer, description: Assigned on create. Use it in the URL path. }
            client_id: { type: integer, description: The account that owns this item. }
            created_at: { type: string, description: "Timestamp without a UTC offset, e.g. `2026-06-18T09:20:00.123456`." }
            updated_at: { type: string, description: "Timestamp without a UTC offset, e.g. `2026-06-18T09:20:00.123456`." }
            deleted: { type: boolean, description: "Legacy field, always `false`. `DELETE` removes the item permanently." }
        - $ref: "#/components/schemas/WatchlistIdentity"
        - $ref: "#/components/schemas/WatchlistFilters"
      example:
        id: 123
        client_id: 42
        website: ticketmaster
        event: 0200642CF18CAA24
        region: US
        min_price_filter: null
        max_price_filter: 250.0
        min_avg_price_threshold: null
        max_avg_price_threshold: null
        stock_threshold_filter: 1
        stock_threshold_filter_max: null
        min_stock_increase_filter: 4
        min_stock_decrease_filter: null
        seat_quantity: 2
        priority: 0
        section: null
        keywords: null
        date_filter: null
        row_filter: null
        change_type: stock_increase
        standard_tickets: true
        resale_tickets: false
        general_admission_tickets: false
        platinum_tickets: false
        vip_tickets: false
        no_restricted_view: false
        no_accessibility: false
        no_presale: false
        auto_add_to_watchlist: false
        task_active: true
        muted_until: null
        notes: null
        tm_did: null
        roles_json: null
        webhook_setting_id: null
        role_id: null
        website_id: null
        created_at: "2026-06-18T09:20:00.123456"
        updated_at: "2026-06-18T09:20:00.123456"
        deleted: false

    # ------------------------------------------------------------------ Account
    UsageResponse:
      type: object
      properties:
        customer_id: { type: string }
        plan: { type: [string, "null"], description: Plan name. `null` without an active subscription. }
        plan_ids:
          type: [object, "null"]
          properties:
            subscription_id: { type: string }
            price_id: { type: string }
            product_id: { type: string }
        status:
          type: string
          description: Subscription status. Only `active` and `trialing` are served by the gateway; `none` means no subscription.
          example: active
        usage_status:
          type: string
          enum: [ok, approaching_limit, over_included, at_hard_cap, none]
          description: "`approaching_limit` from 80% of `included`; `over_included` once overage starts; `at_hard_cap` when the gateway is refusing requests."
        cancel_at_period_end: { type: boolean }
        canceled_at: { type: [string, "null"], format: date-time }
        trial_end: { type: [string, "null"], format: date-time }
        unit: { type: [string, "null"], description: Billing unit label, usually `credits`. }
        as_of: { type: string, format: date-time }
        period:
          type: [object, "null"]
          properties:
            start: { type: string, format: date-time }
            end: { type: string, format: date-time }
            days_remaining: { type: [integer, "null"] }
            elapsed_fraction: { type: [number, "null"], description: 0 to 1. }
        credits:
          type: object
          properties:
            included: { type: integer, description: Credits bundled into the plan for the period. }
            used: { type: integer, description: Credits used this period. Can trail your most recent requests by a minute or so. }
            remaining: { type: integer, description: "Included credits left: `included` (or `hard_cap`, if lower) minus `used`, never below 0. It reads `0` during overage, while paid requests are still served until `hard_cap`." }
            overage: { type: integer, description: Credits used beyond `included`. }
            percent_used: { type: number }
            projected_used: { type: [integer, "null"], description: End-of-period usage at the current pace. `null` at the very start of a period. }
            hard_cap:
              description: Credits at which the gateway stops serving, or `"unlimited"`.
              oneOf:
                - type: integer
                - type: string
                  const: unlimited
            in_overage: { type: boolean }
