---
url: https://commerceai.ai/docs/reference/search-api.md
---
# Search API

Reference for the Search API. One request takes a product query and returns a ranked list of US products, each with offers from the retailers that sell it. It does not hold a conversation; use the [`shopping_agent`](https://commerceai.ai/docs/reference/shopping-agent.md) tool for comparison, product Q\&A, and follow-ups.

## Endpoint

| Item | Value |
|---|---|
| REST | `POST https://commerceai.ai/api/v1/catalog/search` with a JSON body |
| Auth | `Authorization: Bearer <YOUR_API_KEY>` on every request. The same API keys work for Search and `shopping_agent`. |
| MCP | Not available. Search is REST only; `shopping_agent` is the only MCP tool. |
| Price | 5 credits per request. See [Price](https://commerceai.ai/docs/pricing.md). |

## Quickstart

Set your API key, then send one query. Each example costs 5 credits.

```bash
export COMMERCEAI_API_KEY="<YOUR_API_KEY>"
```

### cURL

```bash
curl https://commerceai.ai/api/v1/catalog/search \
  -H "Authorization: Bearer $COMMERCEAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": "wireless earbuds", "filters": {"price_max": 50}, "limit": 5}'
```

### Python

Requires Python 3.9+ and `requests`.

```bash
pip install requests
```

```python
import os
import requests

response = requests.post(
    "https://commerceai.ai/api/v1/catalog/search",
    headers={"Authorization": f"Bearer {os.environ['COMMERCEAI_API_KEY']}"},
    json={"query": "wireless earbuds", "filters": {"price_max": 50}, "limit": 5},
    timeout=30,
)
data = response.json()
if not response.ok:
    raise SystemExit(f"{data.get('error') or data.get('code')}: {data['message']}")

for product in data["products"]:
    cheapest = product["offers"][0]
    print(product["title"], cheapest["price"], cheapest["source_site"], cheapest.get("buy_url"))
print("credits used:", data["usage"]["credits_used"])
```

### JavaScript

Requires Node.js 18+ (built-in `fetch`). No package to install.

```js
const response = await fetch('https://commerceai.ai/api/v1/catalog/search', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.COMMERCEAI_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ query: 'wireless earbuds', filters: { price_max: 50 }, limit: 5 }),
  signal: AbortSignal.timeout(30_000),
})
const data = await response.json()
if (!response.ok) throw new Error(`${data.error ?? data.code}: ${data.message}`)

for (const product of data.products) {
  const [cheapest] = product.offers
  console.log(product.title, cheapest.price, cheapest.source_site, cheapest.buy_url)
}
console.log('credits used:', data.usage.credits_used)
```

## Request

| Parameter | Type | Required | Description |
|---|---|---|---|
| `query` | string | Yes | Product keywords or a short description, 1 to 200 characters. Leading and trailing spaces are trimmed. |
| `filters` | object | No | Narrows the results. See [Filters](#filters). |
| `limit` | integer | No | Number of products to return, 1 to 20. Default 20. There is no next page. |
| `offer_limit` | integer | No | Offers per product, 1 to 20. Default 3. Each retailer contributes at most its cheapest offer. |
| `market` | string | No | Default `US`. Any other market returns `outcome: "UNSUPPORTED"` without searching and is not charged. |
| `request_id` | string | No | Your own UUID for this request. The server generates one if omitted. Reusing an id returns `IDEMPOTENCY_CONFLICT`. |

Unknown parameters are rejected with `INVALID_ARGUMENT`.

### Filters

| Filter | Type | Description |
|---|---|---|
| `price_min` | number | Lowest price in USD. Must be positive and not above `price_max`. |
| `price_max` | number | Highest price in USD. Must be positive. |
| `websites` | string\[] | Only return offers from these retailers: `amazon`, `walmart`, `macys`. Any other value is rejected. |
| `brands` | string\[] | Brands to prefer. At most 20, each at most 64 characters. |

Unknown filters are rejected with `INVALID_ARGUMENT`.

## Response

| Field | Type | Description |
|---|---|---|
| `api_version` | string | Always `v1`. |
| `request_id` | string | Your `request_id`, or the one the server generated. Quote it when you contact support. |
| `outcome` | string | `SUCCESS`: products found. `NO_RESULTS`: nothing matched; `products` is empty. `UNSUPPORTED`: the market is not supported; nothing was searched. |
| `applied_filters` | object | The filters the search used, after trimming and de-duplication. |
| `products` | object\[] | Matching products, best match first. See [Product](#product). |
| `usage` | object | `credits_used`: credits this request used. |
| `trace_id` | string | Present on some responses. Quote it with `request_id` when you contact support. |

### Product

| Field | Type | Description |
|---|---|---|
| `title` | string | Product name. |
| `brand` | string | Brand, when known. |
| `category` | string | Category, when known. |
| `image_url` | string | Main image, when available. |
| `price_min`, `price_max` | number | Lowest and highest offer price, in `currency`. |
| `currency` | string | `USD`. |
| `availability` | string | `in_stock`, `out_of_stock`, or `unknown`. |
| `rating`, `review_count` | number | Rating and number of reviews, when available. |
| `offer_count` | integer | Number of offers returned for this product. |
| `offers` | object\[] | Offers sorted by price, lowest first. See [Offer](#offer). |

Fields with no value are omitted rather than sent as `null`. Products and offers can carry more fields than listed here, such as `description`, `gtin`, `image_urls`, `specs`, and `reviews`; ignore fields you do not use.

### Offer

| Field | Type | Description |
|---|---|---|
| `source_site` | string | Retailer, lowercase, for example `walmart`. |
| `merchant_name` | string | Seller name, when it differs from the retailer. |
| `price` | number | Price in `currency`. |
| `currency` | string | `USD`. |
| `availability` | string | `in_stock`, `out_of_stock`, or `unknown`. |
| `buy_url` | string | Link to the product page on the retailer's site. |
| `updated_at` | integer | When the price was last checked, in Unix seconds. |

## Billing

| Result | Credits |
|---|---|
| `SUCCESS` | 5 |
| `NO_RESULTS` | 5 |
| `UNSUPPORTED` market | 0 |
| Request that ends in an error | 0 |

Search requests are billed separately from `shopping_agent`. Searches the agent runs inside a turn are billed as its tool calls, not as Search API requests.

## Errors

Errors return an HTTP status and this body:

```json
{
  "error": "INVALID_ARGUMENT",
  "message": "invalid argument",
  "request_id": "0192f1c4-7b6e-7c3a-9d2e-5f4a1b2c3d4e",
  "retryable": false
}
```

| HTTP | `error` | Retry |
|---|---|---|
| 400 | `INVALID_ARGUMENT` | No. Fix the request. |
| 401 | `UNAUTHENTICATED` | No. The API key is missing, invalid, or deleted. The response also carries a `WWW-Authenticate: Bearer` header. |
| 403 | `INSUFFICIENT_CREDITS`, `ACCOUNT_DISABLED`, `FORBIDDEN` | No. Top up or contact support. |
| 409 | `IDEMPOTENCY_CONFLICT` | No. Send a new `request_id`. |
| 429 | `RATE_LIMITED`, `SHARED_CAPACITY` | Yes, after the `Retry-After` header. |
| 502 | `UPSTREAM_ERROR` | No. |
| 503 | `SERVICE_UNAVAILABLE` | Yes, with backoff. |
| 504 | `UPSTREAM_TIMEOUT` | Yes, with backoff. |

Retry only when `retryable` is `true`. Do not parse `message`; it can change.

## Limits

Search has its own rate limits, separate from the [`shopping_agent` limits](https://commerceai.ai/docs/guide/rate-limits.md). Requests over the limit return HTTP 429 `RATE_LIMITED` with a `Retry-After` header in seconds. Need higher limits? Email support@commerceai.ai.

## Next

* [Agent Capabilities](https://commerceai.ai/docs/reference/shopping-agent.md)
* [Price](https://commerceai.ai/docs/pricing.md)
* [Errors & Retries](https://commerceai.ai/docs/reference/errors.md)
