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.

POST/v1/relationships/ask

Ask 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 | null

Page 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.

querystringrequired

The 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-Keystring

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

The 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:

routeThe question it turned out to beWhat is populated
relationshipsPeople at one company connected to anotherrelationships — the list envelope
reachWho one or more named people can reach at one or more companiesreach — the reach envelope
institutionsThe employers and schools two companies share, nobody rankedinstitutions — the shared-institutions envelope
compareTwo ways in, weighed against each othercompare
fan_outPeople, each with what they can reachfan_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 own total and total_is_estimate. Rows are grouped by company in that order, each row's target_index points into targets, and limit applies per company. The envelope's total is the sum of the per-company totals, and it is exact, because one person has one current employer. Read each target's name before 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_as set to that earlier index. That company was reached once, and its rows and total are under the earlier entry.
  • Several people. seeds[] has one entry per named person: their id, and how many people they reach on their own (total, total_is_estimate). Each row's reached_by lists which of them (by person id) can reach that person. The envelope's total is 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.kindMeansWhat to ask for
no_anchorNo company or person was named, so there is nothing to run the question fromName a company, and a second company or a shared employer or school
no_tenant_contextThe 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 isName your company explicitly as the second side
needs_tenant_listThe question works from a list held in your own systems ("our top 20 accounts")Name the companies or people on the list
needs_pre_resolveThe question picks companies out by something no index records: a resemblance, a competitor set, ownershipName the companies, or describe them by industry, size, location, founding year, technology or funding stage
impossible_on_this_corpusThe corpus cannot express the question with any filterAsk a different question; detail names the part that cannot be answered
route_ambiguousThe question reads two ways and neither is safely preferableSay which you meant
compound_over_budgetThe question needs more separate searches than one request runsNarrow it, or ask the parts separately
wrong_routeOn a typed route with a query: the sentence is a different kind of question from the one that route answersSend it to /v1/relationships/ask, or to the route that answers it
compound_unsupportedOn a typed route with a query: the sentence asks two things at once, and the detail names both partsSend 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.
  • stronger indexes into sides and is null when no side is named. That is a real answer, covering both a tie and the case where either count is approximate, which comparable: false states 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_downs and 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.
  • k is how many rows were drilled into (capped, because each drill-down is a full search).
  • skipped names the people in the top k that could not be. Reaching through somebody needs their LinkedIn URL, and a row without one is skipped rather than guessed at.

Response#

compareComparisonEnvelope | null
comparablebooleandefault: true

false 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: overlapping

Applied to both sides. A comparison across two readings of *knows* is not a comparison.

sidesarray of ComparisonSideEnvelope

One per half of your question, in the order you asked.

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

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

source_corpusSourceCorpus

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

urlstring | null

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

breakdownBreakdown | null

Present only when the question asked for a breakdown.

exactbooleandefault: true

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

group_byGroup By
groupsarray of BreakdownGroup
keystring

The value as the index holds it.

kindstring | null

employment or education, for an institution breakdown.

labelstring

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

peopleinteger

Distinct people in this group. ≥ 0.

more_groupsbooleandefault: false

Smaller groups exist and were not returned.

columnsarray of object | null

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

connection_basis"overlapping" | "shared"default: overlapping

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

coverageCoverage | null

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

auditedbooleandefault: false

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

constraints_unmetarray of string

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

criteriaarray of object

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

enumeratedinteger

How many of them this answer carries. ≥ 0.

enumeration_recallnumber | null

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

linkagearray of object

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

linkage_coveragenumber | null

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

linkage_coverage_is_lower_boundbooleandefault: false

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

similarityarray of object

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

truncated_bystring | null

Which limit stopped the enumeration short, when one did.

universeinteger

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

universe_exhaustedbooleandefault: false

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

universe_is_exactboolean

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

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

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

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

Distinct people at the first company.

people_secondintegerdefault: 0

Distinct people at the second company.

metaResponseMeta

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

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

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

credits_chargednumber | null

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

environment"live" | "test"

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

request_idstring
next_cursorstring | null

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

resolved_searchResolvedSearch | null
categoryEntityCategory

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

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

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

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

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

company_domainstring | null
company_namestring | null

Present only with include_company_context.

countrystring | null
currently_at_targetbooleandefault: false

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

departmentstring | null

The same department vocabulary the request filters on.

full_namestring | null
idstring

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

