---
title: "Find the stakeholders at a company that fit a definition"
method: POST
path: "/v1/stakeholders/search"
tags: ["People"]
---

# Find the stakeholders at a company that fit a definition

`POST /v1/stakeholders/search`

## Request

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

**Required permissions:** `research:trigger`.

Given a company — any company, by its website (or LinkedIn page or name), or an account in your workspace — and a stakeholder definition, return the people at it who fit, best first, each with a reason. **Synchronous and read-only**: the answer comes back in one response, typically in 5–20 seconds (the server gives up at 25 with `503 stakeholder_search_failed`, so set your client timeout above that and retry once on `retry_later`), and nothing is written to your workspace. Import the ones you want with `POST /v1/people/batch`.

**Company identity.** Trayo identifies the company from its LinkedIn page first, then its website, then its name. The response includes people who fit your definition and explains each result in `whyThisPerson`. `company.resolvedBy` says which identity was used. Before importing anyone, make sure that `company.name` and `company.website` identify the intended company. A guessed handle or a common name can identify a different company.

Public REST and MCP usage is recorded without daily usage caps, workspace quota cutoffs or lookup quotas for now. Workspace request pacing still applies. **The definition.** Send one per call, in plain prose — the roles that own or buy what you sell, the roles that influence, the roles to skip. Omit it to use the workspace's own definition; if that is unset too, a search for senior roles applies and `definition.source` says `default` so you know the result is senior people of any function rather than your buying committee. A definition sent here changes nothing on the workspace.

**Read `candidates` with `people`.** `candidates: 0` means Trayo found no people with the requested keywords at this company. `candidates > 0` with `people: []` means the selection judged none of them to fit — a real answer, not an error. Fewer than `limit` come back when fewer fit.

**Near-misses.** When nobody fits, `nearMisses` holds up to 3 of the closest candidates, each with `belowThreshold: true` and a reason saying what their role lacks, and `next` names their titles. They did not fit: offer them as alternatives rather than importing them as the buying committee.

