---
title: "Research"
method: POST
path: "/api/facade/research"
tags: ["facade"]
---

# Research

`POST /api/facade/research`

## Request

- Base URL: `https://api.vruum.ai`
- URL: `https://api.vruum.ai/api/facade/research`
- Auth: `HTTPBearer` (a scheme the document does not define)

Run prospect/company research and save the results — the consolidated research tool.

- start_account_brief: explicitly delegate one account brief to Vruum's
  durable worker, for an objective or for the account alone. First fetch
  research_work_list with company_id (and objective_id for an objective
  brief); inspect existing work and use the returned scope planning_version.
  Payload requires {company_id, request_id (UUID), source_mode
  (saved_public or refresh_public)}, plus {objective_id,
  expected_planning_version (positive integer)} for an objective brief.
  Without objective_id the brief is framed by the workspace's own profile
  and live offers, and any workspace member may request it.
  Search and model work runs after this call and survives leaving the page.
  A matching active or unresolved job is reused; retain request_id on retries.
  Start only after the user requests this exact scope. A model suggestion
  is not approval. The native assistant uses its write approval card;
  an external harness must obtain its user's authorization before calling.
  saved_public uses permitted saved public sources and business configuration.
  Both modes add the account's captured mail (newest 12 messages from the
  last 180 days whose participants share the account's domain). The brief
  may propose commitments from that mail (owed_by us/them, due_on) as open
  Claims; record a verdict on one only when the user asks.
  refresh_public also authorizes one public-domain search and live verification
  of up to three company pages before synthesis. Search excerpts are not evidence.
  Retain the exact source_mode on retries; existing work with another mode
  conflicts. Failed or partial refreshes remain visible in source coverage.
  Neither mode grants other private source access, transcript persistence, browser
  credential storage, or external-harness execution handoff.
  Poll fetch research_work; read Claims and citations with fetch account_brief,
  both id=returned Action UUID. The worker automatically recovers saved
  provider response IDs and terminal receipts after interruption.
  A missing or unavailable provider response needs operator investigation;
  manual repair is unsupported and a fresh request_id cannot authorize replay.
- cancel_account_brief: id=Action UUID; payload={expected_version} from
  the current saved work. Stops continuation and requests cancellation of
  pending saved model responses. A dispatched search or model call may still
  finish; cancellation does not prove the provider stopped.
  Both account-brief writes accept one request only, without items or batches.
- preview_gaps: READ ONLY assignment, no research or spend. id=objective UUID;
  payload={subjects:[{type, company_id}], question_ids?, stage?,
  use_public_web?:true, use_history?:false}. Each subject names its type
  and exactly that type's ids: company {company_id}, person {person_id},
  person_at_company {person_id, company_id}, deal {deal_id}. Up to 20
  subjects and 40 unique gaps. Reuses current account answers across people.
  Equivalent reviewed fact definitions also reuse original observations across
  objectives when their subject, schema, sources, quality and freshness match.
  Reused answers retain original evidence IDs; do not save duplicate copies.
  Read gaps, question definitions, source permissions and limits before choosing work.
- stop_gaps: id=objective UUID, payload={action_id}. Stop future calls and
  answer writes. Already sent provider work can finish.
- propose_gaps: same input; saves an Action without executing research.
- approve_gaps: id=objective UUID, payload={action_id}. Only after the user
  approves that exact saved assignment. Starts bounded Vruum research.
  Poll fetch research_run id=action UUID. Never repeat approval to retry.
  For research on YOUR compute, use preview_gaps, fetch research_sources,
  your permitted tools, then save_answer. Do not approve Vruum research as
  a substitute for external research. Your harness pays its own compute.
  Use the exact subject, question ID and definition fingerprint from the
  assignment. Public URL excerpts remain reported. Never assert customer
  confirmation without an explicit operator attestation.
