---
title: "Read a shelf label's price/name/EAN from a photo (SA-0165)"
method: POST
path: "/api/v1/public/catalog/shelf-label/read"
tags: ["Observations"]
---

# Read a shelf label's price/name/EAN from a photo (SA-0165)

`POST /api/v1/public/catalog/shelf-label/read`

## Request

- Base URL: `https://api.shelfatlas.com`
- URL: `https://api.shelfatlas.com/api/v1/public/catalog/shelf-label/read`
- Auth: HTTP bearer

One vision call: uploads the given photo and reads the shelf-edge label's name, price, EAN and stock hint, then returns catalog products matching what was read (the EAN if legible, otherwise a name search) as `candidates`. `image_key` is the uploaded photo's Blob pathname — not yet accepted by `POST /api/v1/observations` (which has no image field), kept for a future observation-with-photo write. An unreadable label is still a 200: `reading.reason` is `unreadable` and `candidates` is empty — it consumed the vision call same as a readable one, it just has nothing to offer. Only an infra failure (Blob or the vision deployment unavailable) is a 503.

## Request body

- ShelfLabelReadRequest
  - `image_base64` string, required: Base64-encoded image bytes (no `data:` URL prefix). Decoded size over 512 KB is a 413 — downscale client-side first.
  - `content_type` 'image/jpeg' | 'image/png' | 'image/webp', required
  - `store_id` string, uuid: Optional (SA-0179): the store the photo was taken in. Lets an item with no product be matched to that store's chain's current campaign offer (`matched_by: offer`).

## Response `200`

The reading, any matching catalog candidates, and the uploaded photo key.

- object
  - `ok` true, required
  - `data` ShelfLabelReadResult, required
    - `reading` ShelfLabelReading, required
      - `name` string, nullable, required
      - `price_cents` integer, nullable, required: DKK×100.
      - `ean` string, nullable, required
      - `confidence` number, required: 0.0–1.0 as judged by the vision model.
      - `reason` 'unreadable' | 'null', nullable, required: `unreadable` when the label was read but not usable; null on a good read.
    - `candidates` ShelfLabelCandidate[], required
      - `ean` string, required
      - `name` string, nullable, required
    - `items` ShelfPhotoReadItem[], required: SA-0179 slice 2 — every item the photo read, alongside the (first-priced-item) `reading`/`candidates` above kept for older clients.
      - `name` string, required
      - `brand` string, nullable, required
      - `size_text` string, nullable, required
      - `quantity_unit` 'ml' | 'g' | 'pcs' | 'null', nullable, required
      - `volume_ml` number, nullable, required: ONE unit size, never the pack total.
      - `unit_count` integer, nullable, required
      - `price_cents` integer, nullable, required: DKK×100. Null when no shelf-edge price label was legible for this item — never invented.
      - `spot` boolean, required: True when the shelf label is marked as a spot/campaign price.
      - `printed_unit_price` ShelfPhotoPrintedUnitPrice, required
        - `value` number, required: The label's own printed unit price, e.g. 10.73 for "1 L = 10,73".
        - `per` 'l' | 'kg', required
      - `max_per_customer` integer, nullable, required: A printed purchase cap ("Maks. N stk."), never a pack count.
      - `ean` string, nullable, required
      - `confidence` number, required: 0.0–1.0 as judged by the vision model.
      - `tier` 'sure' | 'check', required: `sure` when ≥2 independent vision calls agreed on this item (pre-selected); `check` otherwise.
      - `known_product` ShelfLabelCandidate, required
        - `ean` string, required
        - `name` string, nullable, required
      - `matched_by` 'ean' | 'auto' | 'offer' | 'null', nullable: How the item was matched (SA-0179): its own EAN, the size-aware product auto-match, or the store's chain's current campaign offer (then `offer` is set and `known_product` is null).
      - `offer` object, nullable
        - `id` string, uuid, required
        - `raw_name` string, required
        - `price_cents` integer, required
      - `candidates` ShelfLabelCandidate[], required
        - `ean` string, required
        - `name` string, nullable, required
    - `image_key` string, required: The uploaded photo's Blob pathname — not yet accepted by `POST /api/v1/observations` (no image field there today); kept for a future observation-with-photo write.

## Other responses

- `400`: Invalid parameters (`invalid_params`, `incompatible_params`).
- `401`: Missing, invalid, or revoked API key (`missing_api_key` / `invalid_api_key` / `revoked_api_key`).
- `413`: Decoded image exceeds the 512 KB cap (`payload_too_large`).
- `429`: Rate limit or monthly limit reached (`rate_limited` / `monthly_limit_reached`).
- `503`: Temporary backend failure (`service_unavailable`).

---

[API](https://skmtc.dev/shelfatlas/apis/shelfatlas-catalog-api.md) · [All operations](https://skmtc.dev/shelfatlas/apis/shelfatlas-catalog-api/llms.txt) · [OpenAPI document](https://skmtc.dev/shelfatlas/apis/shelfatlas-catalog-api/revisions/943a8773856b?raw)
