Try it without a key
Every endpoint below answers unauthenticated requests with the first 50 rows, so you can check the data shape, the field names and how fresh it is before you talk to anyone. No sign-up, no key, no card. Paste this into a terminal:
curl "https://basketwatchireland.com/api/tesco/products?limit=5"
Authenticating
When you want the full dataset, send your key on every request as an X-API-Key header (preferred) or an ?api_key= query parameter. Keep it private.
curl -H "X-API-Key: YOUR_KEY" \
"https://basketwatchireland.com/api/tesco/products?limit=5"
X-Credits-Charged) and what is left (X-Credits-Remaining). Check your balance any time with GET /api/credits. See pricing and buy credits, from €49.99 with no upper limit, or book a demo.Buying access as an agent
An autonomous agent can buy its own credits and start querying, with no human, no browser and no card entry. BasketWatch accepts machine payments over MPP, settling by card through Stripe shared payment tokens.
curl "https://basketwatchireland.com/api/credits/mpp"
Free and unauthenticated: an agent that cannot read the price without paying cannot decide whether to buy. Returns the packs, the per-record rate and the purchase URL.
curl -X POST "https://basketwatchireland.com/api/credits/mpp/buy?pack=4999"
Answers 402 Payment Required with a WWW-Authenticate: Payment
challenge naming the stripe method and charge intent. The
request parameter carries the amount in minor units and the
networkId your shared payment token must be scoped to. pack is in
euro cents, so 4999 is the €49.99 pack and 99999 the
€999.99 one. Every pack converts at the same €0.001 per record as the card packs
on the pricing page.
curl -X POST "https://basketwatchireland.com/api/credits/mpp/buy?pack=4999" \
-H "Authorization: Payment <base64url(credential)>"
The credential echoes the challenge back intact alongside your token, as
{"challenge": {...}, "payload": {"spt": "spt_..."}}. A successful purchase
returns a funded api_key, the credits added and the balance. Send that key as
X-API-Key on every request from then on.
PaymentIntent rather than charging
the card twice. Send X-API-Key with the purchase to top up a key you already
hold; omit it and a new metered key is minted and returned.Agents holding USDC can pay over x402
instead, at /api/credits/x402, where that door is enabled. Both doors are
advertised in the 402 you receive when your balance runs out, and the service is
described for machines at /llms.txt.
Using BasketWatch from an AI assistant (MCP)
If you work in Claude Desktop, Claude Code, Cursor or any other MCP client, you can ask questions about Irish grocery prices in plain language and have the assistant call this API for you. The server runs locally against your own key, so nothing is proxied through us and your credits meter exactly as they do on a direct request.
{
"mcpServers": {
"basketwatch": {
"command": "python",
"args": ["-m", "basketwatch_mcp"],
"env": {
"BASKETWATCH_API_BASE": "https://basketwatchireland.com",
"BASKETWATCH_API_KEY": "YOUR_KEY",
"BASKETWATCH_MCP_MAX_ROWS": "200"
}
}
}
}
Seven tools are exposed: search_products,
compare_price_across_stores, get_promotions,
recent_price_changes, newly_added_products,
removed_products and list_products. Then you simply ask:
- “Which supermarket is cheapest for Barry’s Tea 80 bags today?”
- “What went up in price at Dunnes yesterday, and was it a shelf rise or an offer ending?”
- “List everything on promotion at SuperValu right now under €2.”
curl,
your own code or your assistant calling a tool. One record returned costs one credit, calls
that return no rows are free, and GET /api/credits shows the balance and ledger
at no charge.BASKETWATCH_MCP_MAX_ROWS rows per call, 200 by default, which keeps an
assistant’s answers fast and your spend predictable when a model calls a tool
repeatedly. Raise it for longer answers, or use the HTTP API directly for bulk work, where
limit and offset page the full dataset. Without a key you still get
a live 50-row sample per call, so you can try the tools before buying anything.Conventions
- One path shape: per store. Every dataset is
/api/{store}/<dataset>, where{store}isaldi,tesco,supervaluordunnes— the same path shape for all four. A few datasets are specific to one retailer (promotions and out-of-stock, specialbuys); these are flagged on the cards below. - CSV. Append
.csvto any dataset for a CSV download (e.g./api/dunnes/products.csv), ready for Excel, Power Query or pandas. - Snapshot date. Every response includes
scrape_date(the nightly snapshot the values come from). - Money. Prices are euro decimals;
unit_priceis per kg or per litre withunit_basisnaming the unit. - Pagination.
limit+offsetpage large results (defaultlimit100);qfree-text filters name/brand.limitis clamped, not validated: an over-large value returns the cap rather than an error, so when paging, compare against your effective page size rather than assuming a short page means the end of the data. - Search.
?q=is a relevance-ranked full-text search over product name and brand, best match first. It is token-based rather than a literal substring, so word order does not matter and hyphens and accents are folded:coca colafindsCoca-Cola, andMoëtandMoetare the same query. Pack sizes in the query are normalised too, so2l,2 litreand2ltrall match the same products however the retailer spelled it. Adding?sort=,?category=or a historical?on=switches to a filtered scan instead, because those override relevance ordering. - What counts as a row. The unit
limitcounts depends on the request. Plain/products: one row per SKU, cap 100,000. With?fields=: one row per SKU, cap 25,000. With?from=/to=: one row per SKU per collection day, cap 50,000. Solimit=1000on a month-long range may return 33 products across 30 days, not 1,000 products. - Dates. Three ways to ask.
on=YYYY-MM-DDis a single snapshot day.days=Nis a lookback ending today.from=YYYY-MM-DD&to=YYYY-MM-DDis an explicit range, both ends inclusive, and either bound may be given alone. Ranges are capped at 400 days, and a malformed or inverted range returns 400 rather than being silently ignored. - A range on the catalogue returns a series. Without dates,
/api/{store}/productsis one row per SKU. Withfrom=/to=it is one row per SKU per collection day, ordered by id then date so each SKU stays contiguous and pages cannot skip rows. Filters,?fields=and the CSV form all still apply. A month of Tesco is roughly 675,000 rows, so page it: with a range the limit caps at 50,000. - Product copy, barcodes and identity. Catalogue endpoints return prices by default. Add
?fields=to attach the rest of what we hold per SKU:description,ingredients,regulated_name,abv,nutrition,product_type,pack,own_label,match_id,brand_canonical,pmp. (barcodesis returned by default and does not need requesting.) Comma-separated, any combination; an unrecognised name returns 400 rather than being ignored. pack: structured size. Pack size arrives from retailers as free text, and the same size is spelled many ways: Coca-Cola at one retailer alone appears as2ltr,2 Litreand2L. Multipacks frequently state the volume only in the product name (24 x 330ml) whilepack_sizesays12 Pack.?fields=packreturns a parsed object —unit_count,unit_size,unit_measure,total_ml,total_g— so you can compute €/litre or €/kg without writing your own parser. A size we cannot read returns nulls rather than a guess: a wrong volume silently corrupts every derived figure, so we would rather tell you we do not know.match_id: the same product across retailers. The identifier of the cross-retailer match group this SKU belongs to, ornullif it is unmatched. Join on it to line the same product up across banners instead of rebuilding the matching yourself. About half of the live catalogue is matched; most groups are joined on barcode, sobarcodesandmatch_idare two views of the same work.brand_canonical: a grouping key for the brand. The same brand reaches us spelled several ways because each retailer types it its own way:JACOB'S/JACOBS/Jacob's,NESTLE/NESTLÉ,Dr. Oetker/DR OETKER. Across the live catalogue 1,092 brands carry two or more spellings, so brand-level aggregation is wrong until they are folded together. This field is a normalised KEY for that (lower case, accents stripped, punctuation and spaces removed), not a display name: choosing a prettiest spelling would mean deciding which retailer's rendering is correct, sobrandkeeps the raw string exactly as published. It folds typography, not identity, so it will not mergeCokewithCoca-Cola; that needs a curated synonym table rather than a rule.pmp: price-marked packs. A pack printed with a price on the wrapper. Retailers put it in the product name rather than any structured field (Lucozade Sport 750ml PMP€2.50), so this parses it intois_pmpandmarked_price. Small but real: 27 products live, and only 12 state the amount. The rest are a barePMPwith the figure dropped, somarked_priceisnullwhileis_pmpstays true; they are price-marked, we simply do not know at what.own_label: a floor, not a census. True when the product's brand is a known retailer own-label brand, checked against the same curated list our matcher uses. Read it as a lower bound: it is accurate where a retailer brands own label under its own name, and it under-reports where a retailer runs many separate house marques. Measured against the live catalogue it flags 23.6% of Tesco and 28.3% of Dunnes, but only 33.9% of Aldi — whose true own-label share is far higher, because most of its marques are not on the list. We would rather understate it than infertruefor every brand we do not recognise.- Barcodes are returned by default. Every catalogue row carries
barcodes, the GTIN/EAN codes we hold for that SKU, as a list of strings. You do not need?fields=for it: a barcode is the join key for the whole dataset, so it is identity rather than an optional extra. A product can carry more than one (7.4% do, and a handful carry many more), and a product with no code we have found returns[], notnull. It never changes the row count: you still get one row per SKU, solimitandoffsetpage exactly as they would otherwise, and asking for it explicitly does not reduce your page size. Coverage: Dunnes 99.7%, SuperValu 98.3%, Tesco 96.0%, Aldi 68.9%. Aldi used to sit near zero because it publishes no codes in its catalogue; its own-label range has since been resolved against Aldi's own barcode lookup, so most of the Aldi shelf now carries a confirmed code. Note those are Aldi own-brand codes, which no other retailer sells, so they identify the product rather than bridge it to a rival. These are the same codes our cross-retailer matcher joins on, so you can reproduce or extend the matching yourself. To go the other way, from a code to its products at every retailer, use the barcode lookup. Barcodes need a key: the keyless trial returns prices and pack data only.
Stores: alditescosupervaludunnes
Catalogue & search
Full catalogue with the current shelf price per SKU. Add ?q= to filter, ?limit=&offset= to page, ?fields= to attach product copy, and ?from=&to= to get every day in a range rather than a single snapshot.
curl -H "X-API-Key: YOUR_KEY" \
"https://basketwatchireland.com/api/tesco/products?limit=1"
[
{
"id": "tesco:320395135",
"name": "Biona Organic Rye Sesame Crispbread 200g",
"brand": "BIONA",
"category_path": "Food Cupboard > Crackers, Rice Cakes & Breadsticks",
"url": "https://www.tesco.ie/shop/en-IE/products/320395135",
"pack_size": "200g",
"first_seen": "2026-05-27",
"last_seen": "2026-06-29",
"scrape_date": "2026-06-29",
"price": 2.75,
"unit_price": 13.75,
"unit_basis": "1 KG",
"clubcard_price": null,
"offer_text": null,
"available": 1,
"removed": 0
}
]
Structured pack size, own-label flag and the cross-retailer match id, for building a comparable panel.
[
{
"id": "tesco:315853194",
"name": "Coca-Cola Zero Sugar Soft Drink 2 Litre",
"price": 2.85,
"pack": {
"unit_count": null, "unit_size": 2000.0, "unit_measure": "ml",
"total_ml": 2000.0, "total_g": null
},
"own_label": false,
"match_id": 1454102
},
{
"id": "tesco:315469891",
"name": "Coca-Cola Original Taste Soft Drink 24 x 330ml",
"price": 15.00,
"pack": {
"unit_count": 24, "unit_size": 330.0, "unit_measure": "ml",
"total_ml": 7920.0, "total_g": null
},
"own_label": false,
"match_id": null
}
]
GTIN/EAN codes per SKU, as an array. Join on these to match the same product across retailers, or against your own product master. To go from a code to the product, use the barcode lookup.
curl -H "X-API-Key: YOUR_KEY" \
"https://basketwatchireland.com/api/tesco/products?limit=2"
[
{
"id": "tesco:254656868",
"name": "Tesco Butter 227G",
"price": 2.35,
"barcodes": ["5099839718014"]
},
{
"id": "tesco:301225533",
"name": "Heinz Tomato Ketchup 910G",
"price": 4.50,
"barcodes": ["5000157024671", "0000050157024"]
}
]
Product copy in bulk: descriptions, ingredient declarations, the EU-1169 regulated name, ABV, per-100g nutrition and our product type, attached to the catalogue rows rather than fetched one SKU at a time. This is the catalogue route with a query string, not a separate endpoint, so every catalogue parameter still applies: q, category, on, from/to, paging and the CSV form.
curl -H "X-API-Key: YOUR_KEY" \
"https://basketwatchireland.com/api/tesco/products?fields=description,ingredients,nutrition&limit=1"
[
{
"id": "tesco:320395135",
"name": "Biona Organic Rye Sesame Crispbread 200g",
"price": 2.75,
"description": "Organic Rye Sesame Crispbread",
"ingredients": "Wholegrain Rye Flour*(86.5%), Sesame Seeds* (8.7%), Sea Salt, *= Certified Organic Ingredients",
"nutrition": { "energy": 1570, "fat": 1.2, "protein": 9.4 }
}
]
- One request per store. A whole catalogue comes back in a single call: the page cap with
fields=is 25,000 rows and the largest store is about 22,000 SKUs. Copy is heavy, so size the request accordingly (Tesco with description and ingredients is roughly 10MB, Dunnes nearer 40MB because its descriptions run long);limitandoffsetpage it down if you would rather pull in chunks. - Add a date range for copy over time.
?fields=description,ingredients&from=2026-08-01&to=2026-08-31returns each SKU once per collection day with its copy attached, so you can track a price series and the label text together. The copy itself is static and repeats on every row of a SKU; only the price fields move. - Coverage is not 100%. Retailers publish this copy unevenly. Descriptions: Dunnes 93%, SuperValu 90%, Tesco 83%, Aldi 52%. Ingredients: roughly two thirds of the catalogue, and only ~21% of Aldi. Nutrition is SuperValu/Dunnes only; ABV is effectively so (a handful of Aldi rows, none for Tesco). Absent values come back as
null, never as an error. - Nutrition is a per-100g/ml object (energy in kJ, fat, carbs, sugar, protein, salt in g).
Every price observation per SKU across a date range: one row per product per collection day. This is the time series behind the single-day snapshot that on= returns, and the endpoint to use for trend, elasticity or promo-cadence work.
curl -H "X-API-Key: YOUR_KEY" \
"https://basketwatchireland.com/api/tesco/price-history?from=2026-08-01&to=2026-08-31&q=milk"
[
{
"id": "tesco:320395135",
"name": "Biona Organic Rye Sesame Crispbread 200g",
"brand": "BIONA",
"category_path": "Food Cupboard > Crackers, Rice Cakes & Breadsticks",
"pack_size": "200g",
"scrape_date": "2026-08-01",
"price": 2.79,
"unit_price": 13.95,
"unit_basis": "1 KG",
"clubcard_price": null,
"offer_text": null,
"was_price": null,
"offer_valid": null,
"available": 1
},
{ "id": "tesco:320395135", "scrape_date": "2026-08-02", "price": 2.75 }
]
- Narrow it.
q=(name or brand),category=, orids=with a comma-separated list of product ids. Unfiltered, a month of Tesco is around 675,000 rows, so page it withlimit(default 1,000, maximum 50,000) andoffset. - Ordering. Rows come back ordered by product id, then date, so each SKU's series stays contiguous and paging cannot skip or repeat a row.
- Default span. With no
from, the last 30 days ending atto(or today).
Fast catalogue size for a store, without returning rows.
curl -H "X-API-Key: YOUR_KEY" \
"https://basketwatchireland.com/api/dunnes/products/count"
{ "total": 15069 }
A single product: current price, unit price, pack, plus its full price history. With a key the row also carries the product copy (description, ingredients, regulated_name, abv, nutrition, product_type) and its barcodes; the keyless trial returns the catalogue fields and history only, and only for the products in the 50-row sample, so the lookup can be tried without being used to read the catalogue one product at a time. For copy across many SKUs use ?fields= on the catalogue route rather than looping here.
curl -H "X-API-Key: YOUR_KEY" \
"https://basketwatchireland.com/api/product/tesco:320395135"
{
"product": {
"id": "tesco:320395135",
"name": "Biona Organic Rye Sesame Crispbread 200g",
"brand": "BIONA",
"url": "https://www.tesco.ie/shop/en-IE/products/320395135",
"image_url": "https://digitalcontent.api.tesco.com/v2/media/ghs/.../1014470272.jpeg",
"category_path": "Food Cupboard > Crackers, Rice Cakes & Breadsticks",
"pack_size": "200g",
"pack_qty": 200.0,
"pack_unit": "G",
"first_seen": "2026-05-27",
"last_seen": "2026-06-29",
"description": "Organic Rye Sesame Crispbread",
"ingredients": "Wholegrain Rye Flour*(86.5%), Sesame Seeds* (8.7%), Sea Salt, *= Certified Organic Ingredients",
"regulated_name": "Organic Rye Sesame Crispbread",
"abv": null,
"nutrition": null,
"product_type": "other"
},
"history": [
{ "scrape_date": "2026-05-27", "price": 2.79 },
{ "scrape_date": "2026-06-29", "price": 2.75 }
]
}
Look a barcode up: every product carrying that GTIN, EAN or UPC code, across all four retailers in one call, in the same row shape as the catalogue. Send the code as printed or scanned: spaces, hyphens and leading zeros are ignored, so 05000127014084, 5000127014084 and the 12-digit UPC-A form of a code all find the same products, and a code this API returned round-trips. ?store= narrows the search to one retailer and ?fields= attaches the product copy exactly as on the catalogue (ingredients, nutrition and the rest; nutrition is null where we do not hold it, which is currently every Tesco product). Live listings come first, in store order. A product its retailer has since delisted is still returned, flagged removed: 1, because its identity data still stands. Each row's matched_barcodes says which code found it. Billed per barcode found, not per row: a lookup costs one credit whether the product is listed at one retailer or all four, and a code we do not hold returns found: false and costs nothing.
curl -H "X-API-Key: YOUR_KEY" \
"https://basketwatchireland.com/api/barcode/5000127014084?fields=ingredients,nutrition"
{
"barcode": "5000127014084",
"found": true,
"products": [
{
"store": "tesco",
"id": "tesco:253158380",
"matched_barcodes": ["5000127014084"],
"name": "Kellogg's Corn Flakes Breakfast Cereal 1kg",
"brand": "Kellogg's",
"pack_size": "1kg",
"price": 4.99,
"removed": 0,
"barcodes": ["5000127014084"],
"ingredients": "Maize, Barley Malt Extract, Sugar, Salt, Niacin, Iron, Vitamin B6, Riboflavin, Thiamin, Folic Acid, Vitamin D, Vitamin B12",
"nutrition": null
},
{
"store": "supervalu",
"id": "supervalu:1011636000",
"matched_barcodes": ["5000127014084"],
"name": "Kellogg's Corn Flakes Cereal (1 kg)",
"brand": "Corn Flakes",
"pack_size": "1 kg",
"price": 5.25,
"removed": 0,
"barcodes": ["5000127014084", "5013503014110"],
"ingredients": "Maize, BARLEY Malt Extract, Sugar, Salt, Niacin, Iron, Vitamin B6, Riboflavin, Thiamin, Folic Acid, Vitamin D, Vitamin B12",
"nutrition": { "energy": 1604.0, "fat": 0.9, "carbs": 84.0, "sugar": 8.0, "protein": 7.0, "salt": 1.1 }
},
{
"store": "dunnes",
"id": "dunnes:100114495",
"matched_barcodes": ["5000127014084"],
"name": "Kellogg's Corn Flakes Breakfast Cereal 1kg",
"price": 4.99,
"removed": 0
}
]
}
Rows carry the catalogue's other fields too (unit_price, offer_text, clubcard_price, url, scrape_date and so on); the example is trimmed for length.
The same lookup for a list: up to 100 codes per request, comma-separated. Every product found comes back once in products, with matched_barcodes listing the codes it carries; codes we do not hold are listed in not_found and anything that is not a barcode in invalid, so a list can be checked for coverage in one call. ?store= and ?fields= work as above. Billing is one credit per code found (the matched count), however many retailers list each product; the misses and invalid codes in a list are free.
curl -H "X-API-Key: YOUR_KEY" \
"https://basketwatchireland.com/api/barcodes?codes=5000127014084,5000157024671,5099999999990"
{
"requested": 3,
"valid": 3,
"matched": 2,
"products": [
{ "store": "tesco", "id": "tesco:252261477", "matched_barcodes": ["5000157024671"], "name": "Heinz Beans in Tomato Sauce 415g" },
{ "store": "tesco", "id": "tesco:253158380", "matched_barcodes": ["5000127014084"], "name": "Kellogg's Corn Flakes Breakfast Cereal 1kg" },
{ "store": "supervalu", "id": "supervalu:1011892000", "matched_barcodes": ["5000157024671"], "name": "Heinz Baked Beans (415 g)" },
{ "store": "supervalu", "id": "supervalu:1011636000", "matched_barcodes": ["5000127014084"], "name": "Kellogg's Corn Flakes Cereal (1 kg)" },
{ "store": "dunnes", "id": "dunnes:100111378", "matched_barcodes": ["5000157024671"], "name": "Heinz Baked Beans 415g" },
{ "store": "dunnes", "id": "dunnes:100114495", "matched_barcodes": ["5000127014084"], "name": "Kellogg's Corn Flakes Breakfast Cereal 1kg" }
],
"not_found": ["5099999999990"],
"invalid": []
}
Cross-retailer comparison: each matched product with its price and unit price in every store that stocks it, plus how many stores it spans (coverage).
curl -H "X-API-Key: YOUR_KEY" \
"https://basketwatchireland.com/api/compare?q=heinz%20ketchup"
{
"as_of": "2026-06-29",
"matches": [
{
"match_id": 561961,
"brand": "Heinz",
"pack": "910g",
"category": "Food Cupboard",
"display_name": "Heinz Tomato Ketchup 910g",
"coverage": 4,
"stores": {
"aldi": {
"id": "000000000000336799",
"name": "Tomato Ketchup 910g",
"price": 3.99,
"unit_price": 4.38,
"unit_basis": "1 KG",
"was_price": null,
"clubcard_price": null,
"scrape_date": "2026-06-29"
},
"tesco": {
"id": "tesco:254722768",
"name": "Heinz Top Down Squeezy Tomato Ketchup 910g",
"price": 5.50,
"unit_price": 6.04,
"unit_basis": "1 KG",
"was_price": null,
"clubcard_price": 4.50,
"scrape_date": "2026-06-29"
},
"supervalu": {
"id": "supervalu:1003041000",
"name": "Heinz Tomato Ketchup (910 g)",
"price": 5.50,
"unit_price": 6.04,
"unit_basis": "1 KG",
"was_price": null,
"clubcard_price": null,
"scrape_date": "2026-06-29"
},
"dunnes": {
"id": "dunnes:100684285",
"name": "Heinz Tomato Ketchup 910g",
"price": 5.50,
"unit_price": 6.04,
"unit_basis": "1 KG",
"was_price": null,
"clubcard_price": null,
"scrape_date": "2026-06-29"
}
}
}
]
}
Aldi Specialbuys, the rotating middle-aisle range: active lines grouped by category, plus recently ended and newly listed buys. Aldi-only.
curl -H "X-API-Key: YOUR_KEY" \
"https://basketwatchireland.com/api/aldi/specialbuys?limit=1"
{
"active_by_category": [
{ "category_path": "SpecialBuys > Clothing", "count": 54, "avg_price": 6.63 }
],
"recently_ended": [
{ "id": "000000000708833001", "name": "Bottle with Carabiner", "brand": "CROFTON", "category_path": "SpecialBuys > Kitchen", "last_seen": "2026-06-26", "last_price": 3.99 }
],
"newly_listed": [
{ "id": "000000000738367001", "name": "Adult Fiction", "brand": "HACHETTE", "category_path": "SpecialBuys > Hobbies and Crafts", "first_seen": "2026-06-28", "launch_price": 3.99 }
]
}
Changes & lifecycle
Day-over-day movements in three separate buckets: shelf-price increases / decreases, promotion changes (started / ended / changed), and loyalty-price changes. The loyalty bucket (clubcard_changes) reflects each retailer's own scheme — Tesco Clubcard, SuperValu Real Rewards; Dunnes and Aldi have no loyalty pricing, so it stays empty. Each response covers a single retailer (the example below is Tesco). Accepts ?on=YYYY-MM-DD.
curl -H "X-API-Key: YOUR_KEY" \
"https://basketwatchireland.com/api/tesco/changes"
{
"date": "2026-06-29",
"prev_date": "2026-06-28",
"shelf_increases": [
{
"id": "tesco:254656789",
"name": "Flora Original Spread 500g",
"brand": "FLORA",
"category_path": "Fresh Food > Butter, Spreads & Margarine",
"url": "https://www.tesco.ie/shop/en-IE/products/254656789",
"pack_size": "500g",
"prev_price": 2.50,
"price_now": 2.75,
"delta": 0.25,
"pct": 10.0,
"promo": null
}
],
"shelf_decreases": [],
"promotions": [
{
"id": "tesco:317990690",
"name": "Cuisine de France Lye Pretzel 85g",
"brand": "CUISINE DE FRANCE",
"kind": "started",
"offer_now": "Any 3 for €3 Clubcard Price",
"was_price": null,
"shelf_price": 1.75
}
],
"clubcard_changes": [
{
"id": "tesco:313603753",
"name": "Garnier Ambre Solaire Bronze Oil 150ml",
"brand": "GARNIER",
"kind": "added",
"clubcard_now": 8.50,
"clubcard_prev": null,
"shelf_price": 17.00,
"offer_text": "€8.50 Half Price Clubcard Price"
}
]
}
Newly listed SKUs first seen in the latest snapshot. Use days=N for a lookback, or from=/to= for an explicit range (both ends inclusive).
curl -H "X-API-Key: YOUR_KEY" \
"https://basketwatchireland.com/api/tesco/new-products"
[
{ "id": "tesco:324436415", "name": "Adams Polishes Detail Spray 473ml", "brand": "ADAM'S POLISHES", "category_path": "Home & Living > DIY & Car Care", "first_seen": "2026-06-29", "launch_price": 13.00 }
]
Delisted SKUs absent from the latest snapshot (two-snapshot confirmed).
curl -H "X-API-Key: YOUR_KEY" \
"https://basketwatchireland.com/api/dunnes/removed"
[
{ "id": "dunnes:100221987", "name": "Dunnes My Family Fabric Conditioner 3L", "brand": "Dunnes Stores", "category_path": "Dunnes > Household & Cleaning > Laundry", "first_seen": "2026-05-30", "last_seen": "2026-06-27", "last_price": 3.50 }
]
Present-but-unavailable lines (the retailer's availability flag), distinct from removed. Tesco only — Tesco's catalogue exposes a per-SKU availability flag; the other retailers don't.
curl -H "X-API-Key: YOUR_KEY" \
"https://basketwatchireland.com/api/tesco/out-of-stock"
{
"date": "2026-06-29",
"count": 882,
"items": [
{ "id": "tesco:317208754", "name": "Sheep Hill Sauvignon Blanc 75cl", "brand": "A GABB FAMILY WINE", "category_path": "Drinks > Wine", "url": "https://www.tesco.ie/shop/en-IE/products/317208754", "pack_size": "75cl", "price": 18.00, "clubcard_price": 9.00 }
]
}
Pack-size reductions: same product, smaller pack, with the old and new size and the effective unit-price change. Returns an empty array until a pack-size reduction is detected; the example below shows the shape.
curl -H "X-API-Key: YOUR_KEY" \
"https://basketwatchireland.com/api/tesco/shrinkflation"
[
{
"id": "tesco:250114433",
"name": "Cadbury Dairy Milk",
"brand": "CADBURY",
"category_path": "Food Cupboard > Confectionery",
"pack_size": "180g",
"start_date": "2026-04-01",
"end_date": "2026-06-29",
"start_price": 2.50,
"end_price": 2.50,
"start_pack_qty": 200.0,
"start_pack_unit": "G",
"end_pack_qty": 180.0,
"end_pack_unit": "G",
"pack_shrink_pct": 10.0,
"start_unit_price": 12.50,
"end_unit_price": 13.89,
"unit_basis": "1 KG",
"unit_price_pct": 11.1,
"headline_pct": 11.1
}
]
Most volatile SKUs: products whose price has changed most often over the tracked window. Set the window with days=N, or bound it exactly with from=/to=.
curl -H "X-API-Key: YOUR_KEY" \
"https://basketwatchireland.com/api/supervalu/volatile"
[
{ "id": "supervalu:1500119003", "name": "Coca-Cola 1.75L", "brand": "Coca-Cola", "category_path": "Drinks > Minerals", "distinct_prices": 4, "observations": 24, "min_price": 1.85, "max_price": 2.95, "avg_price": 2.31, "range_pct": 59.5 }
]
Promotions
Live deals: products on offer, with was-price, offer text and loyalty pricing (Clubcard / Real Rewards). Not available for Aldi (everyday-low-price).
curl -H "X-API-Key: YOUR_KEY" \
"https://basketwatchireland.com/api/dunnes/promotions"
[
{
"id": "dunnes:100827389",
"name": "Vanish Oxi Action Stain Remover Spray 500ml",
"brand": "Vanish",
"category_path": "Dunnes > Household & Cleaning > Laundry",
"url": "https://www.dunnesstoresgrocery.com/sm/delivery/rsid/520/product/100827389",
"pack_size": "500ml",
"last_seen": "2026-06-29",
"scrape_date": "2026-06-29",
"price": 5.00,
"unit_price": 10.00,
"unit_basis": "1 L",
"offer_text": "SAVE €2.50",
"was_price": 7.50,
"offer_valid": "2026-06-16 23:00:00Z - 2026-07-21 22:59:59Z",
"clubcard_price": null
}
]
Analytics
Headline numbers for the latest snapshot: catalogue size, active SKUs today vs yesterday, promotion share, out-of-stock count, average price, price moves and new lines.
curl -H "X-API-Key: YOUR_KEY" \
"https://basketwatchireland.com/api/tesco/kpis"
{
"total_products": 21056,
"active_today": 20518,
"active_yesterday": 20473,
"active_delta": 45,
"on_promo_today": 7473,
"promo_pct_today": 36.4,
"out_of_stock_today": 882,
"avg_price_today": 7.28,
"avg_price_yesterday": 7.26,
"prices_up_today": 0,
"prices_down_today": 0,
"new_today": 29
}
Per-category average price over time, with the SKU count behind each point — the series behind the dashboard graphs.
curl -H "X-API-Key: YOUR_KEY" \
"https://basketwatchireland.com/api/tesco/category-index"
{
"Food Cupboard": [
{ "date": "2026-05-27", "avg_price": 3.51, "n": 4333 },
{ "date": "2026-06-29", "avg_price": 3.52, "n": 4385 }
],
"Drinks": [
{ "date": "2026-05-27", "avg_price": 11.17, "n": 1452 }
]
}
Per-department roll-up: SKU count, average price and price churn, with the retailer browse URL.
curl -H "X-API-Key: YOUR_KEY" \
"https://basketwatchireland.com/api/tesco/category-summary"
[
{ "category": "Food Cupboard", "sku_count": 4385, "avg_price": 3.52, "changed": 0, "changed_pct": 0.0, "price_delta_pct": 0.0, "url": "https://www.tesco.ie/shop/en-IE/browse/food-cupboard/all" }
]
Errors, limits & freshness
| Status | Meaning |
|---|---|
200 | OK. JSON body (or CSV for .csv paths). |
401 | Missing or invalid key on an endpoint that requires one. |
404 | Unknown store, dataset or product id. |
429 | Rate limit exceeded — back off and retry. |
Freshness. Data refreshes nightly across all four retailers; every response carries scrape_date so you always know the snapshot you are reading.
Beyond the API
Need historical back-catalogue, scheduled bulk CSV exports, or a natural-language / agent interface? An Agent & MCP endpoint is available, and bespoke historical pulls are available on request. Talk to us — or get an API key to start.