- save_answer: record one answer for a saved objective research question
  (id = objective UUID; payload = {idempotency_key, question_id,
  definition_fingerprint, subject (one preview_gaps subject),
  state: answered|unknown|unavailable, value, source: {kind, reference?,
  excerpt, observed_at}, note?, supersedes_evidence_id?}). Read the question
  and answer_schema through fetch research_answers first. Source kinds:
  operator_report, public_url (reported only), activity, evidence, meeting,
  customer_confirmation (explicit operator attestation). Existing references
  must match the tenant, subject, excerpt and original observation time.
  Exact retries retain their UUID; changed content needs a new UUID.
  Corrections explicitly supersede one observation. Answers never authorize
  outreach. Generic save_evidence/save_claim cannot write research answers.

Lookup and enrichment actions (results are returned, not stored unless
noted). Synchronous lookups without a saved result (find_email,
linkedin_fetch, enrich_company, careers_parse) are on research_lookup.
- find_linkedin: locate a person's LinkedIn URL (id = person UUID;
  `payload` optional {name_override?, company_override?, auto_save?} —
  auto_save stores a high-confidence match).
- enrich_account: acquire a bounded first-party evidence bundle for a
  tenant-visible account (id = canonical company UUID). The response also
  carries canonical company fields, cached shared research, and freshness
  metadata for the harness to combine with its own web and MCP sources.
  Persist sourced conclusions with save_company. research_lookup
  action=enrich_company remains the domain/page website-fetch primitive.
- enrich_person: enrich each person according to missing contact and profile
  fields (id = person UUID, or an array of up to 100;
  `payload` optional {force_refresh?}). Returns a job_id; poll it with
  fetch type=job. Outcomes report a stable reason code, and jobs with any
  errored outcome terminate as completed_with_failures. Successful outcomes
  report readiness=email_ready, incomplete, or manual_review plus the
  canonical employer binding.
  Vruum applies separate monthly profile and Hunter-credit soft limits.
  Harness web search and other MCP tools remain complementary for deeper
  research. The legacy enrich_profile name is an alias.
- enrich: canonical scoped enrichment. It inspects each person and runs
  only the needed profile or verified-email work. Pass `payload` with exactly one of
  `{person_ids: [...]}`, `{objective_id: ...}`, or `{list_id: ...}`.
  Returns a job_id; poll it with fetch type=job. The enrich_emails action
  remains as a compatibility alias for the same scoped planner.

Save actions (write research rows):
- save_person: UPDATE research on an EXISTING person — pass the person UUID
  as `payload.person_id` (preferred; refreshes researched_at and the
  research fields in place), or matching email/linkedin anchors. Cannot
  create: new prospects go through manage_person action=save_discovered
  with `payload.person` + `payload.assessment`. Never pass the person UUID
  as `id` — this action takes no id.
- save_company: atomically patch sourced public company research. Pass
  `payload={company_id?, allow_create?, idempotency_key, name, industry, person_id?, website?, linkedin_url?,
  company_summary?, company_stage?, current_priorities?, funding_data?,
  growth_metrics?, sources_by_field}`.
  `industry` is required and must be one CanonicalIndustry value:
  Agriculture, Construction, Consumer Goods, Consumer Services, Education,
  Energy & Utilities, Financial Services, Government, Healthcare,
  Hospitality & Travel, Legal Services, Manufacturing, Marketing & Advertising,
  Media & Entertainment, Nonprofit, Professional Services, Real Estate,
  Retail & E-commerce, Software & Technology, Staffing & Recruiting,
  Telecommunications, Transportation & Logistics, Wholesale & Distribution,
  or Other. Provider labels and Unspecified are rejected.
  Omitted research fields are preserved; explicit null clears. When
  researching a specific person's employer,
  ALWAYS pass their UUID as `payload.person_id` — the response's
  `person_link` confirms the write landed on that person's linked company
  (repointing an anchorless duplicate link when needed); a `mismatch`
  status means the person's touches will NOT see this research, and a
  `conflict` status is a transient race — replay the identical payload
  (person_id is exempt from the idempotency hash, so adding it to a
  replay is the supported repair).
  For a researched-prospect save, save_company is also the company identity resolution step.
  For an existing account, ALWAYS pass payload.company_id, including the ID
  returned by enrich_account. Updates never create or merge a company.
  Otherwise call save_company with allow_create=true plus name and website
  and/or linkedin_url. Never combine company_id with allow_create=true.
  Conflicts require identity reconciliation; never retry by dropping the ID.
  Identity selection, missing native identifiers, and research commit together.
  Call it even
  with no new research fields, and capture the returned company_id for
  manage_person action=save_discovered. A company name alone is not a save identity.
  `sources_by_field` must contain exactly every
  supplied non-null research field, with entries
  `{url, title?, observed_at}`. Example:
  `{"allow_create":true,"idempotency_key":"pipeline/example.com/save-v1","name":"Example",
  "industry":"Software & Technology","website":"https://example.com",
  "company_summary":"Example sells …",
  "sources_by_field":{"company_summary":[{"url":"https://example.com/about",
  "title":"About","observed_at":"2026-07-30T20:00:00Z"}]}}`.

