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

# Search Posts

> Search public professional posts by keyword and filters, with a boolean query grammar and cursor pagination

## Endpoint

```
POST /api/v1/posts/search
```

Search finds posts across public professional content by words, by author, or
both. Use it to discover posts matching a topic, then call
[`GET /api/v1/post/comments`](/api-reference/endpoint/get-post-comments) or
[`GET /api/v1/post/reactions`](/api-reference/endpoint/get-post-reactions) on the
ones whose engagement you want in full.

<Note>
  The request is a JSON **body**, not query parameters. Send `query`, or at least
  one of `authorProfiles` / `authorCompanies` — a search with no words and no
  author is a request for the whole network and is rejected with `400`.
</Note>

## Authentication

Include your API key in the request header:

```bash theme={null}
X-API-Key: your-api-key-here
```

## Billing

<Warning>
  **1 credit per call, flat.**

  * 1 credit whether the page comes back with 50 results, 1 result or **none at
    all**. The query ran either way, and an empty page is a real answer: it tells
    you nothing matches.
  * The price does not change with `maxResults`, so a 50-result page costs the
    same as a 1-result page. Ask for 50.
  * **`400`, `401`, `402`, `429` and `503` are free.** Every validation error is
    decided before the search runs, so a malformed body never costs a credit.
  * Each page of a cursor walk is its own call and its own credit.
  * A post that repeats across two pages (see [Pagination](#pagination)) is part
    of a page you already paid for; there is no per-result charge either way.

  This is the "one credit per request" rule from [Quotas](/concepts/quotas),
  applied per page: the page is the request.
</Warning>

## Request body

Filters **AND** together; the values inside one list filter **OR**. `null`
anywhere means "field omitted".

<Warning>
  **Nothing is accepted and then ignored.** An unknown key, an unrecognised enum
  value, a list that is too long and a `maxResults` above 50 are all `400`s that
  name the field — never a quiet drop.

  This matters more here than it looks. The underlying search ignores a value it
  does not recognise and answers normally, so a single typo in `contentType`
  would come back as a perfectly healthy page that silently applied **no filter
  at all**, and you would have paid for it without any way to notice. Refusing
  for free is the only honest option.
</Warning>

<ParamField body="query" type="string">
  The search terms, up to 500 characters. See
  [Query grammar](#query-grammar) for operators, phrases and the operator limit.

  Required unless you send `authorProfiles` or `authorCompanies`.
</ParamField>

<ParamField body="sortBy" type="string">
  `relevance` or `date` (newest first). **There is no default.**

  Omit it and the underlying search applies its own ordering. We do not label an
  order we do not control, so the response reports `sort: null` in that case
  rather than guessing a name for it.

  Two measured caveats before you reach for `date`:

  * it cannot be combined with a hashtag term in `query` — the two do not compose
    upstream and return nothing at all, so the combination is refused with `400`
    instead;
  * on a tight window it narrows hard. `"hiring"` over 24 hours returned **6**
    results unsorted and **1** with `sortBy: "date"`. If you want coverage, omit
    the sort and sort the results yourself on `date`.
</ParamField>

<ParamField body="postedWithin" type="string">
  `24h`, `week` or `month`. Keeps only posts published inside that window.

  This is applied by the search itself, so it changes which results exist, not
  just which ones you see.
</ParamField>

<ParamField body="postedAfter" type="string | integer">
  Keep only posts published at or after this instant — an ISO-8601 timestamp
  (`2026-10-01T00:00:00Z`) or an epoch in milliseconds. Must not be in the future
  and at most 365 days ago.

  Unlike `postedWithin`, this one is applied on our side after the search runs.
  A page trimmed by it comes back short and still costs its 1 credit. Use
  `postedWithin` to pick the window and `postedAfter` to cut inside it.
</ParamField>

<ParamField body="contentType" type="string[]">
  Keep only posts carrying one of these kinds of attachment. Values OR together.
  Accepted: `image`, `video`, `liveVideo`, `jobPost`, `document`.
</ParamField>

<ParamField body="authorProfiles" type="string[]">
  Keep only posts published by these people. 1 to 10 values, each a profile id
  (`ACoAA…`) or any professional profile URL containing one.

  A slug on its own is not accepted here: resolving one would cost an extra fetch
  per value. Resolve it once with
  [`GET /api/v1/profile`](/api-reference/endpoint/get-profile) and pass the id.

  Can be sent **without** `query`, to page everything a set of people posted.
</ParamField>

<ParamField body="authorCompanies" type="string[]">
  Keep only posts published by these organization pages. 1 to 10 values, each a
  numeric page id, a page urn, or a page URL ending in a numeric id. Can be sent
  without `query`.
</ParamField>

<ParamField body="mentionsCompanies" type="string[]">
  Keep only posts that mention these organization pages. 1 to 10 numeric page
  ids, urns or URLs.
</ParamField>

<ParamField body="authorJobTitle" type="string">
  Keep only posts whose author's current title matches this free text, matched as
  a phrase. 1 to 100 characters.
</ParamField>

<ParamField body="maxResults" type="integer" default="50">
  How many results this page may return, 1 to 50.

  50 is both the default and a hard ceiling: above it the underlying search
  returns nothing at all, so a larger value is refused with `400` rather than
  billed for an empty page. The price is the same at 1 and at 50.

  **This is a ceiling, not a promise.** A page can come back with fewer results
  than you asked for because results are de-duplicated by post id within the page
  — a 50-result request measured 49 in production — or simply because it is the
  last page. A short page is not an error.
</ParamField>

<ParamField body="cursor" type="string">
  Opaque cursor for the next page — see [Pagination](#pagination).
</ParamField>

### Not supported

<AccordionGroup>
  <Accordion title="Filters whose effect we could not confirm">
    The filters documented above are the ones whose effect we confirmed on live
    traffic. A filter we could not confirm is rejected with a free `400` instead
    of offered, because an unrecognised filter is not refused by the underlying
    search but silently **ignored** — so it would hand you a perfectly plausible,
    completely unfiltered page that you paid for and have no way to detect.
  </Accordion>

  <Accordion title="Group content">
    `group` is rejected with `400`. Group posts are a separate surface with no
    equivalent in this search.
  </Accordion>

  <Accordion title="Anything relative to the caller">
    Filters whose answer depends on *who is looking* — a person's own network,
    their connections, their follows — are not supported and will not be. Our
    requests are served from rotating capacity, so such a filter would give a
    different answer on every call, which is worse than not offering it.
  </Accordion>

  <Accordion title="Collaborative articles">
    Not a `contentType` value yet. The supported values are `image`, `video`,
    `liveVideo`, `jobPost` and `document`.
  </Accordion>
</AccordionGroup>

## Query grammar

`query` is a boolean expression over words and phrases.

| Form | Example | Meaning |
| - | - | - |
| Bare term | `hiring` | The word, stemmed by the underlying search |
| Quoted phrase | `"head of growth"` | The words in that order |
| `AND` | `saas AND pricing` | Both |
| `OR` | `saas OR fintech` | Either |
| `NOT` | `hiring NOT intern` | Exclude |
| Parentheses | `(saas OR fintech) AND hiring` | Grouping, nested up to 5 levels |
| Hashtag term | `#hiring` | The hashtag |

Operators must be uppercase in the canonical form, every `AND` / `OR` / `NOT`
needs a term on both sides, and a query that only excludes
(`NOT intern`) is rejected — there would be nothing to search for.

### The 5-operator limit

<Warning>
  **A query may use at most 5 boolean operators.** This is a limit of the
  underlying search, not a quota: from the sixth operator it stops answering and
  returns an empty result set that is indistinguishable from "nothing matched".

  Rather than hand you that empty page and charge for it, we refuse the query
  outright:

  ```json theme={null}
  {
    "error": "'query' uses too many boolean operators: 6 (maximum 5). Split it into several searches and combine the results.",
    "code": "INVALID_PARAMETER",
    "details": { "operators": 6, "maxOperators": 5 }
  }
  ```

  The refusal is decided before anything runs — measured at \~2 ms, 0 credits. To
  search a wider expression, **split it into several searches and merge the
  results**, de-duplicating on `id`. Splitting on the top-level `OR` branches is
  usually the cheapest cut.
</Warning>

Each `AND`, `OR` and `NOT` counts as one. Parentheses, phrases and bare terms
count as none, so `("head of growth" OR "vp growth") AND (saas OR fintech)` uses
3 operators and `#hiring` uses 0. There is also a cap of 20 quoted phrases and 5
levels of nesting, both well above normal use.

### What we normalise for you

These rewrites are applied silently, because each of them is something the
underlying search would otherwise handle worse than you expect:

| You send | We send |
| - | - |
| Curly quotes `“head of growth”` | Straight quotes `"head of growth"` |
| Lowercase operators `saas or fintech` | `saas OR fintech` |
| A bare `&` between terms (`saas & pricing`) | `saas AND pricing` |
| Single-quote delimiters `'head of growth'` | A real phrase `"head of growth"` |
| Repeated or exotic whitespace | Single spaces |

Nothing is rewritten inside a quoted phrase, and an apostrophe in the middle of a
word (`we'll be raising soon`) is left alone — only a matched pair of single
quotes at word boundaries becomes a phrase.

**Every rewrite is auditable:** the response echoes the query it actually ran as
`query`. Compare it to what you sent whenever a result set surprises you.

## Pagination

Send the first page without `cursor`, then pass the `cursor` from each response
back into the next request, unchanged, **with the same `query` and filters**.
Stop when `cursor` is `null`.

<CodeGroup>
  ```javascript JavaScript theme={null}
  async function searchAllPosts(body, apiKey, maxPages = 10) {
    const seen = new Set();
    const out = [];
    let cursor = null;

    for (let page = 0; page < maxPages; page++) {
      const response = await fetch('https://api.fetchin.io/api/v1/posts/search', {
        method: 'POST',
        headers: { 'X-API-Key': apiKey, 'Content-Type': 'application/json' },
        body: JSON.stringify({ ...body, maxResults: 50, cursor }),
      });

      if (!response.ok) throw new Error(`${response.status}: ${(await response.json()).error}`);

      const data = await response.json();          // this page cost 1 credit
      for (const post of data.results) {
        if (seen.has(post.id)) continue;           // posts can repeat across pages
        seen.add(post.id);
        out.push(post);
      }

      cursor = data.cursor;
      if (cursor === null) break;                  // result set exhausted
    }

    return out;
  }
  ```

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

  def search_all_posts(body, api_key, max_pages=10):
      seen, out, cursor = set(), [], None

      for _ in range(max_pages):
          resp = requests.post(
              'https://api.fetchin.io/api/v1/posts/search',
              headers={'X-API-Key': api_key},
              json={**body, 'maxResults': 50, 'cursor': cursor},
          )
          resp.raise_for_status()
          data = resp.json()                       # this page cost 1 credit

          for post in data['results']:
              if post['id'] in seen:               # posts can repeat across pages
                  continue
              seen.add(post['id'])
              out.append(post)

          cursor = data['cursor']
          if cursor is None:                       # result set exhausted
              break

      return out
  ```
</CodeGroup>

Rules:

* **`cursor` is opaque.** Pass it back byte for byte; do not parse or build one.
  A cursor issued for a different query or different filters is a `400`
  (`'cursor' belongs to a different search.`), and so is anything this API did
  not issue.
* **`maxResults` may change between pages**, the query and filters may not.
* **`cursor` is `null` in every way a walk ends** — a last page with results, an
  empty page past the end, and a query that matched nothing — so one condition
  closes your loop.
* **De-duplicate on `id`.** The result set is live and re-orders between calls;
  across a measured 4-page walk, 40 results held 37 distinct posts.

<Note>
  **Depth.** One query is exhausted after a few hundred results: `cursor` comes
  back `null` while posts matching your words certainly still exist. To go
  further, split the query — by date window (`postedWithin`, `postedAfter`) or by
  author — and combine the pages.
</Note>

## Response

<ResponseField name="query" type="string | null">
  The query as it was interpreted, after [normalisation](#what-we-normalise-for-you).
  `null` on an author-only search.
</ResponseField>

<ResponseField name="sort" type="string | null">
  The ordering that was applied: `relevance`, `date`, or `null` when `sortBy` was
  omitted and the underlying search used its own.
</ResponseField>

<ResponseField name="cursor" type="string | null">
  Opaque cursor for the next page, or `null` when the result set is exhausted.
</ResponseField>

<ResponseField name="partial" type="object">
  Present **only** when at least one card on this page could not be read and was
  dropped, so a short page is never silently short: `droppedResults` and a
  `reasons` breakdown. Diagnostic and additive — log it, do not branch on the
  keys of `reasons`.
</ResponseField>

<ResponseField name="results" type="array">
  Matching posts, de-duplicated within the page. Can hold fewer items than
  `maxResults`, and can be empty.

  <Expandable title="Post object">
    <ResponseField name="id" type="string" required>
      Activity URN of the post (`urn:li:activity:...`). Stable; de-duplicate on it.
    </ResponseField>

    <ResponseField name="entityId" type="string" required>
      The numeric part of `id`, for convenience.
    </ResponseField>

    <ResponseField name="shareUrn" type="string">
      The same post in the `urn:li:share:...` / `urn:li:ugcPost:...` namespace,
      when the card carries it.
    </ResponseField>

    <ResponseField name="shareUrl" type="string">
      Public permalink to the post.
    </ResponseField>

    <ResponseField name="date" type="string" required>
      ISO 8601 timestamp of when the post was **published**, derived from the post
      id. An edited post reports its original publication date, not the date of the
      edit — see `isEdited`.
    </ResponseField>

    <ResponseField name="postedAgoShort" type="string">
      The relative age the card displayed at fetch time, e.g. `3d`. Informational;
      compute ages from `date`.
    </ResponseField>

    <ResponseField name="isEdited" type="boolean" required>
      `true` when the post was edited after publication. The text returned is the
      current one.
    </ResponseField>

    <ResponseField name="content" type="string">
      The post's own text. Omitted when the post carries no text of its own, which
      happens on an image, video or document posted without a caption — read
      `postType` to tell those apart.
    </ResponseField>

    <ResponseField name="postType" type="string" required>
      What the post is, derived from the attachment that rendered: `text`, `image`,
      `video`, `document`, `article`, `poll`, `job`, `newsletter`, `celebration`,
      or `other` for an attachment kind introduced after this was written.
      `article` means a link preview card is attached.
    </ResponseField>

    <ResponseField name="media" type="array" required>
      Every attachment, in the order the card rendered them. Empty on a text-only
      post. Each entry carries a `type` plus whatever that type provides — `url`,
      `title`, `subtitle`, `thumbnailUrl`, `width`, `height`, `aspectRatio`,
      `pageCount` and `carousel` for a document, `options` for a poll.
    </ResponseField>

    <ResponseField name="images" type="array">
      The post's images with dimensions (`url`, `width`, `height`), largest
      rendition each. Omitted when the post has none.
    </ResponseField>

    <ResponseField name="imageUrl" type="string">
      First entry of `images`, kept for parity with
      [Get Posts](/api-reference/endpoint/get-posts).
    </ResponseField>

    <ResponseField name="article" type="object">
      The link preview card, when one is attached: `title`, `subtitle`, `link`,
      `image`.
    </ResponseField>

    <ResponseField name="author" type="object" required>
      Who published the post. **One shape for both people and organization pages** —
      read `type` to tell them apart. A field the card did not carry is omitted
      rather than guessed.

      <Expandable title="Author object">
        <ResponseField name="type" type="string" required>
          `profile` when a person published the post, `company` when an
          organization page did.
        </ResponseField>

        <ResponseField name="id" type="string">
          The profile id on a `profile`, the numeric page id on a `company`. The
          profile id is the same value as the `profileId` returned by
          [Fetch Profile](/api-reference/endpoint/get-profile), so posts can be
          joined to profiles.
        </ResponseField>

        <ResponseField name="numericId" type="string">
          Numeric member id, when the card carries one. Not resolvable to a profile
          on its own — prefer `id`.
        </ResponseField>

        <ResponseField name="name" type="string">
          Display name of the person or page.
        </ResponseField>

        <ResponseField name="url" type="string">
          Public professional profile or page URL.
        </ResponseField>

        <ResponseField name="headline" type="string">
          The author's headline, when the card carries one.
        </ResponseField>

        <ResponseField name="info" type="string">
          The author's subtitle exactly as the card renders it: the headline for a
          person, the follower-count line for a page. Kept for parity with
          [Get Posts](/api-reference/endpoint/get-posts); read `headline` when you
          want the headline only.
        </ResponseField>

        <ResponseField name="publicIdentifier" type="string">
          Profile slug, without the `/in/` prefix. Only on `type: "profile"`.
        </ResponseField>

        <ResponseField name="universalName" type="string">
          Page slug, without the path prefix. Only on `type: "company"`.
        </ResponseField>

        <ResponseField name="avatar" type="object">
          Author picture (`url`, `width`, `height`), largest rendition the card
          offered. Read the size from the fields, not from the URL.
        </ResponseField>

        <ResponseField name="website" type="string">
          Call-to-action link on the author card, when present, with its label in
          `websiteLabel`.
        </ResponseField>

        <ResponseField name="isPremium" type="boolean">
          `true` when the card carries a premium badge. Omitted otherwise — an
          absent badge is not proof the account is not premium.
        </ResponseField>
      </Expandable>
    </ResponseField>

    <ResponseField name="reactionCount" type="integer" required>
      Total reactions, a **snapshot at fetch time**.
    </ResponseField>

    <ResponseField name="commentCount" type="integer" required>
      Total comments, a snapshot at fetch time.
    </ResponseField>

    <ResponseField name="sharesCount" type="integer" required>
      Times the post was reshared, a snapshot at fetch time. The plural spelling
      matches [Get Posts](/api-reference/endpoint/get-posts).
    </ResponseField>

    <ResponseField name="reactionTypeCounts" type="array" required>
      The complete reaction breakdown in the source's own order, zeros included —
      `LIKE`, `PRAISE`, `EMPATHY`, `INTEREST`, `APPRECIATION`, `ENTERTAINMENT`,
      `SUPER_LIKE` — as `type` / `count` pairs. Use this one when you need every
      type.
    </ResponseField>

    <ResponseField name="reactionsByType" type="object" required>
      The five types [Get Posts](/api-reference/endpoint/get-posts) reports, keyed
      the same way: `like`, `appreciation`, `empathy`, `interest`, `praise`. It has
      no slot for `entertainment` or `superLike`, which is exactly why
      `reactionTypeCounts` exists alongside it.
    </ResponseField>

    <ResponseField name="mentions" type="array" required>
      Entities mentioned in the text, as `type` (`person` or `company`), `name` and
      `url`. Empty when there are none.
    </ResponseField>

    <ResponseField name="hashtags" type="array" required>
      Hashtags in the text, without the leading `#`. Empty when there are none.
    </ResponseField>

    <ResponseField name="outboundLinks" type="array" required>
      Links in the text. Empty when there are none.
    </ResponseField>

    <ResponseField name="isRepost" type="boolean" required>
      `true` when this post quotes another one.
    </ResponseField>

    <ResponseField name="resharedPost" type="object">
      The quoted post, when `isRepost` is `true`: `id`, `shareUrn`, `date`, the
      original's `author` (same shape as above) and its `content`, plus its
      `mentions`, `hashtags` and `outboundLinks`. So a repost is usable without a
      second call — but engagement counts are not carried here; fetch the quoted
      post by its `id` if you need them.
    </ResponseField>

    <ResponseField name="commentingDisabled" type="boolean">
      Whether the author turned commenting off. Reported only when the card carries
      the setting, and omitted otherwise rather than defaulted to `false`.
    </ResponseField>
  </Expandable>
</ResponseField>

## Example Request

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.fetchin.io/api/v1/posts/search" \
    -H "X-API-Key: your-api-key-here" \
    -H "Content-Type: application/json" \
    -d '{
      "query": "\"head of growth\" AND (saas OR fintech)",
      "postedWithin": "week",
      "maxResults": 25
    }'
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch('https://api.fetchin.io/api/v1/posts/search', {
    method: 'POST',
    headers: {
      'X-API-Key': 'your-api-key-here',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      query: '"head of growth" AND (saas OR fintech)',
      postedWithin: 'week',
      maxResults: 25,
    }),
  });

  const page = await response.json();
  console.log(page.query, page.sort, page.results.length);
  ```

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

  response = requests.post(
      'https://api.fetchin.io/api/v1/posts/search',
      headers={'X-API-Key': 'your-api-key-here'},
      json={
          'query': '"head of growth" AND (saas OR fintech)',
          'postedWithin': 'week',
          'maxResults': 25,
      },
  )

  page = response.json()
  print(page['query'], page['sort'], len(page['results']))
  ```

  ```php PHP theme={null}
  <?php

  $ch = curl_init('https://api.fetchin.io/api/v1/posts/search');
  curl_setopt($ch, CURLOPT_POST, true);
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  curl_setopt($ch, CURLOPT_HTTPHEADER, [
      'X-API-Key: your-api-key-here',
      'Content-Type: application/json',
  ]);
  curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
      'query' => '"head of growth" AND (saas OR fintech)',
      'postedWithin' => 'week',
      'maxResults' => 25,
  ]));

  $page = json_decode(curl_exec($ch), true);
  curl_close($ch);
  print_r($page);
  ?>
  ```
</CodeGroup>

Everything a set of authors posted this month, with no keywords at all:

```json theme={null}
{
  "authorCompanies": ["1035"],
  "postedWithin": "month",
  "maxResults": 50
}
```

## Example Response

<ResponseExample>
  ```json 200 Success theme={null}
  {
    "query": "\"head of growth\" AND (saas OR fintech)",
    "sort": null,
    "results": [
      {
        "id": "urn:li:activity:7486820978292072449",
        "entityId": "7486820978292072449",
        "shareUrn": "urn:li:ugcPost:7486820977411145728",
        "shareUrl": "https://www.linkedin.com/feed/update/urn:li:activity:7486820978292072449/",
        "date": "2026-10-04T16:33:39.632Z",
        "postedAgoShort": "3d",
        "isEdited": false,
        "content": "We just hired our first Head of Growth. Three things I wish I had known before writing the job description for a SaaS company this size...",
        "postType": "image",
        "media": [
          {
            "type": "image",
            "url": "https://media.licdn.com/dms/image/v2/D4E22AQ.../feedshare-shrink_2048_1536/0/1759590819000?e=1762992000&v=beta&t=...",
            "width": 2048,
            "height": 1365,
            "aspectRatio": 1.5
          }
        ],
        "images": [
          {
            "url": "https://media.licdn.com/dms/image/v2/D4E22AQ.../feedshare-shrink_2048_1536/0/1759590819000?e=1762992000&v=beta&t=...",
            "width": 2048,
            "height": 1365
          }
        ],
        "imageUrl": "https://media.licdn.com/dms/image/v2/D4E22AQ.../feedshare-shrink_2048_1536/0/1759590819000?e=1762992000&v=beta&t=...",
        "author": {
          "type": "profile",
          "id": "ACoAAA8BYqEBCGLg_vT_ca6mMEqkpp9nVffJ3hc",
          "name": "Marie Dupont",
          "url": "https://www.linkedin.com/in/marie-dupont",
          "publicIdentifier": "marie-dupont",
          "info": "Co-founder & CEO at Northbound",
          "headline": "Co-founder & CEO at Northbound",
          "avatar": {
            "url": "https://media.licdn.com/dms/image/v2/D4E03AQ.../profile-displayphoto-shrink_400_400/0/1726000000000?e=1762992000&v=beta&t=...",
            "width": 400,
            "height": 400
          }
        },
        "reactionCount": 412,
        "commentCount": 37,
        "sharesCount": 8,
        "reactionTypeCounts": [
          { "type": "LIKE", "count": 351 },
          { "type": "PRAISE", "count": 33 },
          { "type": "EMPATHY", "count": 12 },
          { "type": "INTEREST", "count": 9 },
          { "type": "APPRECIATION", "count": 7 },
          { "type": "ENTERTAINMENT", "count": 0 },
          { "type": "SUPER_LIKE", "count": 0 }
        ],
        "reactionsByType": {
          "like": 351,
          "appreciation": 7,
          "empathy": 12,
          "interest": 9,
          "praise": 33
        },
        "mentions": [
          {
            "type": "company",
            "name": "Northbound",
            "url": "https://www.linkedin.com/company/northbound"
          }
        ],
        "hashtags": ["hiring", "saas"],
        "outboundLinks": ["https://northbound.example.com/careers"],
        "isRepost": false,
        "commentingDisabled": false
      },
      {
        "id": "urn:li:activity:7489894675928027136",
        "entityId": "7489894675928027136",
        "shareUrn": "urn:li:share:7489894675324166145",
        "shareUrl": "https://www.linkedin.com/feed/update/urn:li:activity:7489894675928027136/",
        "date": "2026-10-06T04:07:26.255Z",
        "postedAgoShort": "1d",
        "isEdited": true,
        "content": "This matches what we see on the fintech side too.",
        "postType": "text",
        "media": [],
        "author": {
          "type": "company",
          "id": "1035",
          "name": "Northbound",
          "url": "https://www.linkedin.com/company/northbound",
          "universalName": "northbound",
          "info": "12,431 followers"
        },
        "reactionCount": 64,
        "commentCount": 3,
        "sharesCount": 1,
        "reactionTypeCounts": [
          { "type": "LIKE", "count": 58 },
          { "type": "PRAISE", "count": 4 },
          { "type": "EMPATHY", "count": 1 },
          { "type": "INTEREST", "count": 1 },
          { "type": "APPRECIATION", "count": 0 },
          { "type": "ENTERTAINMENT", "count": 0 },
          { "type": "SUPER_LIKE", "count": 0 }
        ],
        "reactionsByType": {
          "like": 58,
          "appreciation": 0,
          "empathy": 1,
          "interest": 1,
          "praise": 4
        },
        "mentions": [],
        "hashtags": [],
        "outboundLinks": [],
        "isRepost": true,
        "resharedPost": {
          "id": "urn:li:activity:7486820978292072449",
          "shareUrn": "urn:li:ugcPost:7486820977411145728",
          "date": "2026-10-04T16:33:39.632Z",
          "author": {
            "type": "profile",
            "id": "ACoAAA8BYqEBCGLg_vT_ca6mMEqkpp9nVffJ3hc",
            "name": "Marie Dupont",
            "url": "https://www.linkedin.com/in/marie-dupont",
            "publicIdentifier": "marie-dupont",
            "headline": "Co-founder & CEO at Northbound"
          },
          "content": "We just hired our first Head of Growth. Three things I wish I had known before writing the job description for a SaaS company this size...",
          "hashtags": ["hiring", "saas"]
        }
      }
    ],
    "cursor": "eyJ2IjoxLCJxIjoiOWYyYzFlN2E0YjhkMzA1NiIsImYiOiJjMWE0ZTkwYjdkMzI2ODU0IiwiaSI6NTAsIm4iOjUwLCJjIjowfQ"
  }
  ```

  ```json 200 Nothing matched theme={null}
  {
    "query": "\"quantum pastry logistics\"",
    "sort": null,
    "results": [],
    "cursor": null
  }
  ```

  ```json 400 Too many operators theme={null}
  {
    "error": "'query' uses too many boolean operators: 6 (maximum 5). Split it into several searches and combine the results.",
    "code": "INVALID_PARAMETER",
    "details": {
      "operators": 6,
      "maxOperators": 5
    }
  }
  ```

  ```json 400 Unrecognised filter value theme={null}
  {
    "error": "'contentType[0]' must be one of: image, video, liveVideo, jobPost, document (received 'pdf').",
    "code": "INVALID_PARAMETER"
  }
  ```

  ```json 401 Unauthorized theme={null}
  {
    "error": "Invalid API key",
    "code": "INVALID_API_KEY"
  }
  ```

  ```json 402 Payment Required theme={null}
  {
    "error": "Quota exceeded. Upgrade your plan or wait for renewal.",
    "code": "QUOTA_EXHAUSTED",
    "details": {
      "creditsRemaining": 0,
      "subscriptionCreditsRemaining": 0,
      "paygCreditsRemaining": 0,
      "buyCreditsUrl": "https://fetchin.io/credits"
    }
  }
  ```

  ```json 503 Service Unavailable theme={null}
  {
    "error": "Service temporarily unable to serve this request. Please retry.",
    "code": "SERVICE_UNAVAILABLE"
  }
  ```
</ResponseExample>

## Errors

See [Error Handling](/concepts/error-handling) for the shared envelope and
recommended handling. This endpoint has no `404`: a search that matches nothing
is a `200` with an empty `results`.

| HTTP | `code` | When | Credits |
| - | - | - | - |
| `400` | `INVALID_PARAMETER` | Any body that breaks a rule above: unknown key, unrecognised enum value, `maxResults` over 50, more than 5 boolean operators, unbalanced parentheses or a dangling operator, a query with nothing to search for, a hashtag term together with `sortBy: "date"`, no `query` and no author filter, a malformed or foreign `cursor`, malformed JSON | 0 |
| `401` | `UNAUTHENTICATED` / `INVALID_API_KEY` | Missing or invalid `X-API-Key` | 0 |
| `402` | `QUOTA_EXHAUSTED` | You have no credit left for this call | 0 |
| `429` | `RATE_LIMITED` | Your per-second rate limit, **or** more searches running at the same moment than your plan's concurrency allowance (`Too many searches running at once for your account. This endpoint holds each search open for several seconds, so a burst can exceed your plan even when your request rate does not. Wait for one to finish and retry.`, `Retry-After: 1`) | 0 |
| `500` | `INTERNAL_ERROR` | An unexpected bug on our side | 0 |
| `503` | `SERVICE_UNAVAILABLE` | Search capacity is saturated, or the page could not be read and we will not hand you a page we do not trust. `Retry-After: 5` | 0 |

<Note>
  **Concurrency, not just rate.** Your per-second limit counts request *starts*;
  this endpoint also caps how many of your searches may be running at the same
  moment. That allowance follows your plan's rate limit and is set generously
  above it, precisely so that sending the rate you pay for never hits it — a
  `429` from this cap means you have far more searches open at once than your
  rate implies, not that your plan is too small. Lower your concurrency and
  honour `Retry-After`.
</Note>

<Tip>
  A `503` here is never a partial page. If a page cannot be read in full we
  refuse it and charge nothing, rather than return a short page you would have to
  distrust. Retry it.
</Tip>

## Performance

Observed, not guaranteed: roughly **1–2 s** for a small page (up to a handful of
results) and **4.5–7 s** for a full 50-result page, measured across local and
production runs on 2026-10-07. Treat that as a range to size a timeout against
rather than a service level — plan a per-request timeout of at least 30 seconds.
Because a page is held open for seconds, parallel searches add up fast: see the
concurrency note above.

## Limitations

Stated plainly, because you will hit every one of them in an afternoon of real
use.

<AccordionGroup>
  <Accordion title="Engagement counts are a snapshot">
    `reactionCount`, `commentCount`, `sharesCount`, `reactionTypeCounts` and
    `reactionsByType` are read at the moment of the fetch. A post found an hour
    ago has moved since. Re-read the post when the number matters, and do not
    treat two pages of the same walk as counted at the same instant.
  </Accordion>

  <Accordion title="The date is the publication date, even on an edited post">
    An edited post reports when it was originally published, not when it was
    edited, and `isEdited: true` is the only signal that the two differ. There is
    no edit timestamp.
  </Accordion>

  <Accordion title="Media URLs expire">
    Image, video, document and avatar URLs are signed by the source's CDN and
    stop working after an expiry date we do not control. Download what you want
    to keep; do not store the URL as if it were permanent. Read image sizes from
    `width` / `height`, not from the path.
  </Accordion>

  <Accordion title="Results can repeat across pages">
    The result set behind a query is live and re-orders between requests, so the
    same post can appear on two consecutive pages — measured at **8%** over a
    4-page walk (40 results, 37 distinct). There is no snapshot to page against.
    De-duplicate on `id`.
  </Accordion>

  <Accordion title="A hashtag query cannot be sorted by date">
    `#hiring` on its own works. `#hiring` with `sortBy: "date"` returns nothing
    at all upstream, so the combination is refused with `400` instead of billing
    you for an empty page. Drop the `#`, or drop the sort.
  </Accordion>

  <Accordion title="Sorting by date on a tight window narrows hard">
    `sortBy: "date"` with `postedWithin: "24h"` returns markedly fewer results
    than the same query unsorted — measured 1 against 6 on `"hiring"`. When you
    want coverage of a narrow window, omit `sortBy` and order the results
    yourself on `date`.
  </Accordion>

  <Accordion title="Depth is a few hundred results per query">
    `cursor` goes `null` well before you have everything that matches your words.
    Split by date window or by author, as in [Pagination](#pagination).
  </Accordion>

  <Accordion title="Keyword matching is not ours to tune">
    Stemming, synonyms and relevance are the underlying search's, not ours. Use
    quoted phrases when you need the words in that order, and
    [verify against the echoed `query`](#what-we-normalise-for-you) when a result
    set surprises you.
  </Accordion>
</AccordionGroup>

## Notes

<Tip>
  Ask for `maxResults: 50`. The call costs 1 credit at any page size, so a
  smaller page is strictly worse value.
</Tip>

<Info>
  Nothing you search for is written back anywhere, and the posts returned are not
  re-fetched as a side effect of a search.
</Info>


## OpenAPI

````yaml POST /api/v1/posts/search
openapi: 3.1.0
info:
  title: Fetchin API
  version: 1.0.0
  description: Public B2B data API for fetching posts and profile information
servers:
  - url: https://api.fetchin.io
    description: Production server
security:
  - ApiKeyAuth: []
paths:
  /api/v1/posts/search:
    post:
      summary: Search posts
      description: >-
        Search public professional posts by keyword and filters. The request is
        a JSON body, not query parameters. `query` supports quoted phrases,
        AND/OR/NOT and parentheses, with a hard ceiling of 5 boolean operators
        that comes from the underlying search - a 6th is refused with 400 rather
        than silently matching nothing. Filters cover recency (`postedWithin`,
        `postedAfter`), attachment kind (`contentType`), the author
        (`authorProfiles`, `authorCompanies`, `authorJobTitle`) and mentions
        (`mentionsCompanies`); every filter is validated against a closed list
        of values, so a typo is a free 400 instead of a billed page that quietly
        ignored it. A filter whose effect we could not confirm is refused with a
        free 400 rather than offered: an unrecognised filter is silently ignored
        by the underlying search, which would return a plausible but completely
        unfiltered page you paid for. `sortBy` has NO default: omit it and the
        underlying search applies its own ordering, which the response reports
        as `sort: null`. Pages are walked with an opaque `cursor` and one query
        is exhausted after a few hundred results, so reach further by splitting
        it by date window or by author. Billing is a flat **1 credit per call**
        whatever comes back, zero results included; a 400, a 402 and a 503 cost
        nothing.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: >-
                Send `query`, or at least one of `authorProfiles` /
                `authorCompanies`. Every other field is optional. `null` reads
                as "field omitted" everywhere. The body is validated strictly:
                an unknown key, an unrecognised enum value or a list that is too
                long is rejected with 400 and costs nothing, rather than being
                accepted and ignored.
              properties:
                query:
                  type: string
                  maxLength: 500
                  description: >-
                    The search terms. Supports quoted phrases, `AND` / `OR` /
                    `NOT`, parentheses with nesting up to 5 levels, and hashtag
                    terms. **At most 5 boolean operators** - this is a limit of
                    the underlying search, and a 6th is refused with 400 rather
                    than silently matching nothing, so split the query into
                    several searches and combine the results. At most 20 quoted
                    phrases. Curly quotes, lowercase operators, single-quote
                    delimiters and a bare `&` are normalised silently, and the
                    interpreted query is echoed back as the response's `query`.
                    Required unless `authorProfiles` or `authorCompanies` is
                    set.
                  example: '"head of growth" AND (saas OR fintech) NOT intern'
                sortBy:
                  type: string
                  enum:
                    - relevance
                    - date
                  description: >-
                    How to order the results. **No default**: omit it and the
                    underlying search applies its own ordering, which the
                    response reports as `sort: null`. `date` is newest first.
                    `date` cannot be combined with a hashtag term in `query`
                    (400), and on a tight `postedWithin` window it returns
                    markedly fewer results than no sort at all.
                postedWithin:
                  type: string
                  enum:
                    - 24h
                    - week
                    - month
                  description: >-
                    Keep only posts published within this window. Applied by the
                    search itself, so it also changes which results exist.
                postedAfter:
                  type:
                    - string
                    - integer
                  description: >-
                    Keep only posts published at or after this instant, as an
                    ISO-8601 timestamp or an epoch in milliseconds. Must not be
                    in the future and at most 365 days ago. Applied by us after
                    the search runs, so a page filtered this way comes back
                    short and still costs its 1 credit. Combine it with
                    `postedWithin` to narrow inside a window.
                  example: '2026-10-01T00:00:00Z'
                contentType:
                  type: array
                  description: >-
                    Keep only posts carrying one of these kinds of attachment.
                    Values OR together.
                  uniqueItems: true
                  minItems: 1
                  maxItems: 5
                  items:
                    type: string
                    enum:
                      - image
                      - video
                      - liveVideo
                      - jobPost
                      - document
                  example:
                    - document
                authorProfiles:
                  type: array
                  description: >-
                    Keep only posts published by these people. 1 to 10 values,
                    each a profile id or any profile URL containing one. A slug
                    on its own is not accepted here - resolve it with `GET
                    /api/v1/profile` first. Can be sent without `query` to page
                    a set of authors.
                  minItems: 1
                  maxItems: 10
                  items:
                    type: string
                  example:
                    - ACoAAA8BYqEBCGLg_vT_ca6mMEqkpp9nVffJ3hc
                authorCompanies:
                  type: array
                  description: >-
                    Keep only posts published by these organization pages. 1 to
                    10 values, each a numeric page id, a page urn, or a page URL
                    ending in a numeric id. Can be sent without `query`.
                  minItems: 1
                  maxItems: 10
                  items:
                    type: string
                  example:
                    - '1035'
                mentionsCompanies:
                  type: array
                  description: >-
                    Keep only posts that mention these organization pages. 1 to
                    10 numeric page ids, urns or URLs.
                  minItems: 1
                  maxItems: 10
                  items:
                    type: string
                  example:
                    - '1035'
                authorJobTitle:
                  type: string
                  minLength: 1
                  maxLength: 100
                  description: >-
                    Keep only posts whose author's current title matches this
                    free text. Matched as a phrase.
                  example: Head of Growth
                maxResults:
                  type: integer
                  minimum: 1
                  maximum: 50
                  default: 50
                  description: >-
                    How many results this page may return. 50 is both the
                    default and the hard ceiling; a larger value is refused with
                    400 because the underlying search answers nothing at all
                    above it. A page can return fewer results than this: results
                    are de-duplicated by post id within the page (a 50-result
                    request measured 49), and the last page of a walk is short
                    by nature.
                cursor:
                  type: string
                  maxLength: 1024
                  description: >-
                    Opaque cursor for the next page. Pass back the `cursor` from
                    the previous response, byte for byte, with the same `query`
                    and filters; omit it to start a new search. A cursor issued
                    for a different query, or any value this API did not issue,
                    is refused with 400. Keep paging while `cursor` is not
                    `null`.
            examples:
              keywords:
                summary: Boolean keyword search, newest first
                value:
                  query: '"head of growth" AND (saas OR fintech)'
                  postedWithin: week
                  sortBy: date
                  maxResults: 25
              byAuthor:
                summary: Everything an author posted this month, no keywords
                value:
                  authorCompanies:
                    - '1035'
                  postedWithin: month
                  maxResults: 50
              documentsOnly:
                summary: Only posts carrying a document
                value:
                  query: pricing benchmark
                  contentType:
                    - document
              nextPage:
                summary: The next page of the first search
                value:
                  query: '"head of growth" AND (saas OR fintech)'
                  postedWithin: week
                  sortBy: date
                  maxResults: 25
                  cursor: >-
                    eyJ2IjoxLCJxIjoiOWYyYzFlN2E0YjhkMzA1NiIsImYiOiJjMWE0ZTkwYjdkMzI2ODU0IiwiaSI6MjUsIm4iOjI1LCJjIjowfQ
      responses:
        '200':
          description: >-
            A page of matching posts. Billed 1 credit, including when `results`
            is empty. Observed latency, not a guarantee: ~1-2 s for a small page
            and 4.5-7 s for a full 50-result page.
          content:
            application/json:
              schema:
                type: object
                required:
                  - query
                  - sort
                  - results
                  - cursor
                properties:
                  query:
                    type:
                      - string
                      - 'null'
                    description: >-
                      The query as it was interpreted, after normalisation - so
                      every silent rewrite is auditable. `null` on an
                      author-only search.
                  sort:
                    type:
                      - string
                      - 'null'
                    enum:
                      - relevance
                      - date
                      - null
                    description: >-
                      The ordering that was applied, echoed from the request.
                      `null` when `sortBy` was omitted, meaning the underlying
                      search used its own ordering, which we do not name.
                  results:
                    type: array
                    description: >-
                      Matching posts, de-duplicated within the page. Can hold
                      fewer items than `maxResults`, and can be empty - which
                      still costs 1 credit.
                    items:
                      type: object
                      description: One matching post.
                      required:
                        - id
                        - entityId
                        - date
                        - isEdited
                        - postType
                        - media
                        - author
                        - reactionCount
                        - commentCount
                        - sharesCount
                        - reactionTypeCounts
                        - reactionsByType
                        - mentions
                        - hashtags
                        - outboundLinks
                        - isRepost
                      properties:
                        id:
                          type: string
                          description: >-
                            Activity URN of the post (`urn:li:activity:*`).
                            Stable; de-duplicate on this value.
                          example: urn:li:activity:7486820978292072449
                        entityId:
                          type: string
                          description: The numeric part of `id`, for convenience.
                          example: '7486820978292072449'
                        shareUrn:
                          type: string
                          description: >-
                            The same post in the `urn:li:share:*` /
                            `urn:li:ugcPost:*` namespace, when the card carries
                            it.
                          example: urn:li:ugcPost:7486820977411145728
                        shareUrl:
                          type: string
                          description: Public permalink to the post.
                          example: >-
                            https://www.linkedin.com/feed/update/urn:li:activity:7486820978292072449/
                        date:
                          type: string
                          format: date-time
                          description: >-
                            When the post was PUBLISHED, derived from the post
                            id. An edited post reports its original publication
                            date, not the date of the edit - see `isEdited`.
                        postedAgoShort:
                          type: string
                          description: >-
                            The relative age the card displayed at fetch time,
                            e.g. `3d`. Informational; compute ages from `date`.
                          example: 3d
                        isEdited:
                          type: boolean
                          description: >-
                            `true` when the post was edited after publication.
                            The text returned is the current one; `date` is
                            still the original publication date.
                        content:
                          type: string
                          description: >-
                            The post's own text. Omitted when the post carries
                            no text of its own, which happens on an image, video
                            or document posted without a caption.
                        postType:
                          type: string
                          description: >-
                            What the post is, derived from the attachment that
                            rendered: `text`, `image`, `video`, `document`,
                            `article`, `poll`, `job`, `newsletter`,
                            `celebration`, or `other` for an attachment kind
                            added after this was written. `article` means a link
                            preview card is attached.
                          example: image
                        media:
                          type: array
                          description: >-
                            Every attachment of the post, in the order the card
                            rendered them. Empty on a text-only post.
                          items:
                            type: object
                            description: >-
                              One attachment, as published. Which fields are
                              filled depends on `type`. URLs served by the
                              source's CDN are signed and expire; download what
                              you keep.
                            required:
                              - type
                            properties:
                              type:
                                type: string
                                description: >-
                                  Kind of attachment: `image`, `video`,
                                  `document`, `article`, `poll`, `job`,
                                  `newsletter` or `celebration`.
                                example: image
                              url:
                                type: string
                              title:
                                type: string
                              subtitle:
                                type: string
                              assetUrn:
                                type: string
                              aspectRatio:
                                type: number
                              width:
                                type: integer
                              height:
                                type: integer
                              thumbnailUrl:
                                type: string
                              pageCount:
                                type: integer
                                description: Pages of a document attachment.
                              carousel:
                                type: boolean
                                description: >-
                                  `true` when a document attachment renders as a
                                  carousel.
                              options:
                                type: array
                                items:
                                  type: string
                                description: The answer options of a poll.
                              entityId:
                                type: string
                        images:
                          type: array
                          description: >-
                            The post's images with dimensions, largest rendition
                            each. Omitted when the post has none.
                          items:
                            type: object
                            properties:
                              url:
                                type: string
                              width:
                                type: integer
                              height:
                                type: integer
                        imageUrl:
                          type: string
                          description: >-
                            First entry of `images`. Kept for parity with `GET
                            /api/v1/posts`.
                        article:
                          type: object
                          description: The link preview card, when one is attached.
                          properties:
                            title:
                              type: string
                            subtitle:
                              type: string
                            link:
                              type: string
                            image:
                              type: string
                        author:
                          type: object
                          description: >-
                            Who published the post. One shape for both people
                            and organization pages; read `type` to tell them
                            apart. A field that could not be read is omitted
                            rather than guessed.
                          required:
                            - type
                          properties:
                            type:
                              type: string
                              enum:
                                - profile
                                - company
                              description: >-
                                `profile` when a person published the post,
                                `company` when an organization page did.
                            id:
                              type: string
                              description: >-
                                Author id: the profile id when `type` is
                                `profile`, the numeric page id when it is
                                `company`. The profile id is the same value as
                                the `profileId` returned by `GET
                                /api/v1/profile`, so posts can be joined to
                                profiles.
                              example: ACoAAA8BYqEBCGLg_vT_ca6mMEqkpp9nVffJ3hc
                            numericId:
                              type: string
                              description: >-
                                Numeric member id, when the card carries one.
                                Not resolvable to a profile on its own; prefer
                                `id`.
                            name:
                              type: string
                              example: Bill Gates
                            url:
                              type: string
                              description: >-
                                Public professional profile or page URL of the
                                author.
                              example: https://www.linkedin.com/in/williamhgates
                            info:
                              type: string
                              description: >-
                                The author's subtitle exactly as the card
                                renders it: the headline for a person, the
                                follower-count line for a page. Kept for parity
                                with `GET /api/v1/posts`; read `headline`
                                instead when you want the headline only.
                            publicIdentifier:
                              type: string
                              description: >-
                                Profile slug, without the `/in/` prefix. Only on
                                `type: "profile"`.
                              example: williamhgates
                            universalName:
                              type: string
                              description: >-
                                Page slug, without the path prefix. Only on
                                `type: "company"`.
                              example: microsoft
                            headline:
                              type: string
                              description: >-
                                The author's headline, when the card carries
                                one.
                              example: >-
                                Chair, Gates Foundation and Founder,
                                Breakthrough Energy
                            avatar:
                              type: object
                              description: >-
                                Author picture, largest rendition the card
                                offered. Read the size from `width`/`height`,
                                not from the URL. The URL is signed by the
                                source and stops working after its expiry;
                                download the file to keep it.
                              properties:
                                url:
                                  type: string
                                width:
                                  type: integer
                                height:
                                  type: integer
                            website:
                              type: string
                              description: >-
                                Call-to-action link on the author card, when
                                present.
                            websiteLabel:
                              type: string
                              description: Label of that call-to-action link, when present.
                            isPremium:
                              type: boolean
                              description: >-
                                `true` when the author card carries a premium
                                badge. Omitted otherwise - an absent badge is
                                not proof that the account is not premium.
                        reactionCount:
                          type: integer
                          description: >-
                            Total reactions, as a SNAPSHOT at fetch time.
                            Re-read the post to refresh it.
                        commentCount:
                          type: integer
                          description: Total comments, as a snapshot at fetch time.
                        sharesCount:
                          type: integer
                          description: >-
                            Times the post was reshared, as a snapshot at fetch
                            time. The plural spelling matches `GET
                            /api/v1/posts`.
                        reactionTypeCounts:
                          type: array
                          description: >-
                            The complete reaction breakdown in the source's own
                            order, zeros included: `LIKE`, `PRAISE`, `EMPATHY`,
                            `INTEREST`, `APPRECIATION`, `ENTERTAINMENT`,
                            `SUPER_LIKE`. Use this one when you need every type.
                          items:
                            type: object
                            properties:
                              type:
                                type: string
                              count:
                                type: integer
                        reactionsByType:
                          type: object
                          description: >-
                            The five reaction types `GET /api/v1/posts` reports,
                            keyed the same way (`like`, `appreciation`,
                            `empathy`, `interest`, `praise`). It has no slot for
                            `entertainment` or `superLike`, which is why
                            `reactionTypeCounts` exists alongside it.
                          additionalProperties:
                            type: integer
                        mentions:
                          type: array
                          description: >-
                            Entities mentioned in the post text. Empty when
                            there are none.
                          items:
                            type: object
                            description: An entity mentioned in the post text.
                            required:
                              - type
                              - name
                              - url
                            properties:
                              type:
                                type: string
                                enum:
                                  - person
                                  - company
                              name:
                                type: string
                              url:
                                type: string
                        hashtags:
                          type: array
                          description: >-
                            Hashtags in the post text, without the leading `#`.
                            Empty when there are none.
                          items:
                            type: string
                        outboundLinks:
                          type: array
                          description: Links in the post text. Empty when there are none.
                          items:
                            type: string
                        isRepost:
                          type: boolean
                          description: >-
                            `true` when this post quotes another one. The quoted
                            post is in `resharedPost`.
                        resharedPost:
                          type: object
                          description: >-
                            The quoted post, when `isRepost` is `true`: its own
                            author and text, so a repost is usable without a
                            second call. Engagement counts are not carried here
                            - fetch the quoted post by its `id` if you need
                            them.
                          properties:
                            id:
                              type: string
                            shareUrn:
                              type: string
                            date:
                              type: string
                              format: date-time
                            author:
                              type: object
                              description: >-
                                Who published the post. One shape for both
                                people and organization pages; read `type` to
                                tell them apart. A field that could not be read
                                is omitted rather than guessed.
                              required:
                                - type
                              properties:
                                type:
                                  type: string
                                  enum:
                                    - profile
                                    - company
                                  description: >-
                                    `profile` when a person published the post,
                                    `company` when an organization page did.
                                id:
                                  type: string
                                  description: >-
                                    Author id: the profile id when `type` is
                                    `profile`, the numeric page id when it is
                                    `company`. The profile id is the same value
                                    as the `profileId` returned by `GET
                                    /api/v1/profile`, so posts can be joined to
                                    profiles.
                                  example: ACoAAA8BYqEBCGLg_vT_ca6mMEqkpp9nVffJ3hc
                                numericId:
                                  type: string
                                  description: >-
                                    Numeric member id, when the card carries
                                    one. Not resolvable to a profile on its own;
                                    prefer `id`.
                                name:
                                  type: string
                                  example: Bill Gates
                                url:
                                  type: string
                                  description: >-
                                    Public professional profile or page URL of
                                    the author.
                                  example: https://www.linkedin.com/in/williamhgates
                                info:
                                  type: string
                                  description: >-
                                    The author's subtitle exactly as the card
                                    renders it: the headline for a person, the
                                    follower-count line for a page. Kept for
                                    parity with `GET /api/v1/posts`; read
                                    `headline` instead when you want the
                                    headline only.
                                publicIdentifier:
                                  type: string
                                  description: >-
                                    Profile slug, without the `/in/` prefix.
                                    Only on `type: "profile"`.
                                  example: williamhgates
                                universalName:
                                  type: string
                                  description: >-
                                    Page slug, without the path prefix. Only on
                                    `type: "company"`.
                                  example: microsoft
                                headline:
                                  type: string
                                  description: >-
                                    The author's headline, when the card carries
                                    one.
                                  example: >-
                                    Chair, Gates Foundation and Founder,
                                    Breakthrough Energy
                                avatar:
                                  type: object
                                  description: >-
                                    Author picture, largest rendition the card
                                    offered. Read the size from
                                    `width`/`height`, not from the URL. The URL
                                    is signed by the source and stops working
                                    after its expiry; download the file to keep
                                    it.
                                  properties:
                                    url:
                                      type: string
                                    width:
                                      type: integer
                                    height:
                                      type: integer
                                website:
                                  type: string
                                  description: >-
                                    Call-to-action link on the author card, when
                                    present.
                                websiteLabel:
                                  type: string
                                  description: >-
                                    Label of that call-to-action link, when
                                    present.
                                isPremium:
                                  type: boolean
                                  description: >-
                                    `true` when the author card carries a
                                    premium badge. Omitted otherwise - an absent
                                    badge is not proof that the account is not
                                    premium.
                            content:
                              type: string
                            mentions:
                              type: array
                              items:
                                type: object
                                description: An entity mentioned in the post text.
                                required:
                                  - type
                                  - name
                                  - url
                                properties:
                                  type:
                                    type: string
                                    enum:
                                      - person
                                      - company
                                  name:
                                    type: string
                                  url:
                                    type: string
                            hashtags:
                              type: array
                              items:
                                type: string
                            outboundLinks:
                              type: array
                              items:
                                type: string
                        commentingDisabled:
                          type: boolean
                          description: >-
                            Whether the author turned commenting off. Reported
                            only when the post card carries the setting, and
                            omitted otherwise rather than defaulted.
                  cursor:
                    type:
                      - string
                      - 'null'
                    description: >-
                      Opaque cursor for the next page, or `null` when this
                      result set is exhausted. Pass it back unchanged with the
                      same query and filters. One query is exhausted after a few
                      hundred results.
                  partial:
                    type: object
                    description: >-
                      Present only when at least one card on the page could not
                      be read and was dropped, so a short page is never silently
                      short. Diagnostic and additive: log it, do not branch on
                      the keys of `reasons`.
                    properties:
                      droppedResults:
                        type: integer
                      reasons:
                        type: object
                        additionalProperties:
                          type: integer
              example:
                query: '"head of growth" AND (saas OR fintech)'
                sort: null
                results:
                  - id: urn:li:activity:7486820978292072449
                    entityId: '7486820978292072449'
                    shareUrn: urn:li:ugcPost:7486820977411145728
                    shareUrl: >-
                      https://www.linkedin.com/feed/update/urn:li:activity:7486820978292072449/
                    date: '2026-10-04T16:33:39.632Z'
                    postedAgoShort: 3d
                    isEdited: false
                    content: >-
                      We just hired our first Head of Growth. Three things I
                      wish I had known before writing the job description for a
                      SaaS company this size...
                    postType: image
                    media:
                      - type: image
                        url: >-
                          https://media.licdn.com/dms/image/v2/D4E22AQ.../feedshare-shrink_2048_1536/0/1759590819000?e=1762992000&v=beta&t=...
                        width: 2048
                        height: 1365
                        aspectRatio: 1.5
                    images:
                      - url: >-
                          https://media.licdn.com/dms/image/v2/D4E22AQ.../feedshare-shrink_2048_1536/0/1759590819000?e=1762992000&v=beta&t=...
                        width: 2048
                        height: 1365
                    imageUrl: >-
                      https://media.licdn.com/dms/image/v2/D4E22AQ.../feedshare-shrink_2048_1536/0/1759590819000?e=1762992000&v=beta&t=...
                    author:
                      type: profile
                      id: ACoAAA8BYqEBCGLg_vT_ca6mMEqkpp9nVffJ3hc
                      name: Marie Dupont
                      url: https://www.linkedin.com/in/marie-dupont
                      publicIdentifier: marie-dupont
                      info: Co-founder & CEO at Northbound
                      headline: Co-founder & CEO at Northbound
                      avatar:
                        url: >-
                          https://media.licdn.com/dms/image/v2/D4E03AQ.../profile-displayphoto-shrink_400_400/0/1726000000000?e=1762992000&v=beta&t=...
                        width: 400
                        height: 400
                    reactionCount: 412
                    commentCount: 37
                    sharesCount: 8
                    reactionTypeCounts:
                      - type: LIKE
                        count: 351
                      - type: PRAISE
                        count: 33
                      - type: EMPATHY
                        count: 12
                      - type: INTEREST
                        count: 9
                      - type: APPRECIATION
                        count: 7
                      - type: ENTERTAINMENT
                        count: 0
                      - type: SUPER_LIKE
                        count: 0
                    reactionsByType:
                      like: 351
                      appreciation: 7
                      empathy: 12
                      interest: 9
                      praise: 33
                    mentions:
                      - type: company
                        name: Northbound
                        url: https://www.linkedin.com/company/northbound
                    hashtags:
                      - hiring
                      - saas
                    outboundLinks:
                      - https://northbound.example.com/careers
                    isRepost: false
                    commentingDisabled: false
                  - id: urn:li:activity:7489894675928027136
                    entityId: '7489894675928027136'
                    shareUrn: urn:li:share:7489894675324166145
                    shareUrl: >-
                      https://www.linkedin.com/feed/update/urn:li:activity:7489894675928027136/
                    date: '2026-10-06T04:07:26.255Z'
                    postedAgoShort: 1d
                    isEdited: true
                    content: This matches what we see on the fintech side too.
                    postType: text
                    media: []
                    author:
                      type: company
                      id: '1035'
                      name: Northbound
                      url: https://www.linkedin.com/company/northbound
                      universalName: northbound
                      info: 12,431 followers
                    reactionCount: 64
                    commentCount: 3
                    sharesCount: 1
                    reactionTypeCounts:
                      - type: LIKE
                        count: 58
                      - type: PRAISE
                        count: 4
                      - type: EMPATHY
                        count: 1
                      - type: INTEREST
                        count: 1
                      - type: APPRECIATION
                        count: 0
                      - type: ENTERTAINMENT
                        count: 0
                      - type: SUPER_LIKE
                        count: 0
                    reactionsByType:
                      like: 58
                      appreciation: 0
                      empathy: 1
                      interest: 1
                      praise: 4
                    mentions: []
                    hashtags: []
                    outboundLinks: []
                    isRepost: true
                    resharedPost:
                      id: urn:li:activity:7486820978292072449
                      shareUrn: urn:li:ugcPost:7486820977411145728
                      date: '2026-10-04T16:33:39.632Z'
                      author:
                        type: profile
                        id: ACoAAA8BYqEBCGLg_vT_ca6mMEqkpp9nVffJ3hc
                        name: Marie Dupont
                        url: https://www.linkedin.com/in/marie-dupont
                        publicIdentifier: marie-dupont
                        headline: Co-founder & CEO at Northbound
                      content: >-
                        We just hired our first Head of Growth. Three things I
                        wish I had known before writing the job description for
                        a SaaS company this size...
                      hashtags:
                        - hiring
                        - saas
                cursor: >-
                  eyJ2IjoxLCJxIjoiOWYyYzFlN2E0YjhkMzA1NiIsImYiOiJjMWE0ZTkwYjdkMzI2ODU0IiwiaSI6NTAsIm4iOjUwLCJjIjowfQ
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
components:
  responses:
    BadRequest:
      description: Bad request - a required parameter is missing or a value is invalid.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            missingParameter:
              value:
                error: Missing profileUrlOrUrn parameter
                code: MISSING_PARAMETER
            invalidParameter:
              value:
                error: limit must be a number between 1 and 100
                code: INVALID_PARAMETER
            invalidUrn:
              value:
                error: 'Invalid URN format: urn:li:member:123456789'
                code: INVALID_URN
    Unauthorized:
      description: Unauthorized - the X-API-Key header is missing or invalid.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            missingApiKey:
              value:
                error: Missing API key. Include X-API-Key header.
                code: UNAUTHENTICATED
            invalidApiKey:
              value:
                error: Invalid API key
                code: INVALID_API_KEY
    PaymentRequired:
      description: >-
        Payment required - you are out of credits (QUOTA_EXHAUSTED): the plan
        allowance for the current period is spent AND no pay-as-you-go credits
        are left. Buy pay-as-you-go credits (self-serve,
        https://fetchin.io/credits), upgrade your plan, or wait for renewal;
        retrying will not help and no Retry-After is sent. Distinct from the
        per-second rate limit, which is a 429 (RATE_LIMITED).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            quotaExhausted:
              value:
                error: Quota exceeded. Upgrade your plan or wait for renewal.
                code: QUOTA_EXHAUSTED
                details:
                  creditsRemaining: 0
                  subscriptionCreditsRemaining: 0
                  paygCreditsRemaining: 0
                  buyCreditsUrl: https://fetchin.io/credits
    TooManyRequests:
      description: >-
        Too many requests - your per-second rate limit (RATE_LIMITED) was
        exceeded. Back off and retry, honoring the Retry-After header. (Running
        out of credits is a separate 402 PaymentRequired / QUOTA_EXHAUSTED, and
        buying credits never raises this per-second limit.)
      headers:
        Retry-After:
          description: Number of seconds to wait before retrying the request.
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            rateLimited:
              value:
                error: Rate limit exceeded. Try again later.
                code: RATE_LIMITED
    InternalError:
      description: >-
        Internal server error - an unexpected error occurred in our API. Safe to
        retry; contact support if it persists.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            internalError:
              value:
                error: Internal server error
                code: INTERNAL_ERROR
    ServiceUnavailable:
      description: >-
        Service unavailable - our API is temporarily unable to serve this
        request (capacity exhausted or overloaded). This is on our side, not
        yours and not an upstream service outage. Retry with backoff, honoring
        the Retry-After header.
      headers:
        Retry-After:
          description: Number of seconds to wait before retrying the request.
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            serviceUnavailable:
              value:
                error: >-
                  Service temporarily unable to serve this request. Please
                  retry.
                code: SERVICE_UNAVAILABLE
  schemas:
    Error:
      type: object
      required:
        - error
        - code
      properties:
        error:
          type: string
          description: >-
            Human-readable description of what went wrong. For display/logging;
            do not branch on this string.
        code:
          type: string
          enum:
            - MISSING_PARAMETER
            - INVALID_PARAMETER
            - INVALID_URN
            - UNAUTHENTICATED
            - INVALID_API_KEY
            - FORBIDDEN
            - NOT_FOUND
            - PROFILE_NOT_FOUND
            - POST_NOT_FOUND
            - COMPANY_NOT_FOUND
            - ENDPOINT_REMOVED
            - RATE_LIMITED
            - QUOTA_EXHAUSTED
            - INTERNAL_ERROR
            - UPSTREAM_ERROR
            - SERVICE_UNAVAILABLE
            - UPSTREAM_TIMEOUT
          description: >-
            Stable, machine-readable error code. Branch your integration on this
            value, not on the HTTP status alone or the message text.
        details:
          type: object
          additionalProperties: true
          description: >-
            Optional structured context. On QUOTA_EXHAUSTED it carries the
            balance breakdown at the moment of the rejection - creditsRemaining,
            subscriptionCreditsRemaining, paygCreditsRemaining - plus
            buyCreditsUrl, the self-serve page for buying more credits. Always
            additive and never guaranteed: read it for diagnostics and logs, but
            branch on `code` and do not require any field of it.
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.