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.
/v1/relationshipsFind 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:
| Route | Answers |
|---|---|
POST /v1/relationships (this one) | People at one company connected to people at another, ranked by how far into it they reach |
/v1/relationships/reach | Expands any single row into the actual people they can reach, and how they know each other |
/v1/relationships/institutions | What 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, ≤ 2Order 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: autoHow 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.
peoplePeopleConstraintsWhich 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, ≤ 20departmentarray of string, ≤ 20Department (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, ≤ 20Seniority 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: overlappingWhat 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 | nullexclude_currentbooleandefault: falseOnly 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: falseAdd each person's current employer. Costs a further lookup upstream.
institution_type"employment" | "education" | nullRestrict the shared institution to a former employer, or a school.
institutionsarray of RelationshipInstitution, ≤ 10by"auto" | "name" | "li_url" | "inst_key"default: autoHow 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: unknownunknown 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: anyany — 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 | nullThe 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_companiesWhich question this is. Rejected alongside query — the planner derives it, and a caller value beside a sentence has no defined precedence.
Headers
Idempotency-KeystringMakes 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_basis | Requires the two people were at the shared institution |
|---|---|
overlapping (default) | At the same time |
shared | At 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 Attributionas_ofstring | nulldomainstring | nullquotestring | nullRESERVED for per-fact citation. Always null in v1.
source_corpusSourceCorpusWhere 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 | nullRESERVED for per-fact citation. Always null in v1.
breakdownBreakdown | nullPresent only when the question asked for a breakdown.
exactbooleandefault: truefalse 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 Bygroupsarray of BreakdownGroupkeystringThe value as the index holds it.
kindstring | nullemployment or education, for an institution breakdown.
labelstringWhat to show: a department or seniority label, or the institution's name.
peopleintegerDistinct people in this group. ≥ 0.
more_groupsbooleandefault: falseSmaller groups exist and were not returned.
columnsarray of object | nullThe 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: overlappingWhich 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 | nullHow 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: falseWhether 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 stringRequirements 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 objectPer 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.
enumeratedintegerHow many of them this answer carries. ≥ 0.
enumeration_recallnumber | nullenumerated / 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 objectOne 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 | nullThe 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: falseTrue 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 objectNarrowings 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 | nullWhich limit stopped the enumeration short, when one did.
universeintegerRecords matching the resolved query, counted, not sampled. ≥ 0.
universe_exhaustedbooleandefault: falseEvery matching record was returned and the count agrees. The claim an unbounded-corpus search engine cannot make.
universe_is_exactbooleanFalse when the count hit a cap. Always serialized: a capped count presented as exact is the same lie as an unmeasured one.
degradationarray of Degradationdetailstringimpact"recall" | "precision" | "freshness" | "fields"reasonDegradationReasonWhy 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 | nullhas_morebooleaninstitutionsarray of SharedInstitutionidstringkindstring | nullmatch_confidencestring | nullnamestring | nullpeople_firstintegerdefault: 0Distinct people at the first company.
people_secondintegerdefault: 0Distinct people at the second company.
metaResponseMetaPresent on every 2xx, including the ones that are not paginated.
as_ofstringcredential"api_key" | "session"default: api_keyWhich 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 | nullWhat 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_idstringnext_cursorstring | nullOpaque 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 | nullcategoryEntityCategory*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 objectfiltersFiltersfilters_source"caller" | "parser" | "caller+parser" | "none"default: nonemode"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 objectparser_versionstringquerystring | nullsemantic_querystring | nullsemantic_scope"company" | "person" | nullsortarray of objectresultsarray of RelationshipHitavatarstring | nullcategoryEntityCategorydefault: personAlways person. Present so a row is self-describing in the same way every other category's rows are.
company_domainstring | nullcompany_namestring | nullPresent only with include_company_context.
countrystring | nullcurrently_at_targetbooleandefault: falseCurrently 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 | nullThe same department vocabulary the request filters on.
full_namestring | nullidstringUse with POST /v1/entities/persons for the full record.
is_alumbooleandefault: falsePreviously worked at the company on the other side. The strongest single signal, and weighted accordingly.
linkedin_urlstring | nullmatch_reason_countintegerdefault: 0How 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 stringEvery 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 | nullHow 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: trueFalse 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 | nulltitlestring | nullviaarray of ViaInstitutionWhy this person matched. Never empty on a returned row.
endedstring | nullWhen it ended. Absent means still there.
idstringThe institution's key in the affiliation corpus. Stable, and the value to send back when narrowing to this institution.
kindstring | nullemployment — a shared employer — or education.
match_confidencestring | nullHow 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_typestringWhy 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 | nullrolestring | nullWhat 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 | nullWhen this person's association began, YYYY-MM-DD.
shareShare | nullPresent only when the question asked for a proportion.
populationintegerEvery person currently at the subject company who meets the same person filters, with no connection required. The denominator. ≥ 0.
population_is_estimatebooleandefault: falsesharenumber | nulltotal / population. null when the population is zero. ≥ 0, ≤ 1.
suggestionSearchSuggestion | nullA 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 | nullThe 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.
messagestringOne ready-to-show sentence.
totalinteger≥ 0.
total_is_estimatebooleanAlways 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.0is a real, checked answer — we counted and found nobody, under the currentconnection_basis. It'snull/absent, not0, when the count was never attempted: avia_institutionquery 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_confidenceofurl,eduorname - a
match_typephrase: 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_byisdepartment,seniority,countryorinstitution, and each entry ingroupshas akey, a displaylabeland apeoplecount (pluskind,employmentoreducation, for an institution).exact: falsemeans 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: truemeans 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".populationis everyone currently at the company who meets the same person filters, with no connection required;shareistotal / population, andnullwhen the population is zero.population_is_estimatesays 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: overlappingcursorstring | nullinstitution_type"employment" | "education" | nulllimitintegerdefault: 10≥ 1, ≤ 100.
peoplePeopleConstraintsConstrains the people being *reached* — the only side with more than one person on it.
countryarray of string, ≤ 20departmentarray of string, ≤ 20Department (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, ≤ 20Seniority 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.
personRelationshipPersonrequiredThe person a reach question runs through, by LinkedIn URL.
li_urlstringrequiredAny 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_companyRelationshipCompanyrequiredThe company being reached into. Its people are what comes back.
by"auto" | "name" | "domain" | "li_url" | "id"default: autoHow 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.
peoplePeopleConstraintsWhich 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, ≤ 20departmentarray of string, ≤ 20Department (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, ≤ 20Seniority 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-KeystringMakes 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 Attributionas_ofstring | nulldomainstring | nullquotestring | nullRESERVED for per-fact citation. Always null in v1.
source_corpusSourceCorpusWhere 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 | nullRESERVED for per-fact citation. Always null in v1.
columnsarray of object | nullThe 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: overlappingoverlapping — 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 | nullHow 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: falseWhether 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 stringRequirements 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 objectPer 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.
enumeratedintegerHow many of them this answer carries. ≥ 0.
enumeration_recallnumber | nullenumerated / 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 objectOne 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 | nullThe 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: falseTrue 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 objectNarrowings 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 | nullWhich limit stopped the enumeration short, when one did.
universeintegerRecords matching the resolved query, counted, not sampled. ≥ 0.
universe_exhaustedbooleandefault: falseEvery matching record was returned and the count agrees. The claim an unbounded-corpus search engine cannot make.
universe_is_exactbooleanFalse when the count hit a cap. Always serialized: a capped count presented as exact is the same lie as an unmeasured one.
degradationarray of Degradationdetailstringimpact"recall" | "precision" | "freshness" | "fields"reasonDegradationReasonWhy 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 | nullhas_morebooleaninstitutionsarray of ReachInstitutionEvery 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.
idstringThe 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 | nullemployment — a shared employer — or education.
match_confidencestring | nullHow 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 | nullpeople_at_targetintegerdefault: 0Distinct 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.
metaResponseMetaPresent on every 2xx, including the ones that are not paginated.
as_ofstringcredential"api_key" | "session"default: api_keyWhich 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 | nullWhat 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_idstringnext_cursorstring | nullOpaque 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 | nullcategoryEntityCategory*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 objectfiltersFiltersfilters_source"caller" | "parser" | "caller+parser" | "none"default: nonemode"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 objectparser_versionstringquerystring | nullsemantic_querystring | nullsemantic_scope"company" | "person" | nullsortarray of objectresultsarray of ReachHitcategoryEntityCategorydefault: 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 | nullcountrystring | nulldepartmentstring | nullfull_namestring | nullhowarray of ConnectionWhy this person is reachable. Never empty on a returned row — sharing something with the anchor is the only reason they are here.
anchor_endedstring | nullanchor_rolestring | nullWhat the person you asked through did there.
anchor_startedstring | nullinststringkindstring | nullemployment — a shared employer — or education.
match_confidencestring | nullmatch_typestringWhy 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 | nulloverlap_monthsinteger | nullWhole 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 | nullAbsent means still there.
their_rolestring | nullWhat the reachable person did there.
their_startedstring | nullidstringUse with POST /v1/entities/persons for the full record.
linkedin_urlstring | nullmatch_typearray of stringEvery 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 | nullWhen the question ran through several named people: which of them (by person id) can reach this person.
senioritystring | nulltarget_indexinteger | nullWhen the question reached into several companies: which one this person is at, as an index into the envelope's targets. ≥ 0.
titlestring | nullseedsarray of SeedReach | nullWhen 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.
idstringThe named person's id.
totalinteger≥ 0.
total_is_estimatebooleandefault: falsesuggestionSearchSuggestion | nullA 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 | nullThe 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.
messagestringOne ready-to-show sentence.
targetsarray of ReachTarget | nullWhen 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 | nullThe company it resolved to.
domainstring | nullindexintegerRows at this company carry the same target_index. ≥ 0.
namestring | nullWhat it resolved to, by name. Read this before trusting a zero.
same_asinteger | nullSet 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: 0People reachable at this company. ≥ 0.
total_is_estimatebooleandefault: falsevaluestringThe company as the question named it.
totalinteger≥ 0.
total_is_estimatebooleanAlways 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_monthsis absent when either start date is unknown, never0: zero would claim they were never there together, which is a stronger statement than not knowing.- Each
howentry carries its ownmatch_typephrase, overlap-aware when the dates allow it ("Overlapped at Acme for 14 months"). The row's top-levelmatch_typemirrors 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, ≤ 2requiredExactly 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: autoHow 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.
peoplePeopleConstraintsWhich 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, ≤ 20departmentarray of string, ≤ 20Department (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, ≤ 20Seniority 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" | nullRestrict the bridges to shared employers, or to schools. Unset returns both, which is the usual question.
limitintegerdefault: 200How 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: 1How 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 | nullKeep 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-KeystringMakes 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[].peopleapplies 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_sideis the quality knob. At the default1, one person on each side who happened to attend the same university is a bridge.
attributionarray of Attributionas_ofstring | nulldomainstring | nullquotestring | nullRESERVED for per-fact citation. Always null in v1.
source_corpusSourceCorpusWhere 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 | nullRESERVED for per-fact citation. Always null in v1.
columnsarray of object | nullThe 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 | nullHow 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: falseWhether 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 stringRequirements 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 objectPer 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.
enumeratedintegerHow many of them this answer carries. ≥ 0.
enumeration_recallnumber | nullenumerated / 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 objectOne 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 | nullThe 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: falseTrue 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 objectNarrowings 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 | nullWhich limit stopped the enumeration short, when one did.
universeintegerRecords matching the resolved query, counted, not sampled. ≥ 0.
universe_exhaustedbooleandefault: falseEvery matching record was returned and the count agrees. The claim an unbounded-corpus search engine cannot make.
universe_is_exactbooleanFalse when the count hit a cap. Always serialized: a capped count presented as exact is the same lie as an unmeasured one.
degradationarray of Degradationdetailstringimpact"recall" | "precision" | "freshness" | "fields"reasonDegradationReasonWhy 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 | nullhas_morebooleanmetaResponseMetaPresent on every 2xx, including the ones that are not paginated.
as_ofstringcredential"api_key" | "session"default: api_keyWhich 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 | nullWhat 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_idstringnext_cursorstring | nullOpaque 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 | nullcategoryEntityCategory*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 objectfiltersFiltersfilters_source"caller" | "parser" | "caller+parser" | "none"default: nonemode"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 objectparser_versionstringquerystring | nullsemantic_querystring | nullsemantic_scope"company" | "person" | nullsortarray of objectresultsarray of SharedInstitutionidstringkindstring | nullmatch_confidencestring | nullnamestring | nullpeople_firstintegerdefault: 0Distinct people at the first company.
people_secondintegerdefault: 0Distinct people at the second company.
suggestionSearchSuggestion | nullA 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 | nullThe 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.
messagestringOne ready-to-show sentence.
totalinteger≥ 0.
total_is_estimatebooleanAlways serialized, never defaulted away. A field clients can forget exists is a field that renders an estimate as exact.
truncatedbooleandefault: falsetrue 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#
| Status | Code | Summary |
|---|---|---|
| 401 | auth.unauthorized | No usable credential (cause deliberately unspecified) |
| 402 | credits.insufficient | The allocation cannot fund this action |
| 403 | auth.browser_origin | A secret key was used from a browser |
| 403 | auth.insufficient_scope | The credential does not hold the required scope |
| 422 | pagination.cursor_invalid | The cursor could not be read (e.g. a filter changed mid-pagination) |
| 422 | request.invalid | A field failed validation (extra.errors has the detail) |
| 429 | quota.burst_exceeded | Interactive burst limit hit — wait Retry-After seconds |
| 501 | not_implemented | A capability that is not available yet |
| 503 | credits.unavailable | The billing service could not confirm the charge; retry with the same Idempotency-Key |
| 503 | upstream.unavailable | The retrieval engine is unreachable (not charged) |
| 504 | upstream.timeout | The retrieval engine did not answer in time (not charged) |