> ## 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.

# Keyword research

> Check Etsy search volume, competition and trend for keywords, see what ranks for them, and score a product's tags

Decide which keywords are worth targeting. Look up demand and competition, explore related keywords, see which products rank, then check the tags of a product you know. You need an [API key](/quickstart).

<Steps>
  <Step title="Look up keywords">
    `GET /v1/research/keywords` takes up to 100 keywords in one call. Repeat `q` once per keyword, because a keyword can contain a comma. Set `language` to an ISO code from `GET /v1/languages`. The default is `en`.

    <CodeGroup>
      ```bash cURL theme={null}
      curl --fail-with-body -sS -G https://api.alura.io/v1/research/keywords \
        -H "Authorization: Bearer $ALURA_API_KEY" \
        --data-urlencode "q=personalized mug" \
        --data-urlencode "q=name mug" \
        --data-urlencode "language=en"
      ```

      ```typescript TypeScript theme={null}
      interface Keyword {
        id: string;
        keyword: string;
        volume: { etsy_monthly: number | null };
        competition: { level: string | null; competing_listings: number | null };
        trend: { change_3m: number | null };
      }
      interface Lookup {
        data: Keyword[];
        pending: string[];
        unfetched: { keyword: string; code: string; retry_after: number }[];
        not_found: string[];
      }

      const url = new URL('https://api.alura.io/v1/research/keywords');
      for (const q of ['personalized mug', 'name mug']) url.searchParams.append('q', q);
      url.searchParams.set('language', 'en');

      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 result = (await res.json()) as Lookup;

      for (const k of result.data) {
        console.log(`${k.keyword} (id ${k.id}): ${k.volume.etsy_monthly ?? 'unknown'} Etsy searches a month, competition ${k.competition.level ?? 'unknown'}`);
      }
      for (const k of result.unfetched) console.log(`${k.keyword}: not available yet (${k.code}), retry in ${k.retry_after}s`);
      ```

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

      # Repeat q once per keyword; keywords may contain commas.
      query = urllib.parse.urlencode([("q", "personalized mug"), ("q", "name mug"), ("language", "en")])
      request = urllib.request.Request(
          f"https://api.alura.io/v1/research/keywords?{query}",
          headers={"Authorization": f"Bearer {os.environ['ALURA_API_KEY']}"},
      )
      with urllib.request.urlopen(request) as response:
          result = json.load(response)

      for k in result["data"]:
          volume = k["volume"]["etsy_monthly"]
          print(f"{k['keyword']} (id {k['id']}): {'unknown' if volume is None else volume} Etsy searches a month, competition {k['competition']['level'] or 'unknown'}")
      for k in result["unfetched"]:
          print(f"{k['keyword']}: not available yet ({k['code']}), retry in {k['retry_after']}s")
      ```
    </CodeGroup>

    ```json Response theme={null}
    {
      "object": "list",
      "data": [
        {
          "object": "research.keyword",
          "id": "88123",
          "keyword": "personalized mug",
          "language": "en",
          "volume": { "etsy_monthly": 12400, "google_monthly": 40500 },
          "competition": { "level": "high", "competing_listings": 182000 },
          "trend": { "change_3m": 0.12, "change_yoy": 0.3 },
          "market": { "avg_price_usd": 24.1, "est_avg_monthly_sales": null }
        }
      ],
      "pending": [],
      "unfetched": [],
      "not_found": []
    }
    ```

    | Field | Meaning |
    | - | - |
    | `volume.etsy_monthly`, `volume.google_monthly` | Average searches a month on Etsy and on Google |
    | `competition.level` | `very_low` to `very_high` |
    | `competition.competing_listings` | How many Etsy listings compete for it |
    | `trend.change_3m`, `trend.change_yoy` | The change in Etsy searches over three months and over a year, as a fraction. `0.12` is +12% |
    | `market.avg_price_usd` | The average price of the listings that rank for it |
    | `market.est_avg_monthly_sales` | `null` for now. See [Keyword market](/methodology/keyword-market) |

    Alura trims spaces and ignores letter case, so `Name  Mug` and `name mug` are the same keyword. The language is part of the keyword: `mug` in English and `mug` in German are two keywords. Keywords Alura doesn't have yet come back in `pending` or `unfetched`, free.
  </Step>

  <Step title="Explore related keywords">
    Each keyword's `id` opens the keywords around it. Related keywords come 25 to a page:

    <CodeGroup>
      ```bash cURL theme={null}
      # 88123 is the id GET /v1/research/keywords returned for "personalized mug".
      curl --fail-with-body -sS https://api.alura.io/v1/research/keywords/88123/related \
        -H "Authorization: Bearer $ALURA_API_KEY"
      ```

      ```typescript TypeScript theme={null}
      interface Keyword {
        keyword: string;
        volume: { etsy_monthly: number | null };
        competition: { level: string | null };
      }

      // 88123 is the id GET /v1/research/keywords returned for "personalized mug".
      const res = await fetch('https://api.alura.io/v1/research/keywords/88123/related', {
        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: Keyword[] };

      for (const k of page.data) {
        console.log(`${k.keyword}: ${k.volume.etsy_monthly ?? 'unknown'} Etsy searches a month, competition ${k.competition.level ?? 'unknown'}`);
      }
      ```

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

      # 88123 is the id GET /v1/research/keywords returned for "personalized mug".
      request = urllib.request.Request(
          "https://api.alura.io/v1/research/keywords/88123/related",
          headers={"Authorization": f"Bearer {os.environ['ALURA_API_KEY']}"},
      )
      with urllib.request.urlopen(request) as response:
          page = json.load(response)

      for k in page["data"]:
          volume = k["volume"]["etsy_monthly"]
          print(f"{k['keyword']}: {'unknown' if volume is None else volume} Etsy searches a month, competition {k['competition']['level'] or 'unknown'}")
      ```
    </CodeGroup>

    ```json Response theme={null}
    {
      "object": "list",
      "data": [
        {
          "object": "research.keyword",
          "id": "88124",
          "keyword": "name mug",
          "volume": { "etsy_monthly": 5400, "google_monthly": 8100 },
          "competition": { "level": "moderate", "competing_listings": 64000 }
        }
      ],
      "has_more": false
    }
    ```
  </Step>

  <Step title="See what ranks for it">
    `GET /v1/research/keywords/{id}/products` lists the best-selling products whose title contains the keyword or that use it as a tag. `GET /v1/research/keywords/{id}/shops` lists the shops behind them. Both rank by Alura's sales estimates, not by Etsy's search order, up to 100 per page.

    <CodeGroup>
      ```bash cURL theme={null}
      curl --fail-with-body -sS -G https://api.alura.io/v1/research/keywords/88123/products \
        -H "Authorization: Bearer $ALURA_API_KEY" \
        --data-urlencode "limit=5" \
        --data-urlencode "fields=title,shop.name,sales.est_monthly_avg"
      ```

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

      const url = new URL('https://api.alura.io/v1/research/keywords/88123/products');
      url.searchParams.set('limit', '5');
      url.searchParams.set('fields', 'title,shop.name,sales.est_monthly_avg');

      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[] };

      for (const p of page.data) console.log(`${p.id} ${p.title} (${p.shop.name ?? 'unknown shop'}): ${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({"limit": 5, "fields": "title,shop.name,sales.est_monthly_avg"})
      request = urllib.request.Request(
          f"https://api.alura.io/v1/research/keywords/88123/products?{query}",
          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"]:
          monthly = p["sales"]["est_monthly_avg"]
          print(f"{p['id']} {p['title']} ({p['shop']['name'] or 'unknown shop'}): {monthly if monthly is not None else 'unknown'} a month")
      ```
    </CodeGroup>

    ```json Response theme={null}
    {
      "object": "list",
      "data": [
        {
          "id": "4524717183",
          "object": "research.product",
          "title": "Personalized Name Mug, Custom Coffee Mug",
          "shop": { "name": "MugCo" },
          "sales": { "est_monthly_avg": 210 }
        }
      ],
      "has_more": false
    }
    ```
  </Step>

  <Step title="Check a product's tags">
    `GET /v1/research/products/{id}/tags` returns a product's tags, each matched to a keyword in the product's language. `keyword` is `null` when Alura has no data for that tag. A product has at most 13 tags, so this is always one page.

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

      ```typescript TypeScript theme={null}
      interface Tag {
        tag: string;
        keyword: { id: string; volume: { etsy_monthly: number | null }; competition: { level: string | null } } | null;
      }

      const res = await fetch('https://api.alura.io/v1/research/products/4524717183/tags', {
        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: Tag[] };

      for (const t of page.data) {
        console.log(t.keyword ? `${t.tag}: ${t.keyword.volume.etsy_monthly ?? 'unknown'} searches a month, competition ${t.keyword.competition.level ?? 'unknown'}` : `${t.tag}: no keyword data`);
      }
      ```

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

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

      for t in page["data"]:
          k = t["keyword"]
          if k is None:
              print(f"{t['tag']}: no keyword data")
          else:
              volume = k["volume"]["etsy_monthly"]
              print(f"{t['tag']}: {'unknown' if volume is None else volume} searches a month, competition {k['competition']['level'] or 'unknown'}")
      ```
    </CodeGroup>

    ```json Response theme={null}
    {
      "object": "list",
      "data": [
        {
          "object": "research.tag",
          "tag": "personalized mug",
          "keyword": { "id": "88123", "volume": { "etsy_monthly": 12400 }, "competition": { "level": "high" } }
        },
        {
          "object": "research.tag",
          "tag": "name mug",
          "keyword": { "id": "88124", "volume": { "etsy_monthly": 5400 }, "competition": { "level": "moderate" } }
        },
        { "object": "research.tag", "tag": "custom gift", "keyword": null }
      ]
    }
    ```
  </Step>
</Steps>

## Search history and trending keywords

* `GET /v1/research/keywords/{id}/history` returns monthly Etsy and Google searches for every completed month, with `null` where Alura has no data. It needs the Growth plan or above.
* `GET /v1/research/keywords/trending` lists the keywords whose Etsy searches rose the most over the last three months. It covers English only for now. See [Trending](/features/trending).

In an AI assistant, `get_related_keywords`, `get_keyword_top_results` and `get_keyword_history` take the keyword's text instead of its `id`. `get_trending` lists trending keywords.

## What it costs

| Call | Cost |
| - | - |
| `GET /v1/research/keywords` | 1 API credit per keyword returned in `data` |
| `GET /v1/research/keywords/{id}` | 1 API credit per call |
| `GET /v1/research/keywords/{id}/related` | 1 API credit per page |
| `GET /v1/research/keywords/{id}/products` | 10 API credits per page |
| `GET /v1/research/keywords/{id}/shops` | 10 API credits per page |
| `GET /v1/research/keywords/{id}/history` | 1 API credit per call |
| `GET /v1/research/keywords/trending` | 10 API credits per page |
| `GET /v1/research/products/{id}/tags` | 1 API credit per page |

Keywords in `pending`, `unfetched` or `not_found` are free. Tags never start a fetch.

## Next steps

<Columns cols={3}>
  <Card title="Find products" icon="magnifying-glass" href="/guides/find-products">
    Search for products that use a keyword.
  </Card>

  <Card title="Bulk lookups" icon="layer-group" href="/guides/bulk-lookups">
    How `pending` and `unfetched` work.
  </Card>

  <Card title="Keyword market" icon="book" href="/methodology/keyword-market">
    How Alura measures a keyword's market.
  </Card>
</Columns>


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