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.
| Question | Why it matters | A good answer looks like |
|---|---|---|
| What identifiers can I match on? | Name-only matching is the main source of wrong records | Domain, registry number and country, with name as a fallback |
| Does a match come with confidence or candidates? | You need to route weak matches to review | A 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 selectively | Each value says where it came from and when it was observed |
| How is “no data” represented? | Unknown and empty must stay different | Distinct states for not found, not applicable and not requested |
| Is there a bulk or asynchronous mode? | Backfills should not run through a synchronous lookup loop | Batch jobs with a job ID, plus a webhook or a polling endpoint |
| Can I get changes only? | Pulling everything again wastes budget | A change feed or a “modified since” filter |
| What may I store, and for how long? | Terms often limit caching and retention | Written terms covering storage, display and what happens at contract end |
| Are errors distinguishable? | Retry logic depends on it | Separate 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_intopointer 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.