**Personas.** Each person carries `matchedPersona`, and `coverage` lists every persona with whether anyone in `people` matched it — so a persona with no match (no champion, say) is stated, not guessed from titles, and `next` says what to do about it. The names come from `personas` in the request, or from the workspace's saved personas when its definition applies. Each label is checked after selection against the job's function and seniority (a saved persona's seniority and rationale are read too) and replaced or cleared when it does not hold; the person stays. One person can cover several personas: a CTO also covers "VP Eng".

**Repeatable.** Repeating a search within a week normally returns the same people.

**Many companies.** For up to 50 companies at once, use `POST /v1/stakeholders/search/batch` rather than one call per company.

**Compared with `POST /v1/find`.** Find's contacts lane searches the web and identifies people from any source; this searches Trayo's data for people who fit your definition. Use find when the company is not in our data (`company_not_found`) or when you want breadth over fit.

## Request body

- StakeholderSearchBodyDto
  - `company` object, required
    - `companyId` union: Trayo's own id for the company, as a number or numeric string: the `companyId` a post, post-engager or person-research result carries, or the `id` of a company find result. Not a workspace account UUID. When sent, it is the key: the company is read by this id and nothing else is tried, so an id Trayo does not hold is `company_not_found`. A `name`, `website` or `linkedinUrl` sent beside it is not used.
      - integer
      - string
    - `accountId` string, uuid: An account in your workspace, on its own. Its existing match is used when it has one; otherwise the account is resolved from its website, LinkedIn handle and name exactly as the app would, so an account we have not yet matched still works when the company is in our data.
    - `website` string: The company's own website, as a URL or a domain (`https://zapier.com`, `zapier.com`). The strongest key after the LinkedIn page: it is matched against our data, then, on a miss, followed through a cross-domain redirect, so a legacy or vanity domain that forwards to the current site still resolves. Sent on its own as the bare site (no path or subdomain), a data or social platform's domain names the platform itself (`zoominfo.com` is ZoomInfo); a page on such a site (`linkedin.com/company/acme`) is not a company website. When our data files the company under a different website, a website sent on its own is also tried as the LinkedIn page it spells (`gong.io` as `gong-io`), accepted only for an established company whose name matches the domain. An alias that does not forward to the company's current site is a miss: send the current website, the LinkedIn page or the name. Does not have to be in your workspace.
    - `linkedinUrl` string: The company's LinkedIn page — the `linkedinUrl` a find result or a person row carries, copied through (`https://www.linkedin.com/company/zapier`); the bare handle (`zapier`) is accepted too. Tried first when present, and the most precise key there is. Showcase pages are not company pages: send the company's own. Copy the handle from a Trayo result or the company's own site rather than guessing it from the name — a slug that belongs to a different company resolves to that company.
    - `name` string: The company name. Tried last and the weakest key, since a common name can match the wrong company — send the website when you have it. A name-only lookup finds only companies Trayo holds under that name; an employer that a Trayo person row names with `companyId: null` has no company record, and its name will usually miss.
  - `definition` string: The stakeholder definition to search with, in plain prose: the roles that own or buy what you sell, the roles that influence the decision, and the roles to skip. Optional: when omitted, the workspace's own definition is used, and when that is unset too, a seniority-only seed (chiefs, VPs, directors of any function). `definition.source` in the response says which applied. Passing one here does not change the workspace setting.
  - `limit` integer: How many people to return, 5 by default, 10 at most. Fewer come back when fewer fit.
  - `personas` string[]: Persona names to label each person with, e.g. `["CISO", "IT leader", "Champion"]`. Optional: when omitted and the workspace's own definition applies, the workspace's saved personas are used. A definition sent with the request has no persona names of its own, so without this field its people come back with `matchedPersona: null` and `coverage: []`.

## Response `200`

The selected people, with the company as resolved and the definition as applied. `people` may be empty — read `candidates`.

- StakeholderSearchResponseDto
  - `company` object, required
    - `companyId` string, required: Trayo's id for the company. Send it back as `companyId` to name this exact company again.
    - `name` string, required: The company as Trayo records it.
    - `website` string, nullable, required: The company's website domain as Trayo files it.
    - `linkedinUrl` string, nullable, required: The company's LinkedIn page, when Trayo has it.
    - `accountId` string, uuid, nullable, required: The workspace account for this company: the one you sent, or the account that already holds its website or LinkedIn page. Null when the company is not in your workspace.
    - `resolvedBy` 'account_match' | 'company_id' | 'linkedin' | 'website' | 'redirect' | 'name', required: Which key identified the company — the same resolver the Trayo app uses, in its order. `account_match`: the account's existing match. `linkedin`: the LinkedIn page, as sent or as spelled by a website sent on its own. `website`: the website domain. `redirect`: the website, after following a cross-domain redirect to the company's current site. `name`: the name, the weakest key. A match is exact on the key you sent, which is only the company you meant if the key was. A slug or name can belong to a different company than the one you mean, and the answer will then describe that company. Read `name` and `website` back before acting, and prefer `website` as the key — a domain resolves to whoever owns it.
    - `nameMatch` 'exact' | 'approximate', nullable, required: Set only when `resolvedBy` is `name`, null otherwise. `exact`: the company's name equals the name looked up once legal suffixes (Inc, GmbH, Ltd) are dropped. `approximate`: the closest name Trayo holds, which can be a different company ("CR Management Consulting" answered with "m3 management consulting"). Check an `approximate` match against what you know of the company before using it, and leave it out when it does not fit.
  - `definition` object, required
    - `text` string, required: The definition the search actually ran with.
    - `source` 'request' | 'workspace' | 'default', required: `request`: you sent one. `workspace`: the workspace's own definition. `default`: neither existed, so a seniority-only seed was used — the results are senior people of any function, not your buying committee.
  - `candidates` integer, required: How many people at the company the keyword search found before selection. Zero with `people: []` means Trayo has nobody matching the keywords; a positive number with `people: []` means the selection judged none of them to fit the definition.
  - `people` object[], required: The selected people, at most `limit`. With several personas, each persona that has someone who fits gets a share: their people take turns, so one persona cannot fill `limit` alone. Repeating a search within a week normally returns the same people. NOT written to your workspace: import the ones you want with `POST /v1/people/batch` (`fullName`, `title`, `linkedinUrl`, `photoUrl`, plus `accountId` or `company`).
    - `fullName` string, required
    - `title` string, nullable, required: Their current title at the searched company; null when Trayo has none, and then `headline` says what they do. Wherever you name the person, give this title and `linkedinUrl` with `fullName`.
    - `headline` string, nullable, required
    - `location` object, nullable, required: Where the person says they are on their LinkedIn profile. Usually their home metro area, but it is the person's own statement, not a verified address, and it is not where their company is headquartered. Null when Trayo has no location for them. Breaking type change: this field used to be a string, usually null; it is now this object, the person's stated LinkedIn location.
      - `city` string, nullable, required: City, e.g. `San Diego`. Null when the profile names only a region or metro area.
      - `state` string, nullable, required: State, province or region, e.g. `California`.
      - `country` string, nullable, required: ISO 3166-1 alpha-2 country code, e.g. `US`.
      - `raw` string, nullable, required: The location as written on the profile, e.g. `Greater San Diego Area`. Usually present; the only populated field when the profile names just a metro area.
    - `linkedinUrl` string, nullable, required: Their LinkedIn profile URL, when Trayo has it. Send it to `POST /v1/contacts/lookup`, `POST /v1/research/person`, or as `linkedinUrl` to `POST /v1/people`.
    - `photoUrl` string, nullable, required: The person's profile photo, as a directly loadable image URL. Null when Trayo holds no real photo for them — show initials then. Every route that returns a person carries this field under this name.
    - `otherCurrentEmployers` string[], required: Up to three other companies with current roles, including board or advisory affiliations. These names do not establish employment, salary or buying authority. An empty list is not proof of employment at the searched company.
    - `otherCurrentRoles` object[], required: Up to six current roles at the other companies, with titles for distinguishing employment from affiliations. Outside board or advisory seats do not disqualify a buyer employed at the searched company. Missing titles or an empty list mean there may be insufficient evidence; do not infer who pays the person.
      - `companyName` string, required
      - `title` string, required
    - `whyThisPerson` string, required: Why this person fits the definition, in the selection's own words. On a near-miss: what their role covers and what it lacks.
    - `matchedPersona` string, nullable, required: The persona this person matched, one of the names in `coverage`. Checked after selection against the job's function and seniority, and replaced or cleared when it does not hold. Null when no persona holds, or when the search had no persona names (see `personas`).
    - `belowThreshold` boolean, required: `false` on every row of `people`. `true` on every row of `nearMisses`: the selection judged this person NOT to fit the definition.
  - `nearMisses` object[], required: Only when `people` is empty and `candidates` is above zero: up to 3 of the closest candidates, best first, each with `belowThreshold: true`. They did NOT fit the definition; show them as alternatives, and import one only when the user picks it. Empty otherwise.
    - `fullName` string, required
    - `title` string, nullable, required: Their current title at the searched company; null when Trayo has none, and then `headline` says what they do. Wherever you name the person, give this title and `linkedinUrl` with `fullName`.
    - `headline` string, nullable, required
    - `location` object, nullable, required: Where the person says they are on their LinkedIn profile. Usually their home metro area, but it is the person's own statement, not a verified address, and it is not where their company is headquartered. Null when Trayo has no location for them. Breaking type change: this field used to be a string, usually null; it is now this object, the person's stated LinkedIn location.
      - `city` string, nullable, required: City, e.g. `San Diego`. Null when the profile names only a region or metro area.
      - `state` string, nullable, required: State, province or region, e.g. `California`.
      - `country` string, nullable, required: ISO 3166-1 alpha-2 country code, e.g. `US`.
      - `raw` string, nullable, required: The location as written on the profile, e.g. `Greater San Diego Area`. Usually present; the only populated field when the profile names just a metro area.
    - `linkedinUrl` string, nullable, required: Their LinkedIn profile URL, when Trayo has it. Send it to `POST /v1/contacts/lookup`, `POST /v1/research/person`, or as `linkedinUrl` to `POST /v1/people`.
    - `photoUrl` string, nullable, required: The person's profile photo, as a directly loadable image URL. Null when Trayo holds no real photo for them — show initials then. Every route that returns a person carries this field under this name.
    - `otherCurrentEmployers` string[], required: Up to three other companies with current roles, including board or advisory affiliations. These names do not establish employment, salary or buying authority. An empty list is not proof of employment at the searched company.
    - `otherCurrentRoles` object[], required: Up to six current roles at the other companies, with titles for distinguishing employment from affiliations. Outside board or advisory seats do not disqualify a buyer employed at the searched company. Missing titles or an empty list mean there may be insufficient evidence; do not infer who pays the person.
      - `companyName` string, required
      - `title` string, required
    - `whyThisPerson` string, required: Why this person fits the definition, in the selection's own words. On a near-miss: what their role covers and what it lacks.
    - `matchedPersona` string, nullable, required: The persona this person matched, one of the names in `coverage`. Checked after selection against the job's function and seniority, and replaced or cleared when it does not hold. Null when no persona holds, or when the search had no persona names (see `personas`).
    - `belowThreshold` boolean, required: `false` on every row of `people`. `true` on every row of `nearMisses`: the selection judged this person NOT to fit the definition.
  - `coverage` object[], required: One entry per persona the search labelled against, in the order they were given: `matched` is true when someone in `people` fits it, by their `matchedPersona` or by the same check, so one person can cover several personas (a CTO also covers "VP Eng"). Near-misses do not count. `candidates` is how many people that persona's own title search found at the company: zero means nobody there holds such a role in Trayo's data; above zero with `matched: false` means candidates existed and none fit. One person found by two personas counts for both, so these can add up to more than the top-level `candidates`. Empty when the search had no persona names (see `personas`).
    - `persona` string, required
    - `matched` boolean, required
    - `candidates` integer, required
  - `next` string, nullable, required: What to do about an empty `people`, or about a persona nobody in `people` matched. Null when people were found and every persona is matched.

## Other responses

- `400`: `company` with neither an `accountId` nor any of `website` / `linkedinUrl` / `name`, or with `accountId` alongside them; a `linkedinUrl` that is a LinkedIn URL but not a company page; a blank definition; a `limit` outside 1–10; or an unknown field. `param` names it. `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.
- `402`: `quota_exceeded` (`retry: do_not_retry`) — This workspace has insufficient quota for this operation. Retrying or changing the request does not help; raise the quota or wait for it to renew.
- `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.
- `404`: `account_not_found` (`retry: do_not_retry`) — No such account in your workspace. Another workspace's account is indistinguishable from a non-existent one. `company_not_found` (`retry: do_not_retry`) — We could not identify this company — not from the account's own record, nor from a LinkedIn page, website (including its redirect), or name — so there are no people to search. Retrying will not help. Send the company's own website if you did not; for any company, `POST /v1/find` on the contacts lane searches the web instead. From `POST /v1/find` with `filters.companyWebsites`, this means NO website resolved and nothing was searched; the message names every website. For one that is ambiguous (several possible companies on the domain) it also names the candidates and their ids: send the right id as `filters.companyIds`. When only some websites fail, the search runs over the rest and lists the others in `unresolvedWebsites`.
- `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. `quota_daily_limit` (`retry: retry_later`) — The workspace daily usage limit blocks new work. Wait for recent usage to leave the 24-hour window, or contact support; changing scopes does not help.
- `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`: `stakeholder_search_not_configured` (`retry: do_not_retry`) — Stakeholder search is unavailable. Retrying will not help. Contact support. For another way to find people, use `POST /v1/find` on the contacts lane. `stakeholder_search_failed` (`retry: retry_later`) — Stakeholder search failed or timed out. Nothing was saved. Send the same request again shortly.

---

[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)
