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

# Post Search & Filter API - Retrieve Posts from Watchlists

> Retrieve and filter LinkedIn or Reddit posts from your watchlists. Search, date range, seniority, trending, language, labels, relevance level, and more.

Retrieve posts collected by your watchlists. The endpoint serves both LinkedIn watchlists (keyword, people, company) and Reddit watchlists from the same URL: the source is auto-detected from the watchlist's `type`, so you do not pass anything to switch between them. The shared response fields are identical for both sources, with a `source` field on each post and a few source-specific extras described in the [Reddit watchlists](#reddit-watchlists) section.

## Query Parameters

<ParamField query="watchlist_id" type="string" required>
  Watchlist ID to retrieve posts from. This parameter is **required** and must be a single watchlist ID.
</ParamField>

<ParamField query="page" type="number" default="1">
  Page number for pagination. Combine with `page_size` to control how many posts you get per page (defaults to 20).
</ParamField>

<ParamField query="page_size" type="number" default="20">
  Number of posts returned per page. Used together with `page` (or on its own) for offset-free pagination, e.g. `page=2&page_size=50` returns posts 51 to 100. Use `page_size` and `page` (or `range_from`/`range_to`); unrecognized aliases such as `limit`, `offset`, `per_page`, or `pageSize` are rejected with a `400` (with a did-you-mean hint naming the correct parameter where the alias is close enough, and the full allowed-parameter list in every case).
</ParamField>

### Filtering Parameters

