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

How-to guide

How to Automate Lead Enrichment with Claude (API and MCP)

Use Claude to size the list and write the script once. A scheduled script then searches, enriches and keeps only mailable rows.

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

You automate lead enrichment with Claude by using it twice: once in a chat to size the audience, and once to write a script that runs without it. The script does the daily work. Claude does the thinking up front.

This guide uses the LeadOcean API for data and Claude Code for the script. Every step has a command you can run.

TL;DR

  • It takes about 30 minutes: 10 to size and connect, 10 to get a working script, 10 to schedule it.
  • The 10-person test spends up to 20 records. Search returns each person at 1 record, and enrich costs 1 more per person.
  • Claude writes the script once. A cron job runs it. Claude only runs again if you add the optional scoring step.

Prerequisites

  • A LeadOcean API key from app.leadocean.io. The free plan has 1,000 records, one-off, and no card.
  • The key in your shell as $LEADOCEAN_API_KEY.
  • Claude Code installed, plus python3, curl and jq.
  • For the optional scoring step, an ANTHROPIC_API_KEY as $ANTHROPIC_API_KEY.
bash
export LEADOCEAN_API_KEY="paste-your-key-here"

Step 1: Size the list for free

Count the audience before you spend a record. A sizing call is free however often you send it. Send count=true and limit=1 as query parameters, with arrays comma-separated.

bash
curl -s -G https://api.leadocean.io/v1/people/search \
  -H "x-api-key: $LEADOCEAN_API_KEY" \
  --data-urlencode "jobLevel=VP" \
  --data-urlencode "jobFunction=Sales & Business Development" \
  --data-urlencode "country=US" \
  --data-urlencode "employeeRange=51-200" \
  --data-urlencode "emailStatus=verified,catch_all_valid,catch_all" \
  --data-urlencode "count=true" \
  --data-urlencode "limit=1" | jq '.meta | {total, totalIsExact, countOnly}'

meta.total stops at 100,000. If totalIsExact is false, the number is a floor. Enum values are exact strings, so look them up instead of guessing.

The REST call names the three email statuses itself. Drop emailStatus to see everyone who matches, then add it back to see who you can mail. The gap between the two numbers tells you how much of the list will never reach an inbox. Size each audience this way before you write a line of code, because a count of a few hundred and a count of fifty thousand need different scripts.

Step 2: Connect Claude and let it write the script

Add the hosted MCP server, sign in with OAuth, then ask Claude Code to draft the pipeline. The server needs no key in the chat. Claude's docs say to run /mcp in a session to authenticate a remote server.

bash
claude mcp add --transport http leadocean https://api.leadocean.io/mcp
text
Use the leadocean MCP server. Call leadocean_list_enum_values for job_level and
leadocean_count_leads for US VPs in Sales & Business Development at companies of
51-200 people. Do not search or look up any person. Then write enrich.py that:
1. POSTs a flat JSON body to https://api.leadocean.io/v1/people/search
   (limit up to 100, emailStatus verified/catch_all_valid/catch_all, cursor paging)
2. reads each row's meta.source_ids.person_id
3. POSTs {"person_id": ..., "reveal_email": true} to /v1/people/enrich
4. skips ids listed in seen.txt and writes enriched.jsonl
Read the key from $LEADOCEAN_API_KEY. Stop if GET /v1/account shows too few records.

The first two calls are free. Review the script before you run it. Check that the key comes from the environment and that nothing loops without a cap.

Step 3: Run the script on ten people

A working version looks like this. Enrich takes one of linkedin_url, email, phone or person_id. Search rows carry the person_id, so you never need a name plus domain lookup.

python
import json, os, sys, time, requests

BASE = "https://api.leadocean.io"
H = {"x-api-key": os.environ["LEADOCEAN_API_KEY"], "Content-Type": "application/json"}
MAX_PEOPLE = int(os.environ.get("MAX_PEOPLE", "10"))
SEARCH = {"jobLevel": ["VP"], "jobFunction": ["Sales & Business Development"],
          "country": ["US"], "employeeRange": ["51-200"],
          "emailStatus": ["verified", "catch_all_valid", "catch_all"]}
seen = set(open("seen.txt").read().split()) if os.path.exists("seen.txt") else set()