is_alumbooleandefault: false

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

linkedin_urlstring | null
match_reason_countintegerdefault: 0

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

match_typearray of string

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

number_of_contacts_overlapping_tenure_withinteger | null

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

profile_completebooleandefault: true

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

senioritystring | null
titlestring | null
viaarray of ViaInstitution

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

endedstring | null

When it ended. Absent means still there.

idstring

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

kindstring | null

employment — a shared employer — or education.

match_confidencestring | null

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

match_typestring

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

namestring | null
rolestring | null

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

startedstring | null

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

shareShare | null

Present only when the question asked for a proportion.

populationinteger

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

population_is_estimatebooleandefault: false
sharenumber | null

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

spanstringdefault:

The half of your question this side answers, in your own words.

suggestionSearchSuggestion | null

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

categoryEntityCategory | null

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

kind"search" | "agentic"

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

messagestring

One ready-to-show sentence.

totalinteger

≥ 0.

total_is_estimateboolean

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

strongerinteger | null

Index 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 | null

What 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 | null
attributionarray of Attribution
as_ofstring | null
domainstring | null
quotestring | null

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

source_corpusSourceCorpus

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

urlstring | null

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

breakdownBreakdown | null

Present only when the question asked for a breakdown.

exactbooleandefault: true

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

group_byGroup By
groupsarray of BreakdownGroup
keystring

The value as the index holds it.

kindstring | null

employment or education, for an institution breakdown.

labelstring

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

peopleinteger

Distinct people in this group. ≥ 0.

more_groupsbooleandefault: false

Smaller groups exist and were not returned.

columnsarray of object | null

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

connection_basis"overlapping" | "shared"default: overlapping

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

coverageCoverage | null

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

auditedbooleandefault: false

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

constraints_unmetarray of string

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

criteriaarray of object

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

enumeratedinteger

How many of them this answer carries. ≥ 0.

enumeration_recallnumber | null

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

linkagearray of object

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

linkage_coveragenumber | null

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

linkage_coverage_is_lower_boundbooleandefault: false

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

similarityarray of object

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

truncated_bystring | null

Which limit stopped the enumeration short, when one did.

universeinteger

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

universe_exhaustedbooleandefault: false

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

universe_is_exactboolean

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

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

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

source_corpusSourceCorpus | null
drill_downsarray of DrillDownEnvelope

One 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 Attribution
as_ofstring | null
domainstring | null
quotestring | null

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

source_corpusSourceCorpus

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

urlstring | null

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

columnsarray of object | null

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

connection_basis"overlapping" | "shared"default: overlapping

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

coverageCoverage | null

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

auditedbooleandefault: false

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

constraints_unmetarray of string

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

criteriaarray of object

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

enumeratedinteger

How many of them this answer carries. ≥ 0.

enumeration_recallnumber | null

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

linkagearray of object

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

linkage_coveragenumber | null

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

linkage_coverage_is_lower_boundbooleandefault: false

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

similarityarray of object

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

truncated_bystring | null

Which limit stopped the enumeration short, when one did.

universeinteger

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

universe_exhaustedbooleandefault: false

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

universe_is_exactboolean

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

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

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

source_corpusSourceCorpus | null
has_moreboolean
institutionsarray of ReachInstitution

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

idstring

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

kindstring | null

employment — a shared employer — or education.

match_confidencestring | null

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

namestring | null
people_at_targetintegerdefault: 0

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

metaResponseMeta

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

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

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

credits_chargednumber | null

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

environment"live" | "test"

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

request_idstring
next_cursorstring | null

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

person_idstringdefault:

The id in results of the person this hangs under.

resolved_searchResolvedSearch | null
categoryEntityCategory

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

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

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

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

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

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

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

anchor_endedstring | null
anchor_rolestring | null

What the person you asked through did there.

anchor_startedstring | null
inststring
kindstring | null

employment — a shared employer — or education.

match_confidencestring | null
match_typestring

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

namestring | null
overlap_monthsinteger | null

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

their_endedstring | null

Absent means still there.

their_rolestring | null

What the reachable person did there.

their_startedstring | null
idstring

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

linkedin_urlstring | null
match_typearray of string

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

reached_byarray of string | null

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

senioritystring | null
target_indexinteger | null

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

titlestring | null
seedsarray of SeedReach | null

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

idstring

The named person's id.

totalinteger

≥ 0.

