Endpoints

Relationships

Find people at one company who are connected to people at another, and expand any of them into who they can actually reach.

POST/v1/relationships

Find people connected across companies and institutions

Who is connected to whom, over which shared employers and schools.

Two questions, chosen with relation:

- between_companies — people at the first company who are connected to people at the second. Order is the contract: swap them and you get the mirror question. - via_institution — people at one company affiliated with named institutions.

Every row carries via: which institutions connect them, and how confident each match is. That is the answer, not decoration — a person with no reason is not a connection.

Two limits worth knowing before you build on this. A connection here means a shared institution, not a personal acquaintance: two people who both worked at Palantir may never have met. And this cannot answer "one person who worked at both A and B" — that is a different question the corpus cannot express, and asking for it is a 422 rather than an empty page.

To expand one row into the people they can actually reach, and how they know each other, send its id to POST /v1/relationships/reach.

Scope: dsp.search.read

Use it when you need a warm path between two companies: who at one is connected to the other, and through what. Outcome: ranked people with their connection strength, each expandable into the specific people they can reach and how they know them.

Three typed routes, and they compose:

RouteAnswers
POST /v1/relationships (this one)People at one company connected to people at another, ranked by how far into it they reach
/v1/relationships/reachExpands any single row into the actual people they can reach, and how they know each other
/v1/relationships/institutionsWhat the two companies have in common, without ranking anybody

This reads the affiliation graph, not the entity index: a different corpus from search, which is why it is a different route even though its rows are people. It is metered under its own action, dsp.relationships: 2 credits for the first 10 results, then 1 credit per next 10. The bridges on their own are 2 credits flat.

Request#

companiesarray of RelationshipCompany, ≤ 2

Order is the contract. For between_companies, the first company's people are what comes back and the second is who they are connected to. Swap them and you get the mirror question.

by"auto" | "name" | "domain" | "li_url" | "id"default: auto

How to read value. auto infers it from the value's shape — stripe.com is a domain, linkedin.com/company/stripe a LinkedIn URL, Stripe a name — using the same detection /v1/similar and /v1/entities use. Set it explicitly to override. Prefer domain or li_url when you have one: name is not a unique key, so it resolves to the best-ranked candidate and 377 companies are called stripe.

peoplePeopleConstraints

Which people on one side of a relationship count. These hang off a company rather than off the request, because a relationship has two sides and they are constrained independently. "Directors at Acme who know anyone at Globex" and "anyone at Acme who knows directors at Globex" are different questions, and a request-level seniority filter cannot tell them apart — it would constrain both at once. Only three axes, because the affiliation corpus carries only three. It holds no firmographics, so industry, headcount, funding and technology cannot be applied to the people on a relationship at all. They are rejected rather than ignored: POST /v1/search is where those questions belong.

countryarray of string, ≤ 20
departmentarray of string, ≤ 20

Department (Function) labels, the same vocabulary the org chart uses: one of the 23 Function names, or the General / Others sentinels. Renamed as of 2026-08 — e.g. Engineering / Software (was Engineering & Technical), IT & Infrastructure (was Information Technology), Human Resources / People, Medical & Health, Legal, Risk & Compliance. Data, Analytics & AI and Security are now their own Functions rather than children of engineering and IT.

seniorityarray of string, ≤ 20

Seniority levels. Exactly 7 values, most senior first: C-Suite, VP, Director / Head, Manager, IC, Entry, In Training. Note Director and Head are ONE level, spelled Director / Head. The pre-2026-08 vocabulary (C Suite without the hyphen, Director, Head, Senior, President, Intern, Founder, Owner, Partner, Investor, Advisor, Consultant, Freelancer, Educator, Healthcare Professional, Utility Worker, Speciality Service Provider, Unemployed) is no longer indexed and matches nothing. Founder / Owner / Partner / Investor / Advisor are governance roles and Consultant / Freelancer are employment types; neither is filterable on this axis, and the remaining four are departments, not levels.

valuestringrequired

≥ 1 chars, ≤ 300 chars.

connection_basis"overlapping" | "shared"default: overlapping

What counts as *knowing* someone when counting number_of_contacts_overlapping_tenure_with. overlapping — the two people were at the shared institution at the same time. shared — both were there at some point. The default is deliberate. On a large former employer shared counts everyone who ever passed through: one measured row reaches 100,085 people under shared and 16,630 under overlapping. The broader reading is available because it is sometimes the question, not because it is a better default.

cursorstring | null
exclude_currentbooleandefault: false

Only count institutions the person has left. This is what 'previously worked at' means, and it is not the same as leaving the field unset.

include_company_contextbooleandefault: false

Add each person's current employer. Costs a further lookup upstream.

institution_type"employment" | "education" | null

Restrict the shared institution to a former employer, or a school.

institutionsarray of RelationshipInstitution, ≤ 10
by"auto" | "name" | "li_url" | "inst_key"default: auto

How to read value. auto infers it: an edu: / name: key — what institutions[].id returns — is an inst_key, a LinkedIn company or school URL is an li_url, anything else is a name.

kind"company" | "school" | "unknown"default: unknown

unknown lets the upstream decide from the index. When a name is genuinely both — Cambridge is a company and a university — the request is refused with both options rather than resolved by coin flip, so say which one if you know.

valuestringrequired

≥ 1 chars, ≤ 300 chars.

institutions_combine"any" | "all"default: any

any — affiliated with at least one. all — affiliated with every one, which is a per-person conjunction the upstream cannot paginate as cheaply, so totals on a large all query may be reported as estimates.

limitintegerdefault: 10

≥ 1, ≤ 100.

querystring | null

The question in words — "who at Adyen do we know from Stripe". Send this or the structured fields below, never both. The upstream plans it into the same structured shape a caller could have sent, and that plan comes back in resolved_search.filters so the interpretation is inspectable rather than a black box. A sentence that does not describe a relationship is a 422 naming what was missing, not an empty page. ≤ 2000 chars.

relation"between_companies" | "via_institution"default: between_companies

Which question this is. Rejected alongside query — the planner derives it, and a caller value beside a sentence has no defined precedence.

Headers

Idempotency-Keystring

Makes a retry safe. Send a unique value per request and reuse it when you retry that same request: with the same body and credential, on the same UTC day, rows already charged are not charged again.

Order is the contract

For between_companies, the first company's people are what comes back and the second is who they are connected to. Swap the two and you get the mirror question. There is no reverse flag, because the arguments already say it.

people hangs off a company rather than the request, so each side is constrained independently: filtering the first company to VPs asks "which of their VPs can reach the second", and filtering the second asks "who can reach their VPs". Those are different questions.

You can also send a query in words instead ("who at Stripe knows people at Adyen"), and the interpretation comes back in resolved_search so you can see what was understood.

