Skip to content
Resource library
GOCUX / Articlesresearch

Enrichment APIs and a company database that stays correct

How to choose an enrichment API, store company data as sourced observations, resolve entities and conflicts, and refresh values so the database stays correct.

Plugging an enrichment API into your product takes an afternoon. Keeping the company records it produces correct for a year is a different project. Values go stale, two sources disagree, a subsidiary gets merged into its parent, and a field someone fixed by hand is quietly overwritten by the next sync. This guide is for developers building that layer, often as something an agent or an AI feature reads from. It covers choosing the API, storing data so it can be audited, resolving entities and conflicts, refreshing without paying twice, and where human review belongs. If you need the basics first, start with what data enrichment is.

Why enriched company data drifts away from the truth

Most enrichment problems are not caused by a bad vendor. They come from a storage design that throws away the information you need to stay correct. The same failure patterns show up again and again:

  • Last write wins. Each sync overwrites the previous value, so you cannot tell what changed, when, or why.
  • Provenance per record, not per value. A record says it was enriched by some provider, but the country, headcount and industry came from different places of different ages.
  • Bad matching. A lookup by company name returns a different company with a similar name, and every field after that is confidently wrong.
  • Null means three things. “The provider has no value”, “we never asked” and “the company has no such thing” collapse into one empty cell.
  • Opaque scores. An intent or fit score arrives with no inputs, so nobody can explain or dispute it.
  • Snapshots that never expire. Data bought once stays in the database long after it stopped being true, with nothing marking its age.

Each of these is fixable in your own schema, whichever provider you choose.

Questions to ask an enrichment API before you integrate

Vendor comparisons tend to focus on coverage. For a database that has to stay correct, the shape of the response matters just as much.

QuestionWhy it mattersA good answer looks like
What identifiers can I match on?Name-only matching is the main source of wrong recordsDomain, registry number and country, with name as a fallback
Does a match come with confidence or candidates?You need to route weak matches to reviewA score or a ranked candidate list, not a single silent pick
Is there a source and date per field?Without them you cannot resolve conflicts or refresh selectivelyEach value says where it came from and when it was observed
How is “no data” represented?Unknown and empty must stay differentDistinct states for not found, not applicable and not requested
Is there a bulk or asynchronous mode?Backfills should not run through a synchronous lookup loopBatch jobs with a job ID, plus a webhook or a polling endpoint
Can I get changes only?Pulling everything again wastes budgetA change feed or a “modified since” filter
What may I store, and for how long?Terms often limit caching and retentionWritten terms covering storage, display and what happens at contract end
Are errors distinguishable?Retry logic depends on itSeparate signals for bad input, not found, rate limited and provider failure

Then run a trial on a golden set: a sample of companies your team knows well, including small ones, subsidiaries and companies in the regions you care about. Check every returned field by hand and count right, wrong, stale and missing. A provider that honestly says unknown is often more useful than one that fills every field. For a broader view of source types, see choosing data sources for company research.

Store observations, then derive the answer

The single most useful design decision is to stop storing “the” value of a field. Store every observation you receive, append-only, and compute the current value from them. Observations are cheap; lost history is expensive.

companies
  id, primary_domain, legal_name, country, status, merged_into

company_identifiers
  company_id, kind (domain | registry | provider_id), value, verified_at

observations                       -- append-only
  id, company_id, field, value,
  source_type (provider | registry | website | person | agent),
  source_ref, observed_at, retrieved_at,
  evidence_text, confidence, ingest_job_id

resolved_values                    -- derived, rebuildable
  company_id, field, value, chosen_observation_id,
  state (confirmed | inferred | unknown | in_review), resolved_at

A few useful properties fall out of this design. You can rebuild resolved values whenever your rules change. You can answer “where did this come from?” by following one foreign key. And a correction by a person is simply another observation, with source_type = person, which your rules can rank above everything else.

Keep observed_at and retrieved_at apart. The first is when the fact was true according to the source; the second is when you fetched it. A provider may hand you, today, a headcount it last saw a long time ago.

Entity resolution: get the key right first

