curl --request GET \
--url https://api.zinc.com/products/search \
--header 'Authorization: <api-key>'import requests
url = "https://api.zinc.com/products/search"
headers = {"Authorization": "<api-key>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: '<api-key>'}};
fetch('https://api.zinc.com/products/search', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.zinc.com/products/search",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.zinc.com/products/search"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://api.zinc.com/products/search")
.header("Authorization", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.zinc.com/products/search")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Authorization"] = '<api-key>'
response = http.request(request)
puts response.read_body{
"status": "completed",
"results": [
{
"product_id": "B000NPYY04",
"title": "Nature's Bounty Fish Oil, 1200 mg Omega-3, 200 Rapid Release Softgels",
"image": "https://m.media-amazon.com/images/I/51KGjV22PWL._AC_US218_.jpg",
"brand": "Nature's Bounty",
"price": 1248,
"stars": 4.5,
"num_reviews": 39510,
"num_offers_estimate": 12,
"num_sales": 30000,
"product_details": [
"200 Count (Pack of 1)"
],
"fresh": false,
"prime": true,
"pantry": false,
"addon": false
},
{
"product_id": "B004U3Y9FU",
"title": "Nature Made Burpless Fish Oil 1000 mg Softgels, 150 Count",
"image": "https://m.media-amazon.com/images/I/516F1UWawAL._AC_US218_.jpg",
"brand": "Nature Made",
"price": 1299,
"stars": 4.7,
"num_reviews": 22087,
"num_offers_estimate": 8,
"num_sales": 20000,
"product_details": [
"150 Count (Pack of 1)"
],
"fresh": false,
"prime": true,
"pantry": false,
"addon": false
}
],
"next_page": 2
}{
"detail": "Insufficient wallet balance for data API call"
}{
"detail": [
{
"loc": [
"<string>"
],
"msg": "<string>",
"type": "<string>"
}
]
}Search Products
Search retailer catalogs for products, prices, and offer data via the Zinc API.
curl --request GET \
--url https://api.zinc.com/products/search \
--header 'Authorization: <api-key>'import requests
url = "https://api.zinc.com/products/search"
headers = {"Authorization": "<api-key>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: '<api-key>'}};
fetch('https://api.zinc.com/products/search', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.zinc.com/products/search",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.zinc.com/products/search"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://api.zinc.com/products/search")
.header("Authorization", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.zinc.com/products/search")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Authorization"] = '<api-key>'
response = http.request(request)
puts response.read_body{
"status": "completed",
"results": [
{
"product_id": "B000NPYY04",
"title": "Nature's Bounty Fish Oil, 1200 mg Omega-3, 200 Rapid Release Softgels",
"image": "https://m.media-amazon.com/images/I/51KGjV22PWL._AC_US218_.jpg",
"brand": "Nature's Bounty",
"price": 1248,
"stars": 4.5,
"num_reviews": 39510,
"num_offers_estimate": 12,
"num_sales": 30000,
"product_details": [
"200 Count (Pack of 1)"
],
"fresh": false,
"prime": true,
"pantry": false,
"addon": false
},
{
"product_id": "B004U3Y9FU",
"title": "Nature Made Burpless Fish Oil 1000 mg Softgels, 150 Count",
"image": "https://m.media-amazon.com/images/I/516F1UWawAL._AC_US218_.jpg",
"brand": "Nature Made",
"price": 1299,
"stars": 4.7,
"num_reviews": 22087,
"num_offers_estimate": 8,
"num_sales": 20000,
"product_details": [
"150 Count (Pack of 1)"
],
"fresh": false,
"prime": true,
"pantry": false,
"addon": false
}
],
"next_page": 2
}{
"detail": "Insufficient wallet balance for data API call"
}{
"detail": [
{
"loc": [
"<string>"
],
"msg": "<string>",
"type": "<string>"
}
]
}product_id, title, image, brand, price, stars, and num_reviews are common to every retailer, and the remaining fields are retailer-specific and null elsewhere. The Response section below lists each field, and the example shows a real Amazon page of results.
Prices are integers in cents. Etsy results also carry currency_code, and their price is in minor units of that currency. They also carry has_variants, which is true when the listing needs an option such as size or color chosen before it can be ordered. See Etsy on Search for how to order one.
Pricing
| Price | $0.01 per call, drawn from your wallet |
| Charged | On each successful call, before any order is placed |
| Empty wallet | 402 with {"detail": "Insufficient wallet balance for data API call"} |
| Sandbox | Free — zn_test_ keys are never metered |
GET /wallet/me rather than retrying into a 402.POST /agent/products/search is the same search at the same price, paid per request instead of from a wallet.
Which retailer to pass
retailer is a Zinc retailer slug, the same identifier GET /retailers/check returns as retailer.retailer:
| Slug | Retailer |
|---|---|
amazon | Amazon (US) |
amazon-uk, amazon-ca, amazon-de, amazon-mx, amazon-fr | Amazon’s other marketplaces |
walmart | Walmart |
bestbuy | Best Buy |
etsy | Etsy |
A Shopify store’s slug (e.g. yetch), or its domain | Any Shopify store that serves its product data (see new Shopify stores) |
GET /retailers/check?url= with any URL on the store.
Two older forms still work: a store’s domain or URL (yetch.studio, https://www.seekinghealth.com/products/...) and the amazon_uk-style marketplace codes. Prefer the slug: the details and offers responses name the retailer by its slug in retailer, so the value you get back is the value to pass on. A Shopify store’s details also carry platform: "shopify".
A retailer Zinc orders from but has no product data for, such as target, returns 422.
New Shopify stores
A Shopify store doesn’t have to be in Zinc’s catalog before you ask for it. With a live key, pass the store’s domain (or any URL on it) asretailer. The first request checks the store, which takes a few seconds, adds it to the catalog, and serves the request as usual. From then on the store answers to its slug and its domain like any other, with no further check.
What can come back from that first request instead:
| Status | Meaning |
|---|---|
422 | The store can’t be served, and the message says why: it isn’t a Shopify store, or it runs on Shopify but doesn’t serve its product data (password-protected, or its product endpoints are withheld). The answer stands for a week. A bare domain is sometimes a marketing site or a bot wall while the store itself is on another host, so if brand.com fails, try the host its product pages are on, such as www.brand.com. |
503 | The check couldn’t run just now: the store rate-limited Zinc, or many new stores are being checked at once. Retry after the Retry-After header. |
429 | Your account has checked 20 new stores today (UTC). Stores already in the catalog are unaffected. |
402 | Your wallet can’t cover the call. Checked before the store is, so nothing is spent. |
/agent/* endpoints only reach stores already in the catalog. Anything else returns 422 ('<domain>' is not in Zinc's retailer catalog). Make one live-key request with the store’s domain to add it, or email support@zinc.com.Going from a result to a product
Pass a result’sproduct_id, with the same retailer, to Get Product Details or Get Product Offers. Best Buy is the exception: search returns the SKU as product_id, but the details endpoint is addressed by the bsin, the trailing id in the product URL.
Paging
Usenext_page from the response rather than counting results. A short or even empty results array with a non-null next_page is not the end of the results; this happens on free_shipping searches, on Best Buy (which server-renders only a few items per page), and on Etsy (which drops non-USD listings after paginating). next_page is null when the results are exhausted, and always null for Shopify stores, which return a single page.Authorizations
Zinc API key (Bearer zn_...)
Headers
Query Parameters
Search term
1Zinc retailer slug: amazon, amazon-uk, amazon-ca, amazon-de, amazon-mx, amazon-fr, walmart, bestbuy, etsy, or a catalogued Shopify store's slug (e.g. retailer=yetch; GET /retailers/check?url=... returns a store's slug). A store's domain is also accepted, as are the older amazon_uk-style codes. A Shopify store not yet in Zinc's catalog is added on a live key's first request for it by domain; any other unknown value is a 422. Responses carry the slug.
Page number for pagination
Only return items that ship for free (Walmart and Best Buy: ship price of 0). Currently a no-op for Amazon: the upstream search data under-reports Prime, so filtering on it would drop valid items — Amazon results are returned unfiltered. Filtering happens after pagination, so per-page counts vary; use next_page in the response to keep paging — an empty page with a non-null next_page is not the end of results. Rejected for Shopify stores: their search data carries no shipping information, so the filter cannot be honored.
Response
Normalized search results. Common fields are always present; retailer-specific fields are null or omitted for other retailers.
Response from the product search endpoint.
Show child attributes
Show child attributes
Only set on free_shipping searches: the page to request to keep paging. An empty results with a non-null next_page is NOT the end of results — request next_page to continue. None means results are exhausted (or the search was unfiltered).

