shelfatlas-com Products API

The Products API from shelfatlas-com — 3 operation(s) for products.

Operations 3

GET /api/v1/public/catalog/products List products #
GET /api/v1/public/catalog/products/{id} Get a product by id or EAN/GTIN #
GET /api/v1/public/catalog/products/{id}/offers List offers matched to a product #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/shelfatlas-com:shelfatlas-com-products-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Discovery needs no key. Ratings and market analysis are Pro.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

shelfatlas-com-products-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: ShelfAtlas Catalog Products API
  version: 1.0.0
  description: 'Read access to normalised Danish retail catalog data — products, offers, stores, and chains. Authenticate with a Bearer `sa_live_` API key. This document is the contract the official ShelfAtlas SDKs conform to. Data coverage: the offer feed covers every major Danish chain, but the curated product catalog is smaller and actively expanding — an empty data array from /products/{id}/offers or /price-history means no offers are linked to that product yet, not an error.'
  termsOfService: https://shelfatlas.com/legal/terms
servers:
- url: https://api.shelfatlas.com
  description: Production
security:
- bearerAuth: []
tags:
- name: Products
paths:
  /api/v1/public/catalog/products:
    get:
      operationId: listProducts
      summary: List products
      tags:
      - Products
      parameters:
      - name: chain_slug
        in: query
        required: false
        schema:
          type: string
          pattern: ^[a-z0-9-]+$
          minLength: 1
          maxLength: 64
        description: Restrict to a single chain by slug.
      - name: brand_id
        in: query
        required: false
        schema:
          type: string
        description: Comma-separated list of UUIDs (1–100). Match products by brand.
      - name: brand_slug
        in: query
        required: false
        schema:
          type: string
          pattern: ^[a-z0-9-]+$
          minLength: 1
          maxLength: 64
        description: Restrict to a single brand by slug — the readable alternative to brand_id, which requires an internal UUID.
      - name: category_id
        in: query
        required: false
        schema:
          type: string
        description: Comma-separated list of UUIDs (1–100). Match products by primary category.
      - name: ids
        in: query
        required: false
        schema:
          type: string
        description: Comma-separated list of UUIDs (1–100). Match products by id.
      - name: exclude_ids
        in: query
        required: false
        schema:
          type: string
        description: Comma-separated list of UUIDs (1–100). Exclude these product ids.
      - name: curated
        in: query
        required: false
        schema:
          type: string
          enum:
          - 'true'
          - 'false'
        description: Only curated catalog products, skipping rows auto-created during ingest.
      - name: has_image
        in: query
        required: false
        schema:
          type: string
          enum:
          - 'true'
          - 'false'
        description: Only products that have (true) or lack (false) a product image.
      - name: q
        in: query
        required: false
        schema:
          type: string
          minLength: 2
          maxLength: 64
        description: Case-insensitive substring search on the product name. Minimum two characters.
      - name: updated_since
        in: query
        required: false
        schema:
          type: string
          format: date-time
        description: 'Only products updated at or after this timestamp (filters on updatedAt). Accepts a bare date (`2026-08-21`) or a full ISO 8601 datetime. Built for incremental sync: poll with the newest updatedAt you have already ingested instead of re-fetching the whole catalog on every call. Composes with every other filter and with cursor pagination. A product link made later (productId appearing on an offer you already hold) does not change ingestedAt, so the offer is not returned again: re-fetch with product_id for the products you follow, or do a full chain_slug sync once a week.'
      - name: include_expired
        in: query
        required: false
        schema:
          type: string
          enum:
          - 'true'
          - 'false'
        description: 'Offer lists are live-only by default: only offers whose validTo has not passed (current and upcoming campaigns). Pass `true` to include expired offers as well (price history). Any other value is a 400 invalid_params.'
      - name: cursor
        in: query
        required: false
        schema:
          type:
          - string
          - 'null'
          maxLength: 512
        description: Opaque pagination cursor returned in a previous response.
      - name: limit
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
          maximum: 200
          default: 50
        description: Page size (1–200, default 50).
      responses:
        '200':
          description: A page of products.
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                - data
                additionalProperties: false
                properties:
                  ok:
                    type: boolean
                    enum:
                    - true
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Product'
                  pagination:
                    type: object
                    additionalProperties: false
                    properties:
                      limit:
                        type: integer
                        description: The effective page size.
                      cursor:
                        type:
                        - string
                        - 'null'
                        description: Opaque cursor for the next page; null on the last page.
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/ChainNotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
  /api/v1/public/catalog/products/{id}:
    get:
      operationId: getProduct
      summary: Get a product by id or EAN/GTIN
      description: Accepts either the ShelfAtlas product UUID or a bare EAN/GTIN (8, 13 or 14 digits) — the identifier an external catalog actually holds. Detection is by format; the shapes are disjoint.
      tags:
      - Products
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}|\d{8}|\d{13}|\d{14})$
        description: Product UUID, or EAN/GTIN (8/13/14 digits).
      responses:
        '200':
          description: The product.
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                - data
                additionalProperties: false
                properties:
                  ok:
                    type: boolean
                    enum:
                    - true
                  data:
                    $ref: '#/components/schemas/Product'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
  /api/v1/public/catalog/products/{id}/offers:
    get:
      operationId: listProductOffers
      summary: List offers matched to a product
      description: Only offers matched to the curated product are returned. The catalog is expanding — an empty page means no offers are linked to this product yet; it is not an error and does not mean the product is inactive.
      tags:
      - Products
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: include_expired
        in: query
        required: false
        schema:
          type: string
          enum:
          - 'true'
          - 'false'
        description: 'Offer lists are live-only by default: only offers whose validTo has not passed (current and upcoming campaigns). Pass `true` to include expired offers as well (price history). Any other value is a 400 invalid_params.'
      - name: cursor
        in: query
        required: false
        schema:
          type:
          - string
          - 'null'
          maxLength: 512
        description: Opaque pagination cursor returned in a previous response.
      - name: limit
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
          maximum: 200
          default: 50
        description: Page size (1–200, default 50).
      responses:
        '200':
          description: A page of offers for the product.
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                - data
                additionalProperties: false
                properties:
                  ok:
                    type: boolean
                    enum:
                    - true
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Offer'
                  pagination:
                    type: object
                    additionalProperties: false
                    properties:
                      limit:
                        type: integer
                        description: The effective page size.
                      cursor:
                        type:
                        - string
                        - 'null'
                        description: Opaque cursor for the next page; null on the last page.
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
components:
  responses:
    RateLimited:
      description: Rate limit or monthly limit reached (`rate_limited` / `monthly_limit_reached`).
      headers:
        Retry-After:
          description: Seconds to wait before retrying (present on `rate_limited`).
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: Resource not found (`not_found`).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    ServiceUnavailable:
      description: Temporary backend failure (`service_unavailable`).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    BadRequest:
      description: Invalid parameters (`invalid_params`, `incompatible_params`).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: Missing, invalid, or revoked API key (`missing_api_key` / `invalid_api_key` / `revoked_api_key`).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    ChainNotFound:
      description: The requested chain_slug does not exist (`chain_not_found`).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  schemas:
    Error:
      type: object
      additionalProperties: true
      required:
      - error
      properties:
        error:
          type: string
          description: Machine-readable error code.
        message:
          type: string
        detail:
          type: string
        retry_after_sec:
          type: integer
          description: Seconds to wait before retrying (429).
        plan:
          type: string
          description: Caller's resolved plan id (present on monthly_limit_reached).
        limit:
          type: integer
          description: The plan monthly request quota (present on monthly_limit_reached).
        used:
          type: integer
          description: Requests already spent this UTC month (present on monthly_limit_reached).
        resets_at:
          type: string
          format: date-time
          description: When the quota resets — next UTC month start (present on monthly_limit_reached).
        next_plan:
          type: object
          nullable: true
          description: The cheapest plan with a larger quota, or null at the top of the ladder.
          properties:
            id:
              type: string
            name:
              type: string
            monthly_price_dkk:
              type: integer
            monthly_request_quota:
              type: integer
        plans_url:
          type: string
          format: uri
          description: Public pricing page — no sign-in required.
        upgrade_url:
          type: string
          format: uri
    Product:
      type: object
      additionalProperties: false
      required:
      - id
      properties:
        id:
          type: string
          format: uuid
        canonicalName:
          type:
          - string
          - 'null'
        variant:
          type:
          - string
          - 'null'
        brandId:
          type:
          - string
          - 'null'
          format: uuid
        primaryCategoryId:
          type:
          - string
          - 'null'
          format: uuid
        imageUrl:
          type:
          - string
          - 'null'
        ean:
          type:
          - string
          - 'null'
          description: 'EAN/GTIN barcode, when known. Currently always null on public API responses — every EAN we hold today comes from a source that is not part of the public catalog API (see priceKind''s description), and no other chain''s feed carries a barcode to attach either. Never treat this as a stable identifier: `id` is the one guaranteed-present, guaranteed-stable key for a product, and GET /products/{id} also accepts a bare EAN/GTIN as a convenience lookup when you do have one from your own data.'
        unitCount:
          type: integer
        volumeMl:
          type:
          - integer
          - 'null'
        isActive:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        packageType:
          type:
          - string
          - 'null'
          description: '''can'' | ''bottle'' | ''carton'' | ''pouch'' | ''box'' | ''jar'' | ''other'''
        packageCount:
          type:
          - integer
          - 'null'
        packageSizeValue:
          type:
          - string
          - 'null'
          description: Decimal string (Postgres numeric).
        packageSizeUnit:
          type:
          - string
          - 'null'
          description: '''ml'' | ''cl'' | ''dl'' | ''l'' | ''g'' | ''kg'' | ''stk'''
    Offer:
      type: object
      additionalProperties: false
      required:
      - id
      properties:
        id:
          type: string
          format: uuid
        chainId:
          type:
          - string
          - 'null'
          format: uuid
        storeId:
          type:
          - string
          - 'null'
          format: uuid
        productId:
          type:
          - string
          - 'null'
          format: uuid
          description: The offer's best link to a curated product (highest-confidence match), or null when the offer is not linked yet. The per-chain linking rate is published on /docs/app-developers; a linked product carries ean, brand and package size via GET /products/{id}.
        rawName:
          type: string
        itemKey:
          type:
          - string
          - 'null'
          description: 'The offer''s stable item identity: a deterministic normalised name+size key, the same across weeks and chains for the same item. It is what /offers/search groups by (k:<item_key>) and what /offers/history?item_key= takes. Opaque — compare, never parse.'
        chainProductRef:
          type:
          - string
          - 'null'
          description: The chain's own product number, source-qualified (e.g. "rema1000-catalog:61534"), when the offer is linked to the chain's catalog; null otherwise. When set, imageUrl is the chain's own packshot and originalPrice its normal price.
        price:
          type: string
          description: Decimal string (Postgres numeric).
        originalPrice:
          type:
          - string
          - 'null'
        unitPrice:
          type:
          - string
          - 'null'
        unitPriceUnit:
          type:
          - string
          - 'null'
          enum:
          - l
          - kg
          - stk
          - null
          description: 'Unit of `unitPrice`: per litre, per kilo or per piece. Null when unknown.'
        currency:
          type: string
          minLength: 3
          maxLength: 3
        validFrom:
          type: string
          format: date-time
        validTo:
          type: string
          format: date-time
        ingestedAt:
          type: string
          format: date-time
          description: When this offer was last actually observed by ingest — the freshness signal for "how current is this price".
        imageUrl:
          type:
          - string
          - 'null'
        volumeMl:
          type:
          - integer
          - 'null'
        unitCount:
          type: integer
        priceKind:
          type: string
          enum:
          - campaign
          - regular
          description: 'What kind of price this is: ''campaign'' — a published offer (leaflet or a chain month-campaign); ''regular'' — the everyday shelf price observed in-store. Public API responses today only ever contain ''campaign'' rows — everyday shelf prices are not part of the public catalog API. (Both kinds are real, currently-valid prices where they do appear; this field labels them, it doesn''t filter within a response.)'
        cropUrl:
          type:
          - string
          - 'null'
          description: Our own verified crop of this offer's leaflet tile — an image of exactly this offer. Present only on chain-scoped lists (chain_slug), null where the chain's crops are not published.
        matchedProductIds:
          type: array
          items:
            type: string
            format: uuid
          description: Which of the query's products this offer matched. Present only on product-scoped offer queries (product_id / product_ids / /products/:id/offers). Use this to attribute offers to the products you asked for; productId is the offer's single best link and may name a product outside the query.
        previousPrice:
          type:
          - string
          - 'null'
          description: 'SA-0186: what the same item cost in the same chain in its previous catalogue (the newest campaign offer with the same item key whose validity ended within the 21 days before this offer''s validFrom). Decimal string like `price`; null when unknown — no item key, a key without a pack size, a regular shelf price, or no such offer. Present on GET /offers and /offers/price-drops.'
        isNewThisWeek:
          type: boolean
          description: 'SA-0186: true when this campaign offer started within the last 7 days and the chain had no offer for the same item (any price kind) in the 21 days before it. False when unknown. Present on GET /offers and /offers/price-drops.'
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: 'A ShelfAtlas API key: `Authorization: Bearer sa_live_<key>`.'
    deviceTokenAuth:
      type: apiKey
      in: header
      name: X-Device-Token
      description: 'Anonymous per-device identity for POST /api/v1/observations: a UUID the client generates and keeps locally. Any UUID version is accepted — the check is purely the 8-4-4-4-12 hex-digit shape.'