PricesAPI Documentation
Real-time pricing data from thousands of retailers across global retail markets.
The PricesAPI provides programmatic access to product pricing data, seller information, and historical price trends. Build price comparison tools, track competitor pricing, or integrate live product offers into your application.
Base URL
https://api.pricesapi.io/api/v1Also served at https://api.buywisely.com.au/api/v1 — both hostnames resolve to the same API.
Authentication
All API requests require an API key. Pass it in the Authorization header as a bearer token: Authorization: Bearer <your_api_key>. This is the recommended form — header-based auth keeps your key out of server logs, browser history, and referrer leaks.
curl -G "https://api.pricesapi.io/api/v1/products/search" \
-H "Authorization: Bearer your_api_key" \
--data-urlencode "q=https://www.jbhifi.com.au/products/apple-macbook-neo-13-inch-with-a18-pro-chip-512gb-8gb-silver" \
--data-urlencode "country=us"Query-string fallback
You can also pass the key as a ?api_key= query parameter, but we don't recommend it: keys in the query string are written to server access logs, browser history, and Referer headers. Use the Authorization header wherever you can.
Keep your API key secure! Never commit API keys to version control or expose them in client-side code. Your API key starts with the pricesapi_ prefix.
Quick Start
One call returns product candidates with their merchant offers inline. The call is synchronous: a cold (uncached) request runs the live discover + offers pipeline and can block 30–90 seconds, while a warm (cached) request returns in under 100 ms. Set a long read timeout (≥ 95 s) so cold calls don't abort — see Latency & Caching.
const apiKey = 'your_api_key';
const params = new URLSearchParams({
q: 'https://www.jbhifi.com.au/products/apple-macbook-neo-13-inch-with-a18-pro-chip-512gb-8gb-silver',
country: 'us',
limit: '3',
});
// Cold calls can take 30-90s — set a long read timeout.
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 95_000);
const response = await fetch(
`https://api.pricesapi.io/api/v1/products/search?${params}`,
{ headers: { Authorization: `Bearer ${apiKey}` }, signal: controller.signal }
);
clearTimeout(timeout);
const { data: { products }, meta } = await response.json();
console.log(`${products.length} matches (cache: ${meta.cache_source}); ` +
`top: ${products[0].title} @ ${products[0].price} ${products[0].currency}`);Plans & Rate Limits
Monthly call limits + per-minute rate per plan. Click a plan to start. Upgrade in-dashboard anytime.
You're billed 1 credit per HTTP 200 response. Cache hits and empty results (data.products: []) both return 200, so both are billable. Error responses (4xx/5xx) are not billed.
| Plan | Price | Monthly Calls | Rate Limit | |
|---|---|---|---|---|
| Personal | $0 | 1,000 | 6 req/min | Start free |
| DeveloperPopular | $49/month | 25,000 | 20 req/min | Subscribe |
| Business | $199/month | 100,000 | 40 req/min | Subscribe |
| Enterprise | $999/month | 500,000 | 60 req/min | Subscribe |
Search ProductsRecommended
GET /api/v1/products/searchPass a retailer URL or plain text product name. Returns up to limit product candidates with title, price, source, rating, and an image — each candidate carries its merchant offers inline as offers[] plus an offerCount. One call returns candidates and offers together; there is no separate offers request.
This endpoint is synchronous. A cold (uncached) call runs the live discover + offers pipeline inline and can block 30–90 seconds; a warm (cached) call returns in under 100 ms. Set a read timeout of at least 95 seconds. See Latency & Caching.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| q | string | URL or product text (required) |
| country | string | ISO country code (default: au). 47 markets are supported — use gb for the United Kingdom, not uk. An unsupported code returns 400 COUNTRY_NOT_SUPPORTED with the supported list in the error body. See Supported markets. |
| limit | integer | Number of candidates returned (default: 3, max: 5). Values above 5 are silently clamped to 5, and the response sets meta.limit_capped: true and meta.limit_max: 5. A value below 1 or a non-integer returns 400 INVALID_LIMIT. |
| offers_limit | integer | Max number of offers per product returned in each candidate's offers[] array (default: 3, max: 20). Offers are kept cheapest-first, so a smaller value returns the lowest prices and offerCount reflects the truncated length. Values are clamped to the 1–20 range; a value below 1 or a non-integer returns 400 INVALID_OFFERS_LIMIT. |
Example Request
# One call returns candidates with offers[] inline. --max-time covers cold (30-90s) calls.
curl -G "https://api.pricesapi.io/api/v1/products/search" \
-H "Authorization: Bearer your_api_key" \
--max-time 95 \
--data-urlencode "q=macbook pro" \
--data-urlencode "limit=3" \
--data-urlencode "offers_limit=3"Response Fields
The response is { success, data: { query, country, products }, meta }. Each entry in data.products is a product candidate with the fields below.
| Field | Type | Notes |
|---|---|---|
| pid | integer | Internal product id, allocated on first search. |
| title, image, source | string | Product name, thumbnail URL, and primary retailer. |
| price, currency | number, string | Headline price and ISO currency for the candidate. |
| rating, reviews | number | Aggregate rating and review count for the product. |
| offerCount | integer | Number of merchant offers in offers[]. |
| offers[] | array | Merchant offers, inline. Each offer has exactly 7 fields: seller, seller_url, price, currency, shipping, condition, and url. There is no per-offer rating, stock, title, or delivery — those live on the candidate, not the offer. |
Candidates also include position, gid, gpcid, condition, multi_store, delivery, tags, and nearby_distance_km.
Response Metadata
The top-level meta object describes how the response was produced.
| Field | Type | Notes |
|---|---|---|
| latency_ms | number | Wall-clock time we spent serving this request. On a cold call this reflects the full synchronous pipeline (tens of seconds); on a cache hit it is small. It is not a promise of sub-second latency — size your client timeouts off the cold path. |
| cache_source | string | Where the result came from: 'redis' (hot in-memory tier, fastest), 'db' (durable Postgres tier), or 'miss' (no cache — we ran the live pipeline, so this was a cold 30–90 s call). |
| cache_age_s | number | Age of the cached result in seconds. Present only on a 'db' hit. |
| raw_count, gid_bearing_count | number | Diagnostic counts of upstream candidates seen and how many carried a usable product id. |
| degraded | boolean | true when the offers stage failed but candidates were still returned (their offers[] are empty). See Empty & degraded responses. |
| limit_capped, limit_max | boolean, number | Present when a limit above 5 was clamped. limit_max is 5. |
meta may also include discover_ms, products_ms, and blocked for stage-level timing and diagnostics.
Example Response
{
"success": true,
"data": {
"query": "Sony WH-1000XM5",
"country": "us",
"products": [
{
"position": 1,
"pid": 100569554,
"gpcid": "11795796123145720151",
"gid": "17755277695162489284",
"title": "Sony WH-1000XM5 Wireless Noise Cancelling Headphones",
"image": "https://encrypted-tbn0.gstatic.com/shopping?q=tbn:...",
"price": 329.99,
"currency": "USD",
"condition": null,
"source": "Best Buy",
"multi_store": true,
"rating": 4.8,
"reviews": 12400,
"delivery": "Free delivery",
"tags": [],
"nearby_distance_km": null,
"offerCount": 3,
"offers": [
{
"seller": "Best Buy",
"seller_url": "https://www.bestbuy.com",
"price": 248,
"currency": "USD",
"shipping": 0,
"condition": "New",
"url": "https://www.bestbuy.com/product/.../sku/6505727"
},
{
"seller": "Sony",
"seller_url": "https://electronics.sony.com",
"price": 249.99,
"currency": "USD",
"shipping": 0,
"condition": "New",
"url": "https://electronics.sony.com/.../p/wh1000xm5-b"
},
{
"seller": "Target",
"seller_url": "https://www.target.com",
"price": 249.99,
"currency": "USD",
"shipping": null,
"condition": "New",
"url": "https://www.target.com/p/sony-wh-1000xm5-.../A-86314264"
}
]
}
]
},
"meta": {
"latency_ms": 41822,
"raw_count": 38,
"gid_bearing_count": 33,
"cache_source": "miss",
"discover_ms": 12640,
"products_ms": 29182
}
}Here cache_source is "miss" and latency_ms is ~42 s — a cold call that ran the live pipeline. A repeat of the same query returns the cached result with cache_source: "redis" (or "db" with a cache_age_s) and a sub-100 ms latency_ms.
Latency & Caching
/products/search is a synchronous endpoint. A single call runs both the discover and offers (oapv) stages inline and returns when they finish — there is no job id to poll and no webhook.
Cold call — 30–90 s
No cached result (cache_source: "miss"). We run the live pipeline; the request blocks for real, measured time of 30–90 seconds.
Warm call — <100 ms
A cached result exists (cache_source: "redis" or "db"). Returns in under 100 ms. A "db" hit also carries cache_age_s.
Cache tiers
'redis'— hot tier. Fastest; recently-served queries.'db'— durable Postgres tier. Still fast (<100 ms); the response includescache_age_sso you can decide whether the data is fresh enough.'miss'— nothing cached. The call ran the live pipeline and took the cold path.
Set a long read timeout. We recommend at least 95 seconds. Default client timeouts (often 10–30 s) will abort cold calls before they complete. For interactive UIs, show a loading state and consider warming the cache with a background request first.
Empty & Degraded Responses
Both of these are HTTP 200 success responses (and both bill 1 credit) — they are not errors. Handle them on the happy path.
Empty result
When the upstream has nothing for the query, you get a 200 with an empty products array — not a 404.
{
"success": true,
"data": { "query": "asdfghjkl", "country": "au", "products": [] },
"meta": { "latency_ms": 31204, "raw_count": 0, "gid_bearing_count": 0, "cache_source": "miss" }
}Degraded result
When candidates were found but the offers stage failed, you still get the candidates — with empty offers[] — and meta.degraded: true. Retry shortly to fill in offers.
{
"success": true,
"data": {
"query": "Sony WH-1000XM5",
"country": "au",
"products": [
{ "position": 1, "pid": 100569554, "title": "Sony WH-1000XM5", "price": 399,
"currency": "AUD", "source": "JB Hi-Fi", "offerCount": 0, "offers": [] }
]
},
"meta": { "latency_ms": 38110, "raw_count": 21, "gid_bearing_count": 18,
"cache_source": "miss", "degraded": true }
}Supported Countries
Pass the country query parameter to specify the target market. Default is au. 47 markets are supported (full list below). Use gb for the United Kingdom — uk is not a valid code and returns 400 COUNTRY_NOT_SUPPORTED.
Supported markets
Pass an ISO 3166 country code from the list below. These are the markets where we have verified product data coverage. Highest-quality results come from au, us, gb, de, and nl (most retailers indexed). Other supported markets work but coverage depends on the product.
Need a market not listed?
Markets not in the list above (e.g. most of Africa, the Middle East beyond UAE/Israel/Türkiye, Central Asia, parts of South America) don't have reliable product coverage today and will return empty responses. If you depend on a specific market we don't cover yet, email us — we prioritize coverage expansion based on customer demand.
Example with Country Parameter
# Search by product name, get US results
curl -G "https://api.pricesapi.io/api/v1/products/search" \
-H "Authorization: Bearer your_api_key" \
--max-time 95 \
--data-urlencode "q=MacBook Pro M5 14-inch" \
--data-urlencode "country=us"Error Codes
The API uses standard HTTP status codes and returns detailed error information:
Authentication Errors
| Code | Error | Description |
|---|---|---|
| 401 | MISSING_API_KEY | API key not provided in request |
| 401 | INVALID_API_KEY_FORMAT | API key doesn't start with pricesapi_ prefix |
| 401 | INVALID_API_KEY | API key not found in database |
| 403 | SUBSCRIPTION_CANCELLED | Your subscription has been cancelled |
| 403 | CREDITS_EXCEEDED | Monthly API credits limit reached |
| 429 | RATE_LIMIT_EXCEEDED | Per-minute rate limit hit (6/20/40/60 by tier — Personal/Developer/Business/Enterprise) |
Request Errors
| Code | Error | Description |
|---|---|---|
| 400 | MISSING_QUERY | Search query parameter 'q' is required |
| 400 | INVALID_LIMIT | limit is below 1 or not an integer. (Values above 5 are clamped to 5, not rejected.) |
| 400 | INVALID_OFFERS_LIMIT | offers_limit is below 1 or not an integer. (Values are clamped to the 1–20 range, not rejected.) |
| 400 | COUNTRY_NOT_SUPPORTED | The country code is not a supported market. The error body returns the supported list. Use gb, not uk. See Supported Countries. |
| 404 | NOT_FOUND | Requested endpoint does not exist |
| 410 | ENDPOINT_GONE | Endpoint permanently removed. The old /products/:id/offers route (the 2nd call of the old two-step flow) is gone — /products/search now returns offers inline. See the Migration guide. |
Service Errors — transient
A 503 means the scraper backend is temporarily under pressure or unavailable. These are transient — the response includes a Retry-After header (seconds). Back off for that long, then retry. No credit is billed for a 503.
| Code | Error | Description |
|---|---|---|
| 503 | SCRAPER_UNAVAILABLE | The scraper backend is temporarily unavailable. Retry after the Retry-After interval. |
| 503 | SCRAPER_BUSY | The scraper is at capacity. Back off and retry. |
| 503 | SCRAPER_CIRCUIT_OPEN | The circuit breaker is open after repeated upstream failures. Retry after the Retry-After interval. |
Server Errors
| Code | Error | Description |
|---|---|---|
| 500 | SEARCH_FAILED | Product search pipeline error |
| 500 | DB_ERROR | Internal database error while serving the request |
| 500 | VALIDATION_ERROR | Failed to validate API key |
Retrying a 503
Read the Retry-After response header (seconds) and sleep before retrying:
r = requests.get(url, params=params,
headers={'Authorization': f'Bearer {api_key}'}, timeout=95)
if r.status_code == 503:
wait = int(r.headers.get('Retry-After', '5'))
time.sleep(wait)
r = requests.get(url, params=params,
headers={'Authorization': f'Bearer {api_key}'}, timeout=95)Error Response Format
{
"success": false,
"error": {
"code": "INVALID_API_KEY",
"message": "API key is invalid",
"details": "The provided API key was not found"
}
}Complete Code Examples
Full examples showing how to search for products and read the offers attached to each candidate. One call returns both:
const API_KEY = 'your_api_key';
const BASE_URL = 'https://api.pricesapi.io/api/v1';
async function searchProducts(query) {
const params = new URLSearchParams({ q: query, limit: '5' }); // max 5
// Cold calls run the live pipeline (30-90s) — set a long read timeout.
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 95_000);
const response = await fetch(
`${BASE_URL}/products/search?${params}`,
{
headers: { Authorization: `Bearer ${API_KEY}` },
signal: controller.signal,
}
);
clearTimeout(timer);
if (response.status === 503) {
const wait = Number(response.headers.get('Retry-After') ?? 5);
throw new Error(`Scraper busy — retry after ${wait}s`);
}
return response.json();
}
// One call returns candidates WITH offers inline — there is no second /offers call.
searchProducts('laptop').then(({ data, meta }) => {
const products = data.products;
console.log(`Found ${products.length} products (cache: ${meta.cache_source})`);
if (products.length > 0) {
const top = products[0];
console.log(`Top match: ${top.title} @ ${top.price} ${top.currency}`);
console.log(`${top.offerCount} merchant offers:`);
for (const offer of top.offers) {
console.log(` ${offer.seller} — ${offer.price} ${offer.currency} (+${offer.shipping} ship)`);
}
}
});Migration & Changelog
The product API was consolidated into a single synchronous endpoint, GET /api/v1/products/search. If you built against the older two-step flow, read this section — there are 5 breaking changes.
Action required: callers of the old /offers endpoint
GET /api/v1/products/:id/offers now returns 410 ENDPOINT_GONE. Move to the inline offers[] returned by /products/search (see the before/after below). Need help? email support.
Removed endpoints
| Old endpoint | Status | Replacement |
|---|---|---|
| GET /products/:id/offers | 410 ENDPOINT_GONE | Offers are inline on each /products/search candidate. |
| GET /products/lookup | Removed | Use /products/search. |
| GET /products/lookup/bulk | Removed | Use /products/search (one query per call). |
| GET /search?q= | Superseded | Use /products/search. |
| GET /shopping/search | Superseded | Use /products/search. |
Breaking changes
- The two-call flow is gone. You no longer call
/searchfor candidates and then/products/:id/offersfor each one. A single/products/searchcall returns candidates with their offers inline asoffers[]+offerCount. limitmax lowered from 10 to 5. Values above 5 are clamped to 5 (meta.limit_capped: true,meta.limit_max: 5).- Per-offer fields removed. Offers no longer carry
rating,stock,title, ordelivery. Each offer now has exactly 7 fields:seller,seller_url,price,currency,shipping,condition,url. The dropped data lives on the candidate instead (rating,reviews,delivery,title). - Latency is now synchronous. A cold call blocks 30–90 seconds (real, measured) while the pipeline runs; caching brings repeats under 100 ms. The old API implied sub-second responses — raise your client read timeout to ≥ 95 s. See Latency & Caching.
- New 503 +
Retry-Aftersemantics. Under scraper pressure the API returns503(SCRAPER_UNAVAILABLE/SCRAPER_BUSY/SCRAPER_CIRCUIT_OPEN) with aRetry-Afterheader. Back off and retry. See Error Codes.
Before & after: replacing the /offers call
Old two-step flow — one search, then one /offers call per candidate:
// OLD — no longer works. The 2nd call now returns 410 ENDPOINT_GONE.
const search = await fetch(
`${BASE_URL}/search?q=${encodeURIComponent(query)}`,
{ headers: { 'x-api-key': API_KEY } }
).then(r => r.json());
const top = search.data.products[0];
// 2nd round-trip per product — REMOVED
const offers = await fetch(
`${BASE_URL}/products/${top.id}/offers`, // -> 410 ENDPOINT_GONE
{ headers: { 'x-api-key': API_KEY } }
).then(r => r.json());
console.log(offers.data);New single call — offers arrive inline, no second request, and auth uses the Authorization header:
// NEW — one call, offers inline. limit max is 5. Set a long timeout (cold = 30-90s).
const { data, meta } = await fetch(
`${BASE_URL}/products/search?q=${encodeURIComponent(query)}&limit=5`,
{ headers: { Authorization: `Bearer ${API_KEY}` } }
).then(r => r.json());
const top = data.products[0];
// No 2nd request — offers are already here (7 fields each).
for (const offer of top.offers) {
console.log(offer.seller, offer.price, offer.currency, offer.shipping);
}
console.log('cache:', meta.cache_source);