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

# How OutX Works

> The mental model to load before calling any endpoint: what a watchlist is, what happens after a create succeeds, how an update regenerates keywords, and why a new feed is empty at first.

Read this once before you call anything. Most integration failures are not the wrong endpoint. They are the right endpoint called with the wrong idea of what the platform does.

## A watchlist is a standing job, not a search query

OutX watches LinkedIn and Reddit for you. A **watchlist** is a saved monitoring job: you say what to look for, and OutX keeps collecting matching posts on a schedule (`fetchFreqInHours`) until you pause or delete it.

You do not get results back from the call that creates a watchlist. You create the job, wait, then read its feed with [`GET /api-posts?watchlist_id=...`](/docs/api-reference/engagement/posts/get).

Four types, one per endpoint. Pick by what you are tracking, not by platform alone:

| Type    | Platform | Tracks                           |
| ------- | -------- | -------------------------------- |
| Keyword | LinkedIn | Posts about a topic              |
| Reddit  | Reddit   | Posts and comments about a topic |
| People  | LinkedIn | Posts from named profiles        |
| Company | LinkedIn | Posts from named company pages   |

One watchlist covers one platform. "Track this on LinkedIn and Reddit" is two create calls. Full comparison: [Choose a watchlist type](/docs/api-reference/watchlist/overview).

## What happens after a create returns 201

The response comes back before the work is done. In order:

<Steps>
  <Step title="The watchlist row exists">
    You get `id` and `slug` immediately. The `id` never changes. The `slug` only changes if you rename the watchlist.
  </Step>

  <Step title="Keywords and labels are written">
    In prompt mode this happens in the background, seconds after the response. In keywords mode your keywords are stored during the call, and OutX fills in a missing `objective` or label set in the background.
  </Step>

  <Step title="Collection tasks start">
    Each keyword gets a task that runs on the fetch cycle. LinkedIn keyword watchlists also backfill recent matching posts right away, so they are usually non-empty within minutes.
  </Step>

  <Step title="Posts are scored and labeled (keyword and Reddit only)">
    On a keyword or Reddit watchlist, every collected post is scored for relevance against the watchlist's objective, then classified into one of your [intent labels](/docs/api-reference/concepts/intent-labels). A post that has not been through this step yet has an empty `tags` array.

    People and company watchlists have no intent labels, so their posts are never classified. Their `tags` come back as `"na"` and stay that way.
  </Step>
</Steps>

<Warning>
  **An empty feed minutes after create is normal, not an error.** So is a post with empty `tags` on a keyword or Reddit watchlist. Neither means the watchlist is broken. There, `"na"` in `tags` is the real "no match" signal and empty means "not classified yet".

  **On a people or company watchlist, never wait for tags.** Those types have no intent labels, so every post is tagged `"na"` by design. Read the posts themselves.
</Warning>

After a prompt-mode create, poll `GET` on the same endpoint you created with, using the watchlist `id`, until `keywords` is no longer empty. A keyword watchlist polls [`/api-keyword-watchlist`](/docs/api-reference/watchlist/keyword/get) and a Reddit one polls [`/api-reddit-watchlist`](/docs/api-reference/watchlist/reddit/get). People and company watchlists have no keywords and no prompt: poll [`/api-posts`](/docs/api-reference/engagement/posts/get) instead, or read `profiles` / `companies` on their `GET ?id=` response to confirm the members landed.

## Prompt mode vs keywords mode

Keyword and Reddit watchlists accept two body shapes on create. Send one or the other, never both.

* **Prompt mode (recommended).** Send `prompt`: one sentence describing what you want to see. OutX reads it into topic, audience and signal, then generates the keywords, the intent labels and the relevance objective for you. This is the path the product itself uses and the one tuned for feed quality.
* **Keywords mode.** Send `keywords`: you supply the exact terms, with optional `required_keywords` and `excluded_keywords` per term. Use it when you already know the exact strings to match.

