google-shopping-api
Google Shopping API
Google Shopping as JSON. Forty products a page, each with what it costs now, what it cost before, who is selling it and how it is rated, with prices parsed into numbers so comparing them is arithmetic rather than string work.
Endpoint
One credit per request. A page carries forty products, which is the whole grid Google renders before it asks for another page.
curl "https://api.serpix.io/v1/shopping?q=nike+air+max&gl=us" \
-H "Authorization: Bearer $SERPIX_API_KEY"The same parameters are accepted as a JSON body on POST /v1/shopping:
curl -X POST "https://api.serpix.io/v1/shopping" \
-H "Authorization: Bearer $SERPIX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"q": "nike air max", "gl": "us"}'Parameters
| field | type | default | description |
|---|---|---|---|
| qrequired | string | – | The search query. |
| gl | string | – | Country to shop in, two-letter code. Sets the currency and the merchants: the same query on gb comes back in sterling from UK sellers. Defaults to us. |
| hl | string | en | Language of the results page, two-letter code. |
Results
| field | type | default | description |
|---|---|---|---|
| device | desktop | mobile | tablet | desktop | Results as Google serves them to a desktop browser or a phone. |
| page | integer | 1 | 1-based page. Each page is a fresh forty products; pages do not overlap. One credit per page. |
| autocorrect | boolean | true | Set false to search the query exactly as typed, excluding results Google returns for its spell-corrected guess. |
| safe | active | off | off | SafeSearch. active filters explicit results. |
Rejected parameters
Unknown parameters are never silently ignored. A request carrying one is rejected with 422 before any search runs, so a typo can't bill you for results you didn't ask for. Common parameters from raw Google URLs and other SERP APIs get a pointed message naming the equivalent here:
| field | type | default | description |
|---|---|---|---|
| num | 422 | – | Google serves a fixed grid of forty products. Paginate with page. |
| start | 422 | – | Use page, a 1-based page number, instead of a result offset. |
| tbs | 422 | – | Google ignores the price range on this vertical: a $100 ceiling came back with 25 of 40 products priced above it. |
| min_price / max_price / ppr_min / ppr_max | 422 | – | The same inert price filter, under its other names. |
| nfpr | 422 | – | Use autocorrect. |
| location / uule | 422 | – | Use gl and hl. City-level geotargeting is not wired for this vertical. |
| lr | 422 | – | Not supported on this vertical. |
| udm / tbm | 422 | – | Not needed. This endpoint is the shopping vertical. |
| engine | 422 | – | Not needed. This endpoint is Google Shopping. |
| api_key | 422 | – | Authenticate with the Authorization header, never the query string. |
HTTP/2 422
content-type: application/json
{
"detail": [
{
"type": "value_error",
"loc": ["query"],
"msg": "Value error, `num` is not supported: Google serves a fixed page of 40 products; paginate with `page`",
"input": { "q": "nike air max", "num": "100", ... }
}
]
}Response
A search returns one JSON object: two envelope objects that are always present (search_metadata, search_parameters) and the ranking as shopping_results. A field that wasn't served is omitted entirely, never null, never [], so a presence check is all you need before reading one.
{
"search_metadata": {
"id": "srx_TyU4WNhPmEBr48wd",
"status": "success",
"created_at": "2026-07-28T18:15:34Z",
"took_ms": 5637,
"parser_version": "2026.07.1",
"google_url": "https://www.google.com/search?q=nike+air+max&udm=28&hl=en&gl=us&...",
"html_url": "https://api.serpix.io/searches/srx_TyU4WNhPmEBr48wd/GZImiWL8CuCQ2QSx.html",
"json_url": "https://api.serpix.io/searches/srx_TyU4WNhPmEBr48wd/GZImiWL8CuCQ2QSx.json"
},
"search_parameters": {
"q": "nike air max",
"gl": "us",
"hl": "en",
"device": "desktop",
"page": 1
},
"shopping_results": [
{
"position": 1,
"title": "Air Max 90 Nike Men's",
"price": "$101.25",
"extracted_price": 101.25,
"original_price": "$135",
"extracted_original_price": 135,
"source": "Nike",
"multiple_sellers": true,
"rating": 4.6,
"reviews": 13000,
"tag": "25% OFF",
"extensions": ["Free delivery by Mon"],
"product_id": "15610799269751422523",
"thumbnail": "data:image/webp;base64,UklGRhgJAABXRUJQV..."
},
...
],
"pagination": {
"current": 1,
"next": "https://www.google.com/search?q=nike+air+max&udm=28&hl=en&gl=us&start=10&..."
}
}Every response also reports its cost in the X-Serpix-Credits-Charged and X-Serpix-Credits-Remaining headers.
The grid
Each product card becomes one entry of shopping_results. The capture below is the grid exactly as Google served it, above the JSON it parsed into:
The gridHover or tap a tile to pair it with its entry

