> ## Documentation Index
> Fetch the complete documentation index at: https://help.privy.com/llms.txt
> Use this file to discover all available pages before exploring further.

# List top products

> Retrieve your store's top products ranked by views or sales over a
30, 60, or 90 day window, or all-time where the platform supports it.

Entries are the connected store platform's product ids (e.g. the
numeric Shopify product id) ranked best-first. Each entry carries the
product `title`, resolved live from the connected store; it is `null`
when the store cannot supply it (no connected store, the product was
deleted, or the store API was unreachable). Full product details are
at `GET /v1/products/{id}`. Every response repeats this in an in-band
`note`.

Rankings are served from a daily-refreshed cache. `refreshed_at` is
the cache timestamp; `null` means this metric/window combination has
never been computed (as opposed to computed and empty) — the request
itself enqueues the background refresh that fills it, so retry after
a short wait.

Platform support differs by metric, and for `views` by window.
Windowed view rankings (`window_days` 30, 60, or 90) require a
connected Shopify store: dated product views are only tracked through
Shopify's Web Pixel, so those windows return an empty ranking on every
other platform. All-time view rankings (`window_days=0`) come from the
store platform's own view counter and are available for BigCommerce
only — BigCommerce exposes a per-product view count with no date
dimension, so it can be ranked all-time but never windowed. Shopify
has no such counter, so `window_days=0` returns an empty ranking for a
Shopify-only store; a store connected to both platforms is ranked from
its BigCommerce counter.

The all-time ranking is built by scanning the BigCommerce catalog, and
that scan is capped at 25,000 products. On a larger catalog it covers
the products scanned rather than the whole catalog, so treat it as
approximate there.

`sales` is derived from synced order data and works for any store that
syncs orders to Privy (Shopify, BigCommerce, Wix, and orders submitted
through the API). There is no all-time sales bucket, so
`metric=sales` with `window_days=0` is rejected.

**Required scope:** `reports_read`




## OpenAPI

````yaml openapi/privy-api.yaml GET /reports/top_products
openapi: 3.1.0
info:
  title: Privy API
  version: '1.0'
  description: >
    The Privy API lets you programmatically manage your contact list and read
    what you have set up in Privy.

    Create, update, unsubscribe, and remove contacts; report orders;

    and read your campaigns, flows, segments, orders, store products, coupon
    pools,

    performance reports, and account setup.
servers:
  - url: https://api.privy.com/v1
    description: Production
security:
  - bearerAuth: []
tags:
  - name: Account
    description: Retrieve information about the authenticated Privy account.
  - name: Contacts
    description: Manage your contact list.
  - name: Imports
    description: >-
      Read your contact import history — CSV uploads, connected-provider syncs,
      and SMS consent imports.
  - name: Campaigns
    description: >-
      Read your message campaigns — email and SMS newsletters, plus the messages
      your flows send.
  - name: Displays
    description: >-
      Read your on-site displays (popups, flyouts, banners, embedded forms,
      landing pages).
  - name: Coupons
    description: Read your coupon pools, their code inventory, and individual codes.
  - name: Reports
    description: >-
      Read performance reporting — signups, views, conversion rates, and
      attributed revenue — for your business, displays, and flows.
  - name: Flows
    description: Read your automation flows and their configuration.
  - name: Segments
    description: Read your customer segments and their membership.
  - name: Orders
    description: Report orders placed in your store and read your order history.
  - name: Products
    description: Read the product catalog and collections of your connected store.
  - name: Events
    description: Ingest custom events to trigger Flows and other automations.