What connection_basis changes

connection_basisRequires the two people were at the shared institution
overlapping (default)At the same time
sharedAt some point, not necessarily together

That is not a rounding difference. On a large former employer, shared counts everyone who ever passed through: one measured row reaches 16,630 people under overlapping and 100,085 under shared. The basis is echoed on the response, because number_of_contacts_overlapping_tenure_with cannot be read without it.

Response#

attributionarray of Attribution
as_ofstring | null
domainstring | null
quotestring | null

RESERVED for per-fact citation. Always null in v1.

source_corpusSourceCorpus

Where content came from. Closed set — it appears in attribution[]. The first four are the GTM entity graph. The rest are the document corpora behind the non-entity categories, each a distinct index with its own refresh cadence — which is the reason they are named separately rather than lumped under one label: attribution[].as_of means nothing if two corpora updated a month apart share a name.

urlstring | null

RESERVED for per-fact citation. Always null in v1.

breakdownBreakdown | null

Present only when the question asked for a breakdown.

exactbooleandefault: true

false when a count is an estimate, when values were merged under one label, or when the question required several institutions at once — each group is then an upper bound.

group_byGroup By
groupsarray of BreakdownGroup
keystring

The value as the index holds it.

kindstring | null

employment or education, for an institution breakdown.

labelstring

What to show: a department or seniority label, or the institution's name.

peopleinteger

Distinct people in this group. ≥ 0.

more_groupsbooleandefault: false

Smaller groups exist and were not returned.

columnsarray of object | null

The answer table's columns, in order, when a run carries extra columns beyond the entity's own fields -- a job-opening link, a value joined from another dataset. Each entry is {name, description, source}; the value for each sits on every row in results keyed by the column name. Null means a plain entity table. Additive, so it breaks no existing client.

connection_basis"overlapping" | "shared"default: overlapping

Which definition of *knows* produced number_of_contacts_overlapping_tenure_with. overlapping — the two people were at the shared institution at the same time. shared — both were there at some point, which on a large employer counts everyone who ever passed through. Echoed because it changes what the number means, not by a little: one measured row counts 16,630 people under overlapping and 100,085 under shared.

coverageCoverage | null

How much of the addressable set this answer represents. Null means coverage was not measured for this request -- fast and auto do not enumerate -- and never that coverage was zero. Additive, so it breaks no existing client.

auditedbooleandefault: false

Whether the query was checked against the executed filter for dropped requirements. Reported because an empty constraints_unmet only means something if something looked.

constraints_unmetarray of string

Requirements in your query that the search could not express, in your own words. Non-empty means part of the question was not answered -- most often because it needs a dataset this search cannot reach (job postings, employment history, patents, news, funding dates). When this is non-empty, universe_exhausted is withheld: 'we returned everything that matches' cannot be true of a question that was never asked.

criteriaarray of object

Per criterion: evaluated, passed, success_rate, and whether it was *proven* by the query or *judged* by a model. Without this a thin result leaves the caller guessing which requirement thinned it.

enumeratedinteger

How many of them this answer carries. ≥ 0.

enumeration_recallnumber | null

enumerated / universe. Null when the universe was not exact, because a ratio over an estimate is not a recall figure. Note this measures coverage of the *resolved query*, not of the user's intent -- a wrong predicate can be exhaustively enumerated. ≥ 0, ≤ 1.

linkagearray of object

One entry per other dataset this answer joined against: corpus, join_key, observable, of_constrained, ratio, is_lower_bound. ratio is the fraction of your matching accounts that dataset can even see -- measured at 5-60% depending on the dataset and how large the companies are, and never 100%. Non-empty therefore means universe_exhausted is withheld by construction: we returned everything reachable, and a dataset cannot see the rest.

linkage_coveragenumber | null

The joins combined, as a product rather than a minimum: two datasets seeing 27% and 24% of your accounts jointly see about 6.5%, not 24%. Null when no join was performed or when any leg's coverage is unknown.

linkage_coverage_is_lower_boundbooleandefault: false

True when a key set was capped or coverage was estimated rather than measured, making linkage_coverage a floor. Narrowing the question makes it exact.

similarityarray of object

Narrowings done by ranking rather than by predicate. Each entry carries the concept, how many ids it resolved to, the k asked for, whether the list was cut at that ceiling, and the fraction of candidates that could be compared at all. Non-empty means universe_exhausted is withheld on purpose: a cosine is a degree, so a top-k ranking has no complete state, and 2.9% of companies carry no embedding to compare.

truncated_bystring | null

Which limit stopped the enumeration short, when one did.

universeinteger

Records matching the resolved query, counted, not sampled. ≥ 0.

universe_exhaustedbooleandefault: false

Every matching record was returned and the count agrees. The claim an unbounded-corpus search engine cannot make.

universe_is_exactboolean

False when the count hit a cap. Always serialized: a capped count presented as exact is the same lie as an unmeasured one.

degradationarray of Degradation
detailstring
impact"recall" | "precision" | "freshness" | "fields"
reasonDegradationReason

Why an answer is less than it should be. Closed, because a client that cannot branch on the reason can only show the user a shrug. These are the honest constraints — partial vector coverage, smry mapped as keyword so it cannot be scored lexically, a federated corpus being unreachable — surfaced at runtime instead of in a footnote.

source_corpusSourceCorpus | null
has_moreboolean
institutionsarray of SharedInstitution
idstring
kindstring | null
match_confidencestring | null
namestring | null
people_firstintegerdefault: 0

Distinct people at the first company.

people_secondintegerdefault: 0

Distinct people at the second company.

metaResponseMeta

Present on every 2xx, including the ones that are not paginated.

as_ofstring
credential"api_key" | "session"default: api_key

Which credential produced this response. session means the dashboard playground ran it as the signed-in user rather than as an API key — so it is billed to the user, subject to interactive rate limits, and carries no integration compatibility guarantee. Additive and defaulted, so it breaks no existing client.

credits_chargednumber | null

What this request cost, in credits, to at most three decimals. 0 when it cost nothing: a Basic or AI search that found nothing, rows this search already paid for (a cursor sent again, or a request repeated with the same Idempotency-Key on the same UTC day), or a test key. A request without a cursor or a repeated key is a new search, so keep the first page rather than asking for it again. null on a free endpoint.

environment"live" | "test"

Echoed on every response so a developer can never mistake a test-key result for a live one.

request_idstring
next_cursorstring | null

Opaque and signed. Invariant: has_more == (next_cursor is not None). There is no page or offset parameter — not accepted, not deprecated, never present.

resolved_searchResolvedSearch | null
categoryEntityCategory

