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

# List products

> Lists catalog entries **org-wide**, newest first - filter by `location_id` to see one location's catalog. Scope: `products:read`.

Catalogs are per-location because each location can run a different POS with a different menu; the org view unifies them. Cross-location analytics group by `sku`.

See [Pagination](/api-reference/pagination).



## OpenAPI

````yaml /openapi.json get /v1/products
openapi: 3.1.0
info:
  title: VEXiON cards Public API
  description: >-
    ## Rate limiting


    Two independent layers, checked in this order:


    1. **Per-IP throttle (pre-authentication).** Applied to every `/v1` request
    BEFORE the API key is checked, keyed on the caller's IP address:
       - **600 requests/minute** per IP - a flood ceiling, `429 rate_limit_error` / `rate_limited`.
       - **20 failed authentications per 15 minutes** per IP - a key-guessing ceiling. Only responses that are `401` (missing/invalid/revoked key) count against this ceiling; a `403` from a valid key that simply lacks a scope does not. `429 rate_limit_error` / `too_many_failed_attempts`.

       Both ceilings send `Retry-After` (seconds).
    2. **Per-key, per-bucket limiter (post-authentication).** Documented per
    endpoint (see each operation's `429` response and the `rate_limit` field on
    the API Key resource) - e.g. 60/min default, 120/min for transactions, plus
    the notification-broadcast and lookup-filter buckets described on their own
    endpoints.


    ### Rate-limit response headers


    Every rate-limited response carries the legacy `X-RateLimit-Limit` /
    `X-RateLimit-Remaining` / `X-RateLimit-Reset` family (the last is an **epoch
    timestamp**) AND the IETF standards-track headers, side by side:


    - `RateLimit-Policy: "<bucket>";q=<quota>;w=<window seconds>` - the static
    policy for the bucket that was hit.

    - `RateLimit: "<bucket>";r=<remaining>;t=<seconds until reset>` - live
    state. **`t` is a DURATION (seconds remaining), not the epoch timestamp
    `X-RateLimit-Reset` carries - the two are never the same value.**
  version: 1.0.0
servers:
  - url: https://api.vexioncards.one
    description: Production
  - url: https://migration.api.vexioncards.one
    description: Sandbox
security:
  - apiKey: []
tags:
  - name: Customers
  - name: Cards
  - name: Transactions
  - name: Notifications
  - name: Locations
  - name: Products
  - name: API keys
  - name: Organization
  - name: Webhook endpoints
  - name: Events
  - name: Custom fields
  - name: Team members
  - name: Icons
