Endpoints
Ask a relationship question
One box for every relationship question — send a sentence, get back the route it turned out to be, what was understood, and the answer in that route's own shape.
/v1/relationships/askAsk a relationship question in words
One box for every relationship question.
Send a sentence. Which of the three questions it turns out to be — people bridging two companies, who one named person can reach, or the shared employers and schools themselves — is worked out upstream and reported back as route, alongside what was understood and anything that had to be widened or dropped.
Read route before the rows. Exactly one of relationships, reach and institutions is populated, and they are different shapes because they are different answers. A question that reads two ways is refused rather than guessed at: a wrong *kind* of answer is complete, plausible and about something else, which no field on a successful response can warn you about.
A question this corpus cannot answer is refused with the reason and, where there is one, the endpoint that does answer it. The three typed routes are unchanged and remain the way to ask when you already know which you want.
Priced like POST /v1/relationships by the rows it returns, or flat like POST /v1/relationships/institutions when it is routed there.
Scope: dsp.search.read
Use it when you have a relationship question in words and would rather not
decide which of the graph routes answers it. Outcome: the answer, plus
route naming which question it turned out to be and reading naming what the
sentence was taken to mean, so a misread is visible instead of silent.
The three typed routes on
POST /v1/relationships are unchanged and
remain the way to ask when you already know which you want. This one exists
because the caller should not have to know first.
Request#
Three fields, deliberately. This is the sentences-only surface: every structural knob lives on the typed routes, and adding them here would recreate the thing it exists to remove.
cursorstring | nullPage on. The cursor carries the plan that was worked out for your question, so paging never re-reads the sentence and never charges for interpreting it twice.
limitintegerdefault: 10≥ 1, ≤ 100.
querystringrequiredThe question, in words. Any kind of relationship question: - *who at Adyen do we know from Stripe* — people who bridge two companies - *is there anyone at Canva who used to work with Melanie Perkins* — who one named person can reach - *what do Stripe and Adyen people have in common* — the shared employers and schools themselves Which of the three it turns out to be is worked out for you and reported back as route. A question this corpus cannot answer is refused with the reason and, where there is one, the endpoint that does answer it — never answered narrowly. ≥ 1 chars, ≤ 2000 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 cursor carries the plan that was worked out for your question, so paging never re-reads the sentence and never charges for interpreting it twice.
Read route before the rows#
Exactly one of relationships, reach, institutions, compare and fan_out
is populated, and they are different shapes because they are different
answers:
route | The question it turned out to be | What is populated |
|---|---|---|
relationships | People at one company connected to another | relationships — the list envelope |
reach | Who one or more named people can reach at one or more companies | reach — the reach envelope |
institutions | The employers and schools two companies share, nobody ranked | institutions — the shared-institutions envelope |
compare | Two ways in, weighed against each other | compare |
fan_out | People, each with what they can reach | fan_out |
A relationships answer, and each side of a compare, can also carry
breakdown and share
when the sentence asked for a grouping ("by department") or a proportion ("what
percentage of their sales org").
Reach across several people or companies
A reach question can name more than one person ("who can Ada and Grace reach at Microsoft") or more than one company ("reach check for Ada: Adyen, Checkout.com, Mollie"). The answer is still one reach envelope, with extra fields that say which part of the question each row belongs to:
- Several companies.
targets[]has one entry per company you named, in your order:index,value(the company as you named it), what it resolved to (cid,name,domain), and its owntotalandtotal_is_estimate. Rows are grouped by company in that order, each row'starget_indexpoints intotargets, andlimitapplies per company. The envelope'stotalis the sum of the per-company totals, and it is exact, because one person has one current employer. Read each target'snamebefore trusting a zero: it shows what the name resolved to. - Two names for one company. When a name resolved to the same company as an
earlier one, its entry has
same_asset to that earlierindex. That company was reached once, and its rows and total are under the earlier entry. - Several people.
seeds[]has one entry per named person: theirid, and how many people they reach on their own (total,total_is_estimate). Each row'sreached_bylists which of them (by person id) can reach that person. The envelope'stotalis then the sum of the seeds' totals, which is an upper bound, because somebody reachable by two of them counts twice.
On a one-person, one-company question all four fields are null, exactly as on
POST /v1/relationships/reach.
Refusals#
A question that reads two ways is refused rather than guessed at. A wrong kind of answer is complete, plausible and about something else, and no field on a successful response can warn you about that.
A question this corpus cannot answer is refused with 422
request.unsupported. The
detail gives the reason in words, and extra carries two optional fields:
extra.kind— which refusal this is, so you can ask the user for the right thing. It is passed through verbatim from the relationship engine and is present only when the engine names one, so treat an unknown or missing value as a general refusal.extra.try_instead— a suggestion for another way to ask, as text, when there is one.
The kinds you are most likely to see (the list can grow):
extra.kind | Means | What to ask for |
|---|---|---|
no_anchor | No company or person was named, so there is nothing to run the question from | Name a company, and a second company or a shared employer or school |
no_tenant_context | The question has your own organisation as one side ("our team", "who do we know at Snowflake"), and the API does not know which company that is | Name your company explicitly as the second side |
needs_tenant_list | The question works from a list held in your own systems ("our top 20 accounts") | Name the companies or people on the list |
needs_pre_resolve | The question picks companies out by something no index records: a resemblance, a competitor set, ownership | Name the companies, or describe them by industry, size, location, founding year, technology or funding stage |
impossible_on_this_corpus | The corpus cannot express the question with any filter | Ask a different question; detail names the part that cannot be answered |
route_ambiguous | The question reads two ways and neither is safely preferable | Say which you meant |
compound_over_budget | The question needs more separate searches than one request runs | Narrow it, or ask the parts separately |
wrong_route | On a typed route with a query: the sentence is a different kind of question from the one that route answers | Send it to /v1/relationships/ask, or to the route that answers it |
compound_unsupported | On a typed route with a query: the sentence asks two things at once, and the detail names both parts | Send the parts as separate requests |
reading — the correction affordance#
{
"route": "relationships",
"reading": {
"span": "who at Adyen do we know from Stripe",
"searches": 1,
"warnings": ["Firmographic constraints were dropped: the affiliation graph holds none."]
},
"relationships": { "results": [], "…": "…" }
}span— the part of your question this answer covers, in your own words. On a single question it is the whole sentence; on a two-part answer each side carries its own.searches— how many searches the answer took.warnings[]— what was widened, capped or could not be applied, one entry per thing, in plain language. Empty means nothing was dropped.
The two composed routes#
compare and fan_out are two-part answers, and neither is paginated: a
comparison is a fixed pair and a fan-out a fixed number of drill-downs, so there
is no window to advance. next_cursor is null on both, and it means "there are
no pages" rather than "you reached the last one".
compare
Returns sides[]: one per half of your question, in the order you asked, each
an ordinary relationship envelope carrying its own resolved_search.
- Both sides always come back with their evidence. A comparison whose losing side is invisible cannot be checked, and the commonest reason one side wins is that its company name resolved to something larger rather than to a better route in.
strongerindexes intosidesand isnullwhen no side is named. That is a real answer, covering both a tie and the case where either count is approximate, whichcomparable: falsestates outright.
fan_out
Returns the first half's page unchanged, plus drill_downs[]: one per person
that could be drilled into, in the same rank order as results.
- Ignore
drill_downsand the envelope is exactly what the one-part question returns. That is what makes a failed drill-down survivable: it costs the extra, not the answer. kis how many rows were drilled into (capped, because each drill-down is a full search).skippednames the people in the topkthat could not be. Reaching through somebody needs their LinkedIn URL, and a row without one is skipped rather than guessed at.
Response#
compareComparisonEnvelope | nullcomparablebooleandefault: truefalse when the sides cannot be meaningfully weighed — one found nobody, or the counts are approximate. The sides are still returned; what is withheld is the comparison.
connection_basis"overlapping" | "shared"default: overlappingApplied to both sides. A comparison across two readings of *knows* is not a comparison.
sidesarray of ComparisonSideEnvelopeOne per half of your question, in the order you asked.
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.
spanstringdefault: The half of your question this side answers, in your own words.
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.
strongerinteger | nullIndex into sides, or null when no side is named. null is a real answer, not a missing one, and it covers more than a tie: if either count is approximate the two cannot be ranked against each other, because the difference between them may be smaller than the error on either. Both counts are still reported — read them rather than this field when it is null.
credits_chargednumber | nullWhat this question cost, in credits, to at most three decimals. Here rather than on a meta, because a two-part answer carries one per part and the charge covers the whole question. Priced like a relationship page, or flat like /relationships/institutions when it was routed there.
fan_outFanOutEnvelope | nullattributionarray 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 | nulldrill_downsarray of DrillDownEnvelopeOne per person that could be drilled into, in the same rank order as results. Shorter than k when a row carries no LinkedIn URL — see skipped, which is why a short list is never a claim that somebody can reach nobody.
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.
person_idstringdefault: The id in results of the person this hangs under.
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.
has_morebooleaninstitutionsarray of SharedInstitutionidstringkindstring | nullmatch_confidencestring | nullnamestring | nullpeople_firstintegerdefault: 0Distinct people at the first company.
people_secondintegerdefault: 0Distinct people at the second company.
kintegerdefault: 3How many of the results were drilled into. Capped: each drill-down is a full search, so this is the one question whose cost multiplies. ≥ 1.
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.
skippedintegerdefault: 0People in the top k that could not be drilled into. Reaching through somebody needs their LinkedIn URL, and a row without one is skipped rather than guessed at. ≥ 0.
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.
institutionsSharedInstitutionsEnvelope | nullattributionarray 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.
reachReachEnvelope | nullattributionarray 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.
readingAskReadingWhat we understood. Check it before trusting the rows.
searchesintegerdefault: 1How many searches this answer took. ≥ 1.
spanstringdefault: The part of your question this answer covers, in your own words. On a single question it is the whole sentence.
warningsarray of stringWhat was widened, capped or could not be applied — in plain language, one entry per thing. Empty means nothing was dropped.
relationshipsRelationshipEnvelope | nullattributionarray 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.
routeRouteWhich question this turned out to be, and therefore which field below holds the answer. relationships — people at one company connected to another. reach — who one named person can reach somewhere. institutions — the employers and schools two companies share, with nobody ranked. compare — two ways in, weighed against each other. fan_out — people, each with what *they* can reach.
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 |
| 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) |