*What* to search, and what a returned row is. One parameter carries both, because they are the same fact: a search of patent returns patents. The alternative — a source/vertical selector beside a narrower category — lets a caller ask for patents from the people index, a combination that has no meaning and would need rejecting at runtime. A closed set that cannot express the contradiction is better than a validator that catches it. Values are exactly prism's own row types, so category in the request and results[].category in the response are always the same string. company and person are the GTM index; the rest are separate corpora with their own filters, freshness and coverage. Not everything applies everywhere: see SearchRequest, which rejects the GTM-only parameters rather than ignoring them. job_opening is a document corpus like patent/news_article/etc — a posting, not an entity — but its records carry a domain (the posting company's) all the same. It is deliberately not in GTM_CATEGORIES (that set is about the entity/document distinction, and a job posting is still a document), but it *is* in DOMAIN_FILTERABLE_CATEGORIES, which exists specifically to separate those two axes.

conflictsarray of object
filtersFilters
filters_source"caller" | "parser" | "caller+parser" | "none"default: none
mode"fast" | "auto" | "deep"

How hard to work on the query. The distinction is *what the words mean*, not how many results come back: - fast — match my words. The query is matched by meaning (vector retrieval, falling back to keywords where vectors are unavailable); no LLM in the path, so nothing is extracted into filters. - auto — understand my intent. The parser extracts filters. - deep — reason about it. Multi-step, slowest, most expensive. fast was very nearly defined as "filters only, no LLM", which would have made query — the primary parameter — silently inert. A parameter that does nothing is worse than one that is missing.

overridesarray of object
parser_versionstring
querystring | null
semantic_querystring | null
semantic_scope"company" | "person" | null
sortarray of object
resultsarray of RelationshipHit
avatarstring | null
categoryEntityCategorydefault: person

Always person. Present so a row is self-describing in the same way every other category's rows are.

company_domainstring | null
company_namestring | null

Present only with include_company_context.

countrystring | null
currently_at_targetbooleandefault: false

Currently employed or enrolled at the other company/side specifically — not merely at some institution shared with it. Renamed from currently_there, which was scored over *any* matched institution: a person still enrolled at a shared university could read currently_there: true with no employment at the other side at all. This field answers the narrower, correct question, in parity with is_alum, which was always scoped this way.

departmentstring | null

The same department vocabulary the request filters on.

full_namestring | null
idstring

Use with POST /v1/entities/persons for the full record.

is_alumbooleandefault: false

Previously worked at the company on the other side. The strongest single signal, and weighted accordingly.

linkedin_urlstring | null
match_reason_countintegerdefault: 0

How many distinct institutions connect this person to the other side. Exact, not an estimate — 0 never appears here, because a row with no reason is not returned at all (see via). The sort key when ranking by strength of connection rather than by name.

match_typearray of string

Every reason this person matched, as a phrase — array[0] is the strongest. Not via's own order: via is ordered by how many affiliation records back an institution, which is a volume measure, not a strength one. This array is ranked by connection strength instead — currently employed at the other side outranks having once shared a school with someone there. Each entry corresponds 1:1 to a via entry's own match_type, just reordered. Read together, not as two independent sources of truth.

number_of_contacts_overlapping_tenure_withinteger | null

How many people at the other company this person can reach. 0 is a computed, checked answer — we ran the count and found nobody reachable under the current connection_basis. It is not the same as null/absent, which means the count was never attempted: a via_institution query has no other side to count against, so it is skipped rather than defaulted to 0. Read it together with connection_basis on the envelope — the same person counts 16,630 under overlapping and 100,085 under shared, because sharing a large former employer means sharing it with everyone who ever worked there.

profile_completebooleandefault: true

False when the affiliation graph knows this person but the entity index has no record for them — a real connection with a thin profile. They are returned rather than dropped: removing them would shrink a page while leaving total describing the unshrunk set.

senioritystring | null
titlestring | null
viaarray of ViaInstitution

Why this person matched. Never empty on a returned row.

endedstring | null

When it ended. Absent means still there.

idstring

The institution's key in the affiliation corpus. Stable, and the value to send back when narrowing to this institution.

kindstring | null

employment — a shared employer — or education.

match_confidencestring | null

How this institution was identified: url from a canonical LinkedIn URL, edu from a school record, or name from a normalised name. Always returned, because name matches are a genuinely weaker signal — names are not unique — and a connection shown without this reads as verified when it is an inference.

match_typestring

Why this specific institution counts as a reason — in words, not a code. "Currently works at Acme", "Previously worked at Acme", "Also worked at McKinsey" (a third company both sides happen to share), or "Attended Stanford University". Spelled out rather than an enum because this API is callable by an LLM directly — a code needing a lookup table defeats that. The row's own top-level match_type is this same string, one per via entry, reordered so the strongest reason leads.

namestring | null
rolestring | null

What they did there — a job title, or a degree for a school. Returned because a shared employer with no overlap in time is a much weaker claim than one with it, and a caller shown only the institution's name cannot tell the two apart.

startedstring | null

When this person's association began, YYYY-MM-DD.

shareShare | null

Present only when the question asked for a proportion.

populationinteger

Every person currently at the subject company who meets the same person filters, with no connection required. The denominator. ≥ 0.

population_is_estimatebooleandefault: false
sharenumber | null

total / population. null when the population is zero. ≥ 0, ≤ 1.

suggestionSearchSuggestion | null

A better way to have asked this question -- a different category (kind: search), or agentic search via POST /v1/agents (kind: agentic). Advisory: these results came from the search that was asked for, whatever this says. Null when there is nothing worth suggesting, which is the common case. Additive, so it breaks no existing client.

categoryEntityCategory | null

The category to search instead. Set iff kind is search.

kind"search" | "agentic"

search — a different category fits. agentic — no single search fits; use POST /v1/agents.

messagestring

One ready-to-show sentence.

totalinteger

≥ 0.

total_is_estimateboolean

Always serialized, never defaulted away. A field clients can forget exists is a field that renders an estimate as exact.

Two numbers on each row are the ones worth acting on:

  • number_of_contacts_overlapping_tenure_with — distinct people at the other company this person can reach. 0 is a real, checked answer — we counted and found nobody, under the current connection_basis. It's null/absent, not 0, when the count was never attempted: a via_institution query has no other side to count against.
  • match_reason_count — distinct institutions connecting them. Exact.

Every row carries via: which institutions connect them, each with

  • the dates and role
  • a match_confidence of url, edu or name
  • a match_type phrase: plain language for that one reason, e.g. "Currently works at Acme" or "Attended Stanford University"

The row's own top-level match_type is these same phrases across every via entry, reordered so the strongest one leads. That is not via's own order, which is edge-count, not strength.

breakdown and share: only when a sentence asks