paths:
  /v1/products:
    get:
      tags:
        - Products
      summary: List products
      description: >-
        Lists catalog entries **org-wide**, newest first - filter by
        `location_id` to see one location's catalog. Scope: `products:read`.


        Catalogs are per-location because each location can run a different POS
        with a different menu; the org view unifies them. Cross-location
        analytics group by `sku`.


        See [Pagination](/api-reference/pagination).
      operationId: listProducts
      parameters:
        - name: location_id
          in: query
          required: false
          description: One location's catalog.
          schema:
            type: string
            pattern: ^loc_[A-Za-z0-9]+$
          example: loc_01HW9R4P2ZK7N8MTX1RVLOCS01
        - name: sku
          in: query
          required: false
          description: Exact-match correlation key lookup.
          schema:
            type: string
          example: FW-12
        - name: category
          in: query
          required: false
          description: Exact-match category filter.
          schema:
            type: string
        - name: source
          in: query
          required: false
          description: >-
            Filter by origin. Known values: `pos`, `manual`, `api` - treat as an
            open set.
          schema:
            type: string
          example: pos
        - name: is_active
          in: query
          required: false
          description: '`true` = live catalog only; `false` = archived only; omit for both.'
          schema:
            type: boolean
        - name: search
          in: query
          required: false
          description: Free-text search across name + tags. Case-insensitive partial match.
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: Page size, 1-100. Default 25.
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
        - name: starting_after
          in: query
          required: false
          description: >-
            Cursor - the `prd_` id of the last item on the previous page
            (forward). Numeric offset or page parameters are not supported;
            cursors are the only supported paging mechanism.
          schema:
            type: string
            pattern: ^prd_[A-Za-z0-9]+$
        - name: ending_before
          in: query
          required: false
          description: Reverse cursor - items before this `prd_` id.
          schema:
            type: string
            pattern: ^prd_[A-Za-z0-9]+$
      responses:
        '200':
          description: Paginated product list.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductList'
              examples:
                Org-wide catalog:
                  summary: API + POS entries together
                  value:
                    object: list
                    data:
                      - id: prd_01HW9R7T2CD8N8MTX1RVPRDFW1
                        object: product
                        location_id: loc_01HW9R4P2ZK7N8MTX1RVLOCS01
                        name: Flat White
                        sku: FW-12
                        category: drinks
                        tags:
                          - coffee
                          - hot
                        price_minor: 350
                        source: api
                        pos_ref: null
                        is_active: true
                        created_at: 1750000000
                        updated_at: 1750000000
                      - id: prd_01HW9R7T2CD8N8MTX1RVPRDESP
                        object: product
                        location_id: loc_01HW9R4P2ZK7N8MTX1RVLOCS01
                        name: Espresso doppio
                        sku: ESP-D
                        category: drinks
                        tags:
                          - coffee
                          - hot
                        price_minor: 250
                        source: pos
                        pos_ref:
                          external_product_id: p:nazlrMDNh
                        is_active: true
                        created_at: 1750000000
                        updated_at: 1750000000
                    has_more: false
                    url: /v1/products
                Empty catalog:
                  summary: No products yet
                  value:
                    object: list
                    data: []
                    has_more: false
                    url: /v1/products
        '400':
          description: >-
            Invalid pagination parameter: limit outside 1-100, or a malformed
            cursor.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                Invalid limit:
                  summary: limit out of range
                  value:
                    error:
                      type: invalid_request_error
                      code: invalid_limit
                      message: limit must be an integer between 1 and 100.
                      doc_url: >-
                        https://vexioncards.mintlify.site/api-reference/errors#invalid_limit
                      request_id: req_5f8b2c4a9e1d3
                      param: limit
                Malformed cursor:
                  summary: starting_after is not a valid cursor
                  value:
                    error:
                      type: invalid_request_error
                      code: invalid_cursor
                      message: starting_after is not a valid cf_ cursor.
                      doc_url: >-
                        https://vexioncards.mintlify.site/api-reference/errors#invalid_cursor
                      request_id: req_5f8b2c4a9e1d3
                      param: starting_after
          headers: {}
        '401':
          description: Missing, invalid, or revoked API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Key lacks the required scope.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limited - respect Retry-After.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Server error. Retry-safe on idempotent calls.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  type: api_error
                  code: internal_error
                  message: >-
                    An internal error occurred. The request may be retried with
                    the same Idempotency-Key.
                  doc_url: >-
                    https://vexioncards.mintlify.site/api-reference/errors#internal_error
                  request_id: req_5f8b2c4a9e1d3
              examples:
                Http 500 default:
                  summary: Http 500 default
                  value:
                    error:
                      type: api_error
                      code: internal_error
                      message: >-
                        An internal error occurred. The request may be retried
                        with the same Idempotency-Key.
                      doc_url: >-
                        https://vexioncards.mintlify.site/api-reference/errors#internal_error
                      request_id: req_5f8b2c4a9e1d3
          headers: {}