def call(method, path, **kw):
    for _ in range(5):
        r = requests.request(method, BASE + path, headers=H, timeout=60, **kw)
        if r.status_code != 429:
            return r
        time.sleep(int(r.headers.get("Retry-After", "1")))
    return r

left = call("GET", "/v1/account").json()["data"]["records"]["remaining"]
if left is not None and left < 2 * MAX_PEOPLE:
    sys.exit(f"{left} records left, stopping")

ids, cursor = [], None
while len(ids) < MAX_PEOPLE:
    body = dict(SEARCH, limit=min(100, MAX_PEOPLE - len(ids)))
    if cursor:
        body["cursor"] = cursor
    page = call("POST", "/v1/people/search", json=body)
    page.raise_for_status()
    page = page.json()
    ids += [r["meta"]["source_ids"]["person_id"] for r in page["data"]
            if r["meta"]["source_ids"]["person_id"] not in seen]
    cursor = page["meta"].get("nextCursor")
    if not cursor:
        break

with open("enriched.jsonl", "a") as out:
    for pid in ids:
        r = call("POST", "/v1/people/enrich", json={"person_id": pid, "reveal_email": True})
        seen.add(pid)
        if r.status_code == 404:
            continue
        r.raise_for_status()
        d = r.json()["data"]
        c = d.get("contact_data", {})
        out.write(json.dumps({
            "person_id": pid, "email_status": c.get("email_status"),
            "emails": [e["email"] for e in c.get("contact_emails", [])],
            "phones": [p["phone"] for p in c.get("contact_phones", [])],
            "profile": d.get("profile_data")}) + "\n")
open("seen.txt", "w").write("\n".join(seen))
bash
MAX_PEOPLE=10 python3 enrich.py && wc -l enriched.jsonl

A person we hold nothing for is a 404 and costs 0. Search itself returns each row at 1 record, which is why the test costs up to 20.

Step 4: Keep only mailable rows

Enrichment returns addresses. It does not say which are safe to send. Filter on email_status before anything reaches your outreach tool, because LeadOcean gives no refund for bounces.

bash
jq -c 'select(.email_status == "verified" or .email_status == "catch_all_valid"
  or .email_status == "catch_all")' enriched.jsonl > mailable.jsonl
wc -l enriched.jsonl mailable.jsonl

Step 5: Add an optional Claude scoring pass

Use Claude as a judge only where a filter cannot decide. The docs show claude -p for non-interactive runs, with --output-format json and --json-schema for structured output. They recommend --bare for scripted calls, which needs ANTHROPIC_API_KEY.

bash
while read -r row; do
  echo "$row" | claude --bare -p "ICP: sales leaders at B2B SaaS companies. Judge \
this person's profile and return fit yes or no with a reason under 15 words." \
    --output-format json \
    --json-schema '{"type":"object","properties":{"fit":{"type":"string","enum":["yes","no"]},"reason":{"type":"string"}},"required":["fit","reason"]}' |
  jq -c --argjson r "$row" '$r + .structured_output'
done < mailable.jsonl > scored.jsonl

This step spends Claude tokens, not LeadOcean records. Skip it if your ICP is fully expressed by filters.

Step 6: Schedule it

Put the run in cron so nobody opens a chat. Cron does not load your shell profile, so keep both keys as export lines in ~/.enrich.env and set it to chmod 600. Then add one line with crontab -e.

bash
0 7 * * 1  cd ~/enrich && . ~/.enrich.env && MAX_PEOPLE=100 python3 enrich.py >> run.log 2>&1

Repeat runs on the same filters page through the same people, and search charges for each row it returns again. On the free plan, change the filter each week. Paid plans have no per-record price and no credits. Solo ($97 a month) and Agency ($497) each have a fair-use pace. Reaching it slows the key rather than stopping it.

What a run costs

Each person costs two records in this design: one when search returns the row, one when enrich returns the person. A 404 on enrich costs nothing, and so do counts, enum lookups and GET /v1/account.

RunPeopleRecords
Ten-person test10up to 20
Weekly cron at MAX_PEOPLE=100100up to 200
Free plan total5001,000, then 402

On Free, the 1,000 records are one-off, so the weekly job runs about five times. On Solo, the pace is 10k enrichments every 5 hours and 100k search records every 5 hours, which covers this job many times over. Agency fits teams running several jobs, with 5 keys and 200 requests a second.

