Product Listing API — Developer Documentation
BUY-3253 | Agent-native product catalog API — paginated listing with category and price-range filters
Overview
GET /v1/products provides a high-performance, paginated product listing endpoint optimized for AI agents and developer applications. It supports cursor-based pagination, flexible filtering, and consistent response envelopes designed for agent-native commerce use cases.
Key characteristics:
- Cursor-based pagination — efficient traversal of large datasets (tested against 1M+ product indexes)
- Filtered browsing — category, price range, currency, and region filters
- Agent-optimized response —
{data: [], meta: {total, next_cursor}}envelope for easy parsing - p99 < 100ms — performance target for up to 50 results in Singapore region
Endpoint
GET /v1/products
Authentication
All requests require a Bearer token:
curl "https://api.buywhere.ai/v1/products" \
-H "Authorization: Bearer bw_live_xxxxx"
See Authentication Guide for API key generation.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
category_id | string | No | Filter by category ID (exact match) |
price_min | number | No | Minimum price filter (in specified currency, default: SGD) |
price_max | number | No | Maximum price filter (in specified currency, default: SGD) |
currency | string | No | Currency for price filters and response (SGD, MYR, THB, etc.) |
region | string | No | Geographic region filter (SG, MY, TH, PH) |
sort_by | string | No | Sort order: price_asc, price_desc, relevance |
limit | integer | No | Results per page (1–50, default: 20) |
cursor | string | No | Cursor for pagination (from previous response meta.next_cursor) |
offset | integer | No | Numeric offset fallback (0–10000, use cursor when possible) |
Response Envelope
{
"data": [
{
"id": 12345,
"sku": "DY-VC7-XXXX",
"source": "lazada_sg",
"merchant_id": "LAZADA_SG_001",
"name": "Dyson V15 Detect Vacuum Cleaner",
"description": "Laser detects microscopic dust...",
"price": "749.00",
"currency": "SGD",
"price_sgd": "749.00",
"buy_url": "https://www.lazada.sg/products/...",
"affiliate_url": "https://buywhere.ai/track/...",
"image_url": "https://...jpg",
"brand": "Dyson",
"category": "Vacuum Cleaners",
"category_path": ["Home & Garden", "Cleaning", "Vacuum Cleaners"],
"rating": "4.8",
"review_count": 1247,
"is_available": true,
"in_stock": true,
"stock_level": "high",
"last_checked": "2026-04-18T10:00:00Z",
"data_updated_at": "2026-04-18T08:30:00Z",
"updated_at": "2026-04-18T14:30:00Z"
}
],
"meta": {
"total": 1423,
"next_cursor": "eyJpZCI6MTIzNDUsIm9mZnNldCI6MjB9",
"has_more": true,
"limit": 20
}
}
Meta Fields
| Field | Type | Description |
|---|---|---|
total | integer | Total matching products |
next_cursor | string | Opaque cursor for next page (null when no more results) |
has_more | boolean | True if additional pages exist |
limit | integer | Results per page used for this response |
Examples
Basic Listing (20 products)
curl "https://api.buywhere.ai/v1/products" \
-H "Authorization: Bearer bw_live_xxxxx"
Filter by Category and Price Range
curl "https://api.buywhere.ai/v1/products?category_id=electronics&price_min=50&price_max=500" \
-H "Authorization: Bearer bw_live_xxxxx"
Cursor-Based Pagination
# First page
curl "https://api.buywhere.ai/v1/products?limit=50" \
-H "Authorization: Bearer bw_live_xxxxx"
# Next page using cursor from previous response
curl "https://api.buywhere.ai/v1/products?limit=50&cursor=eyJpZCI6MTIzNDUsIm9mZnNldCI6MjB9" \
-H "Authorization: Bearer bw_live_xxxxx"
Sort by Price (Low to High)
curl "https://api.buywhere.ai/v1/products?sort_by=price_asc&limit=10" \
-H "Authorization: Bearer bw_live_xxxxx"
Filter with Currency Conversion
curl "https://api.buywhere.ai/v1/products?price_min=100&price_max=1000¤cy=MYR" \
-H "Authorization: Bearer bw_live_xxxxx"
Filtering Behavior
Category Filter
category_id performs an exact match against the BuyWhere category identifier. For category hierarchy (e.g., "Electronics > Audio > Headphones"), use the full category path or browse categories via GET /v1/categories.
Price Filters
price_min— inclusive lower boundprice_max— inclusive upper bound- Prices are converted to the specified
currencybefore filtering when provided - Default currency is SGD; all prices are also returned in
price_sgdfor reference
Region Filter
region filters products by the target market:
| Value | Market |
|---|---|
SG | Singapore |
MY | Malaysia |
TH | Thailand |
PH | Philippines |
Sorting
| Value | Behavior |
|---|---|
relevance | Default — most recently updated |
price_asc | Lowest price first |
price_desc | Highest price first |
Performance Characteristics
Performance varies based on load conditions, result size, and catalog size. Targets below represent baseline latency for up to 50 results:
| Condition | Expected Latency | Notes |
|---|---|---|
| Baseline (single request, 20 results) | p50 < 30ms, p99 < 100ms | Low traffic, warm cache |
| Baseline (single request, 50 results) | p50 < 50ms, p99 < 150ms | Maximum page size |
| Moderate load (100 concurrent) | p50 < 150ms, p99 < 400ms | Typical API usage |
| Heavy load (1000+ concurrent) | p50 < 400ms, p99 < 1000ms | Peak traffic scenarios |
Tested index size: 1M+ products (Singapore region)
Performance factors:
- Result size: Larger pages take proportionally longer
- Deep pagination: Cursor-based pagination is O(1); offset pagination degrades on deep offsets
- Category filtering: Adds minimal overhead (< 10ms)
- Price range filtering: Adds minimal overhead (< 10ms)
Cache TTL: 10 minutes (600 seconds)
Error Responses
| Status | Code | Description |
|---|---|---|
| 400 | INVALID_PRICE_RANGE | price_min > price_max |
| 401 | UNAUTHORIZED | Missing or invalid API key |
| 422 | VALIDATION_ERROR | Invalid parameter value |
| 429 | RATE_LIMIT_EXCEEDED | Slow down requests |
| 500 | INTERNAL_ERROR | Server-side failure |
Error Response Format
{
"error": {
"code": "INVALID_PRICE_RANGE",
"message": "price_min cannot be greater than price_max",
"details": {
"price_min": "500.00",
"price_max": "200.00"
}
}
}
SDK Usage
Python SDK
from buywhere import BuyWhere
client = BuyWhere(api_key="bw_live_xxxxx")
# Basic listing
response = client.products.list(limit=20)
# Filtered browsing
response = client.products.list(
category_id="electronics",
price_min=50,
price_max=500,
sort_by="price_asc"
)
# Cursor pagination
while response.meta.has_more:
print(f"Page with {len(response.data)} products")
response = client.products.list(
limit=20,
cursor=response.meta.next_cursor
)
JavaScript/TypeScript SDK
import { BuyWhere } from "@buywhere/sdk";
const client = new BuyWhere({ apiKey: "bw_live_xxxxx" });
// Basic listing
const response = await client.products.list({ limit: 20 });
// Filtered browsing
const filtered = await client.products.list({
categoryId: "electronics",
priceMin: 50,
priceMax: 500,
sortBy: "price_asc"
});
// Cursor pagination
let cursor = response.meta.nextCursor;
while (cursor) {
const page = await client.products.list({ cursor, limit: 20 });
console.log(`Page: ${page.data.length} products`);
cursor = page.meta.nextCursor;
}
Implementation Notes
- Cursor format — cursors are Base64-encoded JSON containing
idandoffset. Do not parse or construct cursors manually. - Cursor vs Offset — use cursor-based pagination for large datasets (> 1000 products). Offset pagination is available as a fallback but may be slower on deep offsets.
- Caching — responses are cached for 10 minutes (600 seconds). Use
Cache-Controlheaders for client-side caching. - Rate limits — applies per API key. See Rate Limits for details.
Related Endpoints
GET /v1/search— Full-text product searchGET /v1/products/{id}— Single product by IDGET /v1/categories— Category treeGET /v1/deals— Price-reduced products