<ParamField query="labels" type="string | array">
  Filter to posts classified into these intent labels (the watchlist's label `key` strings). Pass multiple as comma-separated (`labels=buying-intent,competitor-mention`) or repeated params. Use the special value `no labels` to fetch unclassified posts (stored tag `"na"`). The feed is **not** filtered to the watchlist's default labels automatically; to reproduce the app's default feed, read the labels with `default: true` from the watchlist GET and pass them here. See [Intent labels & defaults](/docs/api-reference/concepts/intent-labels).
</ParamField>

<ParamField query="people_slug" type="string | array">
  Filter by specific people (profile slugs). LinkedIn watchlists only; ignored on Reddit.
</ParamField>

<ParamField query="company_slug" type="string | array">
  Filter by specific companies (company slugs). LinkedIn watchlists only; ignored on Reddit.
</ParamField>

<ParamField query="search_term" type="string">
  Search within post content. LinkedIn watchlists only; ignored on Reddit.
</ParamField>

<ParamField query="is_saved" type="boolean">
  Filter for bookmarked/saved posts only. Works on both LinkedIn and Reddit.
</ParamField>

<ParamField query="lang" type="string | boolean">
  Restrict results to English posts only. Accepts `"en"`, `"english"`,
  `"true"`, or `true`. Any other value is ignored.

  Non-English language filtering is not currently supported, posts in
  other languages can be retrieved by omitting this parameter.

  LinkedIn watchlists only; ignored on Reddit (Reddit posts do not carry a
  language field).
</ParamField>

<ParamField query="start_date" type="string">
  Filter posts from this date onwards (ISO 8601 format: YYYY-MM-DD). Works on both LinkedIn and Reddit.
</ParamField>

<ParamField query="end_date" type="string">
  Filter posts up to this date (ISO 8601 format: YYYY-MM-DD). Works on both LinkedIn and Reddit.
</ParamField>

<ParamField query="post_type" type="string">
  Filter by post type (e.g., "article", "image", "video", "poll"). LinkedIn watchlists only; ignored on Reddit.
</ParamField>

<ParamField query="trending" type="boolean">
  Filter for trending posts with high engagement. LinkedIn watchlists only; ignored on Reddit.
</ParamField>

<ParamField query="interacted" type="boolean">
  Filter for posts you've already liked or commented on. LinkedIn watchlists only; ignored on Reddit.
</ParamField>

<ParamField query="seniority_level" type="string | array">
  Filter by author's seniority level. Supports multiple comma-separated values (e.g. `"VP,Director"`). Values must match exactly one of: `"Entry Level"`, `"Manager"`, `"Senior"`, `"Director"`, `"VP"`, `"CXO/Founder"`. An unrecognized value is rejected with a `400`. LinkedIn watchlists only; ignored on Reddit (Reddit does not have author profile data).
</ParamField>

<ParamField query="linkedin_post_slug" type="string">
  Retrieve a specific post by its LinkedIn slug. LinkedIn watchlists only; ignored on Reddit.
</ParamField>

<ParamField query="relevance_level" type="string | array">
  Filter by post relevance bucket. Supports multiple comma-separated values (e.g. `"high,medium"`). Allowed values: `"high"` (relevance score 8 to 10), `"medium"` (4 to 7), `"low"` (1 to 3); any other value is rejected with a `400`. Works on both LinkedIn and Reddit watchlists. Note: the bucketed `relevance_level` response field is currently only returned on Reddit rows; on LinkedIn, use the numeric `relevance_score` field on each post.
</ParamField>

### Sorting Parameters

<ParamField query="sort_by" type="string" default="recent">
  Sort order for posts. Options:

  * `recent`: most recent posts first (default; both sources)
  * `popular_first`: highest engagement (likes plus comments) first (LinkedIn). On Reddit, this aliases to `relevance_first`.
  * `engagement`: alias for `popular_first` (LinkedIn). On Reddit, also aliases to `relevance_first`.
  * `relevance_first`: Reddit watchlists only. Sorts by relevance score, highest first, then by post date.

  Any other value is rejected with a `400` (it is no longer silently treated as `recent`).
</ParamField>

<ParamField query="range_from" type="number" default="0">
  Pagination start offset (0-indexed). Use with `range_to` for custom page sizes.
</ParamField>

<ParamField query="range_to" type="number" default="19">
  Pagination end offset (inclusive). Default returns 20 posts (0-19).
</ParamField>

<RequestExample>
  ```bash Get Posts from Specific Watchlist theme={null}
  curl -X GET \
    "https://api.outx.ai/api-posts?watchlist_id=550e8400-e29b-41d4-a716-446655440000&page=1" \
    -H "x-api-key: YOUR_API_KEY"
  ```

  ```bash Filter by Date Range and Labels theme={null}
  curl -X GET \
    "https://api.outx.ai/api-posts?watchlist_id=550e8400-e29b-41d4-a716-446655440000&start_date=2024-01-01&end_date=2024-01-31&labels=hiring&labels=remote" \
    -H "x-api-key: YOUR_API_KEY"
  ```

  ```bash Search and Sort by Engagement theme={null}
  curl -X GET \
    "https://api.outx.ai/api-posts?watchlist_id=550e8400-e29b-41d4-a716-446655440000&search_term=machine%20learning&sort_by=engagement&trending=true" \
    -H "x-api-key: YOUR_API_KEY"
  ```

  ```bash Default-labels feed (the app's initial view) theme={null}
  # 1. Read the watchlist labels, keep the keys where default is true
  curl "https://api.outx.ai/api-keyword-watchlist?id=550e8400-e29b-41d4-a716-446655440000" \
    -H "x-api-key: YOUR_API_KEY"
  # 2. Pass those keys as the labels filter
  curl "https://api.outx.ai/api-posts?watchlist_id=550e8400-e29b-41d4-a716-446655440000&labels=buying-intent,competitor-mention" \
    -H "x-api-key: YOUR_API_KEY"
  ```

  ```javascript JavaScript theme={null}
  // Get posts from specific watchlist
  const watchlistId = '550e8400-e29b-41d4-a716-446655440000';
  const response = await fetch(
    `https://api.outx.ai/api-posts?watchlist_id=${watchlistId}&page=1`,
    {
      headers: { 'x-api-key': 'YOUR_API_KEY' }
    }
  );

  const { data, count } = await response.json();
  console.log(`Found ${count} posts`);

  // Advanced filtering
  const filteredResponse = await fetch(
    `https://api.outx.ai/api-posts?` +
    new URLSearchParams({
      watchlist_id: watchlistId,
      search_term: 'AI',
      sort_by: 'engagement',
      trending: 'true',
      start_date: '2024-01-01'
    }),
    {
      headers: { 'x-api-key': 'YOUR_API_KEY' }
    }
  );
  ```

  ```python Python theme={null}
  import requests
  from datetime import datetime, timedelta

  headers = {'x-api-key': 'YOUR_API_KEY'}
  base_url = 'https://api.outx.ai/api-posts'

  # Get posts from specific watchlist
  params = {
      'watchlist_id': '550e8400-e29b-41d4-a716-446655440000',
      'page': 1
  }
  response = requests.get(base_url, headers=headers, params=params)
  result = response.json()
  print(f"Found {result['count']} posts")

  # Advanced filtering
  end_date = datetime.now()
  start_date = end_date - timedelta(days=30)

  params = {
      'watchlist_id': '550e8400-e29b-41d4-a716-446655440000',
      'search_term': 'AI',
      'sort_by': 'engagement',
      'trending': 'true',
      'start_date': start_date.strftime('%Y-%m-%d'),
      'end_date': end_date.strftime('%Y-%m-%d')
  }
  response = requests.get(base_url, headers=headers, params=params)
  ```
</RequestExample>

<ResponseExample>
  ```json Response theme={null}
  {
    "data": [
      {
        "id": "post-123",
        "linkedin_post_url": "https://linkedin.com/feed/update/urn:li:activity:1234567890",
        "tracking_list_id": "550e8400-e29b-41d4-a716-446655440000",
        "content": "Excited to announce our new AI-powered feature that helps developers...",
        "author_name": "Jane Smith",
        "author_url": "janesmith",
        "author_headline": "CTO at TechCorp | AI Enthusiast",
        "author_image_url": "https://media.licdn.com/dms/image/...",
        "created_at": "2024-01-15T10:35:00Z",
        "posted_at": "2024-01-15T10:30:00Z",
        "likes_count": 245,
        "comments_count": 32,
        "shares_count": 18,
        "bookmark": false,
        "post_type": "image",
        "language": "en",
        "tags": ["ai", "product-launch"],
        "image_urls": ["https://media.licdn.com/dms/image/..."],
        "videos": null,
        "seniority_level": "CXO/Founder",
        "relevance_score": 8,
        "location_countries": ["United States"],
        "tagDescriptions": [
          {
            "tag": "ai",
            "description": "Artificial Intelligence related posts"
          }
        ]
      }
    ],
    "count": 156
  }
  ```
</ResponseExample>

## Response Fields

<ResponseField name="data" type="array">
  Array of post objects
</ResponseField>

<ResponseField name="count" type="number">
  Total number of posts matching the filters
</ResponseField>

### Post Object Fields

<ResponseField name="id" type="string">
  Unique post identifier
</ResponseField>

<ResponseField name="linkedin_post_url" type="string">
  Direct URL to the LinkedIn post. `null` on Reddit watchlists; see `reddit_url` instead.
</ResponseField>

<ResponseField name="tracking_list_id" type="string">
  ID of the watchlist this post belongs to
</ResponseField>

<ResponseField name="content" type="string">
  Post text content
</ResponseField>

<ResponseField name="author_name" type="string">
  Name of the post author
</ResponseField>

<ResponseField name="author_url" type="string">
  LinkedIn profile slug of the author
</ResponseField>

<ResponseField name="author_headline" type="string">
  Headline/bio of the post author
</ResponseField>

<ResponseField name="author_image_url" type="string">
  Profile image URL of the author
</ResponseField>

<ResponseField name="created_at" type="string">
  ISO 8601 timestamp when the post was added to OutX
</ResponseField>

<ResponseField name="posted_at" type="string">
  ISO 8601 timestamp when the post was published on LinkedIn
</ResponseField>

<ResponseField name="likes_count" type="number">
  Number of likes on the post
</ResponseField>

<ResponseField name="comments_count" type="number">
  Number of comments on the post
</ResponseField>

<ResponseField name="shares_count" type="number">
  Number of shares/reposts
</ResponseField>

<ResponseField name="bookmark" type="boolean">
  Whether the post has been bookmarked/saved
</ResponseField>

<ResponseField name="post_type" type="string">
  Type of post (e.g., "text", "image", "video", "article", "poll")
</ResponseField>

<ResponseField name="language" type="string">
  Detected language of the post. Empty string on Reddit watchlists.
</ResponseField>

<ResponseField name="tags" type="array">
  The intent label(s) the post was classified into, matching the `labels` set on the watchlist (a tag equals a label `key`). A post with no confident match has `["na"]`, and `tags` is empty until the post has been enriched. See [Intent labels & defaults](/docs/api-reference/concepts/intent-labels).
</ResponseField>

<Info>
  There is no `sentiment` field. OutX returns intent labels (`tags`), not sentiment, and `relevance_score` measures relevance to the watchlist, not tone. `tags` are written asynchronously during enrichment, so a freshly collected post can return empty `tags` for a while, treat that as "not classified yet," not "no match."
</Info>

<ResponseField name="image_urls" type="array">
  Array of image URLs attached to the post
</ResponseField>

<ResponseField name="videos" type="array">
  Array of video data attached to the post
</ResponseField>

<ResponseField name="seniority_level" type="string">
  Seniority level of the post author. One of `"Entry Level"`, `"Manager"`, `"Senior"`, `"Director"`, `"VP"`, `"CXO/Founder"`. Empty string on Reddit watchlists.
</ResponseField>

<ResponseField name="relevance_score" type="number">
  Relevance score of the post (1-10). High: 8-10, Medium: 4-7, Low: 1-3. Returned for both LinkedIn and Reddit posts.
</ResponseField>

<ResponseField name="location_countries" type="array">
  Array of countries associated with the post author's location. When the location could not be determined, this is the single-element array `["NA"]` rather than an empty array. `null` on Reddit watchlists.
</ResponseField>

<ResponseField name="tagDescriptions" type="array">
  Array of objects with `tag` and `description` for each tag.
</ResponseField>

## Reddit watchlists

The `/api-posts` endpoint also serves Reddit watchlists. The URL, authentication, and pagination are identical to the LinkedIn flow; pass the Reddit watchlist's `id` as `watchlist_id` and the server detects the source from `tracking_lists.type`.

A Reddit row in `data` represents either a Reddit post or a top-level comment that matched one of the watchlist keywords. The shared response fields above are filled best-effort, with these source-specific differences:

* A discriminator field, `source: "reddit"`, is present on every Reddit row. LinkedIn rows do not include this field.
* `linkedin_post_url`, `videos`, and `location_countries` are always `null`.
* `language`, `author_url`, `author_headline`, and `seniority_level` are empty strings (Reddit posts do not carry these data).
* `shares_count` and `comments_count` are always `0`. Use `comment_*` and `post_vote_count` mappings instead.
* `likes_count` is the Reddit post upvote net score (mapped from `post_vote_count`).
* `tags` is a single-element array derived from the row's `label` column.
* `post_type` is `"reddit_post"` for posts and `"reddit_comment"` for comments.

Reddit rows include these additional fields:

<ResponseField name="source" type="string">
  Always `"reddit"` on Reddit rows. Absent on LinkedIn rows.
</ResponseField>

<ResponseField name="reddit_url" type="string">
  Direct URL to the Reddit post or comment.
</ResponseField>

<ResponseField name="post_title" type="string">
  Reddit post title. May be `null` for rows that represent a comment under a post that was not separately ingested.
</ResponseField>

<ResponseField name="post_content" type="string">
  Body of the Reddit post.
</ResponseField>

<ResponseField name="subreddit" type="string">
  Subreddit name (e.g. `r/dataengineering`). Mapped from the underlying `post_group_name` column.
</ResponseField>

<ResponseField name="comment_author" type="string">
  Reddit username of the comment author. `null` when the row is a post.
</ResponseField>

<ResponseField name="comment_content" type="string">
  Comment body. `null` when the row is a post.
</ResponseField>

<ResponseField name="comment_created_at" type="string">
  ISO 8601 timestamp of the comment. `null` when the row is a post.
</ResponseField>

<ResponseField name="comment_vote_count" type="number">
  Reddit comment upvote net score. `0` when the row is a post.
</ResponseField>

<ResponseField name="relevance_level" type="string">
  Bucketed relevance: `"high"`, `"medium"`, or `"low"`. Derived from `relevance_score`. `null` if the post has not been scored.
</ResponseField>

<ResponseField name="primary_keywords" type="array">
  Array containing the watchlist keyword that matched this row. Empty if the row is no longer linked to a keyword (for example after a watchlist update).
</ResponseField>

### Reddit response example

```json Response (Reddit watchlist) theme={null}
{
  "data": [
    {
      "id": "9f3c4f7a-12bc-4d5e-9a01-77b91f4b7c10",
      "source": "reddit",
      "tracking_list_id": "29aaf845-65bc-4cf8-9454-a398400bf067",
      "reddit_url": "https://www.reddit.com/r/dataengineering/comments/abc123/title/",
      "linkedin_post_url": null,
      "content": "Anyone else hitting this with the new connector? ...",
      "author_name": "u/example_user",
      "author_url": "",
      "author_headline": "",
      "author_image_url": null,
      "created_at": "2026-05-04T08:12:33Z",
      "posted_at": "2026-05-04T07:55:00Z",
      "likes_count": 47,
      "comments_count": 0,
      "shares_count": 0,
      "bookmark": false,
      "post_type": "reddit_post",
      "language": "",
      "tags": ["data-integration-pain"],
      "image_urls": null,
      "videos": null,
      "seniority_level": "",
      "relevance_score": 8,
      "relevance_level": "high",
      "location_countries": null,
      "tagDescriptions": [
        {
          "tag": "data-integration-pain",
          "description": "Posts where users describe specific integration failures"
        }
      ],
      "post_title": "Snowflake to BigQuery sync silently dropping rows",
      "post_content": "Anyone else hitting this with the new connector? ...",
      "subreddit": "r/dataengineering",
      "comment_author": null,
      "comment_content": null,
      "comment_created_at": null,
      "comment_vote_count": 0,
      "primary_keywords": ["data integration"]
    }
  ],
  "count": 73
}
```

### Reddit examples

```bash Get posts from a Reddit watchlist theme={null}
curl -X GET \
  "https://api.outx.ai/api-posts?watchlist_id=29aaf845-65bc-4cf8-9454-a398400bf067" \
  -H "x-api-key: YOUR_API_KEY"
