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

# Public API: Keyword Discovery

> Generate keyword research from a seed keyword and retrieve enriched keyword data via the API

Discover new keyword opportunities from a seed keyword — including autocomplete suggestions, related searches, People Also Ask, and search volume metrics.

<Info>
  Full endpoint reference available on [Swagger](https://api.keywordinsights.ai/apidocs/#/Keyword%20Discovery).
</Info>

### Quick Start

Create a keyword discovery order from a seed keyword:

```python theme={null}
import os
import requests

API_KEY = os.environ["KWI_API_KEY"]
BASE_URL = "https://api.keywordinsights.ai"
HEADERS = {"X-API-Key": API_KEY}

payload = {
    "seed_keyword": "content marketing strategies",
    "language": "en",
    "location": "United States",
}

response = requests.post(
    f"{BASE_URL}/api/keyword-discovery/order/",
    headers=HEADERS,
    json=payload,
)

data = response.json()
print(data)
# {"status": true, "payload": {"id": "339ca10b-...", "status": "created", ...}}
```

### Endpoints

| Method | Path                                               | Description                                       |
| ------ | -------------------------------------------------- | ------------------------------------------------- |
| POST   | `/api/keyword-discovery/order/`                    | Create a keyword discovery order                  |
| GET    | `/api/keyword-discovery/order/{order_id}/`         | Get order details and processing status (polling) |
| DELETE | `/api/keyword-discovery/order/{order_id}/`         | Delete an order (owner only)                      |
| GET    | `/api/keyword-discovery/order/{order_id}/keywords` | Get discovered keywords (paginated, filterable)   |
| GET    | `/api/keyword-discovery/orders/`                   | List your orders (paginated)                      |
| GET    | `/api/keyword-discovery/languages/`                | List supported languages                          |
| GET    | `/api/keyword-discovery/locations/`                | List all supported locations                      |
| GET    | `/api/keyword-discovery/locations/{search}/`       | Search locations by name (top 10 fuzzy matches)   |

### Parameters

#### Create Order — Required

| Parameter      | Type   | Description                                                                   |
| -------------- | ------ | ----------------------------------------------------------------------------- |
| `seed_keyword` | string | The main keyword to discover related keywords for                             |
| `language`     | string | ISO 639-1 language code (e.g. `en`). See `/api/keyword-discovery/languages/`  |
| `location`     | string | Location name (e.g. `United States`). See `/api/keyword-discovery/locations/` |

#### Create Order — Optional

| Parameter   | Type   | Description                                                                                                         |
| ----------- | ------ | ------------------------------------------------------------------------------------------------------------------- |
| `folder_id` | string | Dashboard folder ID to organize the order. Also pulls workspace/domain settings if the folder is linked to a domain |

### Polling Order Status

Orders are processed asynchronously. Poll the order endpoint until `payload.status` is `done` (or `error`). Each data source reports its own progress in `payload.processing_status`; keyword metrics (volume, CPC, competition) are attached once `processing_status.enrich_status` is `done`.

```python theme={null}
import time

def wait_for_order(order_id, poll_interval=10, max_wait=300):
    """Poll until the order finishes processing."""
    start = time.time()
    while time.time() - start < max_wait:
        response = requests.get(
            f"{BASE_URL}/api/keyword-discovery/order/{order_id}/",
            headers=HEADERS,
        )
        response.raise_for_status()
        payload = response.json()["payload"]

        status = payload["status"]
        processing = payload.get("processing_status") or {}
        print(f"status: {status}, enrichment: {processing.get('enrich_status')}")

        if status == "error":
            raise RuntimeError("Order failed to process")
        if status == "done" and processing.get("enrich_status") == "done":
            return payload

        time.sleep(poll_interval)

    raise TimeoutError(f"Order not ready after {max_wait}s")
```

The order detail response also includes `n_keywords` (total discovered so far), a short `keywords` preview, and `seed_keyword_data` (metrics for the seed keyword itself).

| Status       | Meaning                                                                             |
| ------------ | ----------------------------------------------------------------------------------- |
| `created`    | Order accepted, processing not started                                              |
| `processing` | Keyword sources are being fetched                                                   |
| `done`       | All sources fetched — check `processing_status.enrich_status` for metric enrichment |
| `error`      | Processing failed (credits are refunded automatically)                              |

### Retrieving Keywords

Once the order is complete, retrieve discovered keywords with filtering and sorting:

```python theme={null}
order_id = "339ca10b-80c1-4248-b8d0-c11377c204c7"

response = requests.get(
    f"{BASE_URL}/api/keyword-discovery/order/{order_id}/keywords",
    headers=HEADERS,
    params={
        "page_size": 50,
        "page_number": 1,
        "sort_by": "volume",
        "sort_order": "desc",
    },
)

data = response.json()["payload"]
print(f"Total keywords: {data['total_list_length']}")

for kw in data["list"]:
    print(f"  {kw['keyword']} — vol: {kw['volume']}, source: {kw['source']}")
```

#### Keywords Query Parameters

**Pagination & sorting:**

| Parameter     | Type    | Default  | Description                                                     |
| ------------- | ------- | -------- | --------------------------------------------------------------- |
| `page_size`   | integer | 10       | Results per page (max 10000)                                    |
| `page_number` | integer | 1        | Page number                                                     |
| `sort_by`     | string  | `volume` | Sort field: `keyword`, `source`, `volume`, `cpc`, `competition` |
| `sort_order`  | string  | `desc`   | `asc` or `desc`                                                 |

**Source filtering:**

| Parameter | Type      | Description                                                |
| --------- | --------- | ---------------------------------------------------------- |
| `source`  | string    | Filter by single source                                    |
| `sources` | string\[] | Filter by multiple sources (pass as repeated query params) |

Available sources: `seed_keyword`, `google_autocomplete`, `google_related_searches`, `people_also_ask`, `quora`, `reddit`, `google_search_console`, `generated_keywords`

**Keyword filtering:**

| Parameter           | Type      | Description                                  |
| ------------------- | --------- | -------------------------------------------- |
| `keywords_included` | string\[] | Only include keywords containing these terms |
| `keywords_excluded` | string\[] | Exclude keywords containing these terms      |
| `include_hidden`    | boolean   | Include hidden keywords (default `false`)    |

**Metric filters:**

| Parameter                             | Type    | Description                                          |
| ------------------------------------- | ------- | ---------------------------------------------------- |
| `volume_from` / `volume_to`           | integer | Search volume range                                  |
| `cpc_from` / `cpc_to`                 | float   | CPC range                                            |
| `competition_from` / `competition_to` | float   | Competition value range                              |
| `competition_cat`                     | string  | Competition category: `all`, `low`, `medium`, `high` |
| `ctr_from` / `ctr_to`                 | float   | CTR range (GSC data)                                 |
| `clicks_from` / `clicks_to`           | integer | Clicks range (GSC data)                              |
| `position_from` / `position_to`       | float   | SERP position range (GSC data)                       |
| `impressions_from` / `impressions_to` | integer | Impressions range (GSC data)                         |

### Managing Orders

#### Listing Your Orders

```python theme={null}
response = requests.get(
    f"{BASE_URL}/api/keyword-discovery/orders/",
    headers=HEADERS,
    params={
        "sort_by": "created_date",
        "sort_order": "desc",
        "search_query": "marketing",  # optional: filter by seed keyword
        "page_size": 10,
    },
)

data = response.json()["payload"]
for order in data["list"]:
    print(f"{order['seed_keyword']} — {order['status']} ({order['n_keywords']} keywords)")
```

| Parameter      | Type    | Default | Description                      |
| -------------- | ------- | ------- | -------------------------------- |
| `sort_by`      | string  | —       | `seed_keyword` or `created_date` |
| `sort_order`   | string  | `asc`   | `asc` or `desc`                  |
| `search_query` | string  | —       | Filter orders by seed keyword    |
| `page_size`    | integer | 10      | Results per page                 |
| `page_number`  | integer | 1       | Page number                      |

#### Deleting an Order

Only the order owner can delete an order:

```python theme={null}
response = requests.delete(
    f"{BASE_URL}/api/keyword-discovery/order/{order_id}/",
    headers=HEADERS,
)
# {"status": true, "payload": {"status": "deleted"}, "error": null}
```

### Languages & Locations

Look up valid values for the create-order `language` and `location` fields:

```python theme={null}
# All supported languages: [{"name": "English", "code": "en"}, ...]
languages = requests.get(
    f"{BASE_URL}/api/keyword-discovery/languages/", headers=HEADERS
).json()["payload"]

# Search locations by name (top 10 fuzzy matches)
locations = requests.get(
    f"{BASE_URL}/api/keyword-discovery/locations/united/", headers=HEADERS
).json()["payload"]
# [{"name": "United States", "code": "us"}, {"name": "United Kingdom", "code": "gb"}, ...]
```

<Tip>
  The full `/api/keyword-discovery/locations/` list is large — prefer the search variant when you know the location name.
</Tip>

### Customizations

#### Filtering by Source

Narrow results to specific keyword sources:

```python theme={null}
# Only autocomplete and People Also Ask keywords
response = requests.get(
    f"{BASE_URL}/api/keyword-discovery/order/{order_id}/keywords",
    headers=HEADERS,
    params={
        "sources": ["google_autocomplete", "people_also_ask"],
        "sort_by": "volume",
        "sort_order": "desc",
    },
)
```

#### Filtering by Metrics

Find high-volume, low-competition keywords:

```python theme={null}
response = requests.get(
    f"{BASE_URL}/api/keyword-discovery/order/{order_id}/keywords",
    headers=HEADERS,
    params={
        "volume_from": 1000,
        "competition_cat": "low",
        "sort_by": "volume",
        "sort_order": "desc",
    },
)
```

### Complete Example

Create a keyword discovery order, poll until it completes, retrieve the top keywords by volume, and clean up:

```python theme={null}
import os
import time
import requests

API_KEY = os.environ["KWI_API_KEY"]
BASE_URL = "https://api.keywordinsights.ai"
HEADERS = {"X-API-Key": API_KEY}


def create_discovery(seed_keyword, language="en", location="United States"):
    """Create a keyword discovery order."""
    response = requests.post(
        f"{BASE_URL}/api/keyword-discovery/order/",
        headers=HEADERS,
        json={
            "seed_keyword": seed_keyword,
            "language": language,
            "location": location,
        },
    )
    response.raise_for_status()
    return response.json()["payload"]


def wait_for_order(order_id, poll_interval=10, max_wait=300):
    """Poll the order endpoint until processing completes."""
    start = time.time()
    while time.time() - start < max_wait:
        response = requests.get(
            f"{BASE_URL}/api/keyword-discovery/order/{order_id}/",
            headers=HEADERS,
        )
        response.raise_for_status()
        payload = response.json()["payload"]
        processing = payload.get("processing_status") or {}

        if payload["status"] == "error":
            raise RuntimeError("Order failed to process")
        if payload["status"] == "done" and processing.get("enrich_status") == "done":
            return payload

        time.sleep(poll_interval)
    raise TimeoutError(f"Order not ready after {max_wait}s")


def get_keywords(order_id, page_size=50, min_volume=0):
    """Fetch discovered keywords with optional volume filter."""
    response = requests.get(
        f"{BASE_URL}/api/keyword-discovery/order/{order_id}/keywords",
        headers=HEADERS,
        params={
            "page_size": page_size,
            "page_number": 1,
            "sort_by": "volume",
            "sort_order": "desc",
            "volume_from": min_volume,
        },
    )
    response.raise_for_status()
    return response.json()["payload"]


# --- Run ---

result = create_discovery("project management software")
order_id = result["id"]
print(f"Order created: {order_id}")

order = wait_for_order(order_id)
print(f"Processing complete: {order['n_keywords']} keywords discovered")

data = get_keywords(order_id, page_size=20, min_volume=500)
print(f"\nTop {len(data['list'])} keywords (of {data['total_list_length']} total):\n")

for kw in data["list"]:
    print(f"  {kw['keyword']:50s} vol: {kw['volume']:>6}  source: {kw['source']}")

# Optional cleanup
requests.delete(f"{BASE_URL}/api/keyword-discovery/order/{order_id}/", headers=HEADERS)
```
