# Google Shopping

Google Shopping product and offer data. Search by keyword, by Google product ID, or by a
structured product record for higher-precision matching.
See the [Google Shopping use case →](https://kwery.co/use-cases/google-shopping-price-api)

## Supported countries 

`country` accepts: `us` `de` `uk (gb)` `au` `at` `br` `ca` `cz` `dk` `fr` `in` `ie` `it` `jp` `mx` `no` `pl` `nz` `nl` `ru` `sg` `za` `es` `se` `fi` `ch` `tr` `ar` `cl` `co` `be` `gr` `hu` `pt` `ro` `sk` `hk` `id` `il` `my` `ph` `sa` `kr` `tw` `th` `ua` `vn` `ae`

`uk` and `gb` are two codes for the same UK marketplace — not a separate locale.

## Keys 

| Key | Values | Description |
|  --- | --- | --- |
| `term` | Free text | Keyword search — returns a list of product cards. |
| `id` | Google Shopping product ID | Direct product lookup with offers. |
| `product` | Structured product record | **GTIN plus additional product data** — the highest-precision lookup (see format below). |


### `product` — send a GTIN plus additional data 

Google Shopping has no direct GTIN lookup, so a bare GTIN can be ambiguous. With
`key: product` you send the GTIN **together with everything else you know about the
product** — title, brand, your price, a deeplink — and kwery uses all of it to select the
right Google Shopping product. This is the recommended key when your catalog has more than
just an EAN.

The value is a `||`-separated record; empty fields are allowed, position is fixed:

```
title||brand||gtin||price||deeplink||size
```

Example:

```
STABILO point 88||STABILO||4006381333962||6.49||https://www.example.de/stabilo-point-88||
```

The title and the deeplink host carry the most matching signal — include them whenever you
have them.

### How `product` matching works 

1. **Known match first.** If this GTIN was matched and verified before, we go straight to
that Google Shopping product. The GTIN is the key we remember a verified match under; it
is not typed into the Google search itself.
2. **Search.** Otherwise we search Google Shopping with your title, the deeplink's host
name (so that retailer surfaces in the offers) and the size field, and take the best
candidate product.
3. **Score the candidate against your record.** The product page is scored on how closely
its title matches yours, how close its prices are to your price, and whether your brand
appears in it. Title similarity carries the most weight, price next, brand least. If a
retailer from your `deeplink` appears in the offer list, the match is treated as
**verified** and scores 1.0 outright.
4. **Gate.** Below the confidence threshold the result is returned as `not found` rather
than as a possibly wrong product — price data about the wrong product is worse than none.
5. **Remember.** A verified match is cached, so the next lookup of that GTIN skips the search
and scores 1.0 on identity.


The confidence from step 3 is published on the result as `trust.components.identity`. For a
result that only just cleared the gate, identity sits just above the threshold; it is not
rounded up. Gate on **`identity`**, not on `trust.score`: identity is one of five components,
so a weak match with perfect pricing and freshness still produces a high overall score.

What moves the match most, in order:

- **`deeplink`** — a URL of the product at any retailer you know sells exactly this variant.
A retailer match verifies identity outright, and it is the one signal that does not depend
on how Google titled the catalog page.
- **`title`** — as variant-specific as your catalogue allows, including the model number.
"Nest Learning Thermostat 3rd Gen T3017US White" scores better than "Nest Learning
Thermostat 3rd Generation White".
- **`price`** — your current price. Without it the price signal is scored neutral.
- **`brand`** — use the brand as Google prints it in product titles. The brand signal is a
literal match against the page title: "Nest" scores on a page titled "Nest Learning
Thermostat"; "Google Nest" does not.


**Variants on family pages.** Google sometimes groups colour or storage variants under one
catalog entry, and that page's `offers[]` then mixes variants. Offers whose title
contradicts yours on colour, capacity, size, pack count or a sibling model code are
flagged in `trust.flagged_offers`, with the class in `trust.flagged_reasons`, and pull
`offer_contamination` down (see the [confidence score guide](/guides/trust#components)).
Put the variant into your title — "Nest Learning Thermostat 3rd Gen T3017US White" — so
there is something to contradict. An offer whose title omits the variant cannot be caught
this way; keep your own check on `offers[].name` for the distinctions that matter to you.

## Topics 

| Topic | Description |
|  --- | --- |
| `search` (default) | Product lookup with seller offers. |


## Product `content` fields 

List responses carry a top-level `currency` beside the `price_min` / `price_avg` /
`price_max` aggregates.

| Field | Description |
|  --- | --- |
| `product_id` | Google Shopping product ID. |
| `identity` | Product identity key. |
| `name` | Product title. |
| `brand_name` | Brand, where available. |
| `description` | Product description. |
| `url` | Product URL. |
| `rating` / `rating_count` | Aggregate reviews. |
| `price_min` / `price_avg` / `price_max` | Offer price aggregates. |
| `offers_count` | Number of offers gathered. |
| `total_offers_count` | Total seller count Google reports for the product (may exceed `offers_count`). |
| `offers` | Seller offers (seller name, price, currency, shipping, condition). |


For `key: term`, the result is a list of product cards, each with the product `id` you can use
for a follow-up lookup.

> When `total_offers_count` is greater than `offers_count`, the snapshot is partial — more
sellers exist than were gathered. On stream deliveries with `include_meta: true`, this is
also surfaced as `meta.partial`.


### More sellers with `max_pages` 

The first page carries the sellers Google shows on the product page, usually 3 to 5. Set
`max_pages` above 1 to collect more: each additional page adds up to 10 sellers to the same
result's `offers`, and `offers_count` and the price fields are recomputed over the full set.

- Additional pages are only fetched when `total_offers_count` says more sellers exist.
- The effective maximum is **5** (the first page plus 4 additional pages).
- Each additional page that returned sellers is billed at Google's
[credit weight](/guides/pricing#source-weights). Pages that fail are free.
- `max_pages` is ignored for `wait` (synchronous) requests.
- If an additional page cannot be fetched, the result keeps the sellers already gathered and
`meta.partial` stays `true`.


## Example request 

cURL
```bash
curl -s https://api.kwery.co/job \
  -H "Authorization: Bearer $KWERY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"source":"google","country":"de","key":"product","values":["STABILO point 88||STABILO||4006381333962||||"]}'
```

Node.js
```js
const res = await fetch('https://api.kwery.co/job', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.KWERY_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ source: 'google', country: 'de', key: 'product', values: ['STABILO point 88||STABILO||4006381333962||||'] }),
});
const data = await res.json();
if (data.error) throw new Error(data.message);
console.log('job id:', data.job._id);
```

Python
```python
import os, requests

res = requests.post(
    "https://api.kwery.co/job",
    headers={"Authorization": f"Bearer {os.environ['KWERY_API_KEY']}"},
    json={"source": "google", "country": "de", "key": "product", "values": ["STABILO point 88||STABILO||4006381333962||||"]},
)
data = res.json()
if data.get("error"):
    raise RuntimeError(data["message"])
print("job id:", data["job"]["_id"])
```

JavaScript
```js
const res = await fetch('https://api.kwery.co/job', {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${KWERY_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ source: 'google', country: 'de', key: 'product', values: ['STABILO point 88||STABILO||4006381333962||||'] }),
});
const data = await res.json();
if (data.error) throw new Error(data.message);
console.log('job id:', data.job._id);
```

PHP
```php
<?php
$ch = curl_init('https://api.kwery.co/job');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_POST => true,
  CURLOPT_HTTPHEADER => [
    'Authorization: Bearer ' . getenv('KWERY_API_KEY'),
    'Content-Type: application/json',
  ],
  CURLOPT_POSTFIELDS => '{"source":"google","country":"de","key":"product","values":["STABILO point 88||STABILO||4006381333962||||"]}',
]);
$data = json_decode(curl_exec($ch), true);
if ($data['error']) { throw new Exception($data['message']); }
echo 'job id: ' . $data['job']['_id'];
```

Go
```go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
)

func main() {
	body := []byte(`{"source":"google","country":"de","key":"product","values":["STABILO point 88||STABILO||4006381333962||||"]}`)
	req, _ := http.NewRequest("POST", "https://api.kwery.co/job", bytes.NewBuffer(body))
	req.Header.Set("Authorization", "Bearer "+os.Getenv("KWERY_API_KEY"))
	req.Header.Set("Content-Type", "application/json")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	var data map[string]any
	json.NewDecoder(resp.Body).Decode(&data)
	fmt.Println("job:", data["job"])
}
```

Java
```java
var client = java.net.http.HttpClient.newHttpClient();
var request = java.net.http.HttpRequest.newBuilder()
    .uri(java.net.URI.create("https://api.kwery.co/job"))
    .header("Authorization", "Bearer " + System.getenv("KWERY_API_KEY"))
    .header("Content-Type", "application/json")
    .POST(java.net.http.HttpRequest.BodyPublishers.ofString(
        "{\"source\":\"google\",\"country\":\"de\",\"key\":\"product\",\"values\":[\"STABILO point 88||STABILO||4006381333962||||\"]}"))
    .build();
var response = client.send(request, java.net.http.HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
```

C#
```csharp
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization",
    $"Bearer {Environment.GetEnvironmentVariable("KWERY_API_KEY")}");
var body = new StringContent(
    """{"source":"google","country":"de","key":"product","values":["STABILO point 88||STABILO||4006381333962||||"]}""",
    System.Text.Encoding.UTF8, "application/json");
var response = await client.PostAsync("https://api.kwery.co/job", body);
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

Ruby
```ruby
require 'net/http'
require 'json'

uri = URI('https://api.kwery.co/job')
req = Net::HTTP::Post.new(uri, {
  'Authorization' => "Bearer #{ENV['KWERY_API_KEY']}",
  'Content-Type' => 'application/json',
})
req.body = '{"source":"google","country":"de","key":"product","values":["STABILO point 88||STABILO||4006381333962||||"]}'
data = JSON.parse(Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }.body)
raise data['message'] if data['error']
puts "job id: #{data['job']['_id']}"
```

## Example response 

Results are fetched with `GET /job/{id}/download` once the job is `finished`. This is a **real response**, captured live on 2026-09-14 — the exact payload a self-serve account receives for the request above.
The payload is complete and unedited. `offers_count: 5` against `total_offers_count: 44` shows a partial snapshot — Google reports 44 sellers, 5 were gathered on this page (see the note above the field table). The `product` record resolved the exact catalog product (`identity`), which is why `trust.score` is 0.98.

```json
{
  "job_id": "6aa7b6bdda6a238d129dc3e7",
  "status": "finished",
  "results": [
    {
      "query": {
        "source": "google",
        "country": "de",
        "key": "product",
        "value": "STABILO point 88||STABILO||4006381333962||||",
        "topic": "search",
        "max_pages": 1,
        "max_age": 1200
      },
      "content": {
        "product_id": "1570378438328501500",
        "identity": "catalogid:1570378438328501500,query:STABILO point 88",
        "name": "Fineliner STABILO point 88",
        "brand_name": "STABILO",
        "description": "Der Fineliner STABILO point 88 im Etui überzeugt durch seine präzise, metallgefasste Spitze mit einer Stärke von 0,4 mm, die auch für den Einsatz mit Schablonen geeignet ist. Die Tinte auf Wasserbasis ist in 8 Farben sortiert und lichtbeständig. Ein weiterer Vorteil des STABILO point 88 Fineliners ist, dass er einige Tage offen liegen bleiben kann, ohne einzutrocknen. Die Kappe lässt sich am Stiftende aufstecken und ist farblich passend zur jeweiligen Schreibfarbe gestaltet. Der Schaft des Fineliners ist orange mit weißen Kantenstreifen und besitzt eine Griffzone aus Kunststoff. Das Etui enthält insgesamt 8 Stifte des STABILO point 88 und eignet sich damit ideal für vielfältige Schreib- und Zeichenaufgaben in unterschiedlichen Farben.",
        "url": "https://www.google.de/search?ibp=oshop&q=STABILO+point+88&prds=catalogid:1570378438328501500&gl=de&hl=de&uule=w+CAIQICIHR2VybWFueQ",
        "rating": 0,
        "rating_count": 0,
        "price_avg": 6.64,
        "price_max": 8,
        "price_min": 5.36,
        "offers_count": 5,
        "total_offers_count": 44,
        "offers": [
          {
            "name": "STABILO - Fineliner - point 88 | 8er Pack",
            "price": 5.93,
            "currency": "EUR",
            "shipping": 0,
            "total": 5.93,
            "seller_name": "Amazon.de - Amazon.de-Seller",
            "seller_url": "https://www.amazon.de",
            "url": "https://www.amazon.de/Fineliner-STABILO-point-verschiedenen-Farben/dp/B08YFBLP4Z?source=ps-sl-shoppingads-lpcontext&ref_=fplfs&psc=1&smid=A2LZXSFK6LSJD5",
            "seller_rating": null,
            "seller_rating_count": null,
            "marketplace_name": "N/A",
            "seller_type": "standalone-shop"
          },
          {
            "name": "STABILO point 88 neue Farben sortiert, 8er-Set, Kunststoffetui (88/8-04)",
            "price": 5.93,
            "currency": "EUR",
            "shipping": 0,
            "total": 5.93,
            "seller_name": "Geizhals Preisvergleich",
            "seller_url": "https://geizhals.de",
            "url": "https://geizhals.de/stabilo-point-88-2021-farben-sortiert-88-8-04-a2619174.html?hloc=de&hloc=at&nocookie=1&highlight=409480119035&mtm_campaign=css&mtm_kwd=2619174&mtm_source=google_css&gcsscid=1",
            "seller_rating": null,
            "seller_rating_count": null,
            "marketplace_name": "N/A",
            "seller_type": "standalone-shop"
          },
          {
            "name": "STABILO point 88 Fineliner - 0,4 mm - 8er Etui",
            "price": 5.36,
            "currency": "EUR",
            "shipping": 0,
            "total": 5.36,
            "seller_name": "Bürohengst",
            "seller_url": "https://www.buerohengst.com",
            "url": "https://www.buerohengst.com/stabilo-point-88-fineliner-0-4-mm-8er-etui",
            "seller_rating": 4.698,
            "seller_rating_count": 1478,
            "marketplace_name": "N/A",
            "seller_type": "standalone-shop"
          },
          {
            "name": "Fineliner STABILO point 88",
            "price": 8,
            "currency": "EUR",
            "shipping": 4.95,
            "total": 12.95,
            "seller_name": "stabilo.com",
            "seller_url": "https://www.stabilo.com",
            "url": "https://www.stabilo.com/de/fineliner-stabilo-point-88/eo88-8-04",
            "seller_rating": 0,
            "seller_rating_count": 0,
            "marketplace_name": "N/A",
            "seller_type": "standalone-shop"
          },
          {
            "name": "STABILO Fineliner point 88",
            "price": 7.99,
            "currency": "EUR",
            "shipping": 3.95,
            "total": 11.94,
            "seller_name": "Thalia.de",
            "seller_url": "https://www.thalia.de",
            "url": "https://www.thalia.de/ul/shop/home/artikeldetails/A1060621280",
            "seller_rating": 4.756,
            "seller_rating_count": 989,
            "marketplace_name": "N/A",
            "seller_type": "standalone-shop"
          }
        ],
        "currency": "EUR"
      },
      "success": true,
      "updated_at": "2026-09-14T08:56:48.497Z",
      "trust": {
        "score": 0.98,
        "components": {
          "identity": 0.9099999999999999,
          "completeness": 1,
          "price_integrity": 1,
          "offer_contamination": 1,
          "freshness": 1
        },
        "flagged_offers": []
      },
      "served_from": "live",
      "observed_at": "2026-09-14T08:56:48.468Z"
    }
  ]
}
```

The `trust` block is explained in [Confidence score](/guides/trust); `served_from` and `observed_at` in [Audit & provenance](/guides/audit-and-provenance).