Two side-fields are null unless a query in words asked for them:

  • breakdown — the matching people grouped by one axis, for questions like "by department" or "most common school". group_by is department, seniority, country or institution, and each entry in groups has a key, a display label and a people count (plus kind, employment or education, for an institution). exact: false means a count is an estimate, values were merged under one label, or the question required several institutions at once, so each group is an upper bound. more_groups: true means smaller groups exist and were not returned.
  • share — the answer as a proportion of the company's own people, for questions like "what percentage of their sales org". population is everyone currently at the company who meets the same person filters, with no connection required; share is total / population, and null when the population is zero. population_is_estimate says whether the denominator is exact.

Drilling into one person#

POST /v1/relationships/reach takes one person and one company and returns the people at that company they can reach, with both sides' roles, dates and the months they overlapped. To ask about several people or several companies in one question, send it in words to /v1/relationships/ask, which returns the same envelope with a few extra fields.

curl -X POST "https://api.sproutsdata.ai/v1/relationships/reach" \
  -H "X-API-Key: $SPROUTS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "person": {
         "li_url": "linkedin.com/in/ada-lovelace"
       },
       "target_company": {
         "value": "microsoft.com",
         "by": "domain"
       },
       "people": {
         "seniority": [
           "VP",
           "Director"
         ]
       }
     }'
connection_basis"overlapping" | "shared"default: overlapping
cursorstring | null
institution_type"employment" | "education" | null
limitintegerdefault: 10

≥ 1, ≤ 100.

peoplePeopleConstraints

Constrains the people being *reached* — the only side with more than one person on it.

countryarray of string, ≤ 20
departmentarray of string, ≤ 20

Department (Function) labels, the same vocabulary the org chart uses: one of the 23 Function names, or the General / Others sentinels. Renamed as of 2026-08 — e.g. Engineering / Software (was Engineering & Technical), IT & Infrastructure (was Information Technology), Human Resources / People, Medical & Health, Legal, Risk & Compliance. Data, Analytics & AI and Security are now their own Functions rather than children of engineering and IT.

seniorityarray of string, ≤ 20

Seniority levels. Exactly 7 values, most senior first: C-Suite, VP, Director / Head, Manager, IC, Entry, In Training. Note Director and Head are ONE level, spelled Director / Head. The pre-2026-08 vocabulary (C Suite without the hyphen, Director, Head, Senior, President, Intern, Founder, Owner, Partner, Investor, Advisor, Consultant, Freelancer, Educator, Healthcare Professional, Utility Worker, Speciality Service Provider, Unemployed) is no longer indexed and matches nothing. Founder / Owner / Partner / Investor / Advisor are governance roles and Consultant / Freelancer are employment types; neither is filterable on this axis, and the remaining four are departments, not levels.

personRelationshipPersonrequired

The person a reach question runs through, by LinkedIn URL.

li_urlstringrequired

Any common form — scheme, www., trailing slash and tracking params are all tolerated. The only accepted form of person, and POST /v1/relationships returns it on every row as linkedin_url, so the two calls compose. It is exact, and present for every person in the affiliation graph. Not an internal id: an id means nothing to whoever reads the answer, and a caller holding one cannot tell whether it is the right person. Not a name: names are not unique, and the wrong person does not return a thin result — it returns a confident result about somebody else. ≥ 1 chars, ≤ 300 chars.

target_companyRelationshipCompanyrequired

The company being reached into. Its people are what comes back.

by"auto" | "name" | "domain" | "li_url" | "id"default: auto

How to read value. auto infers it from the value's shape — stripe.com is a domain, linkedin.com/company/stripe a LinkedIn URL, Stripe a name — using the same detection /v1/similar and /v1/entities use. Set it explicitly to override. Prefer domain or li_url when you have one: name is not a unique key, so it resolves to the best-ranked candidate and 377 companies are called stripe.

peoplePeopleConstraints

Which people on one side of a relationship count. These hang off a company rather than off the request, because a relationship has two sides and they are constrained independently. "Directors at Acme who know anyone at Globex" and "anyone at Acme who knows directors at Globex" are different questions, and a request-level seniority filter cannot tell them apart — it would constrain both at once. Only three axes, because the affiliation corpus carries only three. It holds no firmographics, so industry, headcount, funding and technology cannot be applied to the people on a relationship at all. They are rejected rather than ignored: POST /v1/search is where those questions belong.

countryarray of string, ≤ 20
departmentarray of string, ≤ 20

Department (Function) labels, the same vocabulary the org chart uses: one of the 23 Function names, or the General / Others sentinels. Renamed as of 2026-08 — e.g. Engineering / Software (was Engineering & Technical), IT & Infrastructure (was Information Technology), Human Resources / People, Medical & Health, Legal, Risk & Compliance. Data, Analytics & AI and Security are now their own Functions rather than children of engineering and IT.

seniorityarray of string, ≤ 20

Seniority levels. Exactly 7 values, most senior first: C-Suite, VP, Director / Head, Manager, IC, Entry, In Training. Note Director and Head are ONE level, spelled Director / Head. The pre-2026-08 vocabulary (C Suite without the hyphen, Director, Head, Senior, President, Intern, Founder, Owner, Partner, Investor, Advisor, Consultant, Freelancer, Educator, Healthcare Professional, Utility Worker, Speciality Service Provider, Unemployed) is no longer indexed and matches nothing. Founder / Owner / Partner / Investor / Advisor are governance roles and Consultant / Freelancer are employment types; neither is filterable on this axis, and the remaining four are departments, not levels.

valuestringrequired

≥ 1 chars, ≤ 300 chars.

Headers

Idempotency-Keystring

Makes a retry safe. Send a unique value per request and reuse it when you retry that same request: with the same body and credential, on the same UTC day, rows already charged are not charged again.

The anchor is a LinkedIn URL, and only that. It is exact, it is present for every person in the graph, and the list route returns it on every row as linkedin_url, so the two calls compose with no translation. An internal id would tell you nothing about whether it found the right person, and a name is not unique enough to accept at all.

attributionarray of Attribution
as_ofstring | null
domainstring | null
quotestring | null

RESERVED for per-fact citation. Always null in v1.

source_corpusSourceCorpus

Where content came from. Closed set — it appears in attribution[]. The first four are the GTM entity graph. The rest are the document corpora behind the non-entity categories, each a distinct index with its own refresh cadence — which is the reason they are named separately rather than lumped under one label: attribution[].as_of means nothing if two corpora updated a month apart share a name.

urlstring | null

RESERVED for per-fact citation. Always null in v1.

columnsarray of object | null

The answer table's columns, in order, when a run carries extra columns beyond the entity's own fields -- a job-opening link, a value joined from another dataset. Each entry is {name, description, source}; the value for each sits on every row in results keyed by the column name. Null means a plain entity table. Additive, so it breaks no existing client.