<Note>
  **Prompt mode ignores the direct-mode fields.** On a prompt-mode create, `labels`, `objective`, `description` and `slack_webhook_url` are not read. OutX writes the objective itself, from the prompt. Of the other three, only `labels` and `slack_webhook_url` can be set afterwards with a [PUT](/docs/api-reference/watchlist/keyword/update). **`objective` and `description` are accepted on a keywords-mode create only.** Update accepts neither, so a prompt-built watchlist cannot be given a hand-written objective through the API.
</Note>

A good prompt names three things: what the posts are about, who is posting, and what makes a post worth seeing. "Ops managers at 3PL companies complaining about their warehouse software" works. "Find me buyers" does not, and is refused (see below).

## Updating a watchlist

`PUT` on the same URL, with `id` in the body. Which of two things happens depends on whether you send `prompt`.

### Sending a new prompt regenerates the whole watchlist

This is the part agents get wrong. A `PUT` with a new `prompt` does not edit anything you name. It throws away the current keywords and labels and generates a new set from the new prompt, in the background.

| Kept                                            | Replaced                               |
| ----------------------------------------------- | -------------------------------------- |
| `id` and `slug`                                 | Every keyword, and its collection task |
| `name`                                          | The whole label set                    |
| Posts already collected                         | The relevance objective and its vector |
| `fetchFreqInHours`, Slack webhook, paused state | The stored prompt                      |

You cannot patch one keyword through the prompt path. To change one keyword, use the `keywords` field instead, which replaces the set you send (or adds to it with `append: true`).

The response tells you only that the work started:

```json theme={null}
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "prompt": "your new prompt",
  "updated": true,
  "message": "Prompt updated. Keywords and labels are being regenerated in the background."
}
```

It carries no keywords and no labels, because they have not been generated yet.

There is no completion flag to poll. The old keywords stay in place while the new set is generated, so "`keywords` is not empty" is true the whole time and tells you nothing. The reliable check: read `GET ?id=...` **before** the PUT and keep the `keyword_tracking[].id` values, then poll until every id in the response is one you have not seen and the count stops changing between polls. Rows are created one at a time, so an unchanged count across two polls is what says the swap finished.

<Warning>
  **A prompt-mode update does not backfill.** The new keywords start collecting on the next fetch cycle.

  On a **LinkedIn keyword** watchlist, a `keywords` patch does backfill recent matching posts, so patch keywords instead when you need the feed to refill immediately. **Reddit watchlists never backfill**, on create or on update: every Reddit feed fills from its next fetch cycle onward.
</Warning>

### Everything else is a patch

Send only the fields you want to change: `name`, `fetchFreqInHours`, `keywords`, `labels`, `slack_webhook_url`, `disable`. Omitted fields are untouched. Sending `keywords` alone never overwrites labels.

A paused keyword or Reddit watchlist rejects updates with `409`. The one request that gets through is an explicit re-enable, `{ "id": "...", "disable": false }` in patch mode. Prompt mode has no such exception, so re-enable in its own call, then send the new prompt.

Pausing or resuming any watchlist type stops or restarts all of its collection jobs, and shows up on the type's `GET` as `disabled`, with `disabledAt` carrying the timestamp on the list response.

## Retrying safely

Send `idempotency_key` on every create. It is a string you choose, up to 128 characters, unique per logical create (a UUID is ideal). If a second request arrives with the same key within about two minutes, OutX returns the watchlist the first one made instead of creating a second one.

```json theme={null}
{
  "prompt": "Ops managers at 3PL companies complaining about their warehouse software",
  "idempotency_key": "5f2b9c7a-0c2d-4a3e-9b21-8c0a1f6d4e77"
}
```

A deduplicated response is a `201` carrying `created: false` and `deduplicated: true`, with the existing watchlist's `id`, `name` and `slug`. Treat `created: false` as success, not as an error.

<Warning>
  **This is best effort, not a transaction.** The lookup and the insert are separate steps, so two genuinely simultaneous identical creates can still both go through. Fire retries one at a time rather than in parallel, and read `created` on the response rather than assuming.
</Warning>

