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

# Create Keyword Watchlist

> Create a keyword watchlist by specifying keywords directly or by describing what to track in plain English.

Track LinkedIn posts containing specific keywords with powerful filtering options. Monitor industry trends, job postings, or any topic of interest with customizable fetch frequencies.

<Note>
  **Platform: LinkedIn.** This endpoint creates LinkedIn watchlists only. To track the same keywords or prompt on **Reddit**, make a separate call to [`POST /api-reddit-watchlist`](/docs/api-reference/watchlist/reddit/create) (identical body shape). One watchlist = one platform; "LinkedIn and Reddit" always means two create calls. See [Choose a watchlist type](/docs/api-reference/watchlist/overview).
</Note>

The endpoint accepts two body shapes. Pass either `keywords` (direct mode, documented below) or `prompt` (AI mode, documented in [Prompt mode](#prompt-mode) at the bottom of this page), not both.

## Request Body (direct keywords mode)

<ParamField body="name" type="string">
  Watchlist name. If not provided, a name will be auto-generated based on
  keywords.
</ParamField>

<ParamField body="keywords" type="array" required>
  Array of keywords to track. Can be simple strings or advanced keyword objects with filtering rules.

  **Simple format:**

  ```json theme={null}
  ["hiring", "blockchain", "remote work"]
  ```

  **Advanced format with filters:**

  ```json theme={null}
  [
    {
      "keyword": "software engineer",
      "required_keywords": ["remote", "senior"],
      "excluded_keywords": ["unpaid", "intern"],
      "include_all_required": false
    }
  ]
  ```
</ParamField>

<ParamField body="description" type="string">
  Optional description for the watchlist
</ParamField>

<ParamField body="labels" type="array">
  Custom intent labels OutX uses to classify matching posts. Each label is a `name`, a `description` (what the AI matches a post against), and an optional `default` flag marking it as a pre-applied feed filter (at most 2 can be `true`; if you set `default` on some labels but mark none `true`, the first is promoted). If omitted, OutX generates a label taxonomy in the background. See [Intent labels & defaults](/docs/api-reference/concepts/intent-labels) and [How intent labels are calculated](#how-intent-labels-are-calculated).

  ```json theme={null}
  [
    {
      "name": "hiring",
      "description": "Job postings for senior engineering roles",
      "default": true
    },
    {
      "name": "industry-news",
      "description": "Hiring-market news and layoff announcements",
      "default": false
    }
  ]
  ```
</ParamField>

<ParamField body="objective" type="string">
  Optional one-sentence objective describing who or what you want to find and why (for example `"Find companies actively hiring senior remote engineers so we can pitch our sourcing tool"`). It seeds the relevance-scoring vector so posts rank like a prompt-built watchlist. If omitted, OutX generates one in the background from the name, description, and keywords.
</ParamField>

<ParamField body="fetchFreqInHours" type="number" default="12">
  Fetch frequency in hours. Allowed values: `1, 3, 6, 12, 24, 48, 72`
</ParamField>

<ParamField body="slack_webhook_url" type="string | null">
  Optional Slack incoming-webhook URL to receive notifications when new matching posts are found.
</ParamField>

## How intent labels are calculated

<Info>
  Full reference: [Intent labels, enrichment & default feed filters](/docs/api-reference/concepts/intent-labels), including what `default: true/false` means and how to change defaults later.
</Info>

Labels (shown as **Intents** in the OutX app) are how OutX classifies the meaning of a matched post, not just whether it matched a keyword. Each label is a `name` plus a `description`, and the `description` is what the AI matches a post against.

Labels are stored on the watchlist, not on individual posts. They are applied to posts during a background enrichment pass and then surface on each post in the [Get posts](/docs/api-reference/engagement/posts/get) response as `tags` (the matched label name) and `tagDescriptions`.

<Info>
  The same concept has two names in the API: `labels` on the watchlist, `tags` on a post. A post's `tag` is the `label` it was classified into. A post with no confident match gets the tag `"na"`.
</Info>

### How a post gets classified

Each newly collected post is enriched in two steps:

1. **Relevance scoring (deterministic).** The post is scored against the watchlist using embedding similarity plus keyword matching, producing `relevance_score` (1-10). Low-relevance posts are tagged `"na"` and skip the next step.
2. **Label selection (AI).** For relevant posts, a single model call picks the one label whose `description` best fits the post, or none. The chosen label is written to the post's `tags`.

Because only the `description` is read by the classifier, a clear, specific `description` matters far more than the `name`.

<Warning>
  There is no `sentiment` field on posts. OutX classifies posts into your intent labels (`tags`), not into positive/negative sentiment, and `relevance_score` measures relevance to the watchlist, not tone. If you need sentiment, derive it from the post `content` yourself, or model it as an intent label (for example a label described as "frustrated with or complaining about a competitor").
</Warning>

### Timing: labels are not instant

A post is not labeled the moment you create the watchlist. Two delays stack:

1. **Collection cadence.** New posts only arrive on the watchlist's `fetchFreqInHours` cycle (`1, 3, 6, 12, 24, 48, 72`; default 12h). Until the next scan runs, there are no new posts to classify.
2. **Enrichment.** Every newly collected post starts unlabeled and is classified by a background job. Historical posts pulled in when the watchlist is first created are unlabeled the same way until that job processes them.

Labeled posts therefore appear after the next fetch cycle plus the time enrichment takes to work through its queue, not instantly. An agent polling [Get posts](/docs/api-reference/engagement/posts/get) right after creating a watchlist should treat empty `tags` as "not classified yet," not "no match."

### Editing labels is forward-only

Each post is classified into a label exactly once, when it is first enriched. Adding or changing `labels` with [Update keyword watchlist](/docs/api-reference/watchlist/keyword/update) changes how future posts are classified but does not re-label posts that were already enriched, they keep the tags they had. To re-classify already-collected posts against new labels, those posts must be explicitly re-enriched (OutX re-runs classification over roughly the last 30 days); a plain label update does not do this on its own.

## Advanced Keyword Filtering

Each keyword can have additional filtering rules:

<ResponseField name="keyword" type="string" required>
  The primary keyword to search for
</ResponseField>

<ResponseField name="required_keywords" type="array">
  Context keywords used together with the primary keyword. How they combine is controlled by `include_all_required`.
</ResponseField>

<ResponseField name="excluded_keywords" type="array">
  None of these keywords should be present in the post (NOT logic)
</ResponseField>

<ResponseField name="include_all_required" type="boolean" default="true">
  Controls how `required_keywords` combine. `true` (default) means every required keyword must be present (AND logic). `false` means any one is enough (OR logic). OR usually matches far more posts and is what the AI prompt generator uses, so set `include_all_required` to `false` when a multi-term required list is matching almost nothing. Plain-string keyword entries always use the default (AND).
</ResponseField>

<Info>
  When you create a keyword watchlist, OutX backfills recent matching posts automatically in the background, so the watchlist is usually non-empty within minutes instead of waiting for the first live fetch cycle.
</Info>

<RequestExample>
  ```bash cURL theme={null}
  curl -X POST \
    "https://api.outx.ai/api-keyword-watchlist" \
    -H "Content-Type: application/json" \
    -H "x-api-key: YOUR_API_KEY" \
    -d '{
      "name": "Remote Senior Jobs",
      "keywords": [
        {
          "keyword": "hiring",
          "required_keywords": ["remote", "senior", "engineer"],
          "excluded_keywords": ["unpaid", "intern"]
        },
        {
          "keyword": "job opening",
          "required_keywords": ["work from home"],
          "excluded_keywords": ["junior"]
        }
      ],
      "description": "Track remote senior engineering positions",
      "labels": [
        {
          "name": "hiring",
          "description": "Job postings"
        }
      ],
      "fetchFreqInHours": 6
    }'
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch("https://api.outx.ai/api-keyword-watchlist", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "x-api-key": "YOUR_API_KEY",
    },
    body: JSON.stringify({
      name: "Remote Senior Jobs",
      keywords: [
        {
          keyword: "hiring",
          required_keywords: ["remote", "senior", "engineer"],
          excluded_keywords: ["unpaid", "intern"],
        },
      ],
      fetchFreqInHours: 6,
    }),
  });

  const data = await response.json();
  ```

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

  url = 'https://api.outx.ai/api-keyword-watchlist'
  headers = {
      'Content-Type': 'application/json',
      'x-api-key': 'YOUR_API_KEY'
  }
  payload = {
      'name': 'Remote Senior Jobs',
      'keywords': [
          {
              'keyword': 'hiring',
              'required_keywords': ['remote', 'senior', 'engineer'],
              'excluded_keywords': ['unpaid', 'intern']
          }
      ],
      'fetchFreqInHours': 6
  }

  response = requests.post(url, headers=headers, json=payload)
  ```
</RequestExample>

<ResponseExample>
  ```json Response (201 Created) theme={null}
  {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "Remote Senior Jobs",
    "slug": "remote-senior-jobs-550e8400",
    "type": "keyword",
    "keywords": ["hiring", "job opening"],
    "fetchFreqInHours": 6,
    "created": true,
    "results": [
      {
        "success": true,
        "keyword": "hiring",
        "keyword_id": "660e8400-e29b-41d4-a716-446655440001"
      }
    ],
    "labels": [
      { "key": "hiring", "description": "Job postings", "default": true }
    ],
    "message": "Watchlist created. An objective and intent labels are being generated in the background and will be available shortly."
  }
  ```

  ```json Response (200 OK, existing match) theme={null}
  {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "Remote Senior Jobs",
    "slug": "remote-senior-jobs-550e8400",
    "type": "keyword",
    "keywords": ["hiring", "job opening"],
    "fetchFreqInHours": 6,
    "created": false,
    "labels": [
      { "key": "hiring" },
      { "key": "job opening" }
    ]
  }
  ```
</ResponseExample>

## Idempotency

POST is idempotent on the keyword set. If your team already has a keyword watchlist whose primary keywords match the request exactly (order-insensitive, lower-cased, trimmed), the existing watchlist is returned with:

* HTTP status **200 OK** (instead of **201 Created**)
* `created: false`
* The existing watchlist's `fetchFreqInHours` and `labels` are returned, your request's values are ignored for the duplicate match.

Use this to safely retry creations without hitting the plan-watchlist limit. The plan check is only applied when a brand-new watchlist would be created.

## Response Fields

<ResponseField name="id" type="string">
  Unique identifier for the watchlist
</ResponseField>

<ResponseField name="name" type="string">
  Watchlist name
</ResponseField>

<ResponseField name="slug" type="string">
  URL-friendly slug for the watchlist
</ResponseField>

<ResponseField name="type" type="string">
  Always "keyword" for keyword watchlists
</ResponseField>

<ResponseField name="keywords" type="array">
  Array of tracked keywords
</ResponseField>

<ResponseField name="fetchFreqInHours" type="number">
  Fetch frequency in hours
</ResponseField>

<ResponseField name="created" type="boolean">
  `true` when a new watchlist was created (status 201), `false` when an existing watchlist with the same keyword set was returned (status 200).
</ResponseField>

<ResponseField name="results" type="array">
  Array of keyword creation results
</ResponseField>

<ResponseField name="labels" type="array">
  Intent labels persisted on the watchlist, as `{ key, description, default }` objects. When you did not supply real labels, these are placeholders that are replaced by an AI-generated taxonomy a few seconds later (see `message`).
</ResponseField>

<ResponseField name="message" type="string">
  Present when OutX is generating a missing objective or label taxonomy in the background: `"Watchlist created. An objective and intent labels are being generated in the background and will be available shortly."`
</ResponseField>

## Error Responses

| Status Code | Error Message                                                             | Description                                                                                                                         |
| ----------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| 400         | Either `keywords` array or `prompt` string is required                    | No keywords or prompt was supplied                                                                                                  |
| 400         | Provide either `keywords` or `prompt`, not both                           | Both modes were supplied at once                                                                                                    |
| 400         | No valid keywords provided after cleaning                                 | All keywords were empty strings after normalization                                                                                 |
| 400         | Invalid fetchFreqInHours. Allowed values: 1, 3, 6, 12, 24, 48, 72 (hours) | Unsupported fetch frequency                                                                                                         |
| 401         | Missing API Key / Invalid API Key                                         | Missing or invalid `x-api-key` header                                                                                               |
| 402         | Your plan includes up to N watchlists...                                  | Plan watchlist limit reached (all watchlist types count together). Check quota first with [GET /api-team](/docs/api-reference/team/get). |
| 403         | Plugin installation required                                              | No team member has had the OutX Chrome extension active in the last 48 hours.                                                       |

## Use Cases

<AccordionGroup>
  <Accordion title="Track Job Postings" icon="briefcase">
    Monitor hiring posts for specific roles and locations:

    ```json theme={null}
    {
      "name": "Remote Engineering Jobs",
      "keywords": [
        {
          "keyword": "hiring",
          "required_keywords": ["engineer", "remote"],
          "excluded_keywords": ["intern", "junior"]
        }
      ]
    }
    ```
  </Accordion>

  <Accordion title="Monitor Industry Trends" icon="chart-line">
    Track discussions about emerging technologies:

    ```json theme={null}
    {
      "name": "AI Trends",
      "keywords": ["GPT-4", "Claude", "LLM", "artificial intelligence"],
      "fetchFreqInHours": 3
    }
    ```
  </Accordion>

  <Accordion title="Competitor Analysis" icon="magnifying-glass-chart">
    Watch for mentions of competitors:

    ```json theme={null}
    {
      "name": "Competitor Mentions",
      "keywords": [
        {
          "keyword": "CompetitorName",
          "excluded_keywords": ["partnership", "collaboration"]
        }
      ]
    }
    ```
  </Accordion>
</AccordionGroup>

## Prompt mode

Skip the keyword brainstorming and let OutX generate keywords and intent labels for you. Send a single `prompt` field describing what you want to track in plain English. OutX creates the watchlist immediately and runs keyword and label generation in the background.

### Request Body

<ParamField body="prompt" type="string" required>
  Plain-English description of what to track. URLs are allowed; OutX fetches page metadata to improve keyword quality.

  Examples:

  * `"AI-native CRM tools and competitors like Salesforce, HubSpot"`
  * `"People looking for remote software engineering jobs"`
  * `"Mentions of our product Acme and common complaints"`
</ParamField>

<ParamField body="name" type="string">
  Optional watchlist name. Auto-generated from the prompt if omitted.
</ParamField>

<ParamField body="fetchFreqInHours" type="number" default="12">
  Fetch frequency in hours. Allowed values: `1, 3, 6, 12, 24, 48, 72`.
</ParamField>

<RequestExample>
  ```bash cURL theme={null}
  curl -X POST \
    "https://api.outx.ai/api-keyword-watchlist" \
    -H "Content-Type: application/json" \
    -H "x-api-key: YOUR_API_KEY" \
    -d '{
      "prompt": "Track discussions about AI-native CRM tools, including competitors like Salesforce and HubSpot",
      "name": "AI CRM Watchlist",
      "fetchFreqInHours": 12
    }'
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch("https://api.outx.ai/api-keyword-watchlist", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "x-api-key": "YOUR_API_KEY",
    },
    body: JSON.stringify({
      prompt:
        "Track discussions about AI-native CRM tools, including competitors like Salesforce and HubSpot",
      fetchFreqInHours: 12,
    }),
  });

  const result = await response.json();
  console.log("Watchlist created:", result.id);
  ```

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

  headers = {
      "Content-Type": "application/json",
      "x-api-key": "YOUR_API_KEY",
  }

  payload = {
      "prompt": "Track discussions about AI-native CRM tools, including competitors like Salesforce and HubSpot",
      "fetchFreqInHours": 12,
  }

  response = requests.post(
      "https://api.outx.ai/api-keyword-watchlist",
      headers=headers,
      json=payload,
  )

  result = response.json()
  print("Watchlist created:", result["id"])
  ```
