ItemData Inventory API

The authoritative OpenAPI 3.0 contract is embedded below. Download the raw YAML.

openapi: 3.0.3
info:
  title: ItemData Steam Inventory API
  version: 6.0.0
  description: Cursor-paginated Steam inventory API with a server-controlled Steam page size of 2000, rotating-proxy retries, durable public page snapshots, bounded account-wide private-inventory caching, caller-authorized Steam-session access, and an optional internal Steam-session pool for recently trade-blocked CS2 assets.
servers:
  - url: /
tags:
  - name: Inventory
    description: Fetch normalized Steam inventory pages with cache and billing metadata.
  - name: Plans
    description: Discover the currently published ItemData plans and request limits.
  - name: Operations
    description: Read-only service liveness and dependency health endpoints.
  - name: Reference
    description: Human-readable and machine-readable API contracts.
paths:
  /v1/inventory:
    get:
      tags: [Inventory]
      summary: Fetch one Steam inventory page
      x-itemdata-catalog: true
      x-itemdata-try-it: inventory
      x-itemdata-order: 10
      x-itemdata-credit-cost:
        unit: credit
        base: 1
        maximum: 2
        fresh_surcharge: 1
        stacks: false
        label: 1–2 credits
        description: Successful normal pages cost 1 credit. Any fresh-fetch mode adds +1, and fresh options never stack above 2 credits.
      x-itemdata-rate-limit:
        label: Plan limits
        description: The active plan's minute, day, and month request limits apply before Steam work begins.
      x-itemdata-example:
        query:
          steam_id: '76561198000000000'
          game: cs2
      description: >-
        Returns exactly one Steam page. A successful normal public request
        consumes one base credit and may use a fresh or stale-if-error public
        snapshot. no_cache=true, caller-supplied steam_login_secure or
        trade_url credentials, and
        try_first_seven_days_blocked_items=true each add +1 credit when the
        result is billable because they force a fresh fetch. Fresh-fetch
        additions never stack, so the maximum billable inventory-request price
        is 2 credits.
        Steam's undocumented Community inventory endpoint currently returns
        HTTP 401 with JSON null for structurally valid but unallocated
        SteamID64 values. Because 401 may also represent a public-empty state,
        ItemData treats it as a successful empty public page and follows normal
        public-page caching and billing. An upstream Steam HTTP 403 is treated
        as a confirmed private inventory and finalizes the full
        quote: 1 credit for a normal MISS or private-marker HIT, or 2 credits
        for a fresh-fetch mode. It is cached for 72 hours once per SteamID64;
        during that TTL it applies to every game and normal requests return HIT
        without contacting Steam. no_cache=true ignores both public snapshots
        and the private marker, charges 2 credits for a confirmed fresh 403,
        and refreshes the marker. A non-authoritative success=false result still
        removes an older public snapshot but is not charged.
        Caller-supplied and internal Steam-session requests always perform a
        fresh fetch and bypass all shared cache state. For an authorized
        account, provide a structurally valid steamLoginSecure cookie in
        X-Steam-Login-Secure to fetch the cookie owner's inventory for any
        supported game; for CS2, this avoids the normal 10-day visibility
        delay. steam_id is ignored and the owner SteamID64 is read from the
        cookie. Alternatively, provide a canonical Steam trade URL in
        X-Steam-Trade-URL to fetch that CS2 partner inventory, including 7-10
        day trade-locked items; steam_id is then ignored and the target is read
        from the URL. If both credentials are present, X-Steam-Login-Secure
        takes precedence and the trade URL is ignored. Never put either secret
        in a URL unless legacy compatibility is unavoidable: query strings can
        enter browser history, access logs, and intermediary logs. Set
        try_first_seven_days_blocked_items=true for CS2 to try the internal
        partner-inventory session pool first; this forces non-tradable items to
        be included and falls back to a fresh public inventory request on the
        first page only when Steam rejects the partner request or the pool is
        temporarily unavailable. trade_url cannot be combined with
        try_first_seven_days_blocked_items. Broker authentication/configuration
        and invalid broker responses fail instead of being hidden by public
        fallback. Other failed requests and non-authoritative success=false
        responses consume zero credits. The upstream public Steam page size is
        fixed server-side at 2000; count and limit are not accepted. When
        more_items is true, pass last_assetid as start_assetid on the next
        request. Preserve cursor provenance using
        X-ItemData-Inventory-Source: keep the option enabled after partner, but
        omit it after public-fallback. The actual encoded JSON body is limited
        to 12 MiB for both compact and pretty output.
      operationId: getInventory
      security:
        - BearerAuth: []
        - ApiKeyQuery: []
      parameters:
        - $ref: '#/components/parameters/Key'
        - name: steam_id
          in: query
          required: false
          schema:
            type: string
            minLength: 17
            maxLength: 17
            pattern: '^[0-9]{17}$'
            example: '76561198000000000'
          description: Public individual SteamID64 only, in the inclusive range 76561197960265729 through 76561202255233023. SteamID2, SteamID3, vanity names, profile URLs, whitespace, and other Steam account types are rejected before inventory capacity or credits are reserved. Required unless steam_login_secure or trade_url supplies the target. When either credential supplies the target, steam_id may be omitted; if supplied, it must still satisfy this schema but its value is not used. steam_login_secure takes precedence when both credentials are present.
          x-itemdata-requirement:
            kind: conditional
            unless: [X-Steam-Login-Secure, X-Steam-Trade-URL]
        - $ref: '#/components/parameters/Game'
        - $ref: '#/components/parameters/Language'
        - name: start_assetid
          in: query
          schema: { type: string, maxLength: 32, pattern: '^[0-9]+$' }
          description: Opaque Steam cursor from the previous response's last_assetid. Omit for the first page.
        - $ref: '#/components/parameters/IncludeNonTradable'
        - $ref: '#/components/parameters/WithNoTradable'
        - $ref: '#/components/parameters/NoCache'
        - $ref: '#/components/parameters/TryFirstSevenDaysBlockedItems'
        - name: steam_login_secure
          in: query
          deprecated: true
          schema: { type: string, maxLength: 4096 }
          description: Legacy query form of X-Steam-Login-Secure. Prefer the header because query strings can be retained in browser history and access or intermediary logs.
          x-itemdata-sensitive: true
          x-itemdata-persist: never
        - $ref: '#/components/parameters/SteamLoginSecureHeader'
        - name: trade_url
          in: query
          deprecated: true
          schema: { type: string, maxLength: 2048 }
          description: Legacy query form of X-Steam-Trade-URL. Prefer the header because the trade token is a secret and query strings can be retained in browser history and access or intermediary logs.
          x-itemdata-sensitive: true
          x-itemdata-persist: never
        - $ref: '#/components/parameters/SteamTradeURLHeader'
        - $ref: '#/components/parameters/Format'
        - $ref: '#/components/parameters/Pretty'
      responses:
        '200':
          description: One inventory page, including a cacheable success=false response for a confirmed private inventory. Upstream Steam 401/403 statuses are domain inputs and do not become this API's HTTP 401/403 responses. A Steam 401, including the response observed for an unallocated but structurally valid SteamID64, becomes a billable successful empty page.
          headers:
            X-ItemData-More-Items:
              description: Mirrors more_items from the response body.
              schema: { type: boolean }
            X-ItemData-Next-Cursor:
              description: Present only when another page exists; pass this value as start_assetid. In blocked-item mode, keep the mode enabled only when X-ItemData-Inventory-Source is partner; omit it after public-fallback.
              schema: { type: string, maxLength: 32, pattern: '^[0-9]+$' }
            X-ItemData-Cache:
              description: HIT for a fresh public snapshot or account-wide private marker, MISS after a new normal fetch, STALE for public stale-if-error fallback, and BYPASS for forced, session-authenticated, or successful cache-storage fallback fetches.
              schema: { type: string, enum: [HIT, MISS, STALE, BYPASS] }
            X-ItemData-Inventory-Source:
              description: public for the normal endpoint, partner when the internal session pool succeeded, and public-fallback when partner mode fell back on its first page.
              schema: { type: string, enum: [public, partner, public-fallback] }
            Age:
              description: Whole seconds since the served public page or confirmed private state was observed at Steam.
              schema: { type: integer, format: int64, minimum: 0 }
            X-ItemData-Billing-Status:
              description: confirmed when the debit and usage event are durable. pending is a rare at-most-once safety outcome where COMMIT may have succeeded but reconciliation remained unavailable; retrying the same inventory call is a new billable request.
              schema: { type: string, enum: [confirmed, pending] }
            X-ItemData-Credits-Charged:
              description: Credits finalized for this billable result, including a confirmed Steam HTTP 403. Present only when X-ItemData-Billing-Status is confirmed.
              schema: { type: integer, minimum: 1, maximum: 2 }
            X-ItemData-Credit-Pricing-Version:
              description: Immutable server pricing rule recorded with the usage event. Present only when X-ItemData-Billing-Status is confirmed.
              schema: { type: string, example: inventory.v2 }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Inventory' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/PaymentRequired' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '499': { $ref: '#/components/responses/ClientClosedRequest' }
        '502': { $ref: '#/components/responses/BadGateway' }
        '503': { $ref: '#/components/responses/Unavailable' }
        '504': { $ref: '#/components/responses/GatewayTimeout' }
  /v1/plans:
    get:
      tags: [Plans]
      summary: List published plans
      description: Returns the currently published ItemData plans, credit balances, and minute, day, and month request limits without requiring an API key.
      operationId: listPublishedPlans
      x-itemdata-catalog: true
      x-itemdata-try-it: simple-get
      x-itemdata-order: 20
      x-itemdata-credit-cost:
        unit: credit
        base: 0
        maximum: 0
        fresh_surcharge: 0
        stacks: false
        label: Free
        description: This endpoint does not consume inventory credits.
      x-itemdata-rate-limit:
        label: Public read
        description: Protected by bounded service capacity; no API key is required.
      responses:
        '200':
          description: Published plans ordered by sort_order and code.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PlanList' }
        '503': { $ref: '#/components/responses/ConsoleUnavailable' }
  /v1/live:
    get:
      tags: [Operations]
      summary: Process liveness
      description: Confirms that the ItemData HTTP process is running. This lightweight probe does not verify downstream dependencies.
      operationId: getLiveness
      x-itemdata-catalog: true
      x-itemdata-try-it: simple-get
      x-itemdata-order: 40
      x-itemdata-credit-cost:
        unit: credit
        base: 0
        maximum: 0
        fresh_surcharge: 0
        stacks: false
        label: Free
        description: This endpoint does not consume inventory credits.
      x-itemdata-rate-limit:
        label: Health probe
        description: Intended for lightweight process-liveness checks.
      responses:
        '200':
          description: The process is running and can serve HTTP.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Liveness' }
  /v1/health:
    get:
      tags: [Operations]
      summary: Service, dependency, proxy, and runtime health
      description: Reports dependency-aware service readiness, including the runtime components required to serve inventory traffic.
      operationId: getHealth
      x-itemdata-catalog: true
      x-itemdata-try-it: simple-get
      x-itemdata-order: 30
      x-itemdata-credit-cost:
        unit: credit
        base: 0
        maximum: 0
        fresh_surcharge: 0
        stacks: false
        label: Free
        description: This endpoint does not consume inventory credits.
      x-itemdata-rate-limit:
        label: Health probe
        description: Intended for dependency-aware health and status checks.
      responses:
        '200':
          description: Healthy
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Health' }
        '503':
          description: One or more required dependencies are unavailable
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Health' }
  /v1/docs:
    get:
      tags: [Reference]
      summary: Self-contained OpenAPI documentation
      description: Returns the backend's embedded, English-language OpenAPI documentation for human inspection.
      operationId: getEmbeddedDocumentation
      x-itemdata-catalog: true
      x-itemdata-try-it: none
      x-itemdata-order: 50
      x-itemdata-credit-cost:
        unit: credit
        base: 0
        maximum: 0
        fresh_surcharge: 0
        stacks: false
        label: Free
        description: This endpoint does not consume inventory credits.
      x-itemdata-rate-limit:
        label: Public reference
        description: Served as a short-lived cacheable documentation response.
      responses:
        '200': { description: Human-readable embedded OpenAPI documentation }
  /v1/openapi.yaml:
    get:
      tags: [Reference]
      summary: Raw OpenAPI contract
      description: Returns the authoritative English-language OpenAPI 3.0 YAML contract used to generate the public endpoint catalog.
      operationId: getOpenAPIContract
      x-itemdata-catalog: true
      x-itemdata-try-it: none
      x-itemdata-order: 60
      x-itemdata-credit-cost:
        unit: credit
        base: 0
        maximum: 0
        fresh_surcharge: 0
        stacks: false
        label: Free
        description: This endpoint does not consume inventory credits.
      x-itemdata-rate-limit:
        label: Public reference
        description: Served as a short-lived cacheable machine-readable contract.
      responses:
        '200':
          description: OpenAPI 3.0 YAML
          content:
            application/yaml:
              schema: { type: string }
  /auth/steam:
    get:
      summary: Start Steam OpenID sign-in
      operationId: startSteamSignIn
      parameters:
        - name: return_to
          in: query
          required: false
          schema: { type: string, default: /dashboard, maxLength: 2048 }
          description: Same-site relative path used after authentication.
      responses:
        '302':
          description: Redirect to the fixed Steam OpenID provider.
        '400': { $ref: '#/components/responses/ConsoleBadRequest' }
        '429': { $ref: '#/components/responses/ConsoleRateLimited' }
        '503': { $ref: '#/components/responses/ConsoleUnavailable' }
  /auth/steam/callback:
    get:
      summary: Verify a Steam OpenID assertion
      operationId: finishSteamSignIn
      description: Browser callback used by Steam. A short-lived HttpOnly browser-binding cookie plus state, return_to, provider, signature, nonce, identity, and claimed SteamID64 are verified server-side. Capacity and rate-limit failures redirect with a safe auth_error code.
      responses:
        '302':
          description: Redirect to the configured frontend with a new secure web session, or a safe auth_error code.
  /auth/logout:
    post:
      summary: Revoke the current web session
      operationId: logoutWebSession
      security:
        - WebSession: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      responses:
        '204': { description: Session revoked and cookies cleared }
        '401': { $ref: '#/components/responses/ConsoleUnauthorized' }
        '403': { $ref: '#/components/responses/ConsoleForbidden' }
        '503': { $ref: '#/components/responses/ConsoleUnavailable' }
  /console/v1/me:
    get:
      summary: Load the authenticated dashboard snapshot
      operationId: getConsoleDashboard
      security:
        - WebSession: []
      responses:
        '200':
          description: Account, active-key metadata, current-month usage, credits, and a CSRF token. API-key plaintext is never returned here.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ConsoleDashboard' }
        '401': { $ref: '#/components/responses/ConsoleUnauthorized' }
        '503': { $ref: '#/components/responses/ConsoleUnavailable' }
  /console/v1/api-key/rotate:
    post:
      summary: Create or atomically rotate the account API key
      operationId: rotateConsoleAPIKey
      description: The plaintext credential is returned exactly once. An active key is not replaced while it has unexpired credit reservations.
      security:
        - WebSession: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      responses:
        '201':
          description: New API key. Do not persist this response.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CreatedAPIKey' }
        '401': { $ref: '#/components/responses/ConsoleUnauthorized' }
        '403': { $ref: '#/components/responses/ConsoleForbidden' }
        '409': { $ref: '#/components/responses/APIKeyBusy' }
        '429': { $ref: '#/components/responses/ConsoleRateLimited' }
        '503': { $ref: '#/components/responses/ConsoleUnavailable' }
  /console/v1/api-key/revoke:
    post:
      summary: Revoke the account API key
      operationId: revokeConsoleAPIKey
      security:
        - WebSession: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      responses:
        '200':
          description: API key revoked
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required: [revoked]
                properties:
                  revoked: { type: boolean, enum: [true] }
        '401': { $ref: '#/components/responses/ConsoleUnauthorized' }
        '403': { $ref: '#/components/responses/ConsoleForbidden' }
        '404': { $ref: '#/components/responses/APIKeyNotFound' }
        '409': { $ref: '#/components/responses/APIKeyBusy' }
        '429': { $ref: '#/components/responses/ConsoleRateLimited' }
        '503': { $ref: '#/components/responses/ConsoleUnavailable' }
  /console/v1/admin/plans:
    get:
      summary: List every plan for administration
      operationId: listAdminPlans
      security:
        - WebSession: []
      responses:
        '200':
          description: Draft, published, and archived plans ordered by sort_order and code.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PlanList' }
        '401': { $ref: '#/components/responses/ConsoleUnauthorized' }
        '403': { $ref: '#/components/responses/ConsoleForbidden' }
        '503': { $ref: '#/components/responses/ConsoleUnavailable' }
    post:
      summary: Create a plan
      operationId: createAdminPlan
      security:
        - WebSession: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreatePlanRequest' }
      responses:
        '201':
          description: Plan created and audit event recorded atomically.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PlanResponse' }
        '400': { $ref: '#/components/responses/ConsoleBadRequest' }
        '401': { $ref: '#/components/responses/ConsoleUnauthorized' }
        '403': { $ref: '#/components/responses/ConsoleForbidden' }
        '409':
          description: A plan with this code already exists.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ConsoleError' }
        '429': { $ref: '#/components/responses/ConsoleRateLimited' }
        '503': { $ref: '#/components/responses/ConsoleUnavailable' }
  /console/v1/admin/plans/{code}:
    post:
      summary: Update a plan with optimistic concurrency
      operationId: updateAdminPlan
      security:
        - WebSession: []
      parameters:
        - name: code
          in: path
          required: true
          schema:
            type: string
            minLength: 1
            maxLength: 64
            pattern: '^[a-z][a-z0-9_-]{0,63}$'
        - $ref: '#/components/parameters/CSRFToken'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/UpdatePlanRequest' }
      responses:
        '200':
          description: Updated plan and atomic audit event.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PlanResponse' }
        '400': { $ref: '#/components/responses/ConsoleBadRequest' }
        '401': { $ref: '#/components/responses/ConsoleUnauthorized' }
        '403': { $ref: '#/components/responses/ConsoleForbidden' }
        '404':
          description: Plan not found.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ConsoleError' }
        '409':
          description: Supplied version is stale.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ConsoleError' }
        '429': { $ref: '#/components/responses/ConsoleRateLimited' }
        '503': { $ref: '#/components/responses/ConsoleUnavailable' }
  /console/v1/admin/users/{steam_id64}/plan:
    post:
      summary: Assign or replace an account plan for a fixed duration
      operationId: assignAdminAccountPlan
      description: >-
        Appends an immutable assignment snapshot and atomically resets an
        active API key to the selected plan's published credits and rate
        limit. Idempotency keys are scoped to the authenticated administrator
        and retained durably: retrying the same key and payload returns the
        original assignment without another grant or credit reset. Existing
        in-flight credit reservations cause a conflict.
      security:
        - WebSession: []
      parameters:
        - name: steam_id64
          in: path
          required: true
          schema:
            type: string
            pattern: '^[0-9]{17}$'
        - $ref: '#/components/parameters/CSRFToken'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AssignPlanRequest' }
      responses:
        '201':
          description: Immutable assignment snapshot. The administrator identity and internal duration audit field are not exposed.
          headers:
            Idempotency-Replayed:
              description: True when this response replays the durable result of an earlier request with the same administrator, key, and payload.
              schema: { type: boolean }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PlanAssignmentResponse' }
        '400':
          description: Missing or invalid Idempotency-Key, SteamID64, plan code, duration, content type, or JSON shape.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ConsoleError' }
        '401': { $ref: '#/components/responses/ConsoleUnauthorized' }
        '403': { $ref: '#/components/responses/ConsoleForbidden' }
        '404':
          description: The registered account or selected plan does not exist.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ConsoleError' }
        '409':
          description: The plan is not published with fixed credits, the active API key has an in-flight reservation, or the Idempotency-Key was already used by this administrator with another payload (`idempotency_key_conflict`).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ConsoleError' }
        '429': { $ref: '#/components/responses/ConsoleRateLimited' }
        '503': { $ref: '#/components/responses/ConsoleUnavailable' }