total_is_estimatebooleandefault: false
suggestionSearchSuggestion | null

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

categoryEntityCategory | null

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

kind"search" | "agentic"

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

messagestring

One ready-to-show sentence.

targetsarray of ReachTarget | null

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

cidstring | null

The company it resolved to.

domainstring | null
indexinteger

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

namestring | null

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

same_asinteger | null

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

totalintegerdefault: 0

People reachable at this company. ≥ 0.

total_is_estimatebooleandefault: false
valuestring

The company as the question named it.

totalinteger

≥ 0.

total_is_estimateboolean

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

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

Distinct people at the first company.

people_secondintegerdefault: 0

Distinct people at the second company.

kintegerdefault: 3

How 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.

metaResponseMeta

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

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

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

credits_chargednumber | null

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

environment"live" | "test"

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

request_idstring
next_cursorstring | null

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

resolved_searchResolvedSearch | null
categoryEntityCategory

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

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

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

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

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

company_domainstring | null
company_namestring | null

Present only with include_company_context.

countrystring | null
currently_at_targetbooleandefault: false

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

departmentstring | null

The same department vocabulary the request filters on.

full_namestring | null
idstring

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

is_alumbooleandefault: false

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

linkedin_urlstring | null
match_reason_countintegerdefault: 0

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

match_typearray of string

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

number_of_contacts_overlapping_tenure_withinteger | null

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

profile_completebooleandefault: true

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

senioritystring | null
titlestring | null
viaarray of ViaInstitution

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

endedstring | null

When it ended. Absent means still there.

idstring

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

kindstring | null

employment — a shared employer — or education.

match_confidencestring | null

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

match_typestring

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

namestring | null
rolestring | null

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

startedstring | null

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

shareShare | null

Present only when the question asked for a proportion.

populationinteger

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

population_is_estimatebooleandefault: false
sharenumber | null

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

skippedintegerdefault: 0

People 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 | null

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

categoryEntityCategory | null

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

kind"search" | "agentic"

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

messagestring

One ready-to-show sentence.

totalinteger

≥ 0.

total_is_estimateboolean

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

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

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

source_corpusSourceCorpus

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

urlstring | null

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

columnsarray of object | null

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

coverageCoverage | null

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

auditedbooleandefault: false

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

constraints_unmetarray of string

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

criteriaarray of object

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

enumeratedinteger

How many of them this answer carries. ≥ 0.

enumeration_recallnumber | null

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

linkagearray of object

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

linkage_coveragenumber | null

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

linkage_coverage_is_lower_boundbooleandefault: false

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

similarityarray of object

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

truncated_bystring | null

Which limit stopped the enumeration short, when one did.

universeinteger

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

universe_exhaustedbooleandefault: false

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

universe_is_exactboolean

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

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

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

source_corpusSourceCorpus | null
has_moreboolean
metaResponseMeta

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

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

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

credits_chargednumber | null

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

environment"live" | "test"

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

request_idstring
next_cursorstring | null

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

resolved_searchResolvedSearch | null
categoryEntityCategory

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

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

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

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

Distinct people at the first company.

people_secondintegerdefault: 0

Distinct people at the second company.

suggestionSearchSuggestion | null

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

categoryEntityCategory | null

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

kind"search" | "agentic"

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

messagestring

One ready-to-show sentence.

totalinteger

≥ 0.

total_is_estimateboolean

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

truncatedbooleandefault: false

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

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

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

source_corpusSourceCorpus

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

urlstring | null

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

columnsarray of object | null

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

connection_basis"overlapping" | "shared"default: overlapping

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

coverageCoverage | null

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

auditedbooleandefault: false

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

constraints_unmetarray of string

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

criteriaarray of object

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

enumeratedinteger

How many of them this answer carries. ≥ 0.

enumeration_recallnumber | null

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

linkagearray of object

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

linkage_coveragenumber | null

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

linkage_coverage_is_lower_boundbooleandefault: false

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

similarityarray of object

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

truncated_bystring | null

Which limit stopped the enumeration short, when one did.

universeinteger

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

universe_exhaustedbooleandefault: false

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

universe_is_exactboolean

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

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

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

source_corpusSourceCorpus | null
has_moreboolean
institutionsarray of ReachInstitution

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

idstring

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

kindstring | null

employment — a shared employer — or education.

match_confidencestring | null

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