</RequestExample>

<ResponseExample>
  ```json 201 Created theme={null}
  {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "AI CRM Watchlist",
    "slug": "ai-crm-watchlist-550e8400",
    "type": "keyword",
    "fetchFreqInHours": 12,
    "prompt": "Track discussions about AI-native CRM tools, including competitors like Salesforce and HubSpot",
    "created": true,
    "message": "Watchlist created. Keywords and labels are being generated in the background and will be available shortly."
  }
  ```
</ResponseExample>

Keywords and intent labels populate on the watchlist a few seconds after creation. Use [Get Keyword Watchlist](/docs/api-reference/watchlist/keyword/get) to fetch the latest state.

## Frequently Asked Questions

<AccordionGroup>
  <Accordion title="Is there a maximum number of keywords I can add to a single watchlist?">
    There is no hard per-watchlist keyword cap enforced by the API. What your plan caps is the number of watchlists and the posts collected per watchlist per week, so a very broad keyword set exhausts the weekly post quota faster. Check your plan limits with [GET /api-team](/docs/api-reference/team/get).
  </Accordion>

  <Accordion title="How do I track the same keywords on Reddit too?">
    Make a second create call to [`POST /api-reddit-watchlist`](/docs/api-reference/watchlist/reddit/create) with the same `keywords` or `prompt`. LinkedIn and Reddit watchlists are separate resources with separate IDs and feeds; there is no single call that creates both.
  </Accordion>

  <Accordion title="How soon does tracking start after I create a keyword watchlist?">
    Tracking begins immediately after creation. OutX will start scanning LinkedIn for matching posts on the next fetch cycle, based on the `fetchFreqInHours` value you set. If you set `fetchFreqInHours` to 1, you can expect the first results within an hour.
  </Accordion>

  <Accordion title="What happens when I hit my plan's watchlist limit?">
    When you reach the maximum number of watchlists allowed by your plan, the API will return an error when you try to create a new one. You can either delete an existing watchlist to free up a slot, or upgrade your plan for higher limits.
  </Accordion>
</AccordionGroup>