connection_basis"overlapping" | "shared"default: overlapping

overlapping — the two people were at the shared institution at the same time. shared — both were there at some point. Echoed because it decides which rows exist at all here, not just how a count is worded.

coverageCoverage | null

How much of the addressable set this answer represents. Null means coverage was not measured for this request -- fast and auto do not enumerate -- and never that coverage was zero. Additive, so it breaks no existing client.

auditedbooleandefault: false

Whether the query was checked against the executed filter for dropped requirements. Reported because an empty constraints_unmet only means something if something looked.

constraints_unmetarray of string

Requirements in your query that the search could not express, in your own words. Non-empty means part of the question was not answered -- most often because it needs a dataset this search cannot reach (job postings, employment history, patents, news, funding dates). When this is non-empty, universe_exhausted is withheld: 'we returned everything that matches' cannot be true of a question that was never asked.

criteriaarray of object

Per criterion: evaluated, passed, success_rate, and whether it was *proven* by the query or *judged* by a model. Without this a thin result leaves the caller guessing which requirement thinned it.

enumeratedinteger

How many of them this answer carries. ≥ 0.

enumeration_recallnumber | null

enumerated / universe. Null when the universe was not exact, because a ratio over an estimate is not a recall figure. Note this measures coverage of the *resolved query*, not of the user's intent -- a wrong predicate can be exhaustively enumerated. ≥ 0, ≤ 1.

linkagearray of object

One entry per other dataset this answer joined against: corpus, join_key, observable, of_constrained, ratio, is_lower_bound. ratio is the fraction of your matching accounts that dataset can even see -- measured at 5-60% depending on the dataset and how large the companies are, and never 100%. Non-empty therefore means universe_exhausted is withheld by construction: we returned everything reachable, and a dataset cannot see the rest.

linkage_coveragenumber | null

The joins combined, as a product rather than a minimum: two datasets seeing 27% and 24% of your accounts jointly see about 6.5%, not 24%. Null when no join was performed or when any leg's coverage is unknown.

linkage_coverage_is_lower_boundbooleandefault: false

True when a key set was capped or coverage was estimated rather than measured, making linkage_coverage a floor. Narrowing the question makes it exact.

similarityarray of object

Narrowings done by ranking rather than by predicate. Each entry carries the concept, how many ids it resolved to, the k asked for, whether the list was cut at that ceiling, and the fraction of candidates that could be compared at all. Non-empty means universe_exhausted is withheld on purpose: a cosine is a degree, so a top-k ranking has no complete state, and 2.9% of companies carry no embedding to compare.

truncated_bystring | null

Which limit stopped the enumeration short, when one did.

universeinteger

Records matching the resolved query, counted, not sampled. ≥ 0.

universe_exhaustedbooleandefault: false

Every matching record was returned and the count agrees. The claim an unbounded-corpus search engine cannot make.

universe_is_exactboolean

False when the count hit a cap. Always serialized: a capped count presented as exact is the same lie as an unmeasured one.

degradationarray of Degradation
detailstring
impact"recall" | "precision" | "freshness" | "fields"
reasonDegradationReason

Why an answer is less than it should be. Closed, because a client that cannot branch on the reason can only show the user a shrug. These are the honest constraints — partial vector coverage, smry mapped as keyword so it cannot be scored lexically, a federated corpus being unreachable — surfaced at runtime instead of in a footnote.

source_corpusSourceCorpus | null
has_moreboolean
institutionsarray of ReachInstitution

Every institution connecting the anchor to people at the target company, with how many people each one reaches. Describes the whole answer, not this page — the rows are a window, this is not.

idstring

The institution's key in the affiliation corpus. The same value POST /v1/relationships returns, and the value to send back as an institutions[] anchor there to narrow to this one.

kindstring | null

employment — a shared employer — or education.

match_confidencestring | null

How this institution was identified: url from a canonical LinkedIn URL, edu from a school record, or name from a normalised name. name matches are a genuinely weaker signal, because names are not unique.

namestring | null
people_at_targetintegerdefault: 0

Distinct people at the target company reachable through this institution. Always a real count, never null: an institution nobody is behind is not returned at all. Read it together with the envelope's connection_basis — under shared a large former employer counts everyone who ever passed through it, so the same institution can count an order of magnitude higher than under overlapping.

metaResponseMeta

Present on every 2xx, including the ones that are not paginated.

as_ofstring
credential"api_key" | "session"default: api_key

Which credential produced this response. session means the dashboard playground ran it as the signed-in user rather than as an API key — so it is billed to the user, subject to interactive rate limits, and carries no integration compatibility guarantee. Additive and defaulted, so it breaks no existing client.

credits_chargednumber | null

What this request cost, in credits, to at most three decimals. 0 when it cost nothing: a Basic or AI search that found nothing, rows this search already paid for (a cursor sent again, or a request repeated with the same Idempotency-Key on the same UTC day), or a test key. A request without a cursor or a repeated key is a new search, so keep the first page rather than asking for it again. null on a free endpoint.

environment"live" | "test"

Echoed on every response so a developer can never mistake a test-key result for a live one.

request_idstring
next_cursorstring | null

Opaque and signed. Invariant: has_more == (next_cursor is not None). There is no page or offset parameter — not accepted, not deprecated, never present.

resolved_searchResolvedSearch | null
categoryEntityCategory

*What* to search, and what a returned row is. One parameter carries both, because they are the same fact: a search of patent returns patents. The alternative — a source/vertical selector beside a narrower category — lets a caller ask for patents from the people index, a combination that has no meaning and would need rejecting at runtime. A closed set that cannot express the contradiction is better than a validator that catches it. Values are exactly prism's own row types, so category in the request and results[].category in the response are always the same string. company and person are the GTM index; the rest are separate corpora with their own filters, freshness and coverage. Not everything applies everywhere: see SearchRequest, which rejects the GTM-only parameters rather than ignoring them. job_opening is a document corpus like patent/news_article/etc — a posting, not an entity — but its records carry a domain (the posting company's) all the same. It is deliberately not in GTM_CATEGORIES (that set is about the entity/document distinction, and a job posting is still a document), but it *is* in DOMAIN_FILTERABLE_CATEGORIES, which exists specifically to separate those two axes.

conflictsarray of object
filtersFilters
filters_source"caller" | "parser" | "caller+parser" | "none"default: none
mode"fast" | "auto" | "deep"

How hard to work on the query. The distinction is *what the words mean*, not how many results come back: - fast — match my words. The query is matched by meaning (vector retrieval, falling back to keywords where vectors are unavailable); no LLM in the path, so nothing is extracted into filters. - auto — understand my intent. The parser extracts filters. - deep — reason about it. Multi-step, slowest, most expensive. fast was very nearly defined as "filters only, no LLM", which would have made query — the primary parameter — silently inert. A parameter that does nothing is worse than one that is missing.

