> ## Documentation Index
> Fetch the complete documentation index at: https://docs.alura.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Find products

> Search Etsy products with filters and sorting, page through results, and find products like one you know.

## Search with filters

`GET /v1/research/products` searches other sellers' active Etsy products. This one asks for mugs under \$30 that sell at least 50 a month, best sellers first, and returns only the fields it prints:

<CodeGroup>
  ```bash cURL theme={null}
  curl --fail-with-body -sS -G https://api.alura.io/v1/research/products \
    -H "Authorization: Bearer $ALURA_API_KEY" \
    --data-urlencode "q=mug" \
    --data-urlencode "price_max=30" \
    --data-urlencode "est_monthly_sales_min=50" \
    --data-urlencode "sort=est_monthly_sales" \
    --data-urlencode "fields=title,price.usd,sales.est_monthly_avg"
  ```

  ```typescript TypeScript theme={null}
  interface Product {
    id: string;
    title: string | null;
    price: { usd: string | null } | null;
    sales: { est_monthly_avg: number | null };
  }

  const url = new URL('https://api.alura.io/v1/research/products');
  url.search = new URLSearchParams({
    q: 'mug',
    price_max: '30',
    est_monthly_sales_min: '50',
    sort: 'est_monthly_sales',
    fields: 'title,price.usd,sales.est_monthly_avg',
  }).toString();

  const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.ALURA_API_KEY}` } });
  if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
  const page = (await res.json()) as { data: Product[]; total: { value: number; relation: 'eq' | 'gte' } };

  console.log(`${page.total.relation === 'gte' ? 'at least ' : ''}${page.total.value} products`);
  for (const p of page.data) console.log(`${p.id} ${p.title}: ${p.price?.usd ?? 'unknown'} USD, ${p.sales.est_monthly_avg ?? 'unknown'} a month`);
  ```

  ```python Python theme={null}
  import json
  import os
  import urllib.parse
  import urllib.request

  query = urllib.parse.urlencode({
      "q": "mug",
      "price_max": 30,
      "est_monthly_sales_min": 50,
      "sort": "est_monthly_sales",
      "fields": "title,price.usd,sales.est_monthly_avg",
  })
  request = urllib.request.Request(
      f"https://api.alura.io/v1/research/products?{query}",
      headers={"Authorization": f"Bearer {os.environ['ALURA_API_KEY']}"},
  )
  with urllib.request.urlopen(request) as response:
      page = json.load(response)

  total = page["total"]
  print(f"{'at least ' if total['relation'] == 'gte' else ''}{total['value']} products")
  for p in page["data"]:
      usd = (p["price"] or {}).get("usd") or "unknown"
      monthly = p["sales"]["est_monthly_avg"]
      print(f"{p['id']} {p['title']}: {usd} USD, {monthly if monthly is not None else 'unknown'} a month")
  ```
</CodeGroup>

Useful parameters:

| Parameter | What it does |
| - | - |
| `q` | Words that must all appear in the title, tags, materials, category or shop name |
| `category_id` | An Etsy category and everything under it; find IDs with `GET /v1/categories?q=mugs` |
| `price_min`, `price_max` | Price in USD |
| `est_monthly_sales_min`, `est_sales_min` | Estimated sales per month, or in total |
| `listed_after`, `shop_country`, `who_made`, `is_digital` | Listing date, the shop's country, the maker, digital or physical |
| `sort`, `order` | `relevance` (the default), `est_sales`, `est_monthly_sales`, `est_revenue`, `price`, `newest`, `reviews` or `listing_score`, `desc` or `asc`. Sorting by a field leaves out products where that value is unknown, and `order` doesn't apply to `relevance` |

The [API reference](/api-reference/research/products/search) lists every filter. Sales and revenue are estimates; [Sales estimates](/methodology/sales-estimates) explains how they are made.

A search costs 10 API credits per page with results, whatever `limit` is: `GET /v1/research/products` with `limit=100` gets four times as many products for the same price as the default 25. An empty page is free.

## Every page

Pass `next_cursor` back as `cursor` until it is `null`. Cap the number of pages you fetch, because each page with results is charged:

<CodeGroup>
  ```bash cURL theme={null}
  # First page; for the next one, add --data-urlencode "cursor=<next_cursor>" with the same query.
  curl --fail-with-body -sS -G https://api.alura.io/v1/research/products \
    -H "Authorization: Bearer $ALURA_API_KEY" \
    --data-urlencode "q=mug" \
    --data-urlencode "limit=100"
  ```

  ```typescript TypeScript theme={null}
  interface Page {
    data: { id: string }[];
    has_more: boolean;
    next_cursor: string | null;
  }

  const ids: string[] = [];
  let cursor: string | null = null;
  let pages = 0;

  // Stop after 5 pages: every page with results is charged.
  do {
    const url = new URL('https://api.alura.io/v1/research/products');
    url.searchParams.set('q', 'mug');
    url.searchParams.set('limit', '100');
    if (cursor) url.searchParams.set('cursor', cursor);

    const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.ALURA_API_KEY}` } });
    if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
    const page = (await res.json()) as Page;
    ids.push(...page.data.map((p) => p.id));
    cursor = page.next_cursor;
    pages += 1;
  } while (cursor && pages < 5);

  console.log(`${ids.length} products in ${pages} pages`);
  ```

  ```python Python theme={null}
  import json
  import os
  import urllib.parse
  import urllib.request

  ids = []
  cursor = None
  pages = 0

  # Stop after 5 pages: every page with results is charged.
  while True:
      params = {"q": "mug", "limit": 100}
      if cursor:
          params["cursor"] = cursor
      request = urllib.request.Request(
          f"https://api.alura.io/v1/research/products?{urllib.parse.urlencode(params)}",
          headers={"Authorization": f"Bearer {os.environ['ALURA_API_KEY']}"},
      )
      with urllib.request.urlopen(request) as response:
          page = json.load(response)
      ids.extend(p["id"] for p in page["data"])
      cursor = page["next_cursor"]
      pages += 1
      if not cursor or pages >= 5:
          break

  print(f"{len(ids)} products in {pages} pages")
  ```
