Skip to content

Search

GET /v1/search is the main read endpoint of the MealCP grocery product catalogue API. A single query runs against every indexed retailer in scope and returns normalized product records - name, brand, pack size, latest observed price, unit-normalized price, category, tags, and the retailer’s own product page. You need an API key, and each request costs 1 credit.

  • Price comparison: find the cheapest offer for one product across every chain in a country, with unit_price_* fields making different pack sizes directly comparable.
  • Basket and recipe cost estimates: search each ingredient once, then sum the returned latest_price values.
  • Catalogue enrichment: pull structured names, brands, pack sizes, categories, and tags into your own food or inventory database.
  • AI agents: give an LLM one tool call that answers “what does this cost, where, and in what size?” - one integration instead of per-retailer ones.

Keyword search across the product catalog with faceted filtering. Requires an API key. Queries match retailer-language product names - search what the store prints on the shelf (Finnish for FI, Hungarian for HU, …):

Terminal window
curl -H "X-API-Key: $MEALCP_API_KEY" \
"https://api.mealcp.com/v1/search?q=hapanjuurileip%C3%A4&country=FI&sort=unit_price_asc"

The query above finds sourdough bread in Finland and sorts it by price per kilogram, cheapest first. A few behaviors worth knowing before you build:

  • Matching is against retailer names. MealCP stores the product name as the retailer prints it, so a query should use the local word (tej for milk in Hungary, not milk).
  • All filters combine with AND. Adding country, retailer, brand, category, tag, and price bounds narrows the same query further.
  • An empty q is browse mode. With no query text the endpoint returns the full filtered catalogue, sorted by your chosen sort - useful for category listings or building paginated catalog views.
  • Facets come back with every result, so you can render filter options with counts without extra calls.
Param Type Default Description
q string "" Free-text query; empty returns all (sorted)
country string - ISO 3166-1 alpha-2, e.g. HU
retailer string - Retailer slug
chain string - Chain slug
brand string - Brand name
tag string - Tag slug (see Tags)
category string - Category slug at any level (L1/L2/L3); matches the whole subtree
city string - Store city
currency string - ISO 4217, e.g. HUF
min_price number - Lower price bound
max_price number - Upper price bound
sort string relevance relevance, newest, oldest, price_asc, price_desc, unit_price_asc, unit_price_desc
page int 1 1-based page number
page_size int 24 1–100

Notes on the two sort families:

  • price_* sorts by the shelf price as printed, which only compares fairly when pack sizes match.
  • unit_price_* sorts by normalized price per liter or kilogram, so a 0.5 l carton and a 1 l carton compete on equal terms. Prefer these for “cheapest X” queries.
  • newest / oldest order by latest_observed_at, the freshness of the most recent price observation.

Pagination is page-based: request page=2&page_size=100 to keep walking the result set. The found field tells you when to stop. Parameter values do not change the cost - every search is 1 credit regardless of page_size (see Credits & limits).

Live response (hapanjuurileipä = sourdough bread in Finnish; trimmed to two hits and one facet):