overridesarray of object
parser_versionstring
querystring | null
semantic_querystring | null
semantic_scope"company" | "person" | null
sortarray of object
resultsarray of ReachHit
categoryEntityCategorydefault: person

*What* to search, and what a returned row is. One parameter carries both, because they are the same fact: a search of patent returns patents. The alternative — a source/vertical selector beside a narrower category — lets a caller ask for patents from the people index, a combination that has no meaning and would need rejecting at runtime. A closed set that cannot express the contradiction is better than a validator that catches it. Values are exactly prism's own row types, so category in the request and results[].category in the response are always the same string. company and person are the GTM index; the rest are separate corpora with their own filters, freshness and coverage. Not everything applies everywhere: see SearchRequest, which rejects the GTM-only parameters rather than ignoring them. job_opening is a document corpus like patent/news_article/etc — a posting, not an entity — but its records carry a domain (the posting company's) all the same. It is deliberately not in GTM_CATEGORIES (that set is about the entity/document distinction, and a job posting is still a document), but it *is* in DOMAIN_FILTERABLE_CATEGORIES, which exists specifically to separate those two axes.

company_namestring | null
countrystring | null
departmentstring | null
full_namestring | null
howarray of Connection

Why this person is reachable. Never empty on a returned row — sharing something with the anchor is the only reason they are here.

anchor_endedstring | null
anchor_rolestring | null

What the person you asked through did there.

anchor_startedstring | null
inststring
kindstring | null

employment — a shared employer — or education.

match_confidencestring | null
match_typestring

Why this counts as a connection, in words. Overlap-aware when the dates allow it — "Overlapped at Acme for 14 months" — and a plainer "Also worked at Acme" / "Attended Stanford University together" when either side's dates are unknown. The row's own top-level match_type is this same string, one per how entry, reordered by strength.

namestring | null
overlap_monthsinteger | null

Whole months the two were there at the same time. Absent when either side's start date is unknown — not 0, which would say they were never there together. That is a stronger claim than not knowing, and the difference matters: an overlap of three years and an overlap of one month are otherwise the same row.

their_endedstring | null

Absent means still there.

their_rolestring | null

What the reachable person did there.

their_startedstring | null
idstring

Use with POST /v1/entities/persons for the full record.

linkedin_urlstring | null
match_typearray of string

Every reason this person is reachable, as a phrase — array[0] is the strongest connection. A longer, known overlap outranks an unknown-duration one. Each entry corresponds 1:1 to a how entry's own match_type, reordered by strength rather than by how's own order, which reflects fetch order, not connection quality.

reached_byarray of string | null

When the question ran through several named people: which of them (by person id) can reach this person.

senioritystring | null
target_indexinteger | null

When the question reached into several companies: which one this person is at, as an index into the envelope's targets. ≥ 0.

titlestring | null
seedsarray of SeedReach | null

When the question ran through several named people: how many each one reaches on their own. total is then the sum of these — an upper bound, since somebody reachable by two of them counts twice.

idstring

The named person's id.

totalinteger

≥ 0.

total_is_estimatebooleandefault: false
suggestionSearchSuggestion | null

A better way to have asked this question -- a different category (kind: search), or agentic search via POST /v1/agents (kind: agentic). Advisory: these results came from the search that was asked for, whatever this says. Null when there is nothing worth suggesting, which is the common case. Additive, so it breaks no existing client.

categoryEntityCategory | null

The category to search instead. Set iff kind is search.

kind"search" | "agentic"

search — a different category fits. agentic — no single search fits; use POST /v1/agents.

messagestring

One ready-to-show sentence.

targetsarray of ReachTarget | null

When the question reached into several companies: each one, what it resolved to and how many it holds. Rows are grouped by company in this order, each row's target_index says which, and limit applies per company. total is then the sum of these — exact, since one person has one current employer.

cidstring | null

The company it resolved to.

domainstring | null
indexinteger

Rows at this company carry the same target_index. ≥ 0.

namestring | null

What it resolved to, by name. Read this before trusting a zero.

same_asinteger | null

Set when this name resolved to the same company as an earlier target. That company was reached once; its rows and total are under the index this points to, and this entry holds none of its own. ≥ 0.

totalintegerdefault: 0

People reachable at this company. ≥ 0.

total_is_estimatebooleandefault: false
valuestring

The company as the question named it.

totalinteger

≥ 0.

total_is_estimateboolean

Always serialized, never defaulted away. A field clients can forget exists is a field that renders an estimate as exact.

Each entry in how reports both sides: their role and dates at the shared institution, the anchor's, and overlap_months.

  • overlap_months is absent when either start date is unknown, never 0: zero would claim they were never there together, which is a stronger statement than not knowing.
  • Each how entry carries its own match_type phrase, overlap-aware when the dates allow it ("Overlapped at Acme for 14 months"). The row's top-level match_type mirrors these in strength order, the same pattern as the list route.

The envelope also has seeds and targets, and each row has reached_by and target_index. On this route, with its one person and one company, they are always null. They carry values only when a reach question asked through /v1/relationships/ask names several people or several companies.

The bridges on their own#

POST /v1/relationships/institutions returns the shared employers and schools themselves, with how many distinct people each company has at each one, and stops there.

The list route returns the same list as a side-field, but only alongside a ranked page of people, and that ranking plus a hydration lookup per row is the expensive half of the work. So this is the cheap way to ask "what do these two actually have in common", and the only way to filter that list without paying for the people again.

curl -X POST "https://api.sproutsdata.ai/v1/relationships/institutions" \
  -H "X-API-Key: $SPROUTS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "companies": [
         {
           "value": "stripe.com",
           "by": "domain"
         },
         {
           "value": "adyen.com",
           "by": "domain"
         }
       ],
       "institution_type": "employment",
       "min_people_per_side": 3,
       "limit": 200
     }'
companiesarray of RelationshipCompany, ≥ 2, ≤ 2required

Exactly two. A shared institution is a comparison between two populations, so one company has nothing to share and a third has no defined pairing. Order still matters, even though nothing about the returned list looks one-sided. The upstream excludes the first company's own institution keys from the bridges and keeps the second's, on purpose: the first company is essentially everyone's current employer on that side, so it bridges nothing, while "previously worked at the *other* company" is the single strongest connection in the corpus and was measured as the top bridge for the highest-ranked row. So swapping the two changes which institutions come back and what people_first / people_second count. It is not a symmetric request. Each company's own people constraints apply to that side only, and they change both *which* institutions qualify — an institution needs min_people_per_side matching people on each side to survive — and the per-side counts returned for the ones that do.