shopping_results
[ {
"position": 1,
"title": "Air Max 90 Nike Men's",
"price": "$101.25",
"extracted_price": 101.25,
"original_price": "$135",
"extracted_original_price": 135,
"source": "Nike",
"multiple_sellers": true,
"rating": 4.6,
"reviews": 13000,
"tag": "25% OFF",
"extensions": ["Free delivery by Mon"],
"product_id": "15610799269751422523"
}, {
"position": 2,
"title": "Nike Men's Air Max Excee Sneakers",
"price": "$75.00",
"extracted_price": 75,
"original_price": "$100",
"extracted_original_price": 100,
"source": "Nike",
"multiple_sellers": true,
"rating": 4.3,
"reviews": 7900,
"tag": "25% OFF",
"extensions": ["Free delivery on $75+"],
"product_id": "7304528092508809722"
}, {
"position": 3,
"title": "Nike Women's Air Max 90",
"price": "$101.25",
"extracted_price": 101.25,
"original_price": "$135",
"extracted_original_price": 135,
"source": "Nike",
"multiple_sellers": true,
"rating": 4.6,
"reviews": 3700,
"tag": "25% OFF",
"extensions": ["Free delivery by Mon"],
"product_id": "11633386364053739221"
}, {
"position": 4,
"title": "Nike Men's Air Max Phoenix",
"price": "$87.73",
"extracted_price": 87.73,
"original_price": "$155",
"extracted_original_price": 155,
"source": "Nike",
"multiple_sellers": true,
"rating": 4.5,
"reviews": 144,
"tag": "43% OFF",
"extensions": ["Free delivery by Mon"],
"product_id": "16515621927653627757"
}, {
"position": 5,
"title": "Nike Kids' Air Max 270",
"price": "$99.00",
"extracted_price": 99,
"original_price": "$132",
"extracted_original_price": 132,
"source": "Nike",
"multiple_sellers": true,
"rating": 4.6,
"reviews": 5800,
"tag": "25% OFF",
"extensions": ["Free delivery by Mon"],
"product_id": "2998018693582404652"
}, {
"position": 6,
"title": "Nike Men's Air Max Torch 4 Running Shoes",
"price": "$75.00",
"extracted_price": 75,
"original_price": "$100",
"extracted_original_price": 100,
"source": "Nike",
"multiple_sellers": true,
"rating": 4.7,
"reviews": 3100,
"tag": "25% OFF",
"extensions": ["Free delivery on $75+"],
"product_id": "14220130338569971496"
}]Six cards are marked here; the response carries forty. Prices arrive twice over: as the string Google printed and as a number, so a currency symbol never gets in the way of a comparison.
search_metadata
| field | type | default | description |
|---|---|---|---|
| id | string | – | Unique id of this search, srx_ prefixed. |
| status | string | – | success on every 200. |
| created_at | string | – | When the search ran, ISO 8601 UTC. |
| took_ms | integer | – | End-to-end time to serve this response, in milliseconds. |
| parser_version | string | – | Dated version of the shopping parser that produced this response, so any response can be tied to the parsing behaviour that made it. Each vertical carries its own. |
| google_url | string | – | The exact Google URL fetched. |
| html_url | string | – | Archived copy of the results page exactly as served. Included when available. |
| json_url | string | – | Archived copy of this JSON response. Included when available. |
search_parameters
The parameters the search actually ran with, defaults filled in.
| field | type | default | description |
|---|---|---|---|
| q | string | – | The query as received. |
| gl | string | – | Country shopped in, which set the currency and the merchants. |
| hl | string | – | Language used. |
| device | string | – | Device profile used. |
| page | integer | – | Page served. |
shopping_results
| field | type | default | description |
|---|---|---|---|
| position | integer | – | Rank within the grid, starting at 1. |
| title | string | – | Product name, as the card prints it. |
| price | string | – | Current price with its currency symbol, exactly as displayed. |
| extracted_price | number | – | The same amount as a number. |
| original_price | string | – | The struck-through price, on cards where Google shows one. |
| extracted_original_price | number | – | The same amount as a number. |
| source | string | – | Merchant as displayed. |
| multiple_sellers | boolean | false | True when the card says the offer has other sellers: Google prints & more beside the merchant. |
| rating | number | – | Star rating out of five. |
| reviews | integer | – | Number of reviews behind that rating, as a number: Google's 13K comes back as 13000. |
| tag | string | – | The badge across the photo, such as 25% OFF or LOW PRICE. |
| extensions | list | – | Everything else the card prints, as printed: delivery promises, return windows, stock notes. |
| product_id | string | – | Google's own identifier for the product. |
| thumbnail | string | – | Product photo, inline. |
pagination
Ask for the next page with page. pagination.next is present only while the page you asked for came back full, so its absence is how you know you have reached the end of the vertical.
| field | type | default | description |
|---|---|---|---|
| current | integer | – | The page this response is. |
| next | string | – | Google URL for the next page. Absent on the last one. |
Errors
A rejected request answers with detail, naming the parameter and what to send in its place. A search that could not be completed answers with error and a search_id you can quote. No error is ever charged.
| field | type | default | description |
|---|---|---|---|
| 401 | unauthorized | −0 | Missing, malformed, or revoked key. See Authentication. |
| 402 | payment required | −0 | The balance can't cover a search. See Credits & billing. |
| 422 | invalid request | −0 | Unknown or invalid parameter. The detail names the parameter and what to send in its place. |
| 502 | search_failed | −0 | The search could not be completed. Retry it; you were not charged. |
| 503 | search_unavailable | −0 | Google did not serve a usable results page. Retry it; you were not charged. |
HTTP/2 502
content-type: application/json
x-serpix-credits-charged: 0
{ "error": { "code": "search_failed", "search_id": "srx_kq3v9tR2xLw8mA1c" } }