# Google Shopping Product API > Source: https://scrape.do/documentation/google-scraper-api/shopping/product/ Fetch the full product page (sellers, variants, specs, reviews, videos) for a single Google Shopping product as structured JSON The Google Shopping Product API is the companion endpoint to [`/plugin/google/shopping`](/documentation/google-scraper-api/shopping/search). It fetches the full product page for a single Google Shopping product — every seller offer, variants, specs, reviews, videos, and related discussions — as structured JSON. The recommended flow is **three steps**: call `/plugin/google/shopping` to find a product, capture its `catalog_id`, hit this endpoint to get the product page with its first sellers, then replay the `next_page_token` against [`/plugin/google/shopping/product/stores`](#paginating-every-seller) to walk the rest of the seller list. > [!NOTE] > **Credit Usage:** Each successful request costs **10 credits**. For bulk processing, use the [Async API with plugins](/documentation/async-api/plugins/). > [!NOTE] > **Identifier choice:** `catalog_id` (paired with `q`) is the durable input — persist it in your DB and replay it weeks or months later. `immersive_product_page_token` is short-lived (a few hours) and best used inside the same workflow that produced it. `product_id` always works but returns a lean response without variants / specs / reviews. > [!WARNING] > **Catalog IDs are region-specific.** Google maintains a separate catalog graph per market, so the same product has a different `catalog_id` in each region. If you need US, DE, and FR sellers for the same product, run `/plugin/google/shopping` once per region (passing `gl=us` / `gl=de` / `gl=fr`) and persist the catalog_id you receive in each market. ## Key Features - **Durable Catalog Input**: Pass `catalog_id` + `q` to look up a product by its long-lived Google catalog identifier. Catalog IDs are stable per region, so you can save them in your own storage and replay them later. - **Rich Product Data**: Title, gallery, brand, rating, ratings histogram, thumbnails, variants, specs, videos, customer review images, top insights, related discussions — all parsed into structured fields. - **Complete Seller List**: Default page returns the headline ~5 sellers and a continuation token (`next_page_token`). Replay it against [`/plugin/google/shopping/product/stores`](#paginating-every-seller) to walk every remaining seller. Pass `load_all_stores=true` to have the gateway walk the pagination for you in a single request. - **Per-seller Detail**: Price, shipping, tax, total, payment methods, delivery / returns details, merchant ID, thumbnail, badge text, sale & strikethrough prices, and carrier-style monthly installment plans (`installment_months`, `installment_down_now`, `total`) when applicable. - **Three Response Modes**: `catalog_oapv` (catalog mode, recommended), `oapv` (token mode, rich short-lived payload), and `html` (legacy product-id-only fallback). The chosen mode is echoed in `product_results.source`. - **Worldwide Coverage**: Locale via `gl` + `hl` + `location` / `uule`; carrier financing and EU comma-decimal formatting handled automatically. - **Device Emulation**: Desktop, mobile, or tablet layout with the `device` parameter. - **No Blocks or CAPTCHAs**: All anti-bot measures are handled automatically by Scrape.do. --- ## Endpoint ``` GET https://api.scrape.do/plugin/google/shopping/product ``` > [!NOTE] > Parameter keys are **case-insensitive**. You can use `product_id`, `Product_ID`, `gl`, `GL`, etc. --- ## Request Parameters ### Required | Parameter | Type | Description | |-----------|------|-------------| | `token` | string | Your Scrape.do API authentication token | ### Identity (choose one) Provide exactly one of the input combinations below. When you supply more than one, the priority is **`catalog_id` > `immersive_product_page_token` > `product_id`**. | Input | Type | Description | |-----------|------|-------------| | `catalog_id` + `q` | string + string | **Recommended.** Durable catalog identifier (digit-only) plus the search query. `q` is required when `catalog_id` is supplied — Google's catalog page returns nothing for an empty `q`. Returns the catalog response (`source: "catalog_oapv"`): full product page + the first ~5 sellers + a `next_page_token` when more sellers remain. | | `immersive_product_page_token` | string | Short-lived opaque base64 token from a `shopping_results[]` entry on `/plugin/google/shopping`. Returns the rich token response (`source: "oapv"`): variants, specs, reviews, videos, full seller list. Best used inside the same workflow that produced it. | | `page_token` | string | Alias for `immersive_product_page_token`. Behaves identically. | | `product_id` | string | Google product catalog ID (digit-only). Returns the HTML fallback (`source: "html"`): title, product_id, sellers. Required when you want `sort_by`. | > [!WARNING] > **Do not send `next_page_token` to this endpoint.** Continuation cursors go to [`/plugin/google/shopping/product/stores`](#paginating-every-seller). Sending one here returns HTTP 400 with a redirect message. ### Device | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `device` | string | `desktop` | Device type. One of `desktop`, `mobile`, `tablet`. | ### Routing | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `google_domain` | string | `google.com` | Google domain to query (e.g., `google.de`, `google.com.tr`). | ### Sort | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `sort_by` | string | — | Seller sort order on the HTML fallback path. One of `base_price`, `total_price`, `promotion`, `seller_rating`. | > [!WARNING] > `sort_by` is **only honored on the HTML fallback path**. Passing it alongside `immersive_product_page_token` forces fallback and you lose variants, specs, reviews, and videos. To re-sort sellers without losing the rich payload, sort `stores[]` client-side by `extracted_total` or `extracted_price`. ### Seller list expansion | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `load_all_stores` | boolean | `false` | **Catalog mode only.** When `true`, the gateway walks the continuation cursor internally and returns every seller in one response. The walk is hard-capped (default ~10 pages, ~100 sellers). A capped or partially failed walk returns the sellers collected so far, a `next_page_token` for manual continuation, and the `Scrape.do-Auto-Page-Truncated: true` response header. A walk that fails before collecting any sellers returns HTTP 502. Each additional upstream hop counts toward the request's credit cost. | | `more_stores` | boolean | `false` | **Token mode only (legacy).** When `true`, asks the legacy token flow to expand the seller list inline. Catalog mode uses `next_page_token` + [`/plugin/google/shopping/product/stores`](#paginating-every-seller) instead. | > [!NOTE] > **Cursor opacity:** `next_page_token` is an opaque, signed string the gateway emits in the response. Replay it verbatim against [`/plugin/google/shopping/product/stores`](#paginating-every-seller) — do not parse, transform, or generate cursors yourself. The cursor is **omitted from the response when the seller list is exhausted** — that's your signal that there are no more sellers to fetch. ### Localization | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `hl` | string | `en` | Host language. Overrides the language embedded in `immersive_product_page_token` when both are present. | | `gl` | string | `us` | Country perspective. Overrides the country embedded in `immersive_product_page_token` when both are present. One exception on the token (seller-list) path: the upstream session the token replays into is region-bound, so a `gl` that would move the request to a different market than the token was minted in yields to the token's country there. A `gl` from the same market as the token is still honored, and the `hl` language is unaffected. | | `location` | string | — | Location name (e.g., `Austin,Texas,United States`). Auto-encoded to UULE when `uule` is not set. | | `uule` | string | — | Google-encoded location parameter. Overrides `location` when both are set. | See [Localization](/documentation/google-scraper-api/localization) for the full list of supported `hl`, `gl`, and `google_domain` values. --- ## Example Usage ### By `catalog_id` (recommended — durable input) ```bash # 1. Look up the product and grab its catalog_id CID=$(curl -s "https://api.scrape.do/plugin/google/shopping?q=PodTrak+P8+ZP8&hl=en&gl=us&token=YOUR_TOKEN" \ | jq -r '.shopping_results[0].catalog_id') # 2. Fetch the product page + first sellers + continuation token curl "https://api.scrape.do/plugin/google/shopping/product?catalog_id=$CID&q=PodTrak+P8+ZP8&hl=en&gl=us&token=YOUR_TOKEN" ``` ### Catalog mode in another market ```bash # Same product, German market — re-search per region to get the region-scoped catalog_id CID=$(curl -s "https://api.scrape.do/plugin/google/shopping?q=iPhone+17+Pro&hl=de&gl=de&token=YOUR_TOKEN" \ | jq -r '.shopping_results[0].catalog_id') curl "https://api.scrape.do/plugin/google/shopping/product?catalog_id=$CID&q=iPhone+17+Pro&hl=de&gl=de&token=YOUR_TOKEN" ``` ### Get every seller in one call ```bash curl "https://api.scrape.do/plugin/google/shopping/product?catalog_id=$CID&q=PodTrak+P8+ZP8&load_all_stores=true&token=YOUR_TOKEN" ``` ### By `immersive_product_page_token` (rich short-lived response) ```bash # 1. Mint a token from the search endpoint TOKEN=$(curl -s "https://api.scrape.do/plugin/google/shopping?q=Sony+WH-1000XM5&token=YOUR_TOKEN" \ | jq -r '.shopping_results[0].immersive_product_page_token') # 2. Replay it against the product endpoint (variants/specs/reviews/videos returned) curl "https://api.scrape.do/plugin/google/shopping/product?immersive_product_page_token=$TOKEN&token=YOUR_TOKEN" ``` ### By `product_id` (HTML fallback) ```bash curl "https://api.scrape.do/plugin/google/shopping/product?product_id=7000474567848623813&token=YOUR_TOKEN" ``` --- ## Response ### Top-Level Structure ```json { "search_parameters": { ... }, "product_results": { ... } } ``` ### `search_parameters` Echo of the request parameters sent by the client. ```json { "engine": "google_shopping_product", "product_id": "7000474567848623813", "page_token": "eyJlaSI6ImFhX1Nh...", "google_domain": "google.com", "hl": "en", "gl": "us", "device": "desktop", "more_stores": false, "next_page_token": "" } ``` Optional fields appear only when sent: `location`, `uule`, `sort_by`. ### `product_results` (rich path) Returned when you pass `immersive_product_page_token` and do not pass `sort_by`. `source` is `"oapv"`. ```json { "title": "Sony WH-1000XM5 Wireless Noise Canceling Headphones", "product_id": "7000474567848623813", "brand": "Sony", "rating": 4.6, "reviews": 4800, "reviews_label": "4,800 reviews", "ratings": [ { "stars": 5, "count": 3400 }, { "stars": 4, "count": 900 } ], "thumbnails": [ "https://encrypted-tbn0.gstatic.com/...", "..." ], "reviews_images": [ "https://encrypted-tbn0.gstatic.com/...", "..." ], "stores": [ { /* see stores[] table below */ } ], "next_page_token": "eyJzb3JpIjo1LCJtbm8iOjEwfQ", "variants": [ { "type": "Color", "options": [ { "text": "Black", "selected": true } ] } ], "specs": [ { "section": "General", "items": [ { "label": "Wireless", "value": "Yes" } ] } ], "videos": [ { "title": "Sony WH-1000XM5 Review", "source": "youtube.com", "thumbnail": "...", "link": "..." } ], "user_reviews": [ { "rating": 5, "title": "Best headphones", "body": "...", "author": "John D." } ], "top_insights": [ { "section": "Sound quality", "items": [ "Crystal-clear audio", "Strong bass" ] } ], "about_the_product": { "description": "..." }, "more_options": [ { "title": "Sony WH-1000XM4", "link": "...", "thumbnail": "..." } ], "discussions_and_forums": [ { "title": "...", "source": "reddit.com", "link": "...", "snippet": "..." } ], "extension_ids": { "catalog_id": "...", "gpc_id": "..." }, "source": "oapv" } ``` ### `product_results` (HTML fallback) Returned when you pass only `product_id`, or when you pass `sort_by` alongside the token. `source` is `"html"`. Lean shape — no variants, specs, videos, or reviews — but the seller list works. ```json { "title": "Sony WH-1000XM5 Wireless Noise Canceling Headphones", "product_id": "7000474567848623813", "stores": [ { /* see stores[] table below */ } ], "source": "html" } ``` ### `stores[]` Fields Each entry in `product_results.stores[]` describes a single merchant offer for the product. | Field | Type | Always present | Description | |-------|------|:-:|-------------| | `position` | int | yes | 1-based position in the seller list | | `name` | string | yes | Merchant name (e.g., `"Best Buy"`, `"Amazon.com"`) | | `logo` | string | | Merchant favicon URL | | `link` | string | | Direct link to the merchant's product page. Google `/goto` redirects are resolved before the URL is returned. | | `title` | string | | Product title as listed on the merchant's site (may differ slightly from `product_results.title`) | | `rating` | float | | Merchant rating out of 5 | | `reviews` | float | | Merchant review count (suffix-aware, e.g., `1.2K` is parsed as `1200`) | | `price` | string | | Formatted base price (`"$248.00"`, `"1.299,00 TL"`) | | `extracted_price` | float | | Numeric base price | | `currency` | string | | ISO currency code when detected (`"USD"`, `"EUR"`, `"TRY"`) | | `shipping` | string | | Formatted shipping cost (`"Free"`, `"$8.99"`) | | `shipping_extracted` | float | | Numeric shipping cost (`0` when free) | | `tax` | string | | Formatted tax line when shown | | `extracted_tax` | float | | Numeric tax value | | `tax_hint` | string | | Tax hint shown by Google when an exact tax line isn't available (e.g., `"+tax"`) | | `total` | string | | Formatted total (price + shipping + tax) | | `extracted_total` | float | | Numeric total | | `payment_methods` | string | | Accepted payment methods (e.g., `"PayPal, Affirm accepted"`) | | `details_and_offers` | string[] | | Per-offer notes — delivery window, stock, return policy (e.g., `["Free delivery between May 21 – 27", "In stock", "90-day returns"]`) | | `merchant_id` | string | | Google's internal merchant ID | | `thumbnail` | string | | Merchant-specific product thumbnail when distinct from the canonical image | | `tag` | string | | Badge text Google renders on the offer, in the page locale (e.g. `"Best price"`, `"Bester Preis"`, `"Meilleur prix"`). Text is carried verbatim, not normalized. | | `original_price` | string | | Pre-discount price as displayed, when Google renders a strikethrough | | `extracted_original_price` | float | | Numeric pre-discount price | | `discount` | string | | Discount badge text (e.g., `"20% OFF"`) | | `installment_months` | int | | Number of monthly installments for carrier-style or BNPL offers (Verizon, AT&T, T-Mobile, congstar, MediaMarkt mit Vertrag, …). **When set, `price` is the monthly figure, not the lump sum** — read `total` / `extracted_total` for the lifetime cost. | | `installment_down_now` | string | | Upfront payment for the installment plan, as displayed (e.g., `"$0"`, `"60 €"`). | --- ## Three Response Modes The plugin picks the response mode automatically based on what you sent: | You sent | Mode | `source` | What you get | |---|---|---|---| | `catalog_id` + `q` (no `sort_by`) | Catalog | `"catalog_oapv"` | Full product page (title, gallery, description, ratings) + the first ~5 sellers + `next_page_token` when more sellers exist | | `immersive_product_page_token` (no `sort_by`) | Token | `"oapv"` | Full payload — variants, specs, reviews, videos, all sellers | | `product_id` only | HTML fallback | `"html"` | Title, product_id, sellers | | Any input **and** `sort_by` | HTML fallback | `"html"` | Sorted sellers, no variants / specs / reviews / videos | Inspect `product_results.source` to know which mode served the response. Catalog mode is the recommended default for any workflow that persists product identifiers. --- ## Paginating every seller Catalog mode returns up to ~5 sellers inline plus a `next_page_token` when more remain. Use it in one of two ways: ### Option 1 — Manual pagination via `/plugin/google/shopping/product/stores` The dedicated continuation endpoint accepts the signed token and returns the next batch of sellers (up to ~10 per call). It's the most cost-efficient path because each call is a single hop when the embedded session is still fresh. ```bash GET https://api.scrape.do/plugin/google/shopping/product/stores ``` | Parameter | Type | Description | |---|---|---| | `token` | string | Your Scrape.do API authentication token | | `catalog_id` | string | The same `catalog_id` you used on the matching `/plugin/google/shopping/product` call. Must match the catalog_id embedded inside `next_page_token`. | | `next_page_token` | string | The `next_page_token` value from a prior response. Replay verbatim. The locale (`q`, `gl`, `hl`) is carried inside the token and reused server-side — you do not pass them again. | ```bash # After the catalog call: NEXT=$(echo "$RESP" | jq -r '.product_results.next_page_token // empty') while [ -n "$NEXT" ]; do RESP=$(curl -s "https://api.scrape.do/plugin/google/shopping/product/stores?catalog_id=$CID&next_page_token=$NEXT&token=YOUR_TOKEN") echo "$RESP" | jq '.product_results.stores | length' NEXT=$(echo "$RESP" | jq -r '.product_results.next_page_token // empty') done ``` Response shape is **lean** — only `product_id`, `stores[]`, and (when more sellers remain) `next_page_token`. Title, gallery, variants, etc. are not re-emitted; you already have them from the catalog call. `source` is `"catalog_oapv_more"`. The token is signed and bound to the supplied `catalog_id`. Sending a different `catalog_id` returns HTTP 400 (`"catalog_id mismatch"`). ### Option 2 — One-shot `load_all_stores=true` For workflows that want every seller in a single response, pass `load_all_stores=true` on the catalog call. The gateway walks the continuation cursor for you and returns the merged seller list. The walk is billed per upstream hop and capped at about 10 pages by default. If the cap is reached or a later page fails, the response keeps the sellers collected so far and includes a `next_page_token` for manual continuation plus the `Scrape.do-Auto-Page-Truncated: true` response header. If the walk fails before collecting any sellers, the endpoint returns HTTP 502 and the request is not charged. ```bash curl "https://api.scrape.do/plugin/google/shopping/product?catalog_id=$CID&q=iPhone+17+Pro&load_all_stores=true&token=YOUR_TOKEN" ``` ### Legacy `more_stores` on the token path The legacy token flow (`immersive_product_page_token`) uses `more_stores=true` to inline the expanded seller list. New integrations should prefer the catalog path; `more_stores` stays available for existing callers. --- ## Handling Session Expiry `immersive_product_page_token` values are tied to an upstream Google session that ages out after several hours. When that happens, the plugin returns HTTP **410 Gone**: ```json { "error": "session expired", "message": "The immersive_product_page_token is older than 6h0m0s and is past Google's product-viewer session lifetime. Fetch /plugin/google/shopping again to mint a fresh token, then retry." } ``` (The exact lifetime message may vary; the recovery is always the same.) > [!WARNING] > **Recovery:** When you receive HTTP 410 on `/plugin/google/shopping/product`, re-fetch `/plugin/google/shopping` with the same query, grab a fresh `immersive_product_page_token` from the response, then retry the product call with the new token. Don't store tokens long-term — treat them as ephemeral cursors. See [Transient Errors](/documentation/google-scraper-api/transient-errors#session-expiry-on-immersive-product-tokens) for the full rotate-on-expiry pattern. --- ## Error Handling All endpoints return errors in a consistent JSON format: ```json { "error": "error_code", "message": "Human readable error message" } ``` ### Common Error Codes | Status | Error | Description | |--------|-------|-------------| | `400` | `token is required` | Missing authentication token | | `400` | `q is required when catalog_id is supplied` | `catalog_id` was set without a non-empty `q` | | `400` | `invalid catalog_id` | `catalog_id` was empty, too long, or contained non-digit characters | | `400` | `next_page_token belongs on /plugin/google/shopping/product/stores` | A pagination cursor was sent to `/product` — replay it against `/product/stores` instead | | `400` | `catalog_id mismatch` | On `/product/stores`, the supplied `catalog_id` does not match the catalog_id embedded inside `next_page_token` | | `400` | `product_id (or immersive_product_page_token) is required` | No identifier was supplied at all | | `400` | `invalid immersive_product_page_token` | Token could not be base64-decoded or its JSON payload could not be parsed | | `400` | `invalid product_id` | `product_id` was empty, too long, or contained non-digit characters | | `400` | `device must be one of: desktop, mobile, tablet` | Invalid `device` value | | `400` | `invalid google_domain` | Domain not in the supported list | | `400` | `invalid sort_by` | Sort value not in `base_price`, `total_price`, `promotion`, `seller_rating` | | `400` | `invalid next_page_token` | The pagination cursor's signature failed, payload couldn't be parsed, or one of the bounded fields fell out of range. Fetch a fresh first page from `/plugin/google/shopping/product` and resume from its cursor | | `410` | `session expired` | Token is past its upstream session lifetime. Re-mint via `/plugin/google/shopping` (token path) or simply retry the same request (catalog path — the server re-mints transparently and continues). See [Handling Session Expiry](#handling-session-expiry) below | | `502` | `request failed` | Transient upstream failure. Retry | | `502` | `unexpected response` | Non-200 response from Google | | `502` | `failed to load sellers` | `load_all_stores=true` failed before collecting any sellers. Retry the request | | `500` | `failed to parse product viewer response` | Google returned an unparseable product viewer payload (rare; if persistent, report to support) | | `500` | `decompression failed` | Response body decompression error | > [!NOTE] > **An empty seller list means no sellers are available in that market.** When a `catalog_id` is valid in Google's index but the regional catalog graph has no sellers for it (common for US-specific catalogs queried with `gl=de` / `gl=fr` etc.), the response is `200` with `product_results.stores = []`. Re-search in the target region (`/plugin/google/shopping?gl=de…`) to get its region-specific catalog ID. A failed `load_all_stores=true` walk does not return an ambiguous empty response: it returns HTTP 502 when no sellers were collected, or a partial HTTP 200 response with `Scrape.do-Auto-Page-Truncated: true` when earlier pages succeeded. ---