---
title: "Search stored LinkedIn posts with explicit keyword groups"
method: POST
path: "/v1/posts/search/keywords"
tags: ["People"]
---

# Search stored LinkedIn posts with explicit keyword groups

`POST /v1/posts/search/keywords`

## Request

- Base URL: `https://api.trayo.ai`
- URL: `https://api.trayo.ai/v1/posts/search/keywords`
- Auth: HTTP bearer

**Required permissions:** `people:read`.

Direct search with no generated query or semantic fallback. Split a request into concepts; put spelling variants or synonyms inside the same group. Groups are AND, alternatives are OR, and each alternative matches a normalized whole-token phrase. The response returns the executed `applied` groups. Optional authorIds constrain the authors.

Verifies at most 200 ranked candidates from the recent-post index and returns at most 50 matches, ranked by keyword relevance within that sample. This bounded direct search accepts at most seven days and omits bodies shorter than 120 characters. Coverage is always non-exhaustive; an empty sample cannot prove absence. Read coverage.candidateCapReached and coverage.resultsTruncated, including on empty responses. There is no global match count or continuation cursor. Narrow criteria for a more complete answer. The requested date window is not a guarantee that every recent post has arrived. Nothing is saved. The server has an eight-second request budget; busy and timeout responses are failures, not empty matches.

## Request body

- PostKeywordSearchBodyDto
  - `concepts` array[], required: Required concept groups. Alternatives inside each group are OR; groups are AND. Each alternative matches a whole-token phrase after punctuation normalization. For query expansion, put synonyms in the same group, e.g. [["CRM", "customer relationship management"], ["migration", "switching"]]. At most 12 alternatives total, with 12 ASCII letter/number tokens per phrase. The server executes these groups as sent; it does not invent synonyms.
    - string[]
  - `excludedPhrases` string[]: Exclude a candidate containing any of these whole-token phrases. Applied after the bounded candidate search.
  - `authorIds` string[]: Optional person dataset IDs. Keep IDs as strings.
  - `windowDays` integer: Publication lookback in UTC days. This is a requested window, not a guarantee of source freshness.
  - `limit` integer

## Response `200`

- PostKeywordSearchResponseDto
  - `applied` object, required
    - `concepts` array[], required
      - string[]
    - `excludedPhrases` string[], required
    - `authorIds` string[], required
    - `windowDays` integer, required
    - `limit` integer, required
  - `since` string, required: Inclusive earliest publication date searched, YYYY-MM-DD.
  - `through` string, required: Latest publication date searched, YYYY-MM-DD.
  - `posts` object[], required
    - `postId` string, required
    - `employeeId` string, nullable, required
    - `url` string, nullable, required
    - `publishedAt` string, required
    - `text` string, required
    - `textTruncated` boolean, required
    - `author` object, required
      - `fullName` string, nullable, required
      - `headline` string, nullable, required
      - `linkedinUrl` string, nullable, required
  - `coverage` object, required
    - `mode` 'bounded_ranked_candidates', required
    - `exhaustive` false, required: A ranked sample of the recent-post index. Never a complete scan of all stored or published posts, even if the candidate cap was not reached.
    - `maxWindowDays` 7, required
    - `minimumIndexedBodyCharacters` 120, required: The recent-post index omits shorter bodies. Older posts and sources not yet indexed are also outside coverage.
    - `candidateLimit` 200, required
    - `candidatesExamined` integer, required
    - `candidateCapReached` boolean, required: The ranked candidate limit was reached, or the search stopped reading candidates before filling it. Even when false, this is not an exhaustive search.
    - `matchingPostsInSample` integer, required: Matches among examined candidates, not a global total.
    - `resultsTruncated` boolean, required: The candidate cap or result limit trimmed this sample. A false value does not make retrieval exhaustive.
    - `order` 'lexical_relevance_within_sample', required: Keyword relevance within the examined sample; not a global ranking of all stored posts.

## Other responses

- `400`: `validation_failed` (`retry: fix_input`) — The request did not pass input validation: a missing or malformed body field, a bad `limit`, a path parameter that is not a UUID, a `signalKey` that already exists or is reserved. `message` lists every failed constraint; `param` names the first offending field when one can be identified.
- `401`: Missing, malformed, or incorrect bearer token. Every way authentication can fail returns the identical body, so do not try to infer which one it was, and do not repeat the call with the same key. `unauthorized` (`retry: do_not_retry`) — Missing, malformed, or incorrect bearer token. Identical body for every cause.
- `403`: The key grant and its owner's current workspace role must both include every scope required by the operation. `insufficient_scope` (`retry: do_not_retry`) — Scope denial details distinguish missing_key_scope (use a key granting every missing scope), owner_permission_limit (the owner needs the missing workspace permissions), and invalid_key_scope (replace the key with supported scopes). Membership and undeclared-route denials remain generic.
- `429`: Every route is rate limited per workspace across REST, MCP, all keys and OAuth users, `GET /v1/whoami` included, so a balance poll competes with the work it is watching. For `rate_limited`, wait `Retry-After` seconds (also `details.retryAfterSeconds`), then send the same request once; do not retry sooner. `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` describe the workspace's bucket, and they come back on successful responses too: read them and slow down BEFORE you are refused. A burst from one network can also be refused before the key is checked; that 429 carries `Retry-After` and no `RateLimit-*` headers. `rate_limited` (`retry: retry_later`) — The workspace or source network exhausted its request budget for one bucket. `details.retryAfterSeconds` (and `Retry-After` on REST) is the wait: wait that long, then retry the same request once. Do not poll or retry sooner; an early retry is refused again. If you cannot wait, stop and tell the user when to retry; do not fill the wait with other calls. `details.limit` and `details.bucket` name the exhausted budget. When present, RateLimit-* headers describe the active budget and reset.
- `500`: Something failed on our side. Nothing about the request needs changing. `internal_error` (`retry: retry_later`) — Something failed on our side. `message` never describes it — quote `requestId` when reporting. Retry with exponential backoff and GIVE UP after a few attempts: `retry_later` here does not mean the failure is known to be transient, only that nothing about your request needs changing. A request that fails this way repeatedly will not start working.
- `503`: `post_search_not_configured` (`retry: do_not_retry`) — Post search is unavailable. Retrying will not help. Contact support. `post_search_failed` (`retry: retry_later`) — The post search or engager read failed or exceeded its time budget. Nothing was saved; retry the same request shortly. `data_search_busy` (`retry: retry_later`) — Hosted search capacity (direct Find, post keyword search, job-change search, job posting search) is in use: your workspace already has as many of these searches running as it may, or the service is at capacity. Nothing ran. Wait for details.retryAfterSeconds, then retry, and send fewer at once. `data_search_timeout` (`retry: fix_input`) — A hosted search (Find by filters, with or without `mode: direct`; post keyword search; job-change search; job posting search; post search over dates older than 90 days) ran out of time. Change what `message` names and retry; resending the same search runs out of time again, and a timeout is not an empty result.

---

[API](https://skmtc.dev/trayo/apis/trayo-api.md) · [All operations](https://skmtc.dev/trayo/apis/trayo-api/llms.txt) · [OpenAPI document](https://skmtc.dev/trayo/apis/trayo-api/revisions/0af452dce6de?raw)
