API documentation
For people writing agents that shop. One product query in, one JSON document out. No SDK required, a GET is enough.
Overview
ShoppingQuery answers one question: where can this product be bought, and for how much. You send a GTIN or a title. You get offers, prices, stock, images, content and price history from 250+ shops and marketplaces in 32 countries, in one JSON schema.
Base URL: https://shoppingquery.com (the planned public address). Examples work on any host that runs the service, including localhost.
Format: JSON over HTTPS, GET only. The stream uses Server-Sent Events. Responses carry version: "1"; field names are snake_case and stable.
Three doors:
- GTIN (exact).
GET /v1/ean/{gtin}. The barcode is 8 to 14 digits. Where a source publishes a barcode, the offer is checked against it and flaggedmatch.ean_verified. - Title (keywords).
GET /v1/title?q=. The title is searched as keywords in the selected countries. Setresolve=trueto also look up GTIN candidates and, when one is confident, search by barcode. - Stream (SSE).
GET /v1/search/stream. One event per source as it answers, so the first prices arrive in about a second.
Quickstart
Offers for one barcode in GB and the US:
curl 'https://shoppingquery.com/v1/ean/4548736132580?countries=GB,US'The answer is one JSON object: query (what you asked and how the call went), product (title, brand, images, specs), offers (cheapest first, in EUR), sources (every engine asked, matched or not), stats, timing_ms, version.
Python, with httpx:
import httpx
doc = httpx.get(
"https://shoppingquery.com/v1/ean/4548736132580",
params={"countries": "GB,US"},
timeout=60.0,
).json()
print(doc["product"]["title"])
for o in doc["offers"][:5]:
print(o["retailer"], o["price"]["amount"], o["price"]["currency"],
o.get("availability"), o["match"]["ean_verified"])JavaScript:
const res = await fetch(
"https://shoppingquery.com/v1/ean/4548736132580?countries=GB,US"
);
const doc = await res.json();
console.log(doc.product?.title, doc.offers.length);Offers
GET /v1/ean/{gtin} returns the unified document: product, buybox, every offer, every source. GET /v1/ean/{gtin}/offers returns the same offers and buybox with a little less product prose, and reports cache state per source. For the same source list the offers match.
Parameters
| Parameter | Default | What it does |
|---|---|---|
countries | NL,DE | Comma separated ISO 3166-1 alpha-2 codes. Picks the engines that serve those markets. |
country | empty | Single ISO code, alias for countries (the spelling /content/fast uses). Wins over countries when set. |
sources | empty | Comma separated engine ids (for example idealo.de,bol.com). Overrides countries. |
cross_border | true | Include the multi-market aggregators alongside the country engines. |
condition | empty | Filter offers: new, refurbished, used, comma separated. sources still lists every engine asked. |
match | empty | Filter offers by scope: exact, grouped, other_variant, comma separated. |
amazon | false | Include the amazon.* engines. Their lookup takes about 30 seconds, so it is off unless you ask. |
fresh | false | /offers only. Skip the cache and scrape again. Without it, a source answers from cache when its row is younger than 30 minutes (a miss included). |
Offer fields
| Field | What it is |
|---|---|
retailer | The shop behind the offer (domain when known, else the name the source prints). shop is the display name when the source gives one. |
price.amount, price.currency | Sort key: total when shipping is known, else the item price. Currency as the source states it. |
price.item, price.shipping, price.total, price.basis | Item price and shipping when the source splits them. shipping is present only when the source gives it. basis is item or total. |
price.amount_eur | The amount converted to EUR with the ECB rates of the day, or null when we have no rate. |
condition | new, refurbished or used. Harmonised from the source kind and its own grade label. |
availability | Normalised stock: in_stock, limited, out_of_stock, preorder. Absent when the source does not say. |
country | ISO code of the market this offer is sold in. |
match.ean_verified | True when the barcode is on the page and matches this GTIN. |
match.scope | exact (this colour and size), grouped (the page pools variants), other_variant. |
source, source_type | Engine id that found the offer, and its kind: comparison, aggregator, retailer, marketplace, refurb, classifieds. |
Also on an offer: title, url, url_locked, image, grade, grade_label, refurb_class, match.verification, match.reasons, listings, price_outlier.
Example
curl 'https://shoppingquery.com/v1/ean/4548736132580?countries=GB,US'Example response (trimmed):
{
"query": {
"type": "ean",
"value": "4548736132580",
"seconds": 4.2,
"offers_found": 3,
"sources_queried": 14,
"sources_used": 3
},
"product": {
"gtin": "4548736132580",
"title": "Sony WH-1000XM5 Wireless Noise Cancelling Headphones",
"brand": "Sony",
"image": "https://images.example.com/wh-1000xm5-black.jpg"
},
"offers": [
{
"source": "idealo.de",
"source_type": "comparison",
"country": "GB",
"retailer": "amazon.co.uk",
"shop": "Amazon",
"price": {
"amount": 279.00,
"currency": "GBP",
"amount_eur": 324.10,
"item": 279.00,
"shipping": 0.0,
"total": 279.00,
"basis": "total"
},
"condition": "new",
"availability": "in_stock",
"match": { "scope": "exact", "ean_verified": true, "verification": "ean" }
},
{
"source": "bestbuy.com",
"source_type": "retailer",
"country": "US",
"retailer": "bestbuy.com",
"shop": "Best Buy",
"price": {
"amount": 328.00,
"currency": "USD",
"amount_eur": 302.40,
"item": 328.00,
"basis": "item"
},
"condition": "new",
"availability": "limited",
"match": { "scope": "exact", "ean_verified": true, "verification": "ean" }
}
],
"sources": [
{ "source": "idealo.de", "country": "DE", "status": "matched", "offers": 2 },
{ "source": "bestbuy.com", "country": "US", "status": "matched", "offers": 1 }
],
"stats": { "sources": 2, "matched": 2, "offers": 2,
"by_condition": { "new": 2 }, "by_scope": { "exact": 2 } },
"timing_ms": 4210,
"version": "1"
}Price history
GET /v1/ean/{gtin}/price-history returns one price series per history source, plus a cross-country combined series of the lowest EUR price per day.
Parameters
| Parameter | Default | What it does |
|---|---|---|
country | empty | One ISO code (DE, AT, SE, ...) to run that market's history doors. Empty or all = every market we hold, keyed by country in by_country and merged in combined. |
days | full series | Keep only the last N calendar days, 1 to 3650. |
fresh | false | Skip the cache. Each series is cached 6 hours per (source, product id). |
Each point is one day: date (YYYY-MM-DD), lowest_price, currency. A series carries source, country, product_id, scope (variant for one colour and size, model when the source pools variants), points, and min/max/current price. Combined points add which source and country won that day.
curl 'https://shoppingquery.com/v1/ean/4548736132580/price-history?country=DE&days=30'Example response (trimmed):
{
"query": { "type": "ean", "value": "4548736132580" },
"gtin": "4548736132580",
"country": "DE",
"days": 30,
"histories": [
{
"source": "geizhals.de",
"country": "DE",
"product_id": "2145678",
"scope": "variant",
"currency": "EUR",
"points": [
{ "date": "2026-09-15", "lowest_price": 289.00, "currency": "EUR" },
{ "date": "2026-09-16", "lowest_price": 279.00, "currency": "EUR" }
],
"min_price": 279.00,
"max_price": 289.00,
"current_price": 279.00,
"status": "matched"
}
],
"combined": {
"currency": "EUR",
"days": 2,
"min_price": 279.00,
"avg_price": 284.00,
"max_price": 289.00,
"points": [
{ "date": "2026-09-16", "price_eur": 279.00, "source": "geizhals.de",
"country": "DE", "price": 279.00, "currency": "EUR" }
]
},
"timing_ms": 890,
"version": "1"
}Content
GET /v1/ean/{gtin}/content returns title, brand, description, specifications and variant attributes for one barcode. GET /v1/ean/{gtin}/content/fast returns one harmonised truth (title, thumbnail, descriptions, specs, images, retailer links) in about a second.
Use /content/fast when a human or an agent is waiting. It races the tier-1 exact sources against a hard deadline and returns the first strong answer (partial: true when the deadline fired early). Late sources fill the cache, so a repeat call answers in about 10 ms. Use /content when you want the fuller, slower pass with per-spec source lists.
Parameters
| Parameter | Endpoints | What it does |
|---|---|---|
sources | both | Comma separated engine ids. Overrides the fixed fast set and the wider second wave. |
fresh | both | Skip the cache (content is cached 7 days, a miss 6 hours). |
country | /content/fast | ISO code. Routes the race in waves (this country, then neighbours, then the rest) and prefers that country's language in the canonical title. |
resolve | /content/fast | Also resolve up to 8 of the cheapest clickout URLs to shop pages inside a 3 second budget, in resolved_links. Off by default. |
countries does not apply: content is not market-bound.
curl 'https://shoppingquery.com/v1/ean/4548736132580/content/fast?country=DE'Example response (trimmed):
{
"query": {
"type": "ean",
"value": "4548736132580",
"seconds": 0.9,
"sources_queried": 8,
"sources_used": 3
},
"gtin": "4548736132580",
"title": "Sony WH-1000XM5 Wireless Noise Cancelling Headphones",
"title_scope": "variant",
"brand": "Sony",
"thumbnail": "https://images.example.com/wh-1000xm5-black.jpg",
"description": "Industry-leading noise cancellation with Auto NC Optimizer.",
"short_description": "Over-ear wireless headphones, 30 hours battery.",
"specs": [
{ "name": "Colour", "value": "Black", "sources": ["idealo.de"] },
{ "name": "Weight", "value": "250 g", "sources": ["idealo.de", "coolblue.nl"] }
],
"images_count": 4,
"partial": false,
"deadline_hit": false,
"version": "1"
}Images
GET /v1/ean/{gtin}/images returns the images of this exact variant, duplicates removed (same photo from three shops becomes one tile that lists all three). Each item is url plus the sources that show it.
Parameters: sources (override the fixed set), fresh (skip the 7 day cache). A wider second set runs only when the fast set finds no image.
curl 'https://shoppingquery.com/v1/ean/4548736132580/images'Example response (trimmed):
{
"query": { "type": "ean", "value": "4548736132580" },
"gtin": "4548736132580",
"title": "Sony WH-1000XM5 Wireless Noise Cancelling Headphones",
"images": [
{
"url": "https://images.example.com/wh-1000xm5-black.jpg",
"sources": ["idealo.de", "coolblue.nl", "mediamarkt.de"]
}
],
"version": "1"
}Resellers
GET /v1/ean/{gtin}/resellers returns one row per retailer selling this EAN, cheapest first. Each row names the countries and sources it appeared on, its price range and conditions, and whether any listing was proven by barcode. Default match filter: exact,grouped.
Parameters as on /offers: countries, sources, cross_border, condition, match, amazon, fresh.
curl 'https://shoppingquery.com/v1/ean/4548736132580/resellers?countries=NL,DE'Example response (trimmed):
{
"query": { "type": "ean", "value": "4548736132580" },
"gtin": "4548736132580",
"title": "Sony WH-1000XM5 Wireless Noise Cancelling Headphones",
"resellers": [
{
"retailer": "coolblue.nl",
"name": "Coolblue",
"countries": ["NL"],
"sources": ["kieskeurig.nl", "klarna.nl"],
"offers": 2,
"min_eur": 279.00,
"max_eur": 289.00,
"conditions": ["new"],
"ean_verified": true,
"scope": "exact"
},
{
"retailer": "mediamarkt.de",
"name": "MediaMarkt",
"countries": ["DE"],
"sources": ["idealo.de"],
"offers": 1,
"min_eur": 299.00,
"max_eur": 299.00,
"conditions": ["new"],
"ean_verified": true,
"scope": "exact"
}
],
"version": "1"
}Also useful
GET /v1/brand/{brand}/eans lists every GTIN our sources will prove for a brand (the full list, never sampled). Parameters: countries, sources, max_pages (default 30, cap 200), budget_s (default 20, max 300; when it runs out the document returns with partial: true), fresh.
GET /v1/countries lists the markets and engines behind them. GET /v1/retailers lists which engine surfaces which retailer. GET /v1/fx returns the ECB reference rates (units per 1 EUR) used for the EUR conversions.
Title search
GET /v1/title?q= searches a product title on the shops of the selected countries or sources and returns the same unified document. Every source runs its own title search and reports match.verification: "title", because there is no barcode to prove against. partial is true when the time budget cut a source. Nothing is sent to a barcode comparison site by default. When nothing matches, one relaxed retry drops spec words (capacity, size, colour) and says so: those offers carry match.reasons relaxed_query and are never mixed with strict matches.
Optional barcode lookup: resolve=true also asks idealo.de and geizhals.de for GTIN candidates.
resolve=trueandauto(defaulttrue), top candidate scores 0.6 or higher: run the EAN search for that GTIN. The answer carriesquery.resolved_gtinandquery.candidates.resolve=truewith candidates but no auto-resolve (auto=false, or the score is under 0.6): candidates only,offersempty. Pick one and call/v1/ean/{gtin}yourself.resolve=trueand no candidate: the title search above.
Parameters: q (3 to 200 characters, required), countries, sources, cross_border, condition, match, amazon, resolve, auto. The query is the raw text; quotes and operators are not interpreted.
curl 'https://shoppingquery.com/v1/title?q=Sony%20WH-1000XM5&countries=GB,US'Example response (trimmed):
{
"query": {
"type": "title",
"value": "Sony WH-1000XM5",
"resolved_gtin": "4548736132580",
"candidates": [
{
"gtin": "4548736132580",
"title": "Sony WH-1000XM5",
"brand": "Sony",
"source": "title-resolve",
"score": 0.91
}
]
},
"product": {
"gtin": "4548736132580",
"title": "Sony WH-1000XM5 Wireless Noise Cancelling Headphones",
"brand": "Sony"
},
"offers": [
{
"source": "idealo.de",
"source_type": "comparison",
"country": "GB",
"retailer": "amazon.co.uk",
"price": { "amount": 279.00, "currency": "GBP", "amount_eur": 324.10 },
"condition": "new",
"availability": "in_stock",
"match": { "scope": "exact", "ean_verified": true, "verification": "ean" }
}
],
"version": "1"
}Result groups (view and clusters)
A broad title such as doraemon returns many different products: games, plush toys, manga volumes, sneakers. One price spread over all of them means nothing, so the document groups the offers into products and says how to show them.
view: "product": the offers are one product (a barcode or ASIN lookup, or a title resolved to a GTIN). Readproduct,buyboxandoffersas before.view: "serp": the offers are several products. Showclustersas a result list, and only harmonise (price spread, best offer) inside one cluster.
Any other title lookup is always "serp". top_share (0 to 1) reports the share of offers the biggest group holds; it is a hint and never changes the view.
| Cluster field | Meaning |
|---|---|
cluster_id | "c1", "c2", … biggest first. |
title, brand, image | The most representative member title, the most common brand, the first member image. |
gtin | Set when the group carries exactly one proven barcode. |
offer_count, source_count | Offers in the group, and the sources they came from. |
price_min_eur, price_max_eur | Price range of the group in EUR. |
conditions | Offers per condition, e.g. {"new": 8, "used": 1}. |
offer_ids | Members as "<source>#<index>": the source id and the offer's position in that source's offers (the stream's result rows use the same order). |
offer_indexes | Positions of the members in this document's offers list (after the condition and match filters). Not in the stream. |
Stream (SSE)
GET /v1/search/stream sends one Server-Sent Events frame per source the moment it answers. First prices in about a second; the rest fills in until the budget ends. Feed it a barcode or a title (or both: mode=title with an ean also runs the title search).
Parameters
| Parameter | What it does |
|---|---|
ean | Barcode to search. 8 to 14 digits. Without it, title mode runs. |
title | Title query (3 to 200 characters). Required when ean is empty. |
engines | Comma separated engine ids. Empty = the default set for the call. |
mode | Set to title with an ean to also search by title. |
brand | Optional brand hint for the title search. Defaults to the first word of the title in title-only mode. |
timeout | Wall clock budget in seconds, 5 to 120. Default 30. Late answers are dropped. |
resolve | Add one resolved event before done: up to 8 of the cheapest clickout URLs walked to their shop pages inside a 3 second budget. |
Events
| Event | Payload keys |
|---|---|
meta | total, ean. Title mode adds title, mode ("title"), skipped. |
result | One source: site, country, matched, ean_verified, title, brand, image, url, currency, offer_count, offers, status, note, verification, and the rest of the source row. |
product | Display product for the run: ean, title, brand, image, url, cheapest, colour, description, specs, images, image_count, variants, price_stats, buybox. Sent twice in a full run (early, then final). |
gallery | images (each src, original, count, sources), raw_count. Adds unprocessed when the dedupe step did not finish and the raw source URLs are sent instead. |
consensus | title. EAN mode only, when the title bridges start. |
amazon | Amazon panel, or error when it failed or ran out of budget. EAN mode only. |
resolved | links, each with url, resolved_url, hops, retailer, domain, error. Only with resolve=true. |
wave | Title mode only. wave (1 when the fast first sources are in, 2 at the end). Wave 1 also carries a first view and clusters, so a broad query can show its result groups early. |
done | engines_matched, engines_with_offers, total_offers, cut (sources dropped at the budget), budget_s, and the final view and clusters (see result groups). |
Title mode sends meta, result (including status: "skipped" rows for sources that only speak barcode), product, optional resolved, and done. No gallery, no amazon, no consensus.
const es = new EventSource(
"https://shoppingquery.com/v1/search/stream?ean=4548736132580"
);
es.addEventListener("result", (e) => {
const row = JSON.parse(e.data);
console.log(row.site, row.offer_count, row.offers?.[0]?.price);
});
es.addEventListener("product", (e) => console.log(JSON.parse(e.data).title));
es.addEventListener("done", () => es.close());curl -N 'https://shoppingquery.com/v1/search/stream?ean=4548736132580'Reference
Machine readable contracts: OpenAPI schema and llms.txt.
| Method | Path | What it does |
|---|---|---|
| GET | /v1/ean/{gtin} | Unified document: product, buybox, offers, sources. |
| GET | /v1/ean/{gtin}/offers | Offers and buybox, with cache state per source. |
| GET | /v1/ean/{gtin}/content | Title, brand, description, specs, variant attributes. |
| GET | /v1/ean/{gtin}/content/fast | Harmonised content in about a second. |
| GET | /v1/ean/{gtin}/images | Images of the exact variant, duplicates removed. |
| GET | /v1/ean/{gtin}/resellers | One row per retailer, cheapest first. |
| GET | /v1/ean/{gtin}/price-history | Price series per source, plus the combined EUR series. |
| GET | /v1/title | Title to GTIN to unified document. |
| GET | /v1/search/stream | Search as Server-Sent Events, one event per source. |
| GET | /v1/brand/{brand}/eans | Every GTIN the sources will prove for a brand. |
| GET | /v1/asin/{asin} | Unified document for the EAN behind an Amazon ASIN. |
| GET | /v1/asin/{asin}/offers | Offers for the EAN behind an ASIN. |
| GET | /v1/asin/{asin}/content | Content for the EAN behind an ASIN. |
| GET | /v1/asin/{asin}/content/fast | Harmonised content for one ASIN. |
| GET | /v1/asin/{asin}/images | Images for the EAN behind an ASIN. |
| GET | /v1/asin/{asin}/resellers | Resellers for the EAN behind an ASIN. |
| GET | /v1/countries | Markets and the engines behind them. |
| GET | /v1/retailers | Which engine surfaces which retailer. |
| GET | /v1/fx | ECB reference rates, units per 1 EUR. |
| GET | /v1/whereami | Visitor country from the edge header or browser language. |
| GET | /health | Process health and engine count. |
ASIN routes resolve the ASIN to an exact EAN first, then run the matching EAN route. marketplace defaults to de. Paths: GET /v1/asin/{asin}, GET /v1/asin/{asin}/offers, GET /v1/asin/{asin}/content, GET /v1/asin/{asin}/content/fast, GET /v1/asin/{asin}/images, GET /v1/asin/{asin}/resellers, plus GET /v1/whereami and GET /health.
Errors
Every 4xx and 5xx body is one JSON object:
{ "error": "EAN must be 8 to 14 digits" }| Status | When |
|---|---|
400 | Bad input: EAN not 8 to 14 digits, bad check digit, q or title not 3 to 200 characters, unknown condition or match token, days outside 1 to 3650, timeout outside 5 to 120, empty source selection. |
404 | No such route or file. Also when a lookup has no exact answer (for example an ASIN with no EAN). |
429 | Demo limiter (see below): too many searches a minute, or too many streams at once. |
502 | The search, FX table or resolver failed upstream. |
504 | The call hit its wall clock budget (60 seconds on the product routes). |
An empty offers list is not an error. It means nothing matched; read query and sources.
Limits and credits
Demo endpoint (/v1/demo/stream, the live box on the home page): 5 searches a minute per IP, 3 streams at once, fixed sources per country (you cannot pick engines). Query 3 to 80 characters, barcode or title. Over the limit you get a 429 with a plain sentence.
API keys and billing are not open yet. There is no key to send and nothing to top up. Keys and billing open soon; tell us what you need on the contact page.
Credits per call, as on the pricing page:
| What you ask for | Credits |
|---|---|
| Cached answer (younger than 6 hours) | 1 |
| Live GTIN lookup | 10 |
| Live title search | 25 |
| Price history for a product | +2 |
| Product content (specs, descriptions, all images) | +5 on a live call, +1 on a cached call |
| Streaming (SSE) | Same credits as the lookup behind it, no surcharge |
A cold product search takes 4 to 60 seconds depending on the sources. Prefer the stream when latency matters. Robots and agents: llms.txt, llms-full.txt.