Pre-Black Friday: 20% off, for as long as you stayEARLYBFon Solo and AgencyEnds Nov 28Start free

How-to guide

How to Fix Messy CRM Data With Enrichment (8 Tools Compared)

Audit the gaps, pick a tool, enrich a sample, size the full refresh, write it back. Real requests and a cost you can check.

Get your free API key →Free to start. No credit card. 1,000 records to spend whenever you like.

Your CRM data is a mess because contacts age and nobody owns the fix. The cure is the same every time: find the gaps, enrich from one source, write back only what you trust. These 8 enrichment tools all do the middle step. The recipe below uses LeadOcean for the requests, because every call is shown with real output.

TL;DR

  • Export your contacts, count the gaps (missing email, stale title, no company domain), then enrich only those rows.
  • A 25-row test takes under an hour and spends about 25 LeadOcean records. A person who is not found costs nothing.
  • Free gives you 1,000 records for life, no card. That covers a small CRM end to end.

Prerequisites

  • A CRM export as CSV with at least email or linkedin_url per row.
  • A LeadOcean API key in $LEADOCEAN_API_KEY (free at app.leadocean.io, no card).
  • curl and Python 3 with requests installed.

Step 1: Audit what is actually broken

Count the gaps before you spend anything. Four numbers matter: rows with no email, rows with no job title, rows with no company domain and duplicates.

python
import csv
from collections import Counter

rows = list(csv.DictReader(open("crm_export.csv")))
gaps = Counter()
seen = Counter(r["email"].lower() for r in rows if r.get("email"))
for r in rows:
    if not r.get("email"): gaps["no_email"] += 1
    if not r.get("title"): gaps["no_title"] += 1
    if not r.get("domain"): gaps["no_domain"] += 1
gaps["duplicate_emails"] = sum(1 for n in seen.values() if n > 1)
print(len(rows), dict(gaps))

Enrich the rows with a key to look up (an email or a LinkedIn URL). Deal with the rest by hand.

Step 2: Pick the tool for the job

Pick on entry price and on whether you can call it from a script. Each has an API, but plan access differs (Cognism needs a package that includes it). Check the plan before you build.

ToolEntry paid plan, monthly listBilled yearlyFree tier
LeadOceanSolo $97 a monthn/a1,000 records for life, no card
ApolloBasic $65 per seat, 2,500 credits$49 per seat75 credits a month
HunterStarter $49, 2,000 credits$3450 credits a month
FullEnrichPro from $29, 500 credits$26 (6,000 credits a year)50 credits trial
LushaStarter $49.90, 400 credits$37.4540 credits a month
ClayLaunch $185, 15,000 Actions and 2,500 Data Credits$167100 Data Credits a month
People Data LabsPro $98, 350 credits$78100 credits a month, contact data obscured
CognismNo public priceNo public priceNone listed

Sources, all checked September 2026: Apollo, Hunter, FullEnrich, Lusha, Clay, People Data Labs, Cognism.

Our pick: a developer with a script wants an API with flat pricing, so start with LeadOcean's free plan. A team that lives in a spreadsheet UI should look at Clay. See the wider list in best CRM data enrichment tools.

Step 3: Enrich one row and read the answer

Enrich takes exactly one of linkedin_url, email, phone or person_id. Send a single known contact first.

bash
curl -s -X POST https://api.leadocean.io/v1/people/enrich \
  -H "x-api-key: $LEADOCEAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email": "jane.doe@acme.com", "reveal_email": true}'

One call costs one record. If nobody matches you get a 404 and pay nothing. Since October 2026 the response also carries career history (resume_data: past roles, education, certifications), useful for spotting job changes.

Step 4: Size the full refresh for free

Before you loop over 10,000 rows, size the audience you want to top up. A count spends no records. Send count=true as a query parameter with limit=1.

bash
curl -s -X POST "https://api.leadocean.io/v1/people/search?count=true&limit=1" \
  -H "x-api-key: $LEADOCEAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"country": ["US"], "jobLevel": ["VP"], "jobFunction": ["Sales & Business Development"]}'

Over REST, meta.total is capped at 100,000, so a big audience shows 100,000 with totalIsExact false. The MCP count below has no such cap.

Company lists work the same way with GET /v1/companies/count, and the MCP tool leadocean_count_companies does it from a chat. Use exact enum strings. Look them up with leadocean_list_enum_values or GET /v1/enums.

Step 5: Enrich the gaps in a loop and write back

Loop over the audit rows from Step 1. Respect the rate limit (5 requests a second on Free) and write back only fields you got.

python
import os, time, csv, requests

URL = "https://api.leadocean.io/v1/people/enrich"
H = {"x-api-key": os.environ["LEADOCEAN_API_KEY"]}
rows = list(csv.DictReader(open("crm_export.csv")))
out = []
for r in rows:
    if not r.get("email"):
        continue
    while True:
        resp = requests.post(URL, headers=H, json={"email": r["email"]}, timeout=30)
        if resp.status_code != 429:
            break
        time.sleep(int(resp.headers.get("Retry-After", "1")))
    if resp.status_code == 404:
        continue  # not found, costs nothing
    resp.raise_for_status()
    c = resp.json()["data"].get("contact_data") or {}
    role = (c.get("contact_current_experiences") or [{}])[0]
    out.append({**r,
                "email_status": c.get("email_status"),
                "current_title": role.get("job_title"),
                "current_domain": role.get("company_domain")})
    time.sleep(0.25)

if out:
    with open("crm_enriched.csv", "w", newline="") as f:
        w = csv.DictWriter(f, fieldnames=list(out[0].keys()))
        w.writeheader()
        w.writerows(out)
print(len(out), "rows enriched")

Import the file with your CRM's own CSV importer, mapping on email. For a one-off of tens of thousands of rows use POST /v1/exports (Solo and up) or the Exports page in app.leadocean.io. Read email_status before you send anything: there is no refund for bounces.

What you get

A free count run on 2026-10-21 through the leadocean_count_leads MCP tool, filters US, VP, Sales & Business Development:

code
total: 129494      (mailable: verified, catch_all_valid, catch_all)
totalAll: 350517   (same filters, no email condition)
distinctCompanies: 58121  (estimate above 40,000)
credits: 0

The enrich response has this shape (response shape from the API reference, placeholder values):

json
{
  "success": true,
  "data": {
    "profile_data": { "profile_full_name": "Jane Doe", "profile_headline": "VP Sales at Acme" },
    "contact_data": {
      "has_email": true,
      "has_phone": false,
      "email_status": "verified",
      "email_type": "work",
      "contact_emails": [{ "email": "jane.doe@acme.com", "type": "work", "status": "verified" }],
      "contact_current_experiences": [
        { "job_title": "VP Sales", "company_name": "Acme", "company_domain": "acme.com" }
      ]
    },
    "resume_data": {
      "experiences": [{ "job_title": "Sales Director", "company_name": "Example Co" }]
    },
    "meta": { "fetched_at": "2026-10-01" }
  },
  "meta": { "credits": 1, "source": "own" }
}

Troubleshooting

ErrorCauseFix
401Missing or wrong x-api-keyCheck $LEADOCEAN_API_KEY is exported and sent as the x-api-key header
429Above the plan's rate limit (5 a second on Free, 200 on Agency)Sleep for the Retry-After value and slow the loop
402Records spent (1,000 for life on Free)Upgrade to Solo ($97 a month) or Agency ($497 a month)
404 on enrichNobody matched that identifierSkip the row. It cost nothing
Empty search resultsBad enum value, such as VP spelled Vice PresidentResolve the exact string with GET /v1/enums
Export refusedOver 50,000 rows a request or 500,000 rows a daySplit the request and run the rest tomorrow

FAQ

How many records does a CRM clean-up use?

About one per contact you successfully enrich. Counts, 404s and empty pages are free, so audit first.

Should I overwrite existing CRM fields?

No. Fill empty fields and flag changed titles for review. Write email_status to its own field.

How often should I re-run it?

The dataset refreshes monthly, and each record has its own fetched_at date. Quarterly is enough for most CRMs.

Can I do this without code?

For fresh contacts, yes. The Exports page in app.leadocean.io has a filter builder, a column picker and a price preview before you download. For rows already in your CRM, use the loop above, or ask an MCP client to run leadocean_get_lead per row.

For the longer tool list see dirty CRM data: 9 enrichment tools, what CRM enrichment adds and pricing.

Clean your CRM with a free API key

Free to start. No credit card. 1,000 records to spend whenever you like.

Get your free API key →