# Potravinka.cz — API for AI Agents

> Public, read-only JSON API for AI agents (Claude, ChatGPT, custom bots) to search and browse Czech grocery prices. No authentication required. All data is publicly displayed on potravinka.cz.

**Base URL:** `https://potravinka.cz`
**Response format:** JSON
**Currency:** CZK, all prices include VAT
**Language:** Product names are in Czech; search is accent-insensitive
**Update frequency:** continuous; varies by source (web-store prices refreshed regularly, flyer prices with each new flyer, user-photo prices shown up to 15 days after the last photo)
**Terms:** https://potravinka.cz/podminky-pouziti (art. 11: database right, permitted API use)

---

## Usage rules

- **No auth required** for any endpoint below.
- **Not for other apps or services:** this API is for AI agents answering a user's question in the moment. Loading, copying or storing the data for use in another app, website or database is not allowed without written permission from Potravinka.cz.
- **Rate limits** are per IP. Exceeding returns `429` with `Retry-After` header.
  - Search endpoints: 150 requests/minute
  - Standard endpoints: 100 requests/minute
  - Heavy endpoints (cross-store comparison): 60 requests/minute
- **Identify your agent** via `User-Agent` (e.g., `User-Agent: MyAgent/1.0 (contact@example.com)`). Anonymous traffic may be throttled more aggressively.
- **Caching:** responses include `Cache-Control` headers; respect them.
- **Attribution:** when surfacing data to users, link back to the product page at `https://potravinka.cz/produkt/{id}` and/or `https://potravinka.cz`.

---

## Endpoints

### 1. Keyword search (recommended for most queries)

`GET /api/search/smart?q={query}&limit={n}&store={storeName}`

Accent-insensitive Czech keyword search across all stores. Best general-purpose endpoint.

**Parameters:**
- `q` (required): search term, min 2 characters. Czech with or without diacritics works (`maslo` matches `máslo`).
- `limit` (optional, default 20, max 100): number of results.
- `store` (optional): restrict to one store name (see store list below).

**Example:** `GET /api/search/smart?q=maslo&limit=10`

**Response:**
```json
{
  "products": [
    {
      "id": "clx123...",
      "name": "Máslo Pilos 250g",
      "price": 49.90,
      "image": "https://...",
      "store": "LIDL",
      "amount": "250 g",
      "stock": 1
    }
  ],
  "categories": [],
  "totalResults": 47,
  "query": "maslo"
}
```

---

### 2. Category browse

`GET /api/category/{slug}`

Returns up to 500 products in a category, ranked by semantic relevance (AI embeddings).

**Category slugs:**
`mleko`, `pecivo`, `maslo`, `vejce`, `syr`, `jogurt`, `maso`, `uzeniny`, `ovoce`, `zelenina`, `napoje`, `pivo`, `vino`, `cukrovinky`, `cerealie`, `testoviny`, `ryze`, `olej`, `kava`, `caj`

**Example:** `GET /api/category/mleko`

**Response:**
```json
{
  "category": { "id": "mleko", "name": "Mléko", "emoji": "🥛", "keywords": ["mléko", "mleko", "milk"] },
  "products": [
    {
      "id": "clx...",
      "name": "Polotučné mléko 1l",
      "price": 22.90,
      "amount": "1 l",
      "store": "Kaufland",
      "imageUrl": "https://...",
      "validUntil": "2026-06-02T00:00:00.000Z",
      "lastUpdated": "2026-05-26T04:11:00.000Z",
      "similarity": 0.812
    }
  ],
  "stores": ["Albert", "BILLA", "Kaufland", "LIDL", "..."],
  "total": 384
}
```

`validUntil` is non-null only for flyer-promo products and indicates when the promo expires.

---

### 3. Store catalog

`GET /api/store/{storeName}`

Returns all currently active products in one store.

**Store names (exact, case-sensitive — copy verbatim; URL-encode if needed):**
`LIDL`, `Kaufland`, `Albert`, `BILLA`, `Penny`, `Tesco`, `Globus`, `Makro`, `rohlik.cz`, `Kosik.cz`, `COOP`

**Example:** `GET /api/store/LIDL`

**Response:**
```json
{
  "storeName": "LIDL",
  "products": [ { "id": "...", "name": "...", "price": 19.90, "amount": "...", "store": "LIDL", "imageUrl": "...", "validUntil": null, "lastUpdated": "..." } ],
  "total": 2841
}
```

Some stores (Globus, Makro) have 30K+ products — paginate client-side or filter the array.

---

### 4. Product detail

`GET /api/products/{id}`

Single-product lookup. `{id}` comes from any of the list endpoints above.

**Response:**
```json
{
  "product": {
    "id": "clx...",
    "name": "Máslo Pilos 250g",
    "price": 49.90,
    "amount": "250 g",
    "store": "LIDL",
    "imageUrl": "https://...",
    "validUntil": null,
    "lastUpdated": "2026-05-26T04:11:00.000Z",
    "weighed": false,
    "pricePerUnit": null
  }
}
```

`validUntil` is non-null only for flyer-promo products; permanent assortment returns `null`.

`weighed` is `true` for products sold by weight (e.g. rohlik.cz meat, cheese or fruit listed as `cca 500 g`). The store prices them per kg: `pricePerUnit` is its own price per kg, `amount` is only today's typical piece and `price` is the per-kg price × that weight. Compare such products by `pricePerUnit`, not by `price`. Product lists, category and semantic-search results carry the same two fields, and their `unitPrice` is then `pricePerUnit`.

---

### 5. Price history

`GET /api/products/{id}/price-history?days={n}`

