---
title: "Search posts"
method: POST
path: "/api/v1/posts/search"
---

# Search posts

`POST /api/v1/posts/search`

## Request

- Base URL: `https://api.fetchin.io`
- URL: `https://api.fetchin.io/api/v1/posts/search`
- Auth: API key in header `X-API-Key`

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`, `authorIndustries`) and mentions (`mentionsProfiles`, `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` - author searches included - and one query is exhausted after a few hundred results, so reach further by splitting it by date window or by author. `total` gives the number of matching posts the source reports: an estimate that moves between pages, so walk until `cursor` is null. Billing is a flat **1 credit per call** whatever comes back, zero results included; a 400, a 402 and a 503 cost nothing.

## Request body

- object: 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.
  - `query` string: 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. A quoted phrase is matched as words, not verbatim: short common words inside the quotes are dropped and other forms of a word match. 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.
  - `sortBy` 'relevance' | 'date': 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). A query that matches too many posts does not sort by date: the underlying search then returns a handful of old, unrelated posts (or none) and no cursor, whatever the `postedWithin` window. Narrow it (more `AND`, fewer `OR` alternatives) or split it into several searches.
  - `postedWithin` '24h' | 'week' | 'month': Keep only posts published within this window. Applied by the search itself, so it also changes which results exist.
  - `postedAfter` union: 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.
    - string
    - integer
  - `contentType` string[]: Keep only posts carrying one of these kinds of attachment. Values OR together.
  - `authorProfiles` string[]: Keep only posts published by these people. 1 to 25 values, each a profile id or any profile URL containing one; a 26th is refused with 400 (`'authorProfiles' must contain between 1 and 25 entries.`). 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.
  - `authorCompanies` 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`.
  - `mentionsProfiles` string[]: Keep only posts that mention 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.
  - `mentionsCompanies` string[]: Keep only posts that mention these organization pages. 1 to 10 numeric page ids, urns or URLs.
  - `authorIndustries` string[]: Keep only posts whose author works in one of these industries, as the author declares it on their own profile (not their employer's industry). Values OR together. 1 to 10 values, each an industry name such as `SOFTWARE_DEVELOPMENT` (any case, or written as the label, `Software Development`) or its numeric id. The 434 accepted values are listed on the endpoint page; anything else is refused with 400.
  - `authorJobTitle` string: Keep only posts whose author's current title matches this free text. Matched as a phrase.
  - `maxResults` integer: 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` string: 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`.

## Response `200`

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.

- object
  - `query` string, nullable, required: The query as it was interpreted, after normalisation - so every silent rewrite is auditable. `null` on an author-only search.
  - `sort` 'relevance' | 'date' | 'null', nullable, required: 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.
  - `total` integer: How many posts the source counts for this search: an estimate, revised from one page to the next. Omitted when the source does not report one.
  - `results` object[], required: Matching posts, de-duplicated within the page. Can hold fewer items than `maxResults`, and can be empty - which still costs 1 credit.
    - `id` string, required: Activity URN of the post (`urn:li:activity:*`). Stable; de-duplicate on this value.
    - `entityId` string, required: The numeric part of `id`, for convenience.
    - `shareUrn` string: The same post in the `urn:li:share:*` / `urn:li:ugcPost:*` namespace, when the card carries it.
    - `shareUrl` string: Public permalink to the post, as the card carries it. Keyed on the share id, so it can differ from `url`; both open the same post.
    - `url` string, required: Permalink keyed on the activity id, always present and always in this form.
    - `date` string, date-time, required: 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` string: The post's age at fetch time, computed from `date` in English units: `s`, `m`, `h`, `d`, `w`, `mo`, `yr`. The same whichever account served the request. Informational; compute ages from `date`.
    - `isEdited` boolean, required: `true` when the post was edited after publication. The text returned is the current one; `date` is still the original publication date.
    - `content` string: The post's own text, verbatim - every line is kept, including lines that start with `$`. Omitted when the post carries no text of its own, which happens on an image, video or document posted without a caption.
    - `postType` string, required: What the post is, derived from the attachment that rendered: `text`, `image`, `video`, `document`, `article`, `poll`, `job`, `event`, `newsletter`, `celebration`, or `other` for an attachment kind added after this was written. `article` means a link preview card is attached. On a repost, the original's kind is `resharedPost.postType`.
    - `media` object[], required: Every attachment of the post, in the order the card rendered them. Empty on a text-only post and on a repost, whose original's attachments are under `resharedPost`.
      - `type` string, required: Kind of attachment: `image`, `video`, `document`, `article`, `poll`, `job`, `event`, `newsletter` or `celebration`.
      - `url` string: The attachment's own URL: the image, the link of an article card, the playable MP4 of a video (highest resolution), the full PDF of a document, the page of a job (`https://www.linkedin.com/jobs/view/<id>/`) or of an event (`https://www.linkedin.com/events/<id>/`).
      - `title` string: The uploader's own title on a video; the document's real title (omitted when it has none); the job title; the event name; the question of a poll.
      - `subtitle` string
      - `assetUrn` string
      - `aspectRatio` number
      - `width` integer
      - `height` integer
      - `thumbnailUrl` string: The video's poster frame, the hiring company's logo on a job, the event's banner, an article card's image.
      - `pageCount` integer: Documents: number of pages.
      - `carousel` boolean: `true` when a document attachment renders as a carousel.
      - `options` string[]: Polls: the answer labels.
      - `entityId` string: Jobs: the job id. Events: the event id.
      - `durationMs` integer: Videos: playable length in milliseconds.
      - `streams` object[]: Videos: every MP4 rendition, best first. `url` is the first of them.
        - `url` string
        - `height` integer
      - `coverPages` string[]: Documents: the image URLs of the preview pages.
      - `companyName` string: Jobs: the hiring company, as the card shows it.
      - `location` string: Jobs: the location line, as the card shows it.
      - `dateText` string: Events: the date line exactly as displayed. Reported only when the result was rendered in English, with no guarantee about the time zone it is expressed in. There is no event timestamp.
      - `pollOptions` object[]: Polls: each answer with its votes.
        - `text` string
        - `votes` integer
      - `totalVotes` integer: Polls: how many members voted.
      - `newsletterTitle` string: Newsletter issues: the newsletter's name.
    - `images` object[]: Every image of the post, with dimensions, largest rendition each. Omitted when the post has none. On the rare page served by the fallback search, only the first 4 are listed and `imageCount` gives the total.
      - `url` string
      - `width` integer
      - `height` integer
    - `imageCount` integer: The post's number of images. Omitted when the post has none.
    - `imageUrl` string: First entry of `images`. Kept for parity with `GET /api/v1/posts`.
    - `article` object: The link preview card, when one is attached.
      - `title` string: The linked article's own title (on a newsletter issue too).
      - `subtitle` string
      - `link` string
      - `image` string
      - `newsletterTitle` string: Newsletter issues only: the newsletter's name.
    - `author` object, required: Who published the post. One shape for both people and organization pages; read `type` to tell them apart. Complete whatever the language of the account that served the request. A field that could not be read is omitted rather than guessed.
      - `type` 'profile' | 'company', required: `profile` when a person published the post, `company` when an organization page did.
      - `id` string: Author id: the profile id (`ACoAA...`) 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.
      - `numericId` string: Numeric member id, when known. Not resolvable to a profile on its own; prefer `id`. On a repost it is omitted when it cannot be attributed to this author with certainty.
      - `name` string
      - `url` string: Public professional profile or page URL of the author.
      - `info` string: Kept for parity with `GET /api/v1/posts`: the member's headline on `type: "profile"`, the page's follower line (`"1,289 followers"`) on `type: "company"`.
      - `publicIdentifier` string: Profile slug, without the `/in/` prefix. Only on `type: "profile"`.
      - `universalName` string: Page slug, without the path prefix. Only on `type: "company"`.
      - `followerCount` integer: The page's follower count, as a number. Only on `type: "company"`.
      - `headline` string: The member's headline. Only on `type: "profile"`.
      - `avatar` object: 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.
        - `url` string
        - `width` integer
        - `height` integer
      - `website` string: Call-to-action link on the author card, reported whenever present.
      - `websiteLabel` string: Label of that call-to-action link. It is interface text, so it is reported only when the result was rendered in English.
      - `isPremium` boolean: `true` when the author card carries a premium badge. Omitted otherwise - an absent badge is not proof that the account is not premium.
    - `reactionCount` integer: Total reactions, as a SNAPSHOT at fetch time. Re-read the post to refresh it. Omitted, never reported as 0, when the page carried no count for the post - a 0 is a real zero. The same holds for `commentCount`, `sharesCount`, `reactionTypeCounts` and `reactionsByType`.
    - `commentCount` integer: Total comments, as a snapshot at fetch time. Omitted when the page carried no count.
    - `sharesCount` integer: Times the post was reshared, as a snapshot at fetch time. The plural spelling matches `GET /api/v1/posts`. Omitted when the page carried no count.
    - `reactionTypeCounts` object[]: The complete reaction breakdown in the source's own order, zeros included: `LIKE`, `PRAISE`, `EMPATHY`, `INTEREST`, `APPRECIATION`, `ENTERTAINMENT`, `SUPER_LIKE`. Omitted when the page carried no count.
      - `type` string
      - `count` integer
    - `reactionsByType` object: Only the reaction types the post received, keyed `like`, `praise`, `empathy`, `interest`, `appreciation`, `entertainment`, `superLike`; the values sum to `reactionCount` (same rule as `GET /api/v1/posts`). Omitted when the page carried no count.
    - `mentions` object[], required: Entities mentioned in the post text. Empty when there are none.
      - `type` 'person' | 'company', required
      - `name` string, required
      - `url` string, required
    - `hashtags` string[], required: Hashtags in the post text, without the leading `#`. Empty when there are none.
    - `outboundLinks` string[], required: Links in the post text. Empty when there are none.
    - `isRepost` boolean, required: `true` when this post quotes another one. The quoted post, attachments included, is in `resharedPost`; the reposter's own `media` is then empty and `images` / `article` are omitted.
    - `resharedPost` object: The quoted post, when `isRepost` is `true`: its own author, text and attachments, 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.
      - `id` string
      - `shareUrn` string
      - `date` string, date-time
      - `author` object: Who published the post. One shape for both people and organization pages; read `type` to tell them apart. Complete whatever the language of the account that served the request. A field that could not be read is omitted rather than guessed.
        - `type` 'profile' | 'company', required: `profile` when a person published the post, `company` when an organization page did.
        - `id` string: Author id: the profile id (`ACoAA...`) 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.
        - `numericId` string: Numeric member id, when known. Not resolvable to a profile on its own; prefer `id`. On a repost it is omitted when it cannot be attributed to this author with certainty.
        - `name` string
        - `url` string: Public professional profile or page URL of the author.
        - `info` string: Kept for parity with `GET /api/v1/posts`: the member's headline on `type: "profile"`, the page's follower line (`"1,289 followers"`) on `type: "company"`.
        - `publicIdentifier` string: Profile slug, without the `/in/` prefix. Only on `type: "profile"`.
        - `universalName` string: Page slug, without the path prefix. Only on `type: "company"`.
        - `followerCount` integer: The page's follower count, as a number. Only on `type: "company"`.
        - `headline` string: The member's headline. Only on `type: "profile"`.
        - `avatar` object: 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.
          - `url` string
          - `width` integer
          - `height` integer
        - `website` string: Call-to-action link on the author card, reported whenever present.
        - `websiteLabel` string: Label of that call-to-action link. It is interface text, so it is reported only when the result was rendered in English.
        - `isPremium` boolean: `true` when the author card carries a premium badge. Omitted otherwise - an absent badge is not proof that the account is not premium.
      - `content` string
      - `postType` string: What the original is, from its attachments. Omitted when it has none.
      - `media` object[]: The original's attachments, same shape as `media` above.
        - `type` string, required: Kind of attachment: `image`, `video`, `document`, `article`, `poll`, `job`, `event`, `newsletter` or `celebration`.
        - `url` string: The attachment's own URL: the image, the link of an article card, the playable MP4 of a video (highest resolution), the full PDF of a document, the page of a job (`https://www.linkedin.com/jobs/view/<id>/`) or of an event (`https://www.linkedin.com/events/<id>/`).
        - `title` string: The uploader's own title on a video; the document's real title (omitted when it has none); the job title; the event name; the question of a poll.
        - `subtitle` string
        - `assetUrn` string
        - `aspectRatio` number
        - `width` integer
        - `height` integer
        - `thumbnailUrl` string: The video's poster frame, the hiring company's logo on a job, the event's banner, an article card's image.
        - `pageCount` integer: Documents: number of pages.
        - `carousel` boolean: `true` when a document attachment renders as a carousel.
        - `options` string[]: Polls: the answer labels.
        - `entityId` string: Jobs: the job id. Events: the event id.
        - `durationMs` integer: Videos: playable length in milliseconds.
        - `streams` object[]: Videos: every MP4 rendition, best first. `url` is the first of them.
          - `url` string
          - `height` integer
        - `coverPages` string[]: Documents: the image URLs of the preview pages.
        - `companyName` string: Jobs: the hiring company, as the card shows it.
        - `location` string: Jobs: the location line, as the card shows it.
        - `dateText` string: Events: the date line exactly as displayed. Reported only when the result was rendered in English, with no guarantee about the time zone it is expressed in. There is no event timestamp.
        - `pollOptions` object[]: Polls: each answer with its votes.
          - `text` string
          - `votes` integer
        - `totalVotes` integer: Polls: how many members voted.
        - `newsletterTitle` string: Newsletter issues: the newsletter's name.
      - `images` object[]: The original's images, same shape as `images` above.
        - `url` string
        - `width` integer
        - `height` integer
      - `article` object: The original's link preview card, same shape as `article` above.
        - `title` string: The linked article's own title (on a newsletter issue too).
        - `subtitle` string
        - `link` string
        - `image` string
        - `newsletterTitle` string: Newsletter issues only: the newsletter's name.
      - `mentions` object[]
        - `type` 'person' | 'company', required
        - `name` string, required
        - `url` string, required
      - `hashtags` string[]
      - `outboundLinks` string[]
    - `commentingDisabled` boolean: `true` when comments are closed to the account that read the post: turned off by the author, or limited to the author's connections or to a group's members. Omitted when the setting is not reported, rather than defaulted.
    - `group` object: Present only on a post published in a group: the group's `id`, `name` and `url`.
      - `id` string
      - `name` string
      - `url` string
  - `cursor` string, nullable, required: Opaque cursor for the next page, or `null` when this result set is exhausted - the only end-of-results signal (`total` is an estimate and cannot end a walk). Returned on author searches too. Pass it back unchanged with the same query and filters. Consecutive pages can repeat posts, notably on dense result sets sorted by date: de-duplicate on `id`. One query is exhausted after a few hundred results.
  - `partial` object: 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`.
    - `droppedResults` integer
    - `reasons` object

## Other responses

- `400`: Bad request - a required parameter is missing or a value is invalid.
- `401`: Unauthorized - the X-API-Key header is missing or invalid.
- `402`: 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).
- `429`: 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.)
- `500`: Internal server error - an unexpected error occurred in our API. Safe to retry; contact support if it persists.
- `503`: 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.

---

[API](https://skmtc.dev/fetchin/apis/fetchin-api.md) - [All operations](https://skmtc.dev/fetchin/apis/fetchin-api/llms.txt) - [OpenAPI document](https://skmtc.dev/fetchin/apis/fetchin-api/revisions/19a0b5d0320f?raw)