by"auto" | "name" | "domain" | "li_url" | "id"default: auto

How to read value. auto infers it from the value's shape — stripe.com is a domain, linkedin.com/company/stripe a LinkedIn URL, Stripe a name — using the same detection /v1/similar and /v1/entities use. Set it explicitly to override. Prefer domain or li_url when you have one: name is not a unique key, so it resolves to the best-ranked candidate and 377 companies are called stripe.

peoplePeopleConstraints

Which people on one side of a relationship count. These hang off a company rather than off the request, because a relationship has two sides and they are constrained independently. "Directors at Acme who know anyone at Globex" and "anyone at Acme who knows directors at Globex" are different questions, and a request-level seniority filter cannot tell them apart — it would constrain both at once. Only three axes, because the affiliation corpus carries only three. It holds no firmographics, so industry, headcount, funding and technology cannot be applied to the people on a relationship at all. They are rejected rather than ignored: POST /v1/search is where those questions belong.

countryarray of string, ≤ 20
departmentarray of string, ≤ 20

Department (Function) labels, the same vocabulary the org chart uses: one of the 23 Function names, or the General / Others sentinels. Renamed as of 2026-08 — e.g. Engineering / Software (was Engineering & Technical), IT & Infrastructure (was Information Technology), Human Resources / People, Medical & Health, Legal, Risk & Compliance. Data, Analytics & AI and Security are now their own Functions rather than children of engineering and IT.

seniorityarray of string, ≤ 20

Seniority levels. Exactly 7 values, most senior first: C-Suite, VP, Director / Head, Manager, IC, Entry, In Training. Note Director and Head are ONE level, spelled Director / Head. The pre-2026-08 vocabulary (C Suite without the hyphen, Director, Head, Senior, President, Intern, Founder, Owner, Partner, Investor, Advisor, Consultant, Freelancer, Educator, Healthcare Professional, Utility Worker, Speciality Service Provider, Unemployed) is no longer indexed and matches nothing. Founder / Owner / Partner / Investor / Advisor are governance roles and Consultant / Freelancer are employment types; neither is filterable on this axis, and the remaining four are departments, not levels.

valuestringrequired

≥ 1 chars, ≤ 300 chars.

institution_type"employment" | "education" | null

Restrict the bridges to shared employers, or to schools. Unset returns both, which is the usual question.

limitintegerdefault: 200

How many institutions to return, strongest bridge first. Not a page size — there is no cursor and no page 2. When more exist than this, truncated on the response is true; raise limit or narrow with the filters above to see them. ≥ 1, ≤ 1000.

min_people_per_sideintegerdefault: 1

How many distinct people each side must have at an institution for it to count. The quality knob: at the default 1 a single coincidence — one person on each side who happened to attend the same school — is a bridge, and on a large pair of companies those dominate the tail by count. One consequence worth knowing, because it is not what the name suggests: the threshold is applied to the exact per-side counts *after* the upstream has already trimmed to the strongest limit institutions, not before. So a high threshold can return fewer than limit institutions while institutions that would have passed it exist further down the untrimmed list. Raise limit alongside it when the answer looks short. ≥ 1, ≤ 100.

name_starts_withstring | null

Keep only institutions whose display name starts with this, case-insensitively. A prefix, not a substring and not a fuzzy match: "stan" finds Stanford University, and "ford" does not. That is the honest limit rather than a simplification — the institution name is a bare keyword in the affiliation corpus with no analysed subfield, so a substring search is not on offer. It narrows the documents before they are bucketed, so it also makes the answer arrive faster rather than costing anything. For one institution you can already name, send it to POST /v1/relationships as an institutions[] anchor instead; this is for narrowing a list you have not seen yet. ≥ 1 chars, ≤ 200 chars.

Headers

Idempotency-Keystring

Makes a retry safe. Send a unique value per request and reuse it when you retry that same request: with the same body and credential, on the same UTC day, rows already charged are not charged again.

Order matters here too, and differently. The first company's own institution keys are excluded from the bridges and the second company's are kept. That is deliberate: essentially everyone on the first side has that company as their current employer, so it bridges nobody — while having previously worked at the second company is the strongest connection the corpus holds. Swap the two and you get a different list with different counts.

Each of the four narrowing knobs changes which institutions qualify, not just what is displayed:

  • companies[].people applies to one side alone, and an institution has to keep enough matching people on both sides to survive, so filtering one side can remove bridges entirely.
  • min_people_per_side is the quality knob. At the default 1, one person on each side who happened to attend the same university is a bridge.
attributionarray of Attribution
as_ofstring | null
domainstring | null
quotestring | null

RESERVED for per-fact citation. Always null in v1.

source_corpusSourceCorpus

Where content came from. Closed set — it appears in attribution[]. The first four are the GTM entity graph. The rest are the document corpora behind the non-entity categories, each a distinct index with its own refresh cadence — which is the reason they are named separately rather than lumped under one label: attribution[].as_of means nothing if two corpora updated a month apart share a name.

urlstring | null

RESERVED for per-fact citation. Always null in v1.

columnsarray of object | null

The answer table's columns, in order, when a run carries extra columns beyond the entity's own fields -- a job-opening link, a value joined from another dataset. Each entry is {name, description, source}; the value for each sits on every row in results keyed by the column name. Null means a plain entity table. Additive, so it breaks no existing client.

coverageCoverage | null

How much of the addressable set this answer represents. Null means coverage was not measured for this request -- fast and auto do not enumerate -- and never that coverage was zero. Additive, so it breaks no existing client.

auditedbooleandefault: false

Whether the query was checked against the executed filter for dropped requirements. Reported because an empty constraints_unmet only means something if something looked.

constraints_unmetarray of string

Requirements in your query that the search could not express, in your own words. Non-empty means part of the question was not answered -- most often because it needs a dataset this search cannot reach (job postings, employment history, patents, news, funding dates). When this is non-empty, universe_exhausted is withheld: 'we returned everything that matches' cannot be true of a question that was never asked.

criteriaarray of object

Per criterion: evaluated, passed, success_rate, and whether it was *proven* by the query or *judged* by a model. Without this a thin result leaves the caller guessing which requirement thinned it.

enumeratedinteger

How many of them this answer carries. ≥ 0.

enumeration_recallnumber | null

enumerated / universe. Null when the universe was not exact, because a ratio over an estimate is not a recall figure. Note this measures coverage of the *resolved query*, not of the user's intent -- a wrong predicate can be exhaustively enumerated. ≥ 0, ≤ 1.

linkagearray of object

One entry per other dataset this answer joined against: corpus, join_key, observable, of_constrained, ratio, is_lower_bound. ratio is the fraction of your matching accounts that dataset can even see -- measured at 5-60% depending on the dataset and how large the companies are, and never 100%. Non-empty therefore means universe_exhausted is withheld by construction: we returned everything reachable, and a dataset cannot see the rest.