</CodeGroup>

Paging stops after 10,000 results. [Lists and paging](/api-reference/pagination) has the details.

## Products like this one

`GET /v1/research/products/{id}/similar` finds products whose titles and tags share words with it, best match first, ranking those in the same category higher. It costs 10 API credits per page, like a search:

<CodeGroup>
  ```bash cURL theme={null}
  curl --fail-with-body -sS https://api.alura.io/v1/research/products/4524717183/similar \
    -H "Authorization: Bearer $ALURA_API_KEY"
  ```

  ```typescript TypeScript theme={null}
  interface Product {
    id: string;
    title: string | null;
    shop: { id: string; name: string | null };
  }

  const res = await fetch('https://api.alura.io/v1/research/products/4524717183/similar', {
    headers: { Authorization: `Bearer ${process.env.ALURA_API_KEY}` },
  });
  if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
  const page = (await res.json()) as { data: Product[] };

  for (const p of page.data) console.log(`${p.id} ${p.title} (${p.shop.name})`);
  ```

  ```python Python theme={null}
  import json
  import os
  import urllib.request

  request = urllib.request.Request(
      "https://api.alura.io/v1/research/products/4524717183/similar",
      headers={"Authorization": f"Bearer {os.environ['ALURA_API_KEY']}"},
  )
  with urllib.request.urlopen(request) as response:
      page = json.load(response)

  for p in page["data"]:
      print(f"{p['id']} {p['title']} ({p['shop']['name']})")
  ```
</CodeGroup>

## Open a product

`GET /v1/research/products/{id}` returns the full product: every image, tags, materials, attributes, reviews summary, the full set of sales estimates with `est_last_month` and `est_trend_3m`, and the listing score. It costs 1 API credit. Add `expand=shop` to include the shop, free. To open many at once, see [Bulk lookups](/guides/bulk-lookups).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.