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

# Update Keyword Watchlist

> Patch fields (name, fetchFreq, keywords, labels, slack webhook, disable) or regenerate the entire keyword set from a new prompt.

Modify an existing keyword watchlist without losing its ID or collected posts. Every field is optional, send only the properties you want to change.

<Note>
  **Platform: LinkedIn.** This endpoint updates LinkedIn keyword watchlists only. Reddit watchlists are updated with [`PUT /api-reddit-watchlist`](/docs/api-reference/watchlist/reddit/update) (same body shape). See [Choose a watchlist type](/docs/api-reference/watchlist/overview).
</Note>

<Warning>
  **Disabled watchlists reject updates.** If the watchlist is paused (`disabled: true` on GET), any PUT except re-enabling returns `409 This watchlist is disabled. Enable it before updating it.` Re-enable first with `{ "id": "...", "disable": false }`, then apply your changes.
</Warning>

The endpoint accepts two body shapes. Either patch fields (documented below) or send a new `prompt` to have OutX regenerate the entire keyword set in the background ([Prompt mode](#prompt-mode) at the bottom of this page).

## Request Body (patch mode)

<ParamField body="id" type="string" required>
  Watchlist ID to update.
</ParamField>

<ParamField body="name" type="string">
  New watchlist name.
</ParamField>

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

<ParamField body="keywords" type="array">
  Patch the tracked keywords on the watchlist. Accepts the same shape as the [Create endpoint](/docs/api-reference/watchlist/keyword/create), simple strings or advanced keyword objects with `required_keywords`, `excluded_keywords` (NOT), and `include_all_required` (AND vs OR over the required keywords).

  **Simple format:**

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

  **Advanced format:**

  ```json theme={null}
  [
    {
      "keyword": "CTO",
      "required_keywords": ["hiring", "founder", "vp"],
      "excluded_keywords": ["recruiter", "spam"],
      "include_all_required": false
    }
  ]
  ```

  By default this replaces the set: the watchlist's existing keywords and associated `keyword_tracking` tasks are deleted and recreated from the new list (set `append: true` to add instead). Newly created keywords backfill recent matching posts automatically in the background. Updating keywords does **not** change existing labels. Omit the field to leave keywords untouched.
</ParamField>

<ParamField body="append" type="boolean" default="false">
  When `true`, the supplied `keywords` are **added** to the existing set instead of replacing it. Existing keyword tracking rows and tasks are kept, and only keywords whose primary keyword is not already on the list (case-insensitive) are created. Labels are left untouched unless `labels` is also provided.
</ParamField>

<ParamField body="labels" type="array">
  Replace the custom labels on the watchlist. Same shape as Create: each label is a `name` (or `key`, so a GET response can be echoed back), a `description`, and an optional `default` flag marking it as a pre-applied feed filter (at most 2 `true`; if you set the flag on some labels but mark none `true`, the first is promoted). This is also how you change **which labels are default**: send the full array with the `default` booleans you want. See [Intent labels & defaults](/docs/api-reference/concepts/intent-labels).

  ```json theme={null}
  [
    { "name": "hiring", "description": "Job postings", "default": true },
    { "name": "competitor", "description": "Competitor mentions", "default": false }
  ]
  ```

  Labels are only changed when this field is sent, and sending it **replaces the entire set** (start from the current GET output; omitted labels are dropped). Updating `keywords` alone leaves your existing labels untouched, so a keyword edit will never wipe a hand-built label set. Omit `labels` to leave them as they are.
</ParamField>

<ParamField body="disable" type="boolean">
  Set to `true` to pause tracking, `false` to resume.
</ParamField>

<ParamField body="slack_webhook_url" type="string | null">
  Update or clear the Slack webhook URL for this watchlist. Pass `null` to clear.
</ParamField>

## Semantics

* **Keyword replacement is wipe-and-recreate (default).** Existing `keyword_tracking` rows and their `keyword_tracking` tasks are deleted, then new ones are created from the payload. Posts already collected are **not** deleted, they remain attached to the watchlist and visible via the Posts API.
* **Append mode keeps what you have.** With `append: true`, existing keywords and tasks are kept and only the new, not-yet-present primary keywords are added.
* **New keywords backfill automatically.** Whether replacing or appending, each newly created keyword backfills recent matching posts in the background, so it does not have to wait for the next live fetch to show results.
* **Labels are preserved unless you send `labels`.** Updating `keywords` alone never overwrites existing labels. To change labels, pass a `labels` array.
* **Only `keyword_tracking` tasks are touched.** Any other task types on the same watchlist are left alone.
* **Partial updates are safe.** Sending only `{ "id": "...", "name": "new name" }` changes just the name; keywords, labels, and tasks are untouched.

<RequestExample>
  ```bash Rename and change frequency theme={null}
  curl -X PUT \
    "https://api.outx.ai/api-keyword-watchlist" \
    -H "Content-Type: application/json" \
    -H "x-api-key: YOUR_API_KEY" \
    -d '{
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "name": "Updated Watchlist Name",
      "fetchFreqInHours": 24
    }'
  ```

  ```bash Update keywords and labels theme={null}
  curl -X PUT \
    "https://api.outx.ai/api-keyword-watchlist" \
    -H "Content-Type: application/json" \
    -H "x-api-key: YOUR_API_KEY" \
    -d '{
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "keywords": [
        {
          "keyword": "CTO",
          "required_keywords": ["hiring"],
          "excluded_keywords": ["recruiter", "spam"]
        },
        "founder"
      ],
      "labels": [
        { "name": "hiring", "description": "Job postings", "default": true },
        { "name": "founder-post", "description": "Posts from founders", "default": false }
      ]
    }'
  ```

  ```bash Pause the watchlist theme={null}
  curl -X PUT \
    "https://api.outx.ai/api-keyword-watchlist" \
    -H "Content-Type: application/json" \
    -H "x-api-key: YOUR_API_KEY" \
    -d '{
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "disable": true
    }'
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch("https://api.outx.ai/api-keyword-watchlist", {
    method: "PUT",
    headers: {
      "Content-Type": "application/json",
      "x-api-key": "YOUR_API_KEY",
    },
    body: JSON.stringify({
      id: "550e8400-e29b-41d4-a716-446655440000",
      keywords: [
        {
          keyword: "CTO",
          required_keywords: ["hiring"],
          excluded_keywords: ["recruiter", "spam"],
        },
      ],
      labels: [{ name: "hiring", description: "Job postings" }],
    }),
  });
  ```

  ```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',
  }

  # Update keywords and labels
  payload = {
      'id': '550e8400-e29b-41d4-a716-446655440000',
      'keywords': [
          {
              'keyword': 'CTO',
              'required_keywords': ['hiring'],
              'excluded_keywords': ['recruiter', 'spam'],
          }
      ],
      'labels': [
          {'name': 'hiring', 'description': 'Job postings'},
      ],
  }

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

<ResponseExample>
  ```json Metadata-only update theme={null}
  {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "Updated Watchlist Name",
    "fetchFreqInHours": 24,
    "disable": false,
    "updated": true,
    "message": "Watchlist updated successfully"
  }
  ```

  ```json Keywords + labels update theme={null}
  {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "updated": true,
    "message": "Watchlist updated successfully",
    "keywords": ["cto", "founder"],
    "results": [
      {
        "success": true,
        "keyword": "cto",
        "keyword_id": "660e8400-e29b-41d4-a716-446655440001"
      },
      {
        "success": true,
        "keyword": "founder",
        "keyword_id": "660e8400-e29b-41d4-a716-446655440002"
      }
    ],
    "labels": [
      { "key": "hiring", "description": "Job postings", "default": true },
      { "key": "founder-post", "description": "Posts from founders", "default": false }
    ]
  }
  ```
</ResponseExample>

## Response Fields

<ResponseField name="id" type="string">
  Watchlist ID that was updated.
</ResponseField>

<ResponseField name="name" type="string">
  Updated watchlist name (only present when `name` was in the request).
</ResponseField>

<ResponseField name="fetchFreqInHours" type="number">
  Updated fetch frequency (only present when `fetchFreqInHours` was in the request).
</ResponseField>

<ResponseField name="disable" type="boolean">
  Current active/disabled status (only present when `disable` was in the request).
</ResponseField>

<ResponseField name="keywords" type="array">
  Array of primary keywords after the update (only present when `keywords` was in the request).
</ResponseField>

<ResponseField name="results" type="array">
  Per-keyword creation result (only present when `keywords` was in the request). Each entry has `success`, `keyword`, and either `keyword_id` or `error`.
</ResponseField>

<ResponseField name="labels" type="array">
  Labels persisted on the watchlist (only present when `labels` or `keywords` was in the request).
</ResponseField>

<ResponseField name="updated" type="boolean">
  Whether the update was successful.
</ResponseField>

<ResponseField name="message" type="string">
  Success message.
</ResponseField>

## Error Responses

| Status Code | Error Message                                                             | Description                                                                                                                                 |
| ----------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| 400         | Missing required parameter: id                                            | Watchlist ID is required                                                                                                                    |
| 400         | No valid update fields provided                                           | Body didn't include any updatable fields                                                                                                    |
| 400         | keywords array is required and must not be empty                          | `keywords` was passed but empty or not an array                                                                                             |
| 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                                                                                                                 |
| 400         | disable must be a boolean value                                           | `disable` sent as a non-boolean                                                                                                             |
| 400         | Name cannot be empty                                                      | `name` was an empty or whitespace-only string                                                                                               |
| 401         | Missing API Key / Invalid API Key                                         | Missing or invalid `x-api-key` header                                                                                                       |
| 404         | Watchlist not found or access denied                                      | Watchlist doesn't exist, isn't a keyword list, or isn't yours                                                                               |
| 409         | This watchlist is disabled. Enable it before updating it.                 | The watchlist is paused. Send `{ "id": "...", "disable": false }` first, then retry the update. Applies to both patch mode and prompt mode. |

See [Error Codes](/docs/api-reference/errors) for the full list, including 403 (plugin not active) and 405 (wrong HTTP method).

## Prompt mode

Replace the prompt on an existing watchlist. OutX wipes the existing keywords and labels and regenerates them from the new prompt in the background. Use this when you want to redirect a watchlist to a different angle without recreating it.

### Request Body

<ParamField body="id" type="string" required>
  Watchlist ID to update.
</ParamField>

<ParamField body="prompt" type="string" required>
  New natural-language prompt. OutX will regenerate keywords and labels from this, replacing the current set.
</ParamField>

<RequestExample>
  ```bash cURL theme={null}
  curl -X PUT \
    "https://api.outx.ai/api-keyword-watchlist" \
    -H "Content-Type: application/json" \
    -H "x-api-key: YOUR_API_KEY" \
    -d '{
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "prompt": "Track AI CRM tools, Salesforce alternatives, and enterprise sales automation"
    }'
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch("https://api.outx.ai/api-keyword-watchlist", {
    method: "PUT",
    headers: {
      "Content-Type": "application/json",
      "x-api-key": "YOUR_API_KEY",
    },
    body: JSON.stringify({
      id: "550e8400-e29b-41d4-a716-446655440000",
      prompt:
        "Track AI CRM tools, Salesforce alternatives, and enterprise sales automation",
    }),
  });

  const result = await response.json();
  console.log(result.message);
  ```

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

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

  payload = {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "prompt": "Track AI CRM tools, Salesforce alternatives, and enterprise sales automation",
  }

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

  result = response.json()
  print(result["message"])
  ```
</RequestExample>

<ResponseExample>
  ```json Response theme={null}
  {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "prompt": "Track AI CRM tools, Salesforce alternatives, and enterprise sales automation",
    "updated": true,
    "message": "Prompt updated. Keywords and labels are being regenerated in the background."
  }
  ```
</ResponseExample>

Same wipe-and-recreate semantics as a `keywords` patch: existing `keyword_tracking` rows and pending tasks are deleted, but historical posts remain attached to the watchlist.

## Frequently Asked Questions

<AccordionGroup>
  <Accordion title="What properties can I update on a keyword watchlist?">
    You can update `name`, `fetchFreqInHours`, `keywords`, `labels`, `disable`, and `slack_webhook_url`, any combination. Every field except `id` is optional; unspecified fields are left unchanged.
  </Accordion>

  <Accordion title="What happens to my existing keywords when I update them?">
    When you pass a `keywords` array, the API deletes the watchlist's existing `keyword_tracking` rows and their associated `keyword_tracking` tasks, then creates new ones from your payload. The watchlist ID stays the same, and previously collected posts remain accessible via the Posts API.
  </Accordion>

  <Accordion title="Does updating a watchlist reset existing collected data?">
    No. Changing the name, fetch frequency, keywords, or labels does not delete posts that have already been collected. If you update `fetchFreqInHours`, the new interval takes effect from the next scheduled fetch cycle.
  </Accordion>

  <Accordion title="Can I update labels without changing keywords?">
    Yes. Send only `id` and `labels`. The existing keywords and their tasks are untouched.
  </Accordion>

  <Accordion title="How do I change which labels are shown by default?">
    Fetch the current labels with [GET ?id=...](/docs/api-reference/watchlist/keyword/get), flip the `default` booleans you want (at most 2 can be `true`), and PUT the full array back as `labels`. Keep the `key` and `description` strings unchanged so historical post tags keep matching. See [Intent labels & defaults](/docs/api-reference/concepts/intent-labels).
  </Accordion>

  <Accordion title="Do new or changed labels apply to posts I already collected?">
    No. Each post is classified into a label once, when it is first enriched. Updating or adding `labels` changes how future posts are classified but does not re-label posts that were already collected, they keep the tags they had. To re-classify recent posts against the new labels, those posts must be explicitly re-enriched (OutX re-runs classification over roughly the last 30 days). See [How intent labels are calculated](/docs/api-reference/watchlist/keyword/create#how-intent-labels-are-calculated).
  </Accordion>

  <Accordion title="What if I only want to add or remove a single keyword?">
    To add keywords without disturbing the rest, send just the new keywords with `append: true`, only the ones not already on the list (case-insensitive) are created. To remove a keyword, fetch the current set with [GET /api-keyword-watchlist?id=…](/docs/api-reference/watchlist/keyword/get), then send the full desired list back without `append` (default replace).
  </Accordion>
</AccordionGroup>