paths:
  /reports/top_products:
    get:
      tags:
        - Reports
      summary: List top products
      description: |
        Retrieve your store's top products ranked by views or sales over a
        30, 60, or 90 day window, or all-time where the platform supports it.

        Entries are the connected store platform's product ids (e.g. the
        numeric Shopify product id) ranked best-first. Each entry carries the
        product `title`, resolved live from the connected store; it is `null`
        when the store cannot supply it (no connected store, the product was
        deleted, or the store API was unreachable). Full product details are
        at `GET /v1/products/{id}`. Every response repeats this in an in-band
        `note`.

        Rankings are served from a daily-refreshed cache. `refreshed_at` is
        the cache timestamp; `null` means this metric/window combination has
        never been computed (as opposed to computed and empty) — the request
        itself enqueues the background refresh that fills it, so retry after
        a short wait.

        Platform support differs by metric, and for `views` by window.
        Windowed view rankings (`window_days` 30, 60, or 90) require a
        connected Shopify store: dated product views are only tracked through
        Shopify's Web Pixel, so those windows return an empty ranking on every
        other platform. All-time view rankings (`window_days=0`) come from the
        store platform's own view counter and are available for BigCommerce
        only — BigCommerce exposes a per-product view count with no date
        dimension, so it can be ranked all-time but never windowed. Shopify
        has no such counter, so `window_days=0` returns an empty ranking for a
        Shopify-only store; a store connected to both platforms is ranked from
        its BigCommerce counter.

        The all-time ranking is built by scanning the BigCommerce catalog, and
        that scan is capped at 25,000 products. On a larger catalog it covers
        the products scanned rather than the whole catalog, so treat it as
        approximate there.

        `sales` is derived from synced order data and works for any store that
        syncs orders to Privy (Shopify, BigCommerce, Wix, and orders submitted
        through the API). There is no all-time sales bucket, so
        `metric=sales` with `window_days=0` is rejected.

        **Required scope:** `reports_read`
      operationId: listTopProducts
      parameters:
        - $ref: '#/components/parameters/AccountIdParam'
        - name: metric
          in: query
          required: false
          description: >-
            Ranking metric. `views` availability depends on the store's platform
            and the requested window (see the operation description); `sales`
            works for any store that syncs orders to Privy.
          schema:
            type: string
            enum:
              - views
              - sales
            default: sales
        - name: window_days
          in: query
          required: false
          description: >-
            Ranking window in days. `0` means all-time, which is supported for
            `metric=views` on BigCommerce stores only and is rejected for
            `metric=sales`.
          schema:
            type: integer
            enum:
              - 0
              - 30
              - 60
              - 90
            default: 30
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PerPageParam'
      responses:
        '200':
          description: The ranked product ids for the requested metric and window.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        rank:
                          type: integer
                          description: 1-based rank, best first.
                        product_id:
                          type: string
                          description: >-
                            The connected store platform's product id (e.g. the
                            numeric Shopify product id).
                        title:
                          type:
                            - string
                            - 'null'
                          description: >-
                            Product title resolved live from the connected
                            store; `null` when the store cannot supply it.
                  metric:
                    type: string
                    enum:
                      - views
                      - sales
                  window_days:
                    type: integer
                    enum:
                      - 0
                      - 30
                      - 60
                      - 90
                    description: The window this ranking covers. `0` means all-time.
                  refreshed_at:
                    type:
                      - string
                      - 'null'
                    format: date-time
                    description: >-
                      When this ranking was last computed. `null` if it has
                      never been computed. For the rolling windows the request
                      enqueues the refresh that fills it. The all-time views
                      ranking is computed only for BigCommerce stores that have
                      configured a most-viewed dynamic product block, so `null`
                      there means either the store's platform produces no
                      all-time view counts or no such block exists yet.
                  note:
                    type: string
                    description: >-
                      Fixed caveat that entries are store product ids with
                      live-resolved titles, and full details live at GET
                      /v1/products/{id}.
                  pagination:
                    $ref: '#/components/schemas/PaginationEnvelope'
              example:
                data:
                  - rank: 1
                    product_id: '812345'
                    title: Blue Tee
                  - rank: 2
                    product_id: '819921'
                    title: Beach Towel
                metric: views
                window_days: 30
                refreshed_at: '2026-07-29T06:10:00Z'
                note: >-
                  Entries are the connected store platform's product ids (e.g.
                  the numeric Shopify product id) ranked best-first. Titles are
                  resolved live from the connected store and are null when it
                  cannot supply them; full product details are at GET
                  /v1/products/{product_id}.
                pagination:
                  page: 1
                  per_page: 40
                  total_count: 2
                  total_pages: 1
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/InsufficientScope'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/RateLimited'
      security:
        - bearerAuth: []
components:
  parameters:
    AccountIdParam:
      name: Privy-Account-Id
      in: header
      required: false
      description: >
        Target a subaccount instead of the main connected account.

        Pass a subaccount `id` from get_account's `subaccounts` list. Only
        direct subaccounts

        of the connected account are reachable — omit this for the main account
        itself.
      schema:
        type: string
      example: S1U2B3A4C5C6T7
    PageParam:
      name: page
      in: query
      description: Page number (starts at 1).
      schema:
        type: integer
        default: 1
        minimum: 1
      example: 1
    PerPageParam:
      name: per_page
      in: query
      description: >-
        Number of results per page. Values outside 10–100 are clamped to the
        nearest bound.
      schema:
        type: integer
        default: 40
        minimum: 10
        maximum: 100
      example: 40
  schemas:
    PaginationEnvelope:
      type: object
      properties:
        page:
          type: integer
          example: 1
        per_page:
          type: integer
          example: 25
        total_count:
          type: integer
          example: 142
        total_pages:
          type: integer
          example: 6
    ErrorResponse:
      type: object
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              enum:
                - invalid_client
                - invalid_scope
                - unauthorized
                - insufficient_scope
                - paid_plan_required
                - api_access_required
                - not_found
                - no_connected_store
                - conflict
                - contacts_not_ready
                - validation_failed
                - rate_limited
                - store_unavailable
              example: validation_failed
            message:
              type: string
              example: One or more fields are invalid
            details:
              type: array
              items:
                type: object
                properties:
                  field:
                    type: string
                    example: email
                  message:
                    type: string
                    example: is required
  responses:
    Unauthorized:
      description: Missing or invalid bearer token.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: unauthorized
              message: Bearer token is missing or invalid
    InsufficientScope:
      description: Token lacks the required scope for this endpoint.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: insufficient_scope
              message: Token does not have the required scope
    ValidationFailed:
      description: One or more fields failed validation.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: validation_failed
              message: One or more fields are invalid
              details:
                - field: email
                  message: is required
    RateLimited:
      description: Rate limit exceeded. Retry after the specified time.
      headers:
        Retry-After:
          description: Seconds to wait before retrying.
          schema:
            type: integer
          example: 42
        X-RateLimit-Limit-Minute:
          description: Maximum requests allowed per minute.
          schema:
            type: integer
          example: 60
        X-RateLimit-Remaining-Minute:
          description: Requests remaining in the current minute window.
          schema:
            type: integer
          example: 0
        X-RateLimit-Reset-Minute:
          description: Unix timestamp when the minute window resets.
          schema:
            type: integer
          example: 1711929600
        X-RateLimit-Limit-Day:
          description: Maximum requests allowed per day.
          schema:
            type: integer
          example: 10000
        X-RateLimit-Remaining-Day:
          description: Requests remaining in the current day window.
          schema:
            type: integer
          example: 9500
        X-RateLimit-Reset-Day:
          description: Unix timestamp when the day window resets.
          schema:
            type: integer
          example: 1712016000
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: rate_limited
              message: Rate limit exceeded
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API token or OAuth access token
      description: >
        Send either an API token or an OAuth access token as

        `Authorization: Bearer <token>`. See the Authentication page for
        details.

````