Hypothesis-ledger actions (VRU-840 — beliefs with scoreboards, never prose):
- save_claim: record one falsifiable belief. `payload` = {statement,
  scope ({person_id|company_id|deal_id|objective_id|segment|tenant}),
  origin (diagnostic|system|operator|pin|experiment),
  invalidating_conditions?: [..], evidence?: [{kind, payload, source,
  method?, staleness_class (durable|slow|fast|volatile),
  trust? (first_party|untrusted_capture|third_party), stance?
  (supports|contradicts), weight?, person_id?|company_id?|
  deal_id?|objective_ref?|tenant_scoped? (accounts are the tenant-company
  pair — link company_id)}]}. Confidence is derived from the
  linked evidence and cannot be asserted; identical evidence payloads
  dedupe. `origin='diagnostic'` requires non-empty evidence and
  invalidating_conditions, so a diagnostic cannot save unsupported prose.
  Positioning-diagnostic scopes (`scope.surface='positioning_diagnostic'`)
  always need evidence + invalidating_conditions (any origin) and must
  carry tenant=true, a `dimension`, a trimmed `segment`, and — for
  icp_reachability / price_plausibility / motion_fit only — a cohort
  `objective_id` anchor plus a structured
  `scope.test` ({version:'viability_test.v1', metric, comparator,
  threshold, min_sample>=20, window_days}) that the viability loop
  evaluates from spine outcomes. Returns the claim with its full
  scoreboard. Use `items` to save a batch (e.g. a positioning
  diagnostic's claim set).
- save_evidence: mint one observation with provenance, optionally linking
  it to an existing claim (`payload.claim_id` + stance/weight).
- record_claim_verdict: append a verdict (id = claim UUID; `payload` =
  {status, basis, confidence?, note?, experiment_id?, superseded_by?}).
  basis='causal' requires experiment_id — the laboratory honesty rule.
  A `retired` verdict may name `superseded_by` (the LIVE replacement
  claim's UUID) so readers can follow a retired scope to its successor;
  any non-retired verdict clears the pointer.
Read claims with search type='claims' and fetch type='claim'.

Batch research returns per-item results (partial failure is reported per
item, never silent). The facade's backward-compatible request cap is 100;
enrich_account accepts up to 10 accounts per request. Operators should execute the smaller policy waves
documented by pipeline-fill (10 companies or 5 people at a time) so
progress and retries stay bounded. find_linkedin also takes person-id
arrays. Payloads are validated by the underlying route's own schema.

## Request body

- WriterRequest: Uniform request body for every facade writer.
  - `action` string, required: The operation to perform — see the tool description for the action vocabulary and each action's id/payload semantics.
  - `id` union: Target entity id, or an ARRAY of ids to apply the same action+payload to each (max 100; see the tool description for which entity the id names per action).
    - string
    - string[]
  - `payload` object, nullable: Action-specific fields, validated by the underlying route's own schema (a bad field 422s with that route's precise errors). See the tool description per action.
  - `items` object[], nullable: For create-like actions only: an array of per-item payloads processed in order with per-item results (max 100). Mutually exclusive with `id` and `payload`.

## Response `200`

Successful Response

- unknown

## Other responses

- `422`: Validation Error

---

[API](https://skmtc.dev/vruum/apis/vruum-public-api.md) · [All operations](https://skmtc.dev/vruum/apis/vruum-public-api/llms.txt) · [OpenAPI document](https://skmtc.dev/vruum/apis/vruum-public-api/revisions/b8ada111c30a?raw)