```

```bash High-relevance Reddit posts only theme={null}
curl -X GET \
  "https://api.outx.ai/api-posts?watchlist_id=29aaf845-65bc-4cf8-9454-a398400bf067&relevance_level=high&sort_by=relevance_first" \
  -H "x-api-key: YOUR_API_KEY"
```

## Pagination

Posts are returned in batches of 20 by default. You can use either `page` + `page_size` (simpler) or `range_from` + `range_to` (offset-based) to paginate.

```bash theme={null}
# First 20 posts from a watchlist (default)
GET /api-posts?watchlist_id=YOUR_ID

# 50 posts per page using page_size
GET /api-posts?watchlist_id=YOUR_ID&page=1&page_size=50

# Posts 21-40 using range_from / range_to
GET /api-posts?watchlist_id=YOUR_ID&range_from=20&range_to=39
```

The `count` field in the response shows the total number of posts matching your filters, so you know how many pages to fetch.

## Error Responses

| Status Code | Error Message              | Description                                                                                                                                                                                       |
| ----------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400         | Invalid watchlist ID       | The provided watchlist ID doesn't exist                                                                                                                                                           |
| 400         | Unknown query parameter(s) | An unrecognized parameter was sent. Unknown parameters are rejected rather than silently ignored; the response lists the offending names, a did-you-mean suggestion, and the accepted parameters. |
| 401         | Unauthorized               | Invalid or missing API key                                                                                                                                                                        |
| 403         | Access denied              | Trying to access watchlists from another team                                                                                                                                                     |
| 500         | Failed to fetch posts      | Internal server error                                                                                                                                                                             |

## Frequently Asked Questions

<AccordionGroup>
  <Accordion title="Can I get posts from all my watchlists at once?">
    No. The `api-posts` endpoint now requires a single `watchlist_id` and returns posts for that watchlist only. To build a unified feed across multiple watchlists, make separate requests per watchlist and merge the results on your side.
  </Accordion>

  <Accordion title="What is the maximum number of posts returned per page?">
    The default page size is 20 posts. To change it, pass `page_size` (for example `page_size=50` returns 50 posts per page), or use `range_from` and `range_to` for offset-based pagination (e.g. `range_from=0&range_to=49` returns the first 50). The `count` field in the response tells you the total number of matching posts so you know how many pages to fetch.
  </Accordion>

  <Accordion title="How do I show only the watchlist's default feed (like the app)?">
    The API does not auto-apply default labels. Fetch the watchlist (`GET /api-keyword-watchlist?id=...` or `GET /api-reddit-watchlist?id=...`), collect the label `key` values where `default` is `true`, and pass them as the `labels` parameter. See [Intent labels & defaults](/docs/api-reference/concepts/intent-labels).
  </Accordion>

  <Accordion title="How do I filter by multiple seniority levels?">
    Pass comma-separated values to the `seniority_level` parameter. For example: `seniority_level=VP,Director`. This returns posts authored by people at any of those seniority levels. Values must match exactly one of: `Entry Level`, `Manager`, `Senior`, `Director`, `VP`, `CXO/Founder`. An unrecognized value is rejected with a `400`.
  </Accordion>
</AccordionGroup>