OutX also dedupes without a key: a create is matched against watchlists the same team made in the last couple of minutes with the same defining fields. Keyword and Reddit creates in keywords mode go further, and return an existing watchlist of that type whose keyword set is exactly the same, at any age. That one answers `200` with `created: false`. Sending your own key is more predictable, so send one.

## When a prompt is not good enough

Before anything is created, OutX checks the prompt. A prompt with no subject in it, a bare profile link, an unfilled `[PLACEHOLDER]`, or something too short to build from is refused with `422` and nothing is created. The same check runs on a prompt-mode update, so a bad prompt cannot wipe a working watchlist.

```json theme={null}
{
  "error": "prompt_not_specific_enough",
  "reason": "This says what you want back but not what it should be about. Name the product, category or competitor and we can build the feed.",
  "missing": ["topic"],
  "detected": { "words": 3, "chars": 15, "intent_terms": ["buyers"] },
  "example": "Founders asking which CRM to buy after outgrowing spreadsheets",
  "question": "Buyers of what? Name the product, category, or competitor."
}
```

When `question` is present, one answer is all that is missing. Relay that question to your user word for word, put their answer into the prompt, and retry. Do not guess a topic on their behalf: a guessed topic produces a confident watchlist about the wrong thing. When `question` is absent, rewrite the prompt using `example` as the shape to copy.

## Checking what OutX assumed

A prompt-mode create returns two extra fields describing how your sentence was read:

* `interpretation`: the slots OutX pulled out (`topic`, `audience`, `signals`, `competitors`, `use_case`, `excluded`, and `refined_prompt`, the prompt rewritten as one clear sentence).
* `assumed`: the slot names OutX filled in by inference because your prompt did not state them.

Show `refined_prompt` and `assumed` to your user when they matter. To reject assumptions outright, send `strict: true` and the create fails instead of guessing: `422 prompt_not_specific_enough` listing the inferred slots in `missing`, or `503 interpretation_unavailable` if the reading itself could not run. Both leave nothing created.

## Collection runs through the Chrome extension

OutX runs through the [Chrome extension](/docs/resources/chrome-extension) installed on your browser. It works in the background while you browse, and it uses your active LinkedIn session, so OutX does not take your cookies, passwords, or save any credentials.

That makes the extension a hard dependency for the API, not just for the app. The extension must be installed and must have been active within the last **48 hours**. Otherwise every call, on every endpoint, returns `403 Plugin installation required` (the full message is on the [error codes page](/docs/api-reference/errors#403-forbidden)).

A `403` of that kind is not an API key problem, and nothing you change in the request will fix it. The fix is to install the [extension](https://chromewebstore.google.com/detail/outxai-track-linkedin-pos/epnimaeheelhgeelbppbfkjegklflakj), sign in to LinkedIn in the same browser, and keep the browser open.

## Limits worth knowing before you build

* **Watchlist count is capped by plan**, and all four types count against the same cap. A create over the cap returns `402` naming your limit. Check remaining quota first with [`GET /api-team`](/docs/api-reference/team/get).
* **Labels are forward only.** A post is classified once, when it is first enriched. Changing labels changes how future posts are classified and leaves already-collected posts with the tags they have.
* **Each endpoint only sees its own type.** Asking `/api-keyword-watchlist` for a Reddit watchlist's ID returns `404`.
* **There is no sentiment field.** Model sentiment as an intent label if you need it.

## Next steps

<CardGroup cols={2}>
  <Card title="Choose a watchlist type" icon="list-check" href="/docs/api-reference/watchlist/overview">
    The four types side by side, with a task-to-endpoint map
  </Card>

  <Card title="Quickstart" icon="rocket" href="/docs/api-reference/quickstart">
    Key to first collected posts, end to end
  </Card>

  <Card title="Intent labels" icon="tags" href="/docs/api-reference/concepts/intent-labels">
    How posts get classified and what default labels do
  </Card>

  <Card title="Error codes" icon="triangle-exclamation" href="/docs/api-reference/errors">
    Every status the API returns and how to recover
  </Card>
</CardGroup>
