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/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 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.
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:
{
"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.
| Field | Meaning |
|---|---|
| activity.state | awaiting_first_discovery, discovery_complete, or discovery_issue. |
| activity.attempted_at | UTC timestamp for the latest attempt, or null while waiting for the first one. |
| activity.discovered_product_count | Products discovered by that attempt. Zero is a valid count; null means unavailable. |
{
"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.
| Operation | Request |
|---|---|
| List schedules | GET /api/v1/schedules |
| Add schedules | POST /api/v1/schedules |
| Search history | GET /api/v1/schedules/history?days=7 |
| Read status | GET /api/v1/schedules/:id |
| Pause or resume | PATCH /api/v1/schedules/:id |
| Remove schedule | DELETE /api/v1/schedules/:id |
| Read Schedule settings | GET /api/v1/schedules/settings |
| Update Schedule settings | PATCH /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.
| Plan | Price | Search-credit allowance | Rate Limit | |
|---|---|---|---|---|
| Personal | $0 | 1,000 one-time | 6 req/min | Start free |
| DeveloperPopular | $49/month | 25,000 | 20 req/min | Create account to subscribe |
| Business | $199/month | 100,000 | 40 req/min | Create account to subscribe |
| Enterprise | Custom | Custom volume | 60 req/min | Contact sales |
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.
The image field is either an https thumbnail URL or a self-contained data:image/... URI (base64, typically 10–50 KB) — both drop straight into an <img src>. Data URIs never expire and need no extra fetch; treat the field as an opaque image source. null when no thumbnail was available for that candidate.
This endpoint is synchronous. A cold (uncached) call runs product discovery and offer collection inline. Cached calls avoid that work and are usually faster. Allow a generous read timeout for cold calls. See Latency & Caching.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| q | string | URL or product text (required) |
| country | string | ISO country code (default: au). Global coverage is available across the supported markets below — 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 with no numeric prefix returns 400 INVALID_LIMIT. The leading base-10 integer is used, so 2.9 becomes 2 and 3abc becomes 3. |
| offers_limit | integer | Max number of offers per product returned in each candidate's offers[] array (default: 15, max: 60). A smaller value keeps the first offers in their collected order, and offerCount reflects the truncated length. Values above 60 are clamped to 60. A value below 1 or with no numeric prefix returns 400 INVALID_OFFERS_LIMIT. The leading base-10 integer is used, so 2.9 becomes 2 and 3abc becomes 3. |
Example Request
# One call returns candidates with offers[] inline. Allow a generous cold-call timeout.
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=15"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 carries seller, seller_url, price, currency, product_title, stock_status, delivery_info, condition, and url. Per-offer rating and reviews are not included — 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 product discovery and offer collection on the cold path). |
| 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",
"product_title": "Sony WH-1000XM5 Wireless Headphones",
"stock_status": "In stock",
"delivery_info": "Free delivery",
"condition": "New",
"url": "https://www.bestbuy.com/product/.../sku/6505727"
},
{
"seller": "Sony",
"seller_url": "https://electronics.sony.com",
"price": 249.99,
"currency": "USD",
"product_title": "WH-1000XM5 Noise Cancelling Headphones",
"stock_status": "In stock",
"delivery_info": "Free delivery",
"condition": "New",
"url": "https://electronics.sony.com/.../p/wh1000xm5-b"
},
{
"seller": "Target",
"seller_url": "https://www.target.com",
"price": 249.99,
"currency": "USD",
"product_title": "Sony WH-1000XM5 Wireless ANC Headphones",
"stock_status": "In stock",
"delivery_info": 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 — an illustrative cold call that ran the collection path. A repeat of the same query returns the cached result with cache_source: "redis" (or "db" with a cache_age_s) and its measured latency_ms.
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.
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.
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.
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.
Async Bulk SearchBeta
POST /api/v1/search/jobsSubmit an ordered list of Search requests. The configured Beta ceiling is currently 100 items per job. Submit product names or retailer URLs, then poll its progress and page through the results in original input order. Each item uses the same Search Products behavior, including global country coverage, cache controls, ranked candidates, and inline merchant offers.
This public Beta is available to every valid API account. Use the bearer API key for the account that submitted the job; job reads and cancellation are caller-scoped. Questions? Email support@pricesapi.io.
Credits are reserved atomically. Submission reserves one Search credit for each item or rejects the whole job without creating partial work. A successful non-empty result keeps its reserved credit. Empty, failed, and cancelled-before-start items refund their reservations. Job management requests use no additional credit.
Complete request sequence
curl -X POST "https://api.pricesapi.io/api/v1/search/jobs" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: YOUR_UNIQUE_OPERATION_KEY" \
--data '{
"searches": [
{ "q": "Sony WH-1000XM5", "country": "us", "limit": 3 },
{ "q": "Kindle Paperwhite", "country": "gb", "offers_limit": 10 }
]
}'1. Submit a job
Send strict JSON with a required Idempotency-Key header. The root object may contain only a searches array, and each item may contain only the fields below. Numeric strings and boolean strings are invalid. The complete request body is limited to 100 KB.
| Field | JSON type | Contract |
|---|---|---|
q | string | Required. Trimmed product name or retailer URL; 1–400 UTF-8 bytes and no control characters. |
country | string | Optional global country code, case-insensitive. Defaults to au and is returned lowercase. |
limit | integer | Optional positive JSON integer. Defaults to 3; values above 5 are accepted and clamped to 5. |
offers_limit | integer | Optional positive JSON integer. Defaults to 15; values above 60 are accepted and clamped to 60. |
max_age | integer | Optional non-negative JSON safe integer: the oldest cached or scheduled data this item accepts, in seconds. |
no_cache | boolean | Optional JSON boolean. true requires a live Search and bypasses cache reads. |
The array must contain at least one item. The configured Beta ceiling is currently 100 items per job. Idempotency-Key must be 1–255 characters. Generate and persist one unique key for each logical job. Reuse it only for retries of the same normalized, ordered request; use a new key for every new or changed job. A valid replay returns the original job with replayed: true and never reserves credits twice. Reusing the key with a different request returns 409 IDEMPOTENCY_CONFLICT.
2. Poll status
Read GET /api/v1/search/jobs/:id. A job reports total, pending, running, succeeded, empty, failed, and cancelled, plus the corresponding reserved, consumed, and refunded credit counts. It is finished when pending + running equals zero; there is intentionally no separate aggregate job state.
Start around one poll every two seconds with jitter, then back off toward ten seconds. Honor Retry-After on 429 and 503 responses.
The job's expires_at is its execution cutoff. No pending item starts after it: unfinished pending items are cancelled and refunded, while an item already running may still complete. Status and results remain readable after the cutoff in the current Beta, but they are not durable storage; collect results promptly.
3. Page through results
Read GET /api/v1/search/jobs/:id/results. The default page size is 50 and the valid range is 1–200. Results include pending, running, and terminal items, so partial progress can be inspected while work continues. Pass the exact opaque next_cursor value as cursor on the next request; a null next_cursor marks the final page. Pagination order and cursor boundaries are stable, but item states are live rather than a snapshot. After pending + running reaches zero, make a fresh pass from page 1 to collect the final outcomes.
| Item state | Credit state | Meaning |
|---|---|---|
pending | reserved | Accepted and waiting to start. |
running | reserved | A worker is executing the Search. |
succeeded | consumed | A non-empty Search response completed. |
empty | refunded | Search completed successfully with no products. |
failed | refunded | Search or worker execution failed. |
cancelled | refunded | The item was cancelled before it started. |
4. Request cancellation
Send DELETE /api/v1/search/jobs/:id. Cancellation is idempotent: pending items are cancelled and refunded exactly once, while running items may still complete and terminal items remain unchanged. The operation requests cancellation; it does not delete the job.
Errors
Validation errors use 400; malformed JSON uses 400 INVALID_JSON; an oversized body uses413 PAYLOAD_TOO_LARGE; an idempotency mismatch uses 409 IDEMPOTENCY_CONFLICT; and an unaffordable submission uses 403 INSUFFICIENT_CREDITS. Missing or foreign jobs both return 404 JOB_NOT_FOUND. Rate limits return 429 RATE_LIMIT_EXCEEDED. Temporary dependency failures return 503 BULK_SEARCH_UNAVAILABLE with Retry-After: 5. During a controlled drain, new submissions return 503 BULK_SEARCH_ADMISSION_PAUSED with Retry-After: 60, while existing jobs remain readable and cancellable.
A failed item carries its Search failure envelope in response and a machine-readable error_code. Empty and successful items carry the normal Search success envelope; pending, running, and cancelled items have a null response.
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 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 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.
{
"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. 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.
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 |
| 403 | MONTHLY_LIMIT_EXCEEDED | Monthly Search-credit cap reached during the atomic usage check |
| 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–60 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 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 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 old multi-request 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 updated. Each offer now carries
product_title,stock_status, anddelivery_infoper seller, alongsideseller,seller_url,price,currency,condition, andurl. The oldshippingfield was removed (it was always0— never a real cost). Per-productratingandreviewsremain on the candidate. - 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.
- 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. 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);