namestring | null
people_at_targetintegerdefault: 0

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

metaResponseMeta

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

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

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

credits_chargednumber | null

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

environment"live" | "test"

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

request_idstring
next_cursorstring | null

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

resolved_searchResolvedSearch | null
categoryEntityCategory

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

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

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

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

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

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

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

anchor_endedstring | null
anchor_rolestring | null

What the person you asked through did there.

anchor_startedstring | null
inststring
kindstring | null

employment — a shared employer — or education.

match_confidencestring | null
match_typestring

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

namestring | null
overlap_monthsinteger | null

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

their_endedstring | null

Absent means still there.

their_rolestring | null

What the reachable person did there.

their_startedstring | null
idstring

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

linkedin_urlstring | null
match_typearray of string

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

reached_byarray of string | null

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

senioritystring | null
target_indexinteger | null

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

titlestring | null
seedsarray of SeedReach | null

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

idstring

The named person's id.

totalinteger

≥ 0.

total_is_estimatebooleandefault: false
suggestionSearchSuggestion | null

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

categoryEntityCategory | null

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

kind"search" | "agentic"

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

messagestring

One ready-to-show sentence.

targetsarray of ReachTarget | null

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

cidstring | null

The company it resolved to.

domainstring | null
indexinteger

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

namestring | null

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

same_asinteger | null

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

totalintegerdefault: 0

People reachable at this company. ≥ 0.

total_is_estimatebooleandefault: false
valuestring

The company as the question named it.

totalinteger

≥ 0.

total_is_estimateboolean

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

readingAskReading

What we understood. Check it before trusting the rows.

searchesintegerdefault: 1

How 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 string

What was widened, capped or could not be applied — in plain language, one entry per thing. Empty means nothing was dropped.

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

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

source_corpusSourceCorpus

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

urlstring | null

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

breakdownBreakdown | null

Present only when the question asked for a breakdown.

exactbooleandefault: true

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

group_byGroup By
groupsarray of BreakdownGroup
keystring

The value as the index holds it.

kindstring | null

employment or education, for an institution breakdown.

labelstring

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

peopleinteger

Distinct people in this group. ≥ 0.

more_groupsbooleandefault: false

Smaller groups exist and were not returned.

columnsarray of object | null

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

connection_basis"overlapping" | "shared"default: overlapping

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

coverageCoverage | null

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

auditedbooleandefault: false

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

constraints_unmetarray of string

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

criteriaarray of object

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

enumeratedinteger

How many of them this answer carries. ≥ 0.

enumeration_recallnumber | null

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

linkagearray of object

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

linkage_coveragenumber | null

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

linkage_coverage_is_lower_boundbooleandefault: false

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

similarityarray of object

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

truncated_bystring | null

Which limit stopped the enumeration short, when one did.

universeinteger

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

universe_exhaustedbooleandefault: false

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

universe_is_exactboolean

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

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

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

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

Distinct people at the first company.

people_secondintegerdefault: 0

Distinct people at the second company.

metaResponseMeta

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

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

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

credits_chargednumber | null

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

environment"live" | "test"

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

request_idstring
next_cursorstring | null

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

resolved_searchResolvedSearch | null
categoryEntityCategory

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

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

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

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

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

company_domainstring | null
company_namestring | null

Present only with include_company_context.

countrystring | null
currently_at_targetbooleandefault: false

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

departmentstring | null

The same department vocabulary the request filters on.

full_namestring | null
idstring

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

is_alumbooleandefault: false

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

linkedin_urlstring | null
match_reason_countintegerdefault: 0

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

match_typearray of string

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

number_of_contacts_overlapping_tenure_withinteger | null

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

profile_completebooleandefault: true

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

senioritystring | null
titlestring | null
viaarray of ViaInstitution

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

endedstring | null

When it ended. Absent means still there.

idstring

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

kindstring | null

employment — a shared employer — or education.

match_confidencestring | null

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

match_typestring

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

namestring | null
rolestring | null

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

startedstring | null

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

shareShare | null

Present only when the question asked for a proportion.

populationinteger

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

population_is_estimatebooleandefault: false
sharenumber | null

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

suggestionSearchSuggestion | null

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

categoryEntityCategory | null

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

kind"search" | "agentic"

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

messagestring

One ready-to-show sentence.

totalinteger

≥ 0.

total_is_estimateboolean

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

routeRoute

Which 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#

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