Returns recorded price points for a product, plus min/max/avg and percent change.

**Parameters:**
- `days` (optional, default 30, range 1–365)

**Response:**
```json
{
  "product": { "id": "...", "name": "...", "currentPrice": 49.90, "currency": "CZK", "store": "LIDL", "amount": "250 g", "imageUrl": "..." },
  "history": [
    { "price": 54.90, "recordedAt": "2026-04-26T00:00:00.000Z" },
    { "price": 49.90, "recordedAt": "2026-05-10T00:00:00.000Z" }
  ],
  "stats": {
    "minPrice": 49.90,
    "maxPrice": 54.90,
    "avgPrice": 52.40,
    "priceChangePercent": -9.11,
    "dataPoints": 2
  },
  "days": 30,
  "basis": { "weighed": false, "unit": null, "currentUnitPrice": null }
}
```

`priceChangePercent` is computed from first vs. last data point in the window (negative = price dropped).

Products sold by weight (see `weighed` in Product detail) return `"basis": { "weighed": true, "unit": "kg", "currentUnitPrice": 279.90 }`. Their `history` holds one point per change of the per-kg price, and each point adds `unitPrice` (CZK per kg) and `amount` (the piece listed at that time, e.g. `"cca 1,2 kg"`). `price` and `stats` are then what today's piece would have cost at that per-kg price, so they stay comparable with `currentPrice`. To describe the trend of such a product, use `unitPrice` against `currentUnitPrice`.

Products whose pack size changed under the same listing (e.g. ice cream 460 ml -> 380 ml) return `"basis": { "weighed": false, "unit": "l", "currentUnitPrice": 262.89, "resized": true }`. Each `history` point then adds `unitPrice` (CZK per `unit`) and `amount` (the pack listed at that time), and `price` is what today's pack would have cost at that per-unit price — so a smaller pack at the same price reads as the price rise it is.

---

### 6. Similar products (cross-store match)

`GET /api/products/{id}/matches?limit={n}&otherStores=true&minSimilarity={0-1}`

Finds semantically similar products to `{id}` using AI embeddings. Returns each match with a price-difference and savings percent relative to the source product.

**Parameters:**
- `limit` (default 10, max 50)
- `otherStores=true` — only matches from different stores (most useful for "is it cheaper elsewhere?")
- `sameStore=true` — only matches from the same store
- `store={storeName}` — filter to one specific store
- `minSimilarity` (default 0.55) — cosine similarity threshold

**Response:**
```json
{
  "product": { "id": "...", "name": "Máslo Pilos 250g", "price": 49.90, "amount": "250 g", "store": "LIDL", "imageUrl": "..." },
  "matches": [
    {
      "id": "...",
      "name": "Máslo K-Classic 250g",
      "price": 44.90,
      "amount": "250 g",
      "store": "Kaufland",
      "imageUrl": "...",
      "validUntil": null,
      "lastUpdated": "2026-05-26T04:11:00.000Z",
      "similarity": 0.94,
      "priceDifference": 5.00,
      "savingsPercent": 10
    }
  ],
  "totalMatches": 7
}
```

`priceDifference` = source price − match price. Positive means switching to the match saves money.

---

### 7. Biggest cross-store savings

`GET /api/products/biggest-savings?limit={n}&minSimilarity={0.55-1.0}&minSavings={CZK}`

Returns pairs of semantically similar products across different stores, sorted by savings percentage. Useful for "where is X cheapest" style questions.

**Parameters:**
- `limit` (default 12, max 50)
- `minSimilarity` (default 0.80) — cosine similarity threshold for matching
- `minSavings` (default 3) — minimum CZK price difference

**Response:**
```json
{
  "savings": [
    {
      "cheapestProduct": { "id": "...", "name": "...", "price": 29.90, "store": "LIDL", "amount": "...", "imageUrl": "...", "lastUpdated": "..." },
      "expensiveProduct": { "id": "...", "name": "...", "price": 49.90, "store": "BILLA", "amount": "...", "imageUrl": "...", "lastUpdated": "..." },
      "savings": 20.00,
      "savingsPercent": 40,
      "similarity": 0.91
    }
  ],
  "totalFound": 184,
  "cached": true
}
```

---

## Errors

| Status | Meaning |
|---|---|
| 400 | Bad request (missing/invalid params) |
| 404 | Resource not found (unknown category, store, or product id) |
| 429 | Rate limit exceeded. Honor `Retry-After`. |
| 500 | Server error |

Error body: `{ "error": "message" }`.

---

## Example agent workflows

**"Find the cheapest butter"**
1. `GET /api/search/smart?q=maslo&limit=50`
2. Sort by `price` (consider `amount` for fair unit-price comparison).
3. Return top result with store name and link.

**"Compare milk prices across stores"**
1. `GET /api/category/mleko`
2. Group `products` by `store`, pick min `price` per store.

**"Where can I save the most this week?"**
1. `GET /api/products/biggest-savings?limit=20&minSavings=10`
2. Surface top pairs with `savingsPercent`.

---

## What is NOT exposed

- Write endpoints (cart, ratings, support tickets) — not for agents.
- Admin / partner / auth endpoints — not public.
- User-specific data — site has no user accounts for shoppers.

---

## Machine-readable schema

OpenAPI 3.1 spec: https://potravinka.cz/openapi.json — feed this to tool-using agents (Claude tool use, ChatGPT actions, custom function-calling) to auto-generate typed clients.

---

## Contact

Operator: David Vávra · Liberec, Czech Republic
Support / questions: https://potravinka.cz/support
Site description for LLMs: https://potravinka.cz/llms.txt