components:
  schemas:
    ProductList:
      type: object
      description: Cursor-paginated product list.
      required:
        - object
        - data
        - has_more
      properties:
        object:
          type: string
          const: list
          description: Always `"list"`.
        data:
          type: array
          items:
            $ref: '#/components/schemas/Product'
          description: Product rows.
        has_more:
          type: boolean
          description: True when more pages exist.
        next_cursor:
          type:
            - string
            - 'null'
          description: >-
            Cursor for the next page **in the direction you are already
            paging**, or `null` when this page ends the collection. `has_more:
            true` always comes with a non-null `next_cursor`.


            Feed it back to the SAME parameter you used: `starting_after` when
            paging forward (the default), or `ending_before` when paging
            backward. Treat it as opaque and do not parse it.


            You no longer need to take the last object's `id` yourself. That
            still works and is unchanged, but deriving the cursor from the page
            put the burden of knowing the sort direction on you, and it broke
            silently on any endpoint whose cursor is not the object id.
          example: cus_iofv5j4hn6bphinmmyat27tc5y
        url:
          type: string
          description: Stable collection URL.
          example: /v1/products
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - type
            - code
            - message
          properties:
            type:
              type: string
              enum:
                - invalid_request_error
                - authentication_error
                - permission_error
                - not_found_error
                - rate_limit_error
                - idempotency_error
                - api_error
            code:
              type: string
            message:
              type: string
            retry_after:
              type: integer
              description: >-
                Seconds to wait before retrying. Present on rate-limit (429)
                errors and on the retryable 409 conflicts
                (`idempotency_key_in_use`, `create_in_progress`,
                `concurrent_update`); mirrors the `Retry-After` HTTP header.
                Absent on error types that carry no retry guidance.
              example: 5
            param:
              type:
                - string
                - 'null'
            request_id:
              type: string
            doc_url:
              type: string
              format: uri
            field_errors:
              type: array
              description: >-
                Per-field errors on a 422. At most 100 entries: a request with
                more errors gets the first 100.
              items:
                type: object
                properties:
                  param:
                    type: string
                  code:
                    type: string
                  message:
                    type: string
                  expected_currency:
                    type: string
                    description: >-
                      On `currency_mismatch`: the business currency the request
                      must use. Absent on every other field error.
          description: >-
            The error payload: type (category), code (machine-readable), message
            (human-readable), retry_after (seconds, on rate-limit errors and
            retryable 409 conflicts only), param (offending field), request_id,
            doc_url, field_errors[].
      description: >-
        Standard error envelope. Branch on `error.code`; include
        `error.request_id` in support requests. See the Errors guide.
    Product:
      type: object
      description: >-
        A catalog entry for correlating transaction line items to real products.
        **Catalogs are per-location** (each location can run a different POS
        with a different menu), unified under the organization: list org-wide,
        filter by `location_id`.


        This is a loyalty correlation catalog, not an inventory system - no
        variants, modifiers, or stock levels.
      required:
        - id
        - object
        - location_id
        - name
        - source
        - is_active
        - created_at
      properties:
        id:
          type: string
          pattern: ^prd_[A-Za-z0-9]+$
          description: >-
            Product identifier, `prd_` prefix. Stable forever - line items
            resolve to it.
          example: prd_01HW9R7T2CD8N8MTX1RVPRDFW1
        object:
          type: string
          const: product
          description: Always `"product"`.
        location_id:
          type: string
          pattern: ^loc_[A-Za-z0-9]+$
          description: >-
            The location this catalog entry belongs to. The same drink sold at
            two locations is two Product records (each mapped to its own POS).
          example: loc_01HW9R4P2ZK7N8MTX1RVLOCS01
        name:
          anyOf:
            - type: string
              title: Single name (all languages)
              minLength: 1
              maxLength: 200
            - type: object
              title: Per-language names
              additionalProperties:
                type: string
                minLength: 1
                maxLength: 200
              minProperties: 1
              description: >-
                Keys are language codes (`sk`, `cs`, `en`, `de`). Resolution:
                customer language → organization default language → any.
          description: >-
            Display name - plain string or per-language object, the same
            localization pattern as `Card.name`. POS-synced entries arrive with
            the till's single-language name; supply per-language names on
            manual/API entries when your audience is multilingual (border
            stores, tourist areas). Notifications and future product-facing
            surfaces resolve the name in the customer's language.
          example: Flat White
        sku:
          type:
            - string
            - 'null'
          minLength: 1
          maxLength: 64
          description: >-
            Your stable key, **unique per location** (two locations may both use
            `FW-12`). This is what transaction line items correlate on.
          example: FW-12
        category:
          type:
            - string
            - 'null'
          minLength: 1
          maxLength: 100
          description: Free-form category for basket analytics.
          example: drinks
        tags:
          type: array
          items:
            type: string
          description: >-
            Merchant-owned filing aid, and part of the `search` haystack
            alongside the name. NEVER populated by POS sync: no POS system sends
            tags, so a `source: "pos"` entry arrives untagged. You can tag one
            yourself with PATCH /v1/products/{id} (tags are an overlay field,
            unlike `pos_ref` and `source`, which sync owns), and it becomes
            findable by that tag immediately. Sending `[]` clears them and
            removes them from the search haystack.
          example:
            - coffee
            - hot
        price_minor:
          type:
            - integer
            - 'null'
          minimum: 0
          description: >-
            Reference price in minor units. Informational - the POS bill is
            authoritative at transaction time.
          example: 350
        source:
          type: string
          description: >-
            Where this catalog entry originated. Known values: `pos` (synced
            from the location's connected POS), `manual` (dashboard), `api`
            (created via POST /v1/products). Server-stamped - treat as an open
            set.
          example: pos
        pos_ref:
          type:
            - object
            - 'null'
          description: >-
            The POS mapping for this location's adapter. **Read-only, owned by
            the sync** - never editable via API. Null for manual/API entries.
            The POS vendor name is never surfaced.
          properties:
            external_product_id:
              type: string
              description: The product id in the location's POS.
              example: p:nazlrMDNh
          example: null
        is_active:
          type: boolean
          description: >-
            False = archived (delisted from the POS or manually retired).
            Archived products stay resolvable on historical line items - there
            is no hard delete.
          example: true
        created_at:
          type: integer
          description: Unix seconds.
          example: 1750000000
        updated_at:
          type: integer
          description: Unix seconds of last change.
          example: 1750000000
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: >-
        Your secret API key as a Bearer token: `Authorization: Bearer
        vxn_sk_...`. Keys are created and scoped in the dashboard - see
        [Authentication](/api-reference/introduction#authentication).

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.