{
"query": "hapanjuurileipä",
"found": 3,
"page": 1,
"page_size": 24,
"hits": [
{
"id": "rp_01a0145398d0776c802124defe1b2cac",
"name": "Fazer Bistro Hapanjuurileipä 600g",
"brand": "Fazer",
"country_code": "FI",
"retailer_slug": "s-kaupat-fi",
"chain_slug": "s-ryhma",
"quantity_value": 600.0,
"quantity_unit": "g",
"latest_price": 3.79,
"latest_unit_price": 6.32,
"latest_unit_price_uom": "kg",
"currency": "EUR",
"latest_observed_at": "2026-08-07T07:00:05Z",
"tags": [],
"category_slug": "bread/packaged",
"category_path": ["bakery", "bread", "bread/packaged"],
"store_cities": [],
"url": "https://www.s-kaupat.fi/tuote/fazer-bistro-hapanjuurileipa-600g/6413467539305",
"image_url": "https://cdn.s-cloud.fi/v1/w400h400@_q75/assets/dam-id/5gjL1TEn41c8-6Yz2XXisL.webp"
},
{
"id": "rp_01a0145398cf746e9f384a2da03daf81",
"name": "Fazer Leipurit vaalea hapanjuurileipä 460 g",
"brand": "Fazer Leipurit",
"country_code": "FI",
"retailer_slug": "s-kaupat-fi",
"chain_slug": "s-ryhma",
"quantity_value": 460.0,
"quantity_unit": "g",
"latest_price": 4.27,
"latest_unit_price": 9.28,
"latest_unit_price_uom": "kg",
"currency": "EUR",
"latest_observed_at": "2026-08-07T07:00:05Z",
"tags": [],
"category_slug": "bread/instore-fresh",
"category_path": ["bakery", "bread", "bread/instore-fresh"],
"store_cities": [],
"url": "https://www.s-kaupat.fi/tuote/fazer-leipurit-vaalea-hapanjuurileipa-460-g/6413467458804",
"image_url": "https://cdn.s-cloud.fi/v1/w400h400@_q75/assets/dam-id/AbdGNBehq6KA5nXbOJ9Cir.webp"
}
],
"facets": [
{
"field": "brand",
"counts": [
{ "value": "Fria", "count": 1 },
{ "value": "Fazer", "count": 1 },
{ "value": "Fazer Leipurit", "count": 1 }
]
}
]
}

found is the total match count across all pages - not the number of hits returned. Each entry in facets carries one filterable field’s value counts (retailer_slug, chain_slug, brand, category_slug, tags, …).

A common UI loop with facets: render the returned facet values as filter chips with their counts, let the user pick one, and re-run the same search with the chosen value as the matching parameter (?brand=Fazer). Counts always reflect the current result set, so chips update as filters stack.

Field Description
id Opaque product id (rp_ + 32 hex)
name, brand Product display name; brand, null if none
country_code Country the hit belongs to
retailer_slug, chain_slug Where the product is sold
quantity_value, quantity_unit Pack size, e.g. 600 / g
latest_price Most recent observed price, in currency
latest_unit_price, latest_unit_price_uom Price per unit, e.g. 6.32 / kg
latest_observed_at RFC 3339 timestamp of the latest observation
tags Tag slugs (dietary, allergen, …)
category_slug, category_path Deepest category; full path from L1 root
store_cities Cities where the product has been observed
url, image_url Retailer product page; product image

Every latest_* field is a snapshot of the most recent observation for that product - prices refresh from nightly retailer runs, so check latest_observed_at when freshness matters. Pass a hit’s id to price history when you need more than the latest value.

Terminal window
curl -H "X-API-Key: $MEALCP_API_KEY" \
"https://api.mealcp.com/v1/products/rp_01a0145398d0776c802124defe1b2cac"

GET /v1/products/{product_id} returns a single hit in the exact same shape as hits[] above. The id must match rp_ + 32 hex chars - take it verbatim from a search hit’s id.

Use the lookup when you already hold an id and need the full, current record: re-validating a stored product before showing it, refreshing a cached detail page, or hydrating a price-history view with the product’s name, pack size, and image. It costs the same 1 credit as a search and never accepts partially typed or hand-built ids.

Search uses the shared error envelope - the HTTP status tells you what went wrong and what to do next:

  • 400 - semantic parameter error (e.g. min_price exceeds max_price)
  • 401 - missing/invalid API key
  • 402 - monthly credit cap exhausted
  • 422 - invalid parameters, with an error.details array naming each issue:
  • 429 - rate limit exceeded (see Retry-After header)
  • 503 - search service temporarily unavailable
{
"error": {
"code": "validation_error",
"message": "request validation failed",
"details": [
{
"loc": ["query", "page"],
"msg": "Input should be greater than or equal to 1",
"type": "greater_than_equal"
}
]
}
}

For 429, wait the number of seconds given in Retry-After before retrying. For 402, check window_resets_at via GET /v1/me and schedule the next batch after the balance refills. Retry 503 with backoff - the search service is temporarily unavailable, and other endpoints may still be serving.