HubSpot's built-in data enrichment stops updating a property as soon as a person changes it. "If a user or another system edits an enriched property value, continuous enrichment stops enriching that property," according to HubSpot's documentation.
When you enrich CRM records by calling an API yourself, nothing stops your code from overwriting a value a person changed. You have to build that check. This guide covers how to decide which fields enrichment may change, how to confirm you have the right person or company before writing anything, what to do with nulls and conflicts, and when to refresh.
The example at the end uses a live Company Lookup response for Stripe and shows the CRM changes and change log it produced.
Which CRM fields enrichment changes
Before writing any rules, list the CRM fields an enrichment run can change and what depends on each one:
- Routing fields like country, state, and employee count. If your CRM assigns accounts to reps by region or company size, a wrong value in one of these fields can reassign the account to a rep who doesn't cover it.
- Qualification fields like industry, employee count, and company type. Lead scoring and target-account lists use them, so a wrong value can change a lead's score or drop an account from a list.
- Outreach fields like job title and company. Email sequences insert them into messages, so a wrong title or company ends up in an email a prospect reads.
Enrichment should never write to record owner, lead source, lifecycle stage, or notes. Your team fills these in, for example by deciding who owns an account or noting where a lead came from, and an enrichment provider has no data for them. Leave them out of your field mapping so no enrichment run can change them.
Confirm identity before you write anything
Before you copy anything from a response into a CRM record, make sure the response is about the same person or company. If the lookup matches the wrong one, every value you write is wrong, and nothing in the CRM will show it.
Enrich Layer's Person Lookup needs a first name and a company domain or name. Adding last name, job title, and location improves the match. Company Lookup takes a domain or a company name. Both return the closest match they find. If you send only a first name and a domain, you still get a result back, and it may be the wrong person. Person Lookup also returns scores from 0 to 1 that show how closely the result matches each input:
{
"url": "https://www.linkedin.com/in/example-profile",
"name_similarity_score": 1.0,
"company_similarity_score": 1.0,
"title_similarity_score": 0.0,
"location_similarity_score": 0.0
}
No title or location was sent with this request, and both of those scores came back as 0.0. Enrich Layer doesn't recommend a minimum score, because the right minimum depends on the workflow. A wrong match on a record that feeds a sales email does more harm than one on a record used only for reporting. The closer a score is to 1, the more you can trust the match.
Choose a minimum score for each workflow. Write matches above it to the CRM automatically, and send matches below it to a person to check.
Before writing anything, also check how your pipeline handles a lookup that finds nothing and how your CRM handles duplicates:
- A lookup with no match still returns HTTP 200. The body comes back as
{"url": null}, and the call is charged. A pipeline that only checks status codes will treat it as a success. Check for a null result before doing anything else. On Person Lookup,similarity_checks=skipavoids the charge when nothing is found, but then the lookup returns its closest match without checking how similar it is to your input. - Your CRM's duplicate checks may work differently when records come in through the API. In Salesforce, a duplicate rule that shows users a warning blocks the save instead when the record comes through the API, and records saved in the same batch aren't checked against each other (Salesforce Help). If your pipeline creates new records as well as updating existing ones, test it on a small batch first.
Once a record is matched, store its LinkedIn profile URL in a dedicated field. Later refreshes can then call the profile endpoint directly and skip the lookup. Enrich Layer's Salesforce package uses the same approach.
Give every field an owner and a write rule
Give each field one write rule:
- Fill: write only when the CRM field is blank.
- Replace: overwrite when the value differs and nobody has edited the field since your last enrichment write.
- Review: never write automatically. Save the old and new values where a person can approve or reject the change.
- Preserve: enrichment never writes the field.
The rule you choose also decides who owns the field. With Preserve, only your team changes it. With Replace, enrichment keeps it up to date. With Fill and Review, enrichment can fill or suggest a value, but your team has the final say.
Replace is the only rule that overwrites an existing value, so it needs a way to tell whether a person changed the field. The simplest way is to save the last value your pipeline wrote to each field. If the CRM value no longer matches it, a person or another system changed it. Treat that field as Preserve until someone on your team decides otherwise.
Your CRM may already track who changed each field. HubSpot keeps a history for each property with the date and source of every change (HubSpot), and its API returns that history through the propertiesWithHistory parameter (HubSpot developer docs). Salesforce Field History Tracking covers up to 20 fields per object and keeps 18 months of history (Salesforce Help), so track the fields that use Replace first.
HubSpot's own enrichment settings offer three overwrite rules per property (HubSpot):
- "Fill empty values only"
- "Fill empty values and overwrite existing values"
- "Do not fill any values"
The tables below set a rule for each field Enrich Layer returns.
Contact and lead fields
| CRM field | Enrich Layer source | Rule | When values conflict |
|---|---|---|---|
| First and last name | first_name, last_name | Preserve (Fill on new records only) | Keep the CRM value. Your CRM may use names to find duplicate records. |
| LinkedIn profile URL | url from Person Lookup | Fill, then Preserve | A different URL from a later lookup goes to review. It may point to a different person. |
| Job title | title on the current entry in experiences that matches the CRM account | Fill; Review if different | Reps often correct titles by hand, and email sequences insert them into messages. |
| Account (current company) | Current entries in experiences | Review | If no current role matches the CRM account, the person may have changed jobs. Send it to the account owner to check. |
| Country, state, city | country, state, city | Fill | Keep the CRM value. Expect partial data. City can be null when country is set. |
| Work email | Work Email Lookup, a separate endpoint | Fill | Never replace an existing email. Your CRM may use email to find duplicate records. |
| Owner, lead source, lifecycle stage, notes | None | Preserve | Leave out of the mapping. |
Account fields
| CRM field | Enrich Layer source | Rule | When values conflict |
|---|---|---|---|
| Account name | name | Fill | Keep the CRM value. It often holds a legal or internal name. |
| Website | website | Fill, then Preserve | A different domain goes to review. It can mean a rebrand, an acquisition, or a wrong match. |
| Employee count | company_size_on_linkedin | Replace | Log old and new values. Skip changes smaller than a threshold you choose. |
| Size range | Calculated from the employee count | Replace | Don't copy company_size into a range picklist. The range in that field doesn't always match the employee count. |
| Industry | industry, through your mapping table | Fill | Unmapped values go to review, not to "Other." |
| HQ country and state | hq.country, hq.state | Fill | A different HQ goes to review. Keep it separate from the billing address, which finance may own. |
| Founded year | founded_year | Fill | Keep the CRM value. |
| Company type | company_type, through your mapping table | Fill | Keep the CRM value. |
Industry comes back as one of 487 fixed values, such as "Technology, Information and Internet." Your CRM picklist almost certainly uses different labels, so build the mapping table before the first run and treat anything unmapped as a review item.
Nulls, empty lists, and timestamps
A null in an Enrich Layer profile means the profile had no public value for that field at the time of the check. The value was either never filled in or isn't shown. A person can hide a field they had filled in before, so a null tells you nothing about whether the CRM value is wrong.
Never let a null clear a CRM value. List fields work the same way and come back as an empty list ([]).
Other fields come back empty because you didn't ask for them. On the Company Profile endpoint, funding rounds, exits, acquisitions, categories, and the extra block are add-ons that default to off. Unless your request includes funding_data=include, an empty funding_data means you didn't request it.
Every profile also carries a meta block with a timestamp:
"meta": {
"thin_profile": false,
"last_updated": "2026-08-26T00:00:00Z"
}
last_updated is when the profile was last checked, which isn't necessarily when any field changed. Work experience, education, skills, and profile pictures also keep their last known values when a later check comes back empty. That means a job title can be older than the profile's last_updated date. A recent date doesn't confirm that the current role was just verified, which is one more reason title changes go to review.
Use last_updated to find profiles that haven't been checked recently. To decide whether to overwrite a field, check whether a person changed it since your pipeline last wrote it. When thin_profile is true, the profile doesn't contain enough data to count as a full profile, so skip all writes for that record.
A person can also have more than one current role, such as a full-time job and an advisory position. Pick the entry in experiences whose company matches the CRM account, not the first current entry in the list.
Set refresh cadence by field and trigger
Some fields change more often than others, and every refresh costs credits. Industry, founded year, and headquarters rarely change, so a scheduled sweep once or twice a year is enough.
Employee count changes more often and is used to assign accounts to reps, so refresh it quarterly and before any territory or tier reassignment.
Refresh a contact right before its fields get used: before a sequence enrolls them, before a lead is routed, or before a score recalculation that decides who gets called. That way you spend credits only on records someone is about to work.
On the refresh call, use_cache=if-recent asks for a profile no older than 29 days, on a best-effort basis. If you'd rather get an error than an older copy, add fallback_to_cache=never. The request then returns an error when it can't return a profile checked in the last 29 days.
| Field | Refresh trigger |
|---|---|
| Job title, account link | Before sequence enrollment or routing |
| Employee count, size range | Quarterly, and before territory or tier changes |
| Industry, HQ, founded year, company type | Once or twice a year |
| LinkedIn profile URL, website | Only when blank |
Worked example: one account and one contact
The account half of this example uses a live Company Lookup response for Stripe from October 7, 2026. The CRM record it updates is a made-up starting point. The contact example is made up, but its fields match the structure of a real Person Lookup response.
The account
The CRM record before enrichment:
| Field | CRM value | Last changed by |
|---|---|---|
| Account name | Stripe | Rep |
| Website | stripe.com | Rep |
| Industry | Financial Services | Rep |
| Employee count | 8,000 | Enrichment pipeline (unchanged since) |
| Size range | 5,001โ10,000 | Enrichment pipeline (unchanged since) |
| HQ country | (blank) | |
| Founded year | (blank) | |
| Company type | (blank) |
The request:
curl -G "https://enrichlayer.com/api/v2/company/resolve" \
-H "Authorization: Bearer $ENRICHLAYER_API_KEY" \
--data-urlencode "company_domain=stripe.com" \
--data-urlencode "enrich_profile=enrich"
An excerpt of the response:
{
"url": "https://www.linkedin.com/company/stripe",
"profile": {
"name": "Stripe",
"website": "https://stripe.com",
"industry": "Technology, Information and Internet",
"company_size_on_linkedin": 17145,
"company_type": "PRIVATELY_HELD",
"founded_year": 2010,
"hq": { "country": "US", "state": "California", "is_hq": true },
"meta": { "thin_profile": false, "last_updated": "2026-08-26T00:00:00Z" }
}
}
The URL isn't null and thin_profile is false, so the record passes the identity and quality checks. Here's what the account rules do with each field:
| Field | CRM before | From the response | Rule | Result |
|---|---|---|---|---|
| Account name | Stripe | Stripe | Fill | No change |
| Industry | Financial Services | Technology, Information and Internet | Fill | Kept. A rep set it, and the CRM value isn't blank. |
| Employee count | 8,000 | 17,145 | Replace | Updated. The pipeline wrote 8,000 and nobody changed it since. |
| Size range | 5,001โ10,000 | Calculated as 10,001+ | Replace | Updated |
| HQ country | (blank) | US | Fill | Filled |
| Founded year | (blank) | 2010 | Fill | Filled |
| Company type | (blank) | PRIVATELY_HELD, mapped to "Private" | Fill | Filled |
The rep chose "Financial Services" for industry, and the response says "Technology, Information and Internet." Both are reasonable labels for Stripe. The Fill rule keeps the rep's value and logs the response value, so a reviewer can still see it.
The contact
The CRM has Dana Reyes at Northwind Analytics, a fictional company. Her title is "Head of RevOps," which a rep edited on September 2, 2026. Her city is Denver and there's no stored profile URL.
A Person Lookup with her first name, last name, and company domain returns a name score of 1.0 and a company score of 1.0, so the match is above the minimum score. The profile shows two current roles: "Director of Revenue Operations" at Northwind Analytics since 2023, and an advisory role at another company since 2025. The city field is null.
| Field | CRM before | From the response | Rule | Result |
|---|---|---|---|---|
| LinkedIn profile URL | (blank) | Returned by the lookup | Fill | Filled |
| Job title | Head of RevOps | Director of Revenue Operations (the Northwind entry) | Review | Sent to review. A rep edited the title after the pipeline last wrote it. |
| Account | Northwind Analytics | Current Northwind role found | Review | No change. The current role matches the account. |
| City | Denver | null | Fill | Kept. A null never clears a value. |
The change log
Every decision gets a line in the log, including the values that weren't written:
{"record": "account/stripe", "field": "employee_count", "before": 8000, "after": 17145, "rule": "replace", "result": "updated", "profile_last_checked": "2026-08-26"}
{"record": "account/stripe", "field": "industry", "before": "Financial Services", "candidate": "Technology, Information and Internet", "rule": "fill", "result": "kept", "reason": "rep_value"}
{"record": "account/stripe", "field": "hq_country", "before": null, "after": "US", "rule": "fill", "result": "filled"}
{"record": "contact/dana-reyes", "field": "title", "before": "Head of RevOps", "candidate": "Director of Revenue Operations", "rule": "review", "result": "sent_to_review", "reason": "edited_by_rep_2026-09-02"}
{"record": "contact/dana-reyes", "field": "city", "before": "Denver", "candidate": null, "rule": "fill", "result": "kept", "reason": "null_response"}
Anyone can read the log to see why a field changed or didn't. It also stores the last value the pipeline wrote to each field, which the Replace rule checks against on the next run.
Measure what the rules changed
After each run, count the outcomes per field: filled, replaced, sent to review, and kept. A field where nearly every outcome is "kept" isn't getting much from enrichment. A field that sends many changes to review needs a clearer rule, such as a higher minimum match score.
These numbers show whether a rule is set correctly:
- Review acceptance rate per field. If reviewers approve almost every title change sent to review, consider switching the field to Replace. If they reject most of them, raise the match threshold or leave the field on Fill.
- Edits after enrichment. Count how often a person changes a field within a few weeks of an enrichment write. If reps keep correcting a field after enrichment writes to it, change its rule to Fill or Review.
Also count how many accounts changed territory, tier, or lead score because of an enrichment write, and check a sample of them by hand before the next run.
Where the Salesforce package fits
Enrich Layer's Salesforce package writes only to its own Enrich Layer fields on Contacts, Leads, and Accounts, never to standard Salesforce fields. Re-enriching replaces those values with newer ones, including manual edits, but it never clears a field when the API returns nothing.
Bulk runs, auto-enrich, and the scheduled job skip records that are already enriched, so values are only overwritten when someone clicks Re-Enrich or a Flow action runs. If your admin builds a Flow that copies those values into standard fields like Title or Industry, that Flow is where the rules in this guide apply. The package walkthrough covers identifiers, bulk runs, and failure handling.
Try it on your own records
Pick 50 records from your CRM and run them through Person Lookup and Company Lookup. Apply the tables above in dry-run mode: write the change log but don't write to the CRM. Then read the log. The rows sent to review or kept will show you which rules to change before you let the pipeline write to the CRM.
If you're testing data quality at the same time, How to properly test and evaluate a data enrichment dataset covers sample design. If you're not sure whether you need a lookup or a search, Search API vs. enrichment API covers that choice. To run the requests above, start with the quickstart and a free account.