linkage_coveragenumber | null

The joins combined, as a product rather than a minimum: two datasets seeing 27% and 24% of your accounts jointly see about 6.5%, not 24%. Null when no join was performed or when any leg's coverage is unknown.

linkage_coverage_is_lower_boundbooleandefault: false

True when a key set was capped or coverage was estimated rather than measured, making linkage_coverage a floor. Narrowing the question makes it exact.

similarityarray of object

Narrowings done by ranking rather than by predicate. Each entry carries the concept, how many ids it resolved to, the k asked for, whether the list was cut at that ceiling, and the fraction of candidates that could be compared at all. Non-empty means universe_exhausted is withheld on purpose: a cosine is a degree, so a top-k ranking has no complete state, and 2.9% of companies carry no embedding to compare.

truncated_bystring | null

Which limit stopped the enumeration short, when one did.

universeinteger

Records matching the resolved query, counted, not sampled. ≥ 0.

universe_exhaustedbooleandefault: false

Every matching record was returned and the count agrees. The claim an unbounded-corpus search engine cannot make.

universe_is_exactboolean

False when the count hit a cap. Always serialized: a capped count presented as exact is the same lie as an unmeasured one.

degradationarray of Degradation
detailstring
impact"recall" | "precision" | "freshness" | "fields"
reasonDegradationReason

Why an answer is less than it should be. Closed, because a client that cannot branch on the reason can only show the user a shrug. These are the honest constraints — partial vector coverage, smry mapped as keyword so it cannot be scored lexically, a federated corpus being unreachable — surfaced at runtime instead of in a footnote.

source_corpusSourceCorpus | null
has_moreboolean
metaResponseMeta

Present on every 2xx, including the ones that are not paginated.

as_ofstring
credential"api_key" | "session"default: api_key

Which credential produced this response. session means the dashboard playground ran it as the signed-in user rather than as an API key — so it is billed to the user, subject to interactive rate limits, and carries no integration compatibility guarantee. Additive and defaulted, so it breaks no existing client.

credits_chargednumber | null

What this request cost, in credits, to at most three decimals. 0 when it cost nothing: a Basic or AI search that found nothing, rows this search already paid for (a cursor sent again, or a request repeated with the same Idempotency-Key on the same UTC day), or a test key. A request without a cursor or a repeated key is a new search, so keep the first page rather than asking for it again. null on a free endpoint.

environment"live" | "test"

Echoed on every response so a developer can never mistake a test-key result for a live one.

request_idstring
next_cursorstring | null

Opaque and signed. Invariant: has_more == (next_cursor is not None). There is no page or offset parameter — not accepted, not deprecated, never present.

resolved_searchResolvedSearch | null
categoryEntityCategory

*What* to search, and what a returned row is. One parameter carries both, because they are the same fact: a search of patent returns patents. The alternative — a source/vertical selector beside a narrower category — lets a caller ask for patents from the people index, a combination that has no meaning and would need rejecting at runtime. A closed set that cannot express the contradiction is better than a validator that catches it. Values are exactly prism's own row types, so category in the request and results[].category in the response are always the same string. company and person are the GTM index; the rest are separate corpora with their own filters, freshness and coverage. Not everything applies everywhere: see SearchRequest, which rejects the GTM-only parameters rather than ignoring them. job_opening is a document corpus like patent/news_article/etc — a posting, not an entity — but its records carry a domain (the posting company's) all the same. It is deliberately not in GTM_CATEGORIES (that set is about the entity/document distinction, and a job posting is still a document), but it *is* in DOMAIN_FILTERABLE_CATEGORIES, which exists specifically to separate those two axes.

conflictsarray of object
filtersFilters
filters_source"caller" | "parser" | "caller+parser" | "none"default: none
mode"fast" | "auto" | "deep"

How hard to work on the query. The distinction is *what the words mean*, not how many results come back: - fast — match my words. The query is matched by meaning (vector retrieval, falling back to keywords where vectors are unavailable); no LLM in the path, so nothing is extracted into filters. - auto — understand my intent. The parser extracts filters. - deep — reason about it. Multi-step, slowest, most expensive. fast was very nearly defined as "filters only, no LLM", which would have made query — the primary parameter — silently inert. A parameter that does nothing is worse than one that is missing.

overridesarray of object
parser_versionstring
querystring | null
semantic_querystring | null
semantic_scope"company" | "person" | null
sortarray of object
resultsarray of SharedInstitution
idstring
kindstring | null
match_confidencestring | null
namestring | null
people_firstintegerdefault: 0

Distinct people at the first company.

people_secondintegerdefault: 0

Distinct people at the second company.

suggestionSearchSuggestion | null

A better way to have asked this question -- a different category (kind: search), or agentic search via POST /v1/agents (kind: agentic). Advisory: these results came from the search that was asked for, whatever this says. Null when there is nothing worth suggesting, which is the common case. Additive, so it breaks no existing client.

categoryEntityCategory | null

The category to search instead. Set iff kind is search.

kind"search" | "agentic"

search — a different category fits. agentic — no single search fits; use POST /v1/agents.

messagestring

One ready-to-show sentence.

totalinteger

≥ 0.

total_is_estimateboolean

Always serialized, never defaulted away. A field clients can forget exists is a field that renders an estimate as exact.

truncatedbooleandefault: false

true when more institutions qualified than limit returned. A first-class field rather than a degradation[] entry, because it is the one thing about this response a caller must branch on and has_more — the field they would normally read for it — is structurally false here. A clipped list that looked complete would be a wrong answer rather than a short one: institutions is frequently the whole point ("you both know people from Stanford and McKinsey"). Raise limit, or narrow with institution_type / name_starts_with, to see the rest. degradation[] still carries anything the upstream itself reported about the answer being narrower than the question; this field is not restated there, because one condition reported twice is a condition clients learn to ignore.

Errors#

StatusCodeSummary
401auth.unauthorizedNo usable credential (cause deliberately unspecified)
402credits.insufficientThe allocation cannot fund this action
403auth.browser_originA secret key was used from a browser
403auth.insufficient_scopeThe credential does not hold the required scope
422pagination.cursor_invalidThe cursor could not be read (e.g. a filter changed mid-pagination)
422request.invalidA field failed validation (extra.errors has the detail)
429quota.burst_exceededInteractive burst limit hit — wait Retry-After seconds
501not_implementedA capability that is not available yet
503credits.unavailableThe billing service could not confirm the charge; retry with the same Idempotency-Key
503upstream.unavailableThe retrieval engine is unreachable (not charged)
504upstream.timeoutThe retrieval engine did not answer in time (not charged)