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:

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

ParameterDefaultWhat it does
countriesNL,DEComma separated ISO 3166-1 alpha-2 codes. Picks the engines that serve those markets.
countryemptySingle ISO code, alias for countries (the spelling /content/fast uses). Wins over countries when set.
sourcesemptyComma separated engine ids (for example idealo.de,bol.com). Overrides countries.
cross_bordertrueInclude the multi-market aggregators alongside the country engines.
conditionemptyFilter offers: new, refurbished, used, comma separated. sources still lists every engine asked.
matchemptyFilter offers by scope: exact, grouped, other_variant, comma separated.
amazonfalseInclude the amazon.* engines. Their lookup takes about 30 seconds, so it is off unless you ask.
freshfalse/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

FieldWhat it is
retailerThe 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.currencySort key: total when shipping is known, else the item price. Currency as the source states it.
price.item, price.shipping, price.total, price.basisItem price and shipping when the source splits them. shipping is present only when the source gives it. basis is item or total.
price.amount_eurThe amount converted to EUR with the ECB rates of the day, or null when we have no rate.
conditionnew, refurbished or used. Harmonised from the source kind and its own grade label.
availabilityNormalised stock: in_stock, limited, out_of_stock, preorder. Absent when the source does not say.
countryISO code of the market this offer is sold in.
match.ean_verifiedTrue when the barcode is on the page and matches this GTIN.
match.scopeexact (this colour and size), grouped (the page pools variants), other_variant.
source, source_typeEngine 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

ParameterDefaultWhat it does
countryemptyOne 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.
daysfull seriesKeep only the last N calendar days, 1 to 3650.
freshfalseSkip 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

ParameterEndpointsWhat it does
sourcesbothComma separated engine ids. Overrides the fixed fast set and the wider second wave.
freshbothSkip the cache (content is cached 7 days, a miss 6 hours).
country/content/fastISO 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/fastAlso 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.

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.

  1. resolve=true and auto (default true), top candidate scores 0.6 or higher: run the EAN search for that GTIN. The answer carries query.resolved_gtin and query.candidates.
  2. resolve=true with candidates but no auto-resolve (auto=false, or the score is under 0.6): candidates only, offers empty. Pick one and call /v1/ean/{gtin} yourself.
  3. resolve=true and 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.

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 fieldMeaning
cluster_id"c1", "c2", … biggest first.
title, brand, imageThe most representative member title, the most common brand, the first member image.
gtinSet when the group carries exactly one proven barcode.
offer_count, source_countOffers in the group, and the sources they came from.
price_min_eur, price_max_eurPrice range of the group in EUR.
conditionsOffers per condition, e.g. {"new": 8, "used": 1}.
offer_idsMembers 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_indexesPositions 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

ParameterWhat it does
eanBarcode to search. 8 to 14 digits. Without it, title mode runs.
titleTitle query (3 to 200 characters). Required when ean is empty.
enginesComma separated engine ids. Empty = the default set for the call.
modeSet to title with an ean to also search by title.
brandOptional brand hint for the title search. Defaults to the first word of the title in title-only mode.
timeoutWall clock budget in seconds, 5 to 120. Default 30. Late answers are dropped.
resolveAdd one resolved event before done: up to 8 of the cheapest clickout URLs walked to their shop pages inside a 3 second budget.

Events

EventPayload keys
metatotal, ean. Title mode adds title, mode ("title"), skipped.
resultOne source: site, country, matched, ean_verified, title, brand, image, url, currency, offer_count, offers, status, note, verification, and the rest of the source row.
productDisplay 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).
galleryimages (each src, original, count, sources), raw_count. Adds unprocessed when the dedupe step did not finish and the raw source URLs are sent instead.
consensustitle. EAN mode only, when the title bridges start.
amazonAmazon panel, or error when it failed or ran out of budget. EAN mode only.
resolvedlinks, each with url, resolved_url, hops, retailer, domain, error. Only with resolve=true.
waveTitle 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.
doneengines_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.

MethodPathWhat it does
GET/v1/ean/{gtin}Unified document: product, buybox, offers, sources.
GET/v1/ean/{gtin}/offersOffers and buybox, with cache state per source.
GET/v1/ean/{gtin}/contentTitle, brand, description, specs, variant attributes.
GET/v1/ean/{gtin}/content/fastHarmonised content in about a second.
GET/v1/ean/{gtin}/imagesImages of the exact variant, duplicates removed.
GET/v1/ean/{gtin}/resellersOne row per retailer, cheapest first.
GET/v1/ean/{gtin}/price-historyPrice series per source, plus the combined EUR series.
GET/v1/titleTitle to GTIN to unified document.
GET/v1/search/streamSearch as Server-Sent Events, one event per source.
GET/v1/brand/{brand}/eansEvery 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}/offersOffers for the EAN behind an ASIN.
GET/v1/asin/{asin}/contentContent for the EAN behind an ASIN.
GET/v1/asin/{asin}/content/fastHarmonised content for one ASIN.
GET/v1/asin/{asin}/imagesImages for the EAN behind an ASIN.
GET/v1/asin/{asin}/resellersResellers for the EAN behind an ASIN.
GET/v1/countriesMarkets and the engines behind them.
GET/v1/retailersWhich engine surfaces which retailer.
GET/v1/fxECB reference rates, units per 1 EUR.
GET/v1/whereamiVisitor country from the edge header or browser language.
GET/healthProcess 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" }
StatusWhen
400Bad 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.
404No such route or file. Also when a lookup has no exact answer (for example an ASIN with no EAN).
429Demo limiter (see below): too many searches a minute, or too many streams at once.
502The search, FX table or resolver failed upstream.
504The 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 forCredits
Cached answer (younger than 6 hours)1
Live GTIN lookup10
Live title search25
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.