Price intelligence infrastructure for developers

PricesAPI developer documentation

Everything you need to build price intelligence.

Discover products and current offers globally today, with snapshots, history, monitoring, and delivery rolling out in stages.

Discover → Snapshot → History → Monitor → Deliver

Product Search is available now. Schedules, Async Bulk Search, and Product Snapshot are in public Beta for every valid API account. History and Product Watches are coming next and are not publicly available yet.

Pricing models, included allowances, and usage limits for all Beta features may change before general availability. Existing Product Search pricing and Search-credit allowances remain unchanged unless separately announced.

Start with the Product Search API. Search by product name or retailer URL and get matched products with available merchant offers inline.

PricesAPI provides normalized product and offer data today, with snapshots, history, monitoring, and delivery rolling out in stages. Your application owns its workflows and decisions.

Need runnable proof first? Start with guided examples, then use the Playground or the hosted MCP Beta below.

Base URL

https://api.pricesapi.io/api/v1

Also 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 product discovery and offer collection before returning, while a cached request avoids that work. Use a generous read timeout for cold calls — see Latency & Caching.

javascript
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 run product discovery and offer collection — allow a generous 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}`);

MCP serverBeta

Connect an MCP client that supports custom HTTP headers to https://mcp.pricesapi.io/mcp. The hosted server uses your existing PricesAPI key and the same Search Products service as the REST API.

search_products

Search a product term or URL in one of the supported markets and return matched products with merchant offers.

list_supported_countries

Read the canonical market codes before choosing the country for a product search.

Connect a client

Add the remote server URL and bearer header to your client's MCP configuration. Configuration keys vary by client, but the connection values are the same:

json
{
  "mcpServers": {
    "pricesapi": {
      "url": "https://mcp.pricesapi.io/mcp",
      "headers": {
        "Authorization": "Bearer your_api_key"
      }
    }
  }
}

Keep credentials in the Authorization header; never place them in the endpoint URL or in tool arguments. This is currently a bearer-auth developer pilot. OAuth is not available yet, so a client must let you configure custom HTTP headers.

Billing and latency

Each search_products call makes one Search Products request. A product-bearing response uses 1 credit, including a cache hit. An empty result or error uses 0 credits. list_supported_countries uses 0 credits.

Warm searches usually return quickly. Cold searches can take several minutes while PricesAPI discovers products and offers. Clients that support MCP progress notifications can show that work while the request is running.

SchedulesBeta

Schedules runs selected terms daily or weekly through the same Search contract and canonical catalogue path. Use the dashboard or management API to add terms and review their latest discovery activity. Product-search responses continue to use the existing Search Products contract.

Live Search

Use Search Products when your application needs product candidates and merchant offers. A cold call follows the normal synchronous search path.

Schedules Beta

Use Schedules for recurring catalogue discovery. Weekly and daily cadences are available globally across supported markets. Sub-daily cadences are not available yet.

Read catalogue-discovery activity

Every schedule includes an activity object for its current catalogue-discovery binding. It reports only the latest attempt and discovered-product count.

FieldMeaning
activity.stateawaiting_first_discovery, discovery_complete, or discovery_issue.
activity.attempted_atUTC timestamp for the latest attempt, or null while waiting for the first one.
activity.discovered_product_countProducts discovered by that attempt. Zero is a valid count; null means unavailable.
json
{
  "activity": {
    "state": "discovery_complete",
    "attempted_at": "2026-08-30T03:15:00.000Z",
    "discovered_product_count": 47
  }
}

Manage your Schedules

The public management API is caller-scoped. List or add schedules, inspect one schedule, update it, remove it, review Search history, and read or change account Schedule settings.

OperationRequest
List schedulesGET /api/v1/schedules
Add schedulesPOST /api/v1/schedules
Search historyGET /api/v1/schedules/history?days=7
Read statusGET /api/v1/schedules/:id
Pause or resumePATCH /api/v1/schedules/:id
Remove scheduleDELETE /api/v1/schedules/:id
Read Schedule settingsGET /api/v1/schedules/settings
Update Schedule settingsPATCH /api/v1/schedules/settings
curl "https://api.pricesapi.io/api/v1/schedules" \
  -H "Authorization: Bearer your_api_key"

Optional activity analytics

On GET /api/v1/schedules, activity_days accepts 7, 30, or 90 and defaults to 7. Other values return INVALID_ACTIVITY_DAYS. When available, meta.activity_seriescontains one UTC point per day with completed, issues, andproducts_returned counts.

Search history uses its existing days window. When available, meta.demand_seriescontains daily requests, successful_responses, and errors counts for that caller and market. Analytics metadata is optional: if a supplemental analytics read fails, the field is omitted while the schedules or history rows still return.

Billing: Schedules management calls are free of API credits. Background refreshes use no API credits. A later customer-initiated non-empty Search follows the standard API credit rules.

Plans & Rate Limits

Search-credit allowances + per-minute request rates by plan. Personal credits are a one-time trial; paid-plan credits reset with the Stripe billing period.

You're billed 1 credit only when a Search Products response returns at least one product. Cache hits follow the same rule. Empty results (data.products: []) and 4xx/5xx responses do not use a credit.

PlanPriceSearch-credit allowanceRate Limit
Personal$01,000 one-time6 req/minStart free
DeveloperPopular$49/month25,00020 req/minCreate account to subscribe
Business$199/month100,00040 req/minCreate account to subscribe
EnterpriseCustomCustom volume60 req/minContact sales

Public Beta · every valid API account

Product Snapshot

Read one known product and its latest retained merchant offers without repeating discovery. Supply an explicit PricesAPI or Google Shopping ID and a lower-case market. Both ID types return the same canonical Product representation from /api/v1/products.

This is a retained-data read: it does not run Search, call a scraper, or consume a Search credit. It has a separate account rate limit and usage meter. Coverage is global across supported markets; Australia currently has the deepest retained history, while other markets continue to grow.

bash
curl -G "https://api.pricesapi.io/api/v1/products" \
  -H "Authorization: Bearer your_api_key" \
  --data-urlencode "id=123456" \
  --data-urlencode "id_type=pricesapi" \
  --data-urlencode "market=gb" \
  --data-urlencode "offers_limit=20"

If you hold a Google Shopping product ID, keep the route and response shape unchanged and set id_type=google_shopping. The API never guesses an ID type from its value.

bash
curl -G "https://api.pricesapi.io/api/v1/products" \
  -H "Authorization: Bearer your_api_key" \
  --data-urlencode "id=catalogue-123" \
  --data-urlencode "id_type=google_shopping" \
  --data-urlencode "market=gb"

Product groups are plural

Use /api/v1/product-groups to page known products belonging to one Google Shopping product-group ID. Membership is deterministic but is not Search ranking, one automatically selected product, or guaranteed exhaustive variants.

bash
curl -G "https://api.pricesapi.io/api/v1/product-groups" \
  -H "Authorization: Bearer your_api_key" \
  --data-urlencode "id=group-123" \
  --data-urlencode "id_type=google_shopping" \
  --data-urlencode "market=gb" \
  --data-urlencode "limit=100"

Beta commercial terms: Pricing models, included allowances, and usage limits for all Beta features may change before general availability. Existing Product Search pricing and Search-credit allowances remain unchanged unless separately announced.

A 404 means that exact ID-to-product mapping is unavailable in the requested market. Rerun Product Search with the original query or retailer URL, choose a candidate explicitly, and retain its PricesAPI ID together with the market.

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 — collection path

No cached result (cache_source: "miss"). We run product discovery and offer collection before the synchronous response returns. Duration varies by query and upstream conditions.

Cached call — cache path

A cached result exists (cache_source: "redis" or "db"). This avoids the full collection path. A "db" hit also carries cache_age_s.

Cache tiers

  • 'redis' — hot tier. Fastest; recently-served queries.
  • 'db' — durable Postgres tier. The response includes cache_age_s so 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 generous read timeout. Cold calls wait for product discovery and offer collection, and duration varies. For interactive UIs, show a loading state and consider warming the cache with a background request first.

Empty & Degraded Responses

These are HTTP 200 success responses, not errors. An empty result uses no credit because it contains no products. A degraded result uses 1 credit because it contains at least one product, even when the offers stage is incomplete. Handle both 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.

json
{
  "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.

json
{
  "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. Global coverage is available across the supported markets 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 accepted by Search. Product and merchant availability varies by product and market.

🇺🇸
US
United States
🇦🇺
AU
Australia
🇬🇧
GB
United Kingdom
🇨🇦
CA
Canada
🇮🇪
IE
Ireland
🇳🇿
NZ
New Zealand
🇩🇪
DE
Germany
🇫🇷
FR
France
🇮🇹
IT
Italy
🇪🇸
ES
Spain
🇵🇹
PT
Portugal
🇳🇱
NL
Netherlands
🇧🇪
BE
Belgium
🇦🇹
AT
Austria
🇨🇭
CH
Switzerland
🇸🇪
SE
Sweden
🇳🇴
NO
Norway
🇩🇰
DK
Denmark
🇫🇮
FI
Finland
🇵🇱
PL
Poland
🇨🇿
CZ
Czechia
🇸🇰
SK
Slovakia
🇭🇺
HU
Hungary
🇷🇴
RO
Romania
🇬🇷
GR
Greece
🇺🇦
UA
Ukraine
🇹🇷
TR
Türkiye
🇮🇱
IL
Israel
🇦🇪
AE
UAE
🇸🇦
SA
Saudi Arabia
🇿🇦
ZA
South Africa
🇮🇳
IN
India
🇯🇵
JP
Japan
🇰🇷
KR
South Korea
🇹🇼
TW
Taiwan
🇭🇰
HK
Hong Kong
🇸🇬
SG
Singapore
🇲🇾
MY
Malaysia
🇹🇭
TH
Thailand
🇮🇩
ID
Indonesia
🇵🇭
PH
Philippines
🇻🇳
VN
Vietnam
🇲🇽
MX
Mexico
🇧🇷
BR
Brazil
🇦🇷
AR
Argentina
🇨🇱
CL
Chile
🇨🇴
CO
Colombia

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

CodeErrorDescription
401MISSING_API_KEYAPI key not provided in request
401INVALID_API_KEY_FORMATAPI key doesn't start with pricesapi_ prefix
401INVALID_API_KEYAPI key not found in database
403SUBSCRIPTION_CANCELLEDYour subscription has been cancelled
403CREDITS_EXCEEDEDMonthly API credits limit reached
403MONTHLY_LIMIT_EXCEEDEDMonthly Search-credit cap reached during the atomic usage check
429RATE_LIMIT_EXCEEDEDPer-minute rate limit hit (6/20/40/60 by tier — Personal/Developer/Business/Enterprise)

Request Errors

CodeErrorDescription
400MISSING_QUERYSearch query parameter 'q' is required
400INVALID_LIMITlimit is below 1 or not an integer. (Values above 5 are clamped to 5, not rejected.)
400INVALID_OFFERS_LIMIToffers_limit is below 1 or not an integer. (Values are clamped to the 1–60 range, not rejected.)
400COUNTRY_NOT_SUPPORTEDThe country code is not a supported market. The error body returns the supported list. Use gb, not uk. See Supported Countries.
404NOT_FOUNDRequested endpoint does not exist
410ENDPOINT_GONEEndpoint 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.

CodeErrorDescription
503SCRAPER_UNAVAILABLEThe scraper backend is temporarily unavailable. Retry after the Retry-After interval.
503SCRAPER_BUSYThe scraper is at capacity. Back off and retry.
503SCRAPER_CIRCUIT_OPENThe circuit breaker is open after repeated upstream failures. Retry after the Retry-After interval.

Server Errors

CodeErrorDescription
500SEARCH_FAILEDProduct search pipeline error
500DB_ERRORInternal database error while serving the request
500VALIDATION_ERRORFailed to validate API key

Retrying a 503

Read the Retry-After response header (seconds) and sleep before retrying:

python
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

json
{
  "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 product discovery and offer collection — allow a generous 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.stock_status})`);
    }
  }
});

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 endpointStatusReplacement
GET /products/:id/offers410 ENDPOINT_GONEOffers are inline on each /products/search candidate.
GET /products/lookupRemovedUse /products/search.
GET /products/lookup/bulkRemovedUse /products/search (one query per call).
GET /search?q=SupersededUse /products/search.
GET /shopping/searchSupersededUse /products/search.

Breaking changes

  1. The old multi-request flow is gone. You no longer call /search for candidates and then /products/:id/offers for each one. A single /products/search call returns candidates with their offers inline as offers[] + offerCount.
  2. limit max lowered from 10 to 5. Values above 5 are clamped to 5 (meta.limit_capped: true, meta.limit_max: 5).
  3. Per-offer fields updated. Each offer now carries product_title, stock_status, and delivery_info per seller, alongside seller, seller_url, price, currency, condition, and url. The old shipping field was removed (it was always 0 — never a real cost). Per-product rating and reviews remain on the candidate.
  4. Latency is now synchronous. A cold call waits while product discovery and offer collection run; cached repeats avoid that work. Use a generous client read timeout. See Latency & Caching.
  5. New 503 + Retry-After semantics. Under scraper pressure the API returns 503 (SCRAPER_UNAVAILABLE / SCRAPER_BUSY / SCRAPER_CIRCUIT_OPEN) with a Retry-After header. 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. Allow a generous cold-call timeout.
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, with stock + delivery inline.
for (const offer of top.offers) {
  console.log(offer.seller, offer.price, offer.currency, offer.stock_status);
}
console.log('cache:', meta.cache_source);