components:
  securitySchemes:
    ApiKeyQuery:
      type: apiKey
      in: query
      name: key
    BearerAuth:
      type: http
      scheme: bearer
    WebSession:
      type: apiKey
      in: cookie
      name: itemshard_session
      description: Local HTTP cookie name. HTTPS deployments use the hardened __Host-itemshard_session name.
  parameters:
    Key:
      name: key
      in: query
      required: false
      schema: { type: string, maxLength: 512 }
      description: API key. Authorization Bearer is preferred outside compatibility integrations.
      x-itemdata-sensitive: true
      x-itemdata-persist: never
    Game:
      name: game
      in: query
      schema:
        type: string
        enum: [cs2, rust, dota, dota2, tf2, pubg]
        default: cs2
      description: Supported canonical values are cs2, rust, dota2, tf2, and pubg. dota is accepted as a compatibility alias and is returned canonically as dota2.
    Language:
      name: language
      in: query
      schema:
        type: string
        minLength: 2
        maxLength: 32
        pattern: '^[A-Za-z][A-Za-z_-]{1,31}$'
        default: english
      description: Steam inventory localization language. Defaults to english and accepts Steam-compatible language identifiers.
    IncludeNonTradable:
      name: include_non_tradable
      in: query
      schema: { type: boolean, default: false }
      description: Include non-tradable assets. Filtering never changes total_inventory_count or cursor progression.
    WithNoTradable:
      name: with_no_tradable
      in: query
      schema: { type: boolean, default: false }
      description: SteamWebAPI-compatible alias for include_non_tradable. If both are provided, their values must match.
    NoCache:
      name: no_cache
      in: query
      schema: { type: boolean, default: false }
      description: Force a new public Steam fetch instead of serving a public snapshot or confirmed-private marker. Additional cost is +1 credit for a billable result; a confirmed Steam HTTP 403 therefore costs 2 credits and refreshes the account-wide private marker. The response is not published as another shared immutable object. A non-authoritative success=false response still durably removes any older public snapshot and is refunded. A concurrent no_cache call for the same SteamID64 returns refresh_in_progress and is not charged. Requests authorized by steam_login_secure or trade_url and internal Steam-session requests already bypass the shared cache and do not need this flag. Fresh-fetch additions never stack, so the maximum billable inventory-request price is 2 credits.
    TryFirstSevenDaysBlockedItems:
      name: try_first_seven_days_blocked_items
      in: query
      schema: { type: boolean, default: false }
      description: For CS2 only, try Steam's partner-inventory path through an internal service-account session before the normal public endpoint. It cannot be combined with steam_login_secure or trade_url, always bypasses the shared cache, and forces include_non_tradable=true. Additional cost is +1 credit for a billable result, including a confirmed Steam HTTP 403 from the public fallback. If Steam rejects the first partner request or the pool is temporarily unavailable, ItemData performs one fresh public fallback and reports public-fallback in X-ItemData-Inventory-Source; broker configuration/authentication and protocol failures do not silently fall back. For the next page, keep this flag after a partner response but omit it after public-fallback. A later partner cursor never falls back to the semantically different public cursor.
    Format:
      name: format
      in: query
      schema: { type: string, enum: [json, prettyjson, pretty], default: json }
      description: Use prettyjson for indented JSON; pretty remains a compatibility alias.
    Pretty:
      name: pretty
      in: query
      schema: { type: boolean, default: false }
      description: Set to 1 or true for indented JSON. Pretty output is subject to the same 12 MiB encoded-body limit as compact JSON. ItemData intentionally supports JSON only.
    SteamLoginSecureHeader:
      name: X-Steam-Login-Secure
      in: header
      required: false
      schema: { type: string, maxLength: 4096 }
      description: Preferred transport for a structurally valid steamLoginSecure cookie that contains its owner SteamID64. Supported for every game; for CS2, it fetches the authorized owner's inventory without the normal 10-day visibility delay. steam_id is ignored and derived from the cookie. The value is forwarded only for this request, bypasses all shared cache reads and writes, adds +1 credit for a billable result including a confirmed Steam HTTP 403, and is never persisted. If X-Steam-Trade-URL is also present, this header takes precedence and the trade URL is ignored.
      x-itemdata-sensitive: true
      x-itemdata-persist: never
    SteamTradeURLHeader:
      name: X-Steam-Trade-URL
      in: header
      required: false
      schema:
        type: string
        maxLength: 2048
        example: https://steamcommunity.com/tradeoffer/new/?partner=123456&token=AbCdEf
      description: Preferred transport for a canonical HTTPS Steam trade URL with exactly partner=<AccountID32> and token=<6-128 characters from A-Z, a-z, 0-9, underscore, or hyphen>. CS2 only. Fetches the partner inventory including 7-10 day trade-locked items; steam_id is ignored and derived from partner. It always bypasses shared cache reads and writes, adds +1 credit after success, is never persisted, and cannot be combined with try_first_seven_days_blocked_items. It is ignored when X-Steam-Login-Secure is present.
      x-itemdata-sensitive: true
      x-itemdata-persist: never
    CSRFToken:
      name: X-CSRF-Token
      in: header
      required: true
      schema: { type: string, minLength: 32, maxLength: 512 }
      description: Token returned by GET /console/v1/me. Mutations also require an exact configured browser Origin.
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema:
        type: string
        minLength: 16
        maxLength: 128
        pattern: '^[A-Za-z0-9._:-]+$'
      description: Opaque per-operation key. Keep it unchanged when retrying an ambiguous response. A key is scoped to the authenticated administrator and may never be reused with another target or request body.
  responses:
    BadRequest:
      description: Invalid request
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    PaymentRequired:
      description: Insufficient credits
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Forbidden:
      description: API key is inactive, its assigned plan has expired (`plan_expired`), or access is forbidden
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    RateLimited:
      description: Account-wide fixed-window request rate exceeded. All API keys for the same Steam account and every application replica share the minute, UTC-day, and UTC-month counters.
      headers:
        Retry-After:
          schema: { type: integer }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Unavailable:
      description: Required dependency or proxy gateway unavailable (`cache_unavailable` or `upstream_unavailable`), or an identical refresh is still in progress (`refresh_in_progress`)
      headers:
        Retry-After:
          description: Present for retryable cache coordination failure or refresh contention.
          schema: { type: integer, minimum: 1 }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    BadGateway:
      description: Steam returned invalid data, or the decoded or final encoded inventory exceeded a safety limit (`pagination_limit`)
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    GatewayTimeout:
      description: Steam request timed out
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    ClientClosedRequest:
      description: Request context was canceled by the client
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    ConsoleBadRequest:
      description: Invalid browser authentication request
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ConsoleError' }
    ConsoleUnauthorized:
      description: Missing, expired, or revoked web session
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ConsoleError' }
    ConsoleForbidden:
      description: Origin/CSRF validation failed or the session lacks the required permission
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ConsoleError' }
    ConsoleUnavailable:
      description: Web account service unavailable
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ConsoleError' }
    ConsoleRateLimited:
      description: Browser authentication or console mutation rate exceeded
      headers:
        Retry-After:
          schema: { type: integer }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ConsoleError' }
    APIKeyBusy:
      description: An in-flight credit reservation prevents key rotation or revocation
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ConsoleError' }
    APIKeyNotFound:
      description: No active API key exists
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ConsoleError' }
  schemas:
    Error:
      type: object
      additionalProperties: false
      required: [success, error]
      properties:
        success: { type: boolean, enum: [false] }
        error:
          type: object
          additionalProperties: false
          required: [code, message]
          properties:
            code: { type: string }
            message: { type: string }
    ConsoleError:
      type: object
      additionalProperties: false
      required: [error]
      properties:
        error:
          type: object
          additionalProperties: false
          required: [code, message]
          properties:
            code: { type: string }
            message: { type: string }
    Inventory:
      type: object
      additionalProperties: false
      required: [success, steam_id, game, appid, total_inventory_count, items, more_items]
      properties:
        success: { type: boolean }
        steam_id: { type: string }
        game: { type: string }
        appid: { type: integer }
        total_inventory_count: { type: integer }
        items:
          type: array
          items: { $ref: '#/components/schemas/Item' }
        last_assetid: { type: string, maxLength: 32, pattern: '^[0-9]+$' }
        more_items: { type: boolean }
        error:
          type: object
          additionalProperties: false
          properties:
            code: { type: string }
            message: { type: string }
    Item:
      type: object
      additionalProperties: false
      required:
        - assetid
        - classid
        - instanceid
        - amount
        - market_hash_name
        - market_name
        - name
        - name_color
        - type
        - rarity
        - rarity_color
        - exterior
        - icon_url
        - icon_url_large
        - tradable
        - marketable
        - commodity
        - market_tradable_restriction
        - market_marketable_restriction
        - tags
        - descriptions
        - actions
        - fraudwarnings
        - inspect_link
      properties:
        assetid: { type: string }
        classid: { type: string }
        instanceid: { type: string }
        amount: { type: string }
        market_hash_name: { type: string }
        market_name: { type: string }
        name: { type: string }
        name_color: { type: string }
        type: { type: string }
        rarity: { type: string }
        rarity_color: { type: string }
        exterior: { type: string }
        icon_url: { type: string }
        icon_url_large: { type: string }
        tradable: { type: boolean }
        marketable: { type: boolean }
        commodity: { type: boolean }
        market_tradable_restriction: { type: integer }
        market_marketable_restriction: { type: integer }
        tags:
          type: array
          items: { $ref: '#/components/schemas/Tag' }
        descriptions:
          type: array
          items: { $ref: '#/components/schemas/DescriptionLine' }
        actions:
          type: array
          items: { $ref: '#/components/schemas/Action' }
        fraudwarnings:
          type: array
          nullable: true
          items: { type: string }
        inspect_link: { type: string }
    Tag:
      type: object
      additionalProperties: false
      required: [category, internal_name, localized_category_name, localized_tag_name]
      properties:
        category: { type: string }
        internal_name: { type: string }
        localized_category_name: { type: string }
        localized_tag_name: { type: string }
        color: { type: string }
    DescriptionLine:
      type: object
      additionalProperties: false
      required: [type, value]
      properties:
        type: { type: string }
        value: { type: string }
        color: { type: string }
    Action:
      type: object
      additionalProperties: false
      required: [link, name]
      properties:
        link: { type: string }
        name: { type: string }
    Liveness:
      type: object
      additionalProperties: false
      required: [status]
      properties:
        status: { type: string, enum: [ok] }
    Health:
      type: object
      additionalProperties: false
      required: [status, version, uptime_seconds, dependencies, proxy, system]
      properties:
        status: { type: string, enum: [ok, degraded] }
        version: { type: string }
        uptime_seconds: { type: integer, format: int64, minimum: 0 }
        dependencies:
          type: object
          additionalProperties: { $ref: '#/components/schemas/HealthCheck' }
        proxy:
          type: object
          additionalProperties: true
        system:
          type: object
          additionalProperties: true
    HealthCheck:
      type: object
      additionalProperties: false
      required: [status]
      properties:
        status: { type: string, enum: [ok, unavailable] }
        latency: { type: string }
        error: { type: string }
    ConsoleDashboard:
      type: object
      additionalProperties: false
      required: [account, api_key, plan_assignment, usage, credits, permissions, csrf_token]
      properties:
        account:
          type: object
          additionalProperties: false
          required: [steam_id64, display_name, plan]
          properties:
            steam_id64: { type: string, pattern: '^[0-9]{17}$' }
            display_name: { type: string }
            avatar_url: { type: string }
            plan: { type: string }
        api_key:
          nullable: true
          allOf:
            - $ref: '#/components/schemas/APIKeySummary'
        plan_assignment:
          nullable: true
          allOf:
            - $ref: '#/components/schemas/PlanAssignmentSnapshot'
        usage:
          type: object
          additionalProperties: false
          required: [requests, credits_used, used, remaining, limit, period_start, period_end]
          properties:
            requests: { type: integer, format: int64, minimum: 0 }
            credits_used: { type: integer, format: int64, minimum: 0 }
            used: { type: integer, format: int64, minimum: 0 }
            remaining: { type: integer, format: int64, minimum: 0 }
            limit: { type: integer, format: int64, minimum: 0 }
            period_start: { type: string, format: date-time }
            period_end: { type: string, format: date-time }
        credits:
          type: object
          additionalProperties: false
          required: [remaining]
          properties:
            remaining: { type: integer, format: int64, minimum: 0 }
        permissions:
          type: array
          items:
            type: string
            enum: ['plans:manage', 'plan_assignments:manage']
          uniqueItems: true
        csrf_token: { type: string, minLength: 32, maxLength: 512 }
    APIKeySummary:
      type: object
      additionalProperties: false
      required:
        - prefix
        - created_at
        - rate_limit_per_minute
        - rate_limit_per_day
        - rate_limit_per_month
      properties:
        prefix: { type: string }
        created_at: { type: string, format: date-time }
        last_used_at: { type: string, format: date-time }
        rate_limit_per_minute: { type: integer, minimum: 1, maximum: 200 }
        rate_limit_per_day:
          { type: integer, format: int64, minimum: 1, maximum: 1000000000000 }
        rate_limit_per_month:
          { type: integer, format: int64, minimum: 1, maximum: 1000000000000 }
        plan_expires_at: { type: string, format: date-time }
    PlanAssignmentSnapshot:
      type: object
      additionalProperties: false
      required:
        - plan_code
        - plan_version
        - credits
        - rate_limit_per_minute
        - rate_limit_per_day
        - rate_limit_per_month
        - duration_days
        - starts_at
        - expires_at
      properties:
        plan_code:
          type: string
          minLength: 1
          maxLength: 64
          pattern: '^[a-z][a-z0-9_-]{0,63}$'
        plan_version: { type: integer, format: int64, minimum: 1 }
        credits:
          type: integer
          format: int64
          minimum: 1
          maximum: 1000000000000
        rate_limit_per_minute:
          type: integer
          minimum: 1
          maximum: 200
        rate_limit_per_day:
          type: integer
          format: int64
          minimum: 1
          maximum: 1000000000000
        rate_limit_per_month:
          type: integer
          format: int64
          minimum: 1
          maximum: 1000000000000
        duration_days: { type: integer, minimum: 1, maximum: 3650 }
        starts_at: { type: string, format: date-time }
        expires_at: { type: string, format: date-time }
    AssignPlanRequest:
      type: object
      additionalProperties: false
      required: [plan_code, duration_days]
      properties:
        plan_code:
          type: string
          minLength: 1
          maxLength: 64
          pattern: '^[a-z][a-z0-9_-]{0,63}$'
        duration_days: { type: integer, minimum: 1, maximum: 3650 }
    PlanAssignmentResponse:
      type: object
      additionalProperties: false
      required: [assignment]
      properties:
        assignment:
          type: object
          additionalProperties: false
          required:
            - id
            - steam_id64
            - plan_code
            - plan_version
            - credits
            - rate_limit_per_minute
            - rate_limit_per_day
            - rate_limit_per_month
            - starts_at
            - expires_at
            - active_api_key_updated
          properties:
            id: { type: integer, format: int64, minimum: 1 }
            steam_id64: { type: string, pattern: '^[0-9]{17}$' }
            plan_code:
              type: string
              minLength: 1
              maxLength: 64
              pattern: '^[a-z][a-z0-9_-]{0,63}$'
            plan_version: { type: integer, format: int64, minimum: 1 }
            credits:
              type: integer
              format: int64
              minimum: 1
              maximum: 1000000000000
            rate_limit_per_minute:
              type: integer
              minimum: 1
              maximum: 200
            rate_limit_per_day:
              type: integer
              format: int64
              minimum: 1
              maximum: 1000000000000
            rate_limit_per_month:
              type: integer
              format: int64
              minimum: 1
              maximum: 1000000000000
            starts_at: { type: string, format: date-time }
            expires_at: { type: string, format: date-time }
            active_api_key_updated: { type: boolean }
    CreatedAPIKey:
      type: object
      additionalProperties: false
      required: [api_key, prefix, created_at]
      properties:
        api_key:
          type: string
          description: Plaintext secret returned exactly once.
        prefix: { type: string }
        created_at: { type: string, format: date-time }
    Plan:
      type: object
      additionalProperties: false
      required:
        - code
        - name
        - eyebrow
        - description
        - price_minor
        - currency
        - billing_interval
        - credits
        - rate_limit_per_minute
        - rate_limit_per_day
        - rate_limit_per_month
        - features
        - cta_kind
        - cta_label
        - featured
        - sort_order
        - status
        - version
        - created_at
        - updated_at
        - discount_percent
        - discount_expires_at
      properties:
        code:
          type: string
          minLength: 1
          maxLength: 64
          pattern: '^[a-z][a-z0-9_-]{0,63}$'
        name: { type: string, minLength: 1, maxLength: 128 }
        eyebrow: { type: string, minLength: 1, maxLength: 128 }
        description: { type: string, minLength: 1, maxLength: 1024 }
        price_minor:
          type: integer
          format: int64
          minimum: 0
          maximum: 1000000000000
          nullable: true
        currency:
          type: string
          maxLength: 3
          pattern: '^$|^[A-Z]{3}$'
        billing_interval:
          type: string
          enum: [month, year]
          nullable: true
        credits:
          type: integer
          format: int64
          minimum: 1
          maximum: 1000000000000
          nullable: true
        rate_limit_per_minute: { type: integer, minimum: 1, maximum: 200 }
        rate_limit_per_day:
          { type: integer, format: int64, minimum: 1, maximum: 1000000000000 }
        rate_limit_per_month:
          { type: integer, format: int64, minimum: 1, maximum: 1000000000000 }
        features:
          type: array
          minItems: 1
          maxItems: 32
          uniqueItems: true
          items: { type: string, minLength: 1, maxLength: 256 }
        cta_kind: { type: string, enum: [steam_signup, support] }
        cta_label: { type: string, minLength: 1, maxLength: 128 }
        featured: { type: boolean }
        sort_order: { type: integer, minimum: 0, maximum: 1000000 }
        status: { type: string, enum: [draft, published, archived] }
        version: { type: integer, format: int64, minimum: 1 }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        discount_percent:
          type: integer
          minimum: 1
          maximum: 100
          nullable: true
          description: Percentage removed from price_minor while the paired expiration is still in the future. Null when no discount is configured.
        discount_expires_at:
          type: string
          format: date-time
          nullable: true
          description: Finite RFC3339 instant at which the configured discount stops applying. Null when no discount is configured.
      allOf:
        - $ref: '#/components/schemas/PlanPricingInvariant'
        - $ref: '#/components/schemas/PlanDiscountInvariant'
    PlanList:
      type: object
      additionalProperties: false
      required: [plans]
      properties:
        plans:
          type: array
          items: { $ref: '#/components/schemas/Plan' }
    PlanResponse:
      type: object
      additionalProperties: false
      required: [plan]
      properties:
        plan: { $ref: '#/components/schemas/Plan' }
    CreatePlanRequest:
      type: object
      additionalProperties: false
      required:
        - code
        - name
        - eyebrow
        - description
        - price_minor
        - currency
        - billing_interval
        - credits
        - rate_limit_per_minute
        - rate_limit_per_day
        - rate_limit_per_month
        - features
        - cta_kind
        - cta_label
        - featured
        - sort_order
        - status
      properties:
        code:
          type: string
          minLength: 1
          maxLength: 64
          pattern: '^[a-z][a-z0-9_-]{0,63}$'
        name: { type: string, minLength: 1, maxLength: 128 }
        eyebrow: { type: string, minLength: 1, maxLength: 128 }
        description: { type: string, minLength: 1, maxLength: 1024 }
        price_minor: { type: integer, format: int64, minimum: 0, maximum: 1000000000000, nullable: true }
        currency: { type: string, maxLength: 3, pattern: '^$|^[A-Z]{3}$' }
        billing_interval: { type: string, enum: [month, year], nullable: true }
        credits: { type: integer, format: int64, minimum: 1, maximum: 1000000000000, nullable: true }
        rate_limit_per_minute: { type: integer, minimum: 1, maximum: 200 }
        rate_limit_per_day:
          { type: integer, format: int64, minimum: 1, maximum: 1000000000000 }
        rate_limit_per_month:
          { type: integer, format: int64, minimum: 1, maximum: 1000000000000 }
        features:
          type: array
          minItems: 1
          maxItems: 32
          uniqueItems: true
          items: { type: string, minLength: 1, maxLength: 256 }
        cta_kind: { type: string, enum: [steam_signup, support] }
        cta_label: { type: string, minLength: 1, maxLength: 128 }
        featured: { type: boolean }
        sort_order: { type: integer, minimum: 0, maximum: 1000000 }
        status: { type: string, enum: [draft, published, archived] }
        discount_percent:
          type: integer
          minimum: 1
          maximum: 100
          nullable: true
          description: Optional percentage discount. Omit both discount fields for the legacy no-discount create contract; send both null to explicitly configure no discount.
        discount_expires_at:
          type: string
          format: date-time
          nullable: true
          description: Optional finite RFC3339 expiration instant paired with discount_percent.
      allOf:
        - $ref: '#/components/schemas/PlanPricingInvariant'
        - $ref: '#/components/schemas/PlanDiscountRequestInvariant'
    UpdatePlanRequest:
      type: object
      additionalProperties: false
      required:
        - version
        - name
        - eyebrow
        - description
        - price_minor
        - currency
        - billing_interval
        - credits
        - rate_limit_per_minute
        - rate_limit_per_day
        - rate_limit_per_month
        - features
        - cta_kind
        - cta_label
        - featured
        - sort_order
        - status
      properties:
        version: { type: integer, format: int64, minimum: 1 }
        name: { type: string, minLength: 1, maxLength: 128 }
        eyebrow: { type: string, minLength: 1, maxLength: 128 }
        description: { type: string, minLength: 1, maxLength: 1024 }
        price_minor: { type: integer, format: int64, minimum: 0, maximum: 1000000000000, nullable: true }
        currency: { type: string, maxLength: 3, pattern: '^$|^[A-Z]{3}$' }
        billing_interval: { type: string, enum: [month, year], nullable: true }
        credits: { type: integer, format: int64, minimum: 1, maximum: 1000000000000, nullable: true }
        rate_limit_per_minute: { type: integer, minimum: 1, maximum: 200 }
        rate_limit_per_day:
          { type: integer, format: int64, minimum: 1, maximum: 1000000000000 }
        rate_limit_per_month:
          { type: integer, format: int64, minimum: 1, maximum: 1000000000000 }
        features:
          type: array
          minItems: 1
          maxItems: 32
          uniqueItems: true
          items: { type: string, minLength: 1, maxLength: 256 }
        cta_kind: { type: string, enum: [steam_signup, support] }
        cta_label: { type: string, minLength: 1, maxLength: 128 }
        featured: { type: boolean }
        sort_order: { type: integer, minimum: 0, maximum: 1000000 }
        status: { type: string, enum: [draft, published, archived] }
        discount_percent:
          type: integer
          minimum: 1
          maximum: 100
          nullable: true
          description: Optional percentage discount. Omitting both discount fields preserves the stored schedule; sending both null clears it.
        discount_expires_at:
          type: string
          format: date-time
          nullable: true
          description: Optional finite RFC3339 expiration instant paired with discount_percent.
      allOf:
        - $ref: '#/components/schemas/PlanPricingInvariant'
        - $ref: '#/components/schemas/PlanDiscountRequestInvariant'
    PlanPricingInvariant:
      type: object
      required: [price_minor, currency, billing_interval]
      description: |
        Custom pricing uses a null price, an empty currency, and a null billing
        interval. A zero price requires a three-letter currency and a null
        interval. A positive price requires a three-letter currency and a
        month or year interval.
      oneOf:
        - title: Custom pricing
          properties:
            price_minor: { type: integer, nullable: true, enum: [null] }
            currency: { type: string, enum: [''] }
            billing_interval: { type: string, nullable: true, enum: [null] }
        - title: Free plan
          properties:
            price_minor: { type: integer, enum: [0] }
            currency: { type: string, minLength: 3, maxLength: 3, pattern: '^[A-Z]{3}$' }
            billing_interval: { type: string, nullable: true, enum: [null] }
        - title: Recurring paid plan
          properties:
            price_minor: { type: integer, minimum: 1, maximum: 1000000000000 }
            currency: { type: string, minLength: 3, maxLength: 3, pattern: '^[A-Z]{3}$' }
            billing_interval: { type: string, enum: [month, year] }
    PlanDiscountInvariant:
      type: object
      required: [discount_percent, discount_expires_at]
      description: |
        Discount metadata is always returned as a pair. Null values mean no
        discount is configured. A configured percentage is valid only for a
        positive fixed price and applies while the current instant is strictly
        earlier than discount_expires_at; the stored metadata may remain after
        expiry until an administrator changes or clears it. While active,
        clients derive the effective minor-unit price by rounding
        price_minor * (100 - discount_percent) / 100 to the nearest integer,
        with exact halves rounded up; a 100% discount therefore displays zero
        without changing the plan into a free-plan pricing contract.
      oneOf:
        - title: No configured discount
          properties:
            discount_percent: { type: integer, nullable: true, enum: [null] }
            discount_expires_at: { type: string, format: date-time, nullable: true, enum: [null] }
        - title: Configured percentage discount
          properties:
            price_minor: { type: integer, minimum: 1, maximum: 1000000000000 }
            discount_percent: { type: integer, minimum: 1, maximum: 100 }
            discount_expires_at: { type: string, format: date-time }
    PlanDiscountRequestInvariant:
      type: object
      description: |
        The two discount fields form one optional legacy-compatible pair. A
        create request that omits both fields creates no discount. An update
        request that omits both preserves the stored schedule. Sending both as
        null clears the schedule. Unknown fields and a half-present pair are
        invalid.
      oneOf:
        - title: Legacy request without discount fields
          not:
            anyOf:
              - required: [discount_percent]
              - required: [discount_expires_at]
        - title: Explicitly clear discount
          required: [discount_percent, discount_expires_at]
          properties:
            discount_percent: { type: integer, nullable: true, enum: [null] }
            discount_expires_at: { type: string, format: date-time, nullable: true, enum: [null] }
        - title: Configure percentage discount
          required: [price_minor, discount_percent, discount_expires_at]
          properties:
            price_minor: { type: integer, minimum: 1, maximum: 1000000000000 }
            discount_percent: { type: integer, minimum: 1, maximum: 100 }
            discount_expires_at: { type: string, format: date-time }