Every field hangs on the company identity, so mistakes here multiply. Rules that hold up:

  • Normalize domains before matching: lowercase, strip www. and paths, follow redirects, and record the redirect target as a separate identifier instead of replacing the original.
  • Treat registry numbers as the strongest key where they exist, and store the country with them, because a number is only unique within its own registry.
  • Never merge two records on name similarity alone. Require a shared domain or registry number, or send the pair to review.
  • Model parents and subsidiaries as a relationship, not a merge. A regional entity with its own domain is often the account your team actually sells to.
  • Make merges reversible. Keep the merged record with a merged_into pointer so a wrong merge can be undone without losing data.

Conflict rules you can explain

Once several sources feed the same field, they will disagree. Decide how that is settled, in writing, per field. A rule set short enough to read beats a clever scoring function nobody can explain.

field: hq_country
  1. person-entered value, unless newer registry evidence contradicts it -> review
  2. official registry
  3. company website (imprint, contact page)
  4. enrichment provider
  tie or contradiction between 2 and 3 -> review

field: headcount_range
  newest observation wins if within one band of the current value
  jump of more than one band -> review
  older than the field's freshness window -> state = unknown

Two habits keep conflict handling sane. First, a value entered by a person is never overwritten automatically; a contradicting observation opens a review item instead. Second, the losing observation is kept. When a rule turns out to be wrong, you want the alternative still on file.

Refreshing without paying twice for the same answer

Enrichment calls usually cost money or quota, so refresh is a scheduling problem as much as a data problem.

  • Freshness windows per field class. Identity fields can wait a long time between checks; headcount and technology need a shorter window; events should be appended as they arrive, never refreshed in bulk.
  • Refresh what is in use. Enrich an account again when it enters an active workflow, not on a calendar for the whole table.
  • Cache raw responses with their retrieval date, within what the provider's terms allow, so reprocessing does not mean buying again.
  • Use idempotency keys and job IDs for bulk work, so a retried job does not create duplicate observations.
  • Back off on rate limits and treat “provider unavailable” as its own state. Never write a blank because a call failed.
  • Prefer change feeds where the provider offers them; they turn a full refresh into a small diff.

Give each job a budget and stop with a clear message when it is reached, rather than learning from the invoice how far a backfill went.

Review where it changes outcomes

Reviewing every value does not scale, and reviewing none is how databases rot. Route only the cases where a mistake is expensive: weak entity matches, proposed merges, conflicts between authoritative sources, changes to values a person entered, and large jumps in sized fields. The review item should show the current value, the proposed value, the evidence for each and the rule that flagged it, so a decision takes seconds. Record who decided what, including rejections, so the same conflict is not raised again next week. The companion guide on enriching CRM accounts field by field covers what that review looks like from the sales side.

The long tail: agent research with evidence

Every provider has gaps: small private companies, newly founded ones, firms outside the regions it covers well. Many teams hand that long tail to an agent that reads company pages and public records. That works if the agent returns observations in the same shape as everything else, with a source and a supporting passage per value, and says unknown when it found nothing. It fails when the agent produces fluent guesses that look exactly like confirmed data.

This is where GoCux is relevant, and also where its limits matter. GoCux does not search or crawl the open web and is not an enrichment provider. Web pages reach it only through an authorized browsing or export step, and keep their page reference and retrieval date once imported. From material you supply, Extraction returns each value together with the passage behind it and leaves a missing value as unknown. FindAll turns matching criteria and columns into a table in which each row carries its own evidence, and it reports where coverage ran out. Exporting that data out of the workspace pauses for a person, with the files in scope listed. The output maps cleanly onto an observation table: value, source, evidence, state. For why a missing answer should stay visible, see missing evidence is an open question.

Checklist before you scale the pipeline

  • The provider was trialled on a golden set of your own companies and every field was checked by hand.
  • Observations are append-only and carry source, observed date, retrieval date and evidence.
  • Unknown, not applicable and not requested are distinct states, and failed calls never write blanks.
  • Entities are matched on domain or registry number, merges are reversible, and subsidiaries are relationships.
  • Each field has a written conflict rule; values entered by people are never overwritten automatically.
  • Refresh is driven by freshness windows and active use, with a budget per job.
  • Review covers weak matches, merges, conflicts and overrides, and every decision is recorded.
  • You know what the provider's terms let you store, and what happens to it when the contract ends.

Bring this workflow into your workspace.

Set the scope. Review the evidence. Approve the steps that need it.

Open the workspace

Keep exploring