To cut the cost per person, skip the search half. If you already hold LinkedIn URLs or work emails, send them straight to enrich and pay one record each.

What you get

Step 2 makes two free calls. We ran them on 2026-10-21. The count is mailable people (verified, catch_all_valid or catch_all, the tool default), not everyone matching the other filters.

json
{"total":18567,"totalIsExact":true,"totalAll":60456,"distinctCompanies":12192,"defaults":"Counted only addresses worth sending to (emailStatus: verified or catch_all_valid or catch_all)"}
{"name":"job_level","total":6,"values":["C-Team","Director","Manager","Other","Staff","VP"]}

Each enriched person returns the shape below. This is the response shape from the API reference, with placeholder values.

json
{
  "success": true,
  "data": {
    "profile_data": { "...": "name, title, company" },
    "contact_data": {
      "has_email": true,
      "has_phone": true,
      "email_status": "verified",
      "contact_emails": [{ "email": "jane.doe@acme.com", "type": "work" }],
      "contact_phones": [{ "phone": "+14155550133", "type": "mobile" }],
      "contact_current_experiences": [{ "job_title": "VP Sales", "company_name": "Acme" }]
    },
    "resume_data": {
      "experiences": [{ "job_title": "Sales Director", "company_name": "<past employer>", "company_id": null }],
      "educations": [{ "educational_establishment": "<school>" }]
    }
  }
}

Without reveal_email, the email and phone arrays are absent. has_email and has_phone still tell you whether a value exists.

That count is 18,567 mailable US sales VPs at companies of 51 to 200 people, on 2026-10-21. Without the email condition the same filters match 60,456 (totalAll). They work at 12,192 distinct companies.

Since 17 October 2026, enrich also returns career history in resume_data: past roles with title, company name and dates, plus education, certifications and skills, each when we hold it. Past employers carry no company_id yet. The current role sits in contact_data.contact_current_experiences. Add a past_roles field to the script's output if your scoring step needs it.

Troubleshooting

ErrorCauseFix
401The x-api-key header is missing, or the key is wrong or revoked.Copy a fresh key from the app and reload $LEADOCEAN_API_KEY. In cron, check the env file path.
402Your records are spent. On Free the 1,000 are a one-off allowance and do not reset.Run GET /v1/account for the balance, or move to Solo or Agency on pricing.
429Above the plan's rate limit: 5 requests a second on Free, 50 on Solo, 200 on Agency.Wait the Retry-After seconds and retry. The script above already does.
Empty resultsA filter value is not an exact enum string, such as Vice President instead of VP.Call leadocean_list_enum_values and pass the exact string.
400 Invalid cursorThe cursor was edited, stored from an earlier search, or minted for a different query.Restart the query and pass back meta.nextCursor unchanged.
400 export_limit_rows or 429 export_limit_dailyOne export is capped at 50,000 rows a request and 500,000 rows a day. Bulk export is not on Free.Split the job across requests and days, or narrow the filters.

FAQ

Does Claude run the enrichment each time?

No. Claude writes the script and can score the output, but cron runs the API calls. That keeps the run cheap and repeatable.

How many records does a run spend?

Two per person: one from search and one from enrich. A person we hold nothing for costs 0 on enrich. Counts, filter lookups and account checks are free.

Can I enrich leads I already have?

Yes. Skip the search and POST each email, linkedin_url or phone to /v1/people/enrich. Each request takes exactly one identifier. The CSV recipe covers that route.

Does the MCP route work without a script?

For small batches, yes. Claude calls the tools in chat, and every person returned costs 1 record. Past a few dozen rows, a script keeps long output out of the session. See enrich leads with Claude for the prompt.

Can the Exports page do this?

It builds a file from filters, with a column picker and a price preview. It does not take an uploaded list or run on a schedule. Bulk CSV export is included on Solo and Agency, not on Free.

How fresh is the data?

The dataset refreshes monthly, and each record carries its own fetched_at date. A weekly run on the same filters mostly returns the same people, so dedupe on person_id as the script does.

More recipes live in the blog hub. To compare tools first, read best lead enrichment tools. For CRM-specific lists, see SugarCRM enrichment tools. Plans are on pricing.

Automate your first enrichment run, free

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

Get your free API key →