TL;DR
- The workflow has five steps: write the ICP in
CLAUDE.md, size it for free, pull people, reveal emails for the best rows, draft openers. - It takes about 20 minutes. The example spends about 35 of your 1,000 free records: 25 for the pull, 10 for email reveals.
- Sizing, filter lookups and enum lookups cost nothing. Only people you receive count as records.
Prerequisites
- A LeadOcean account and API key from app.leadocean.io. The free plan has no card and 1,000 records, one-off. Its keys get both the search and enrich scopes.
- The key in your shell as
$LEADOCEAN_API_KEY. - Claude Code installed, plus
curlandjq.
export LEADOCEAN_API_KEY="paste-your-key-here"
mkdir prospecting && cd prospectingStep 1: Write the ICP and connect the data
Put the ICP in a CLAUDE.md file in the project folder. Claude Code's memory docs describe CLAUDE.md as persistent instructions it reads at the start of a session. Use exact filter values, not prose.
## Prospecting project
### ICP
- Titles: Head of Sales, VP Sales
- Country: US
- Industry: Software Development (exact enum value)
- Company size: employeeRange 51-200
- Mailable only: emailStatus verified, catch_all_valid, catch_all
### Rules
- Use the leadocean MCP server. Call leadocean_count_leads before any tool that spends records.
- Never call search, get_lead or export without my go-ahead. Say how many records it will spend first.
- Write openers from fields in the data only. If a field is empty, say so. Never invent a fact.Now add the hosted LeadOcean MCP server. Claude Code's docs say to run /mcp inside a session to sign in to a remote server with OAuth. No key goes into the chat.
claude mcp add --transport http leadocean https://api.leadocean.io/mcp
claude/mcpPick leadocean, approve the sign-in, then confirm the server lists 13 tools. Seven of them never spend records.
Step 2: Size the ICP for free
Count before you pull. A count spends no records, however often you run it, and it tells you whether the ICP is worth a pull at all.
Resolve the exact industry string for "software" with leadocean_list_enum_values.
Then run leadocean_count_leads with the ICP in CLAUDE.md. Report the total and say
which email statuses it counted. Do not call any tool that spends records.The same count over REST is a people search with count=true as a query parameter and limit at 1. It returns meta.total and no rows.
curl -s "https://api.leadocean.io/v1/people/search?count=true" \
-H "x-api-key: $LEADOCEAN_API_KEY" -H "content-type: application/json" \
-d '{
"title": ["Head of Sales", "VP Sales"],
"country": ["US"],
"industry": ["Software Development"],
"employeeRange": ["51-200"],
"emailStatus": ["verified", "catch_all_valid", "catch_all"],
"limit": 1
}' | jq '.meta | {total, totalIsExact, countOnly}'The total is capped at 100,000. If it is too big, add a filter. If it is under 100, widen the title list.
Swap in the filters that define your own ICP. LeadOcean has 50 people filters and 34 company filters. These do most of the work for a first workflow:
jobLevelandjobFunctionfor a role band, such as["VP"]with["Sales & Business Development"]. Values are exact strings.seniorityfor a finer band, in lowercase (c_suite,vp,director).employeeRangefor company size. It is broader and cheaper than min and max headcount.technologyCategoriesfor companies with a detected tool type. It covers about 7.8M crawled sites, so it will not match every company.
An enum value that does not match can return an empty result or a 400. Resolve each one with leadocean_list_enum_values before you trust a zero.
Step 3: Pull 25 people
Pull a small batch first. Each person returned costs one record, so 25 rows is a cheap test of the title match. The body is flat JSON, filters are arrays, and limit runs from 1 to 100.
curl -s https://api.leadocean.io/v1/people/search \
-H "x-api-key: $LEADOCEAN_API_KEY" -H "content-type: application/json" \
-d '{
"title": ["Head of Sales", "VP Sales"],
"excludeTitle": ["assistant", "intern", "associate"],
"country": ["US"],
"industry": ["Software Development"],
"employeeRange": ["51-200"],
"emailStatus": ["verified", "catch_all_valid", "catch_all"],
"limit": 25
}' > people.json
jq '{rows: (.data | length), nextCursor: .meta.nextCursor}' people.jsonSearch rows are thin: profile, current role and email flags, no addresses. Each row carries a person_id at meta.source_ids.person_id. Step 4 uses it.
Prefer to start from accounts? GET /v1/companies/{domain}/people lists the people we hold at one company, one record per person returned.
curl -s "https://api.leadocean.io/v1/companies/acme.com/people?limit=10" \
-H "x-api-key: $LEADOCEAN_API_KEY" | jq '.meta | {count, nextCursor}'Step 4: Reveal emails for the best 10
Do not reveal everyone. Let Claude Code pick the rows worth a record, then enrich only those. Enrich takes exactly one of linkedin_url, email, phone or person_id, and reveal_email returns the address.
Read people.json. Pick the 10 people whose current role and company best fit the ICP
in CLAUDE.md. Write their person_id values to ids.txt, one per line, and show me a
table of name, title and company with one line on why each made the cut.while read -r ID; do
curl -s https://api.leadocean.io/v1/people/enrich \
-H "x-api-key: $LEADOCEAN_API_KEY" -H "content-type: application/json" \
-d "{\"person_id\": \"$ID\", \"reveal_email\": true}" | jq -c .
done < ids.txt > enriched.jsonl
wc -l enriched.jsonlA person we hold nothing for returns a 404 and costs nothing. Revealing the email costs no extra record. If you only need a phone, call GET /v2/people/phone, but that endpoint bills one record per call even when it finds nothing.
Step 5: Qualify and draft the first line
Keep the model out of the data work until now. Search and enrich are plain API calls that give the same answer every run. The model only scores and writes, and you can read what it wrote.
Now the model does what it is good at, working on fields it can see. Tell it to score each row and write one opening line from the data, and to flag gaps instead of filling them.
Read enriched.jsonl. For each person write prospects.csv with these columns:
name, title, company, email, email_status, fit_score (1-5), reason, opener.
The opener is one sentence under 25 words, built only from fields in the record.
Leave opener empty and write "thin data" in reason when there is nothing specific.
Drop anyone whose email_status is not verified, catch_all_valid or catch_all.
Do not send anything. Show me the first three rows.Check the CSV by hand before it goes near your sending tool. LeadOcean gives you records and a deliverability label for each address. It does not send mail, and bounced emails are not refunded, so read email_status first.
When the 10 rows look right, scale up with the same filters. On Free, page the search with cursor at limit 100. Bulk CSV export is included on Solo and Agency, and POST /v1/exports takes up to 50,000 rows a request and 500,000 a day. The lead list recipe covers it.
What you get
Step 2 gives a real number. On 21 October 2026 the free MCP count for this ICP returned this, trimmed. It used the tool's default email condition.
{
"total": 360,
"totalIsExact": true,
"totalAll": 863,
"distinctCompanies": 317,
"defaults": "Counted only addresses worth sending to (emailStatus: verified or catch_all_valid or catch_all)",
"unsupportedFilters": []
}That is 360 mailable people at 317 companies, not everyone matching the titles. It counts only verified, catch_all_valid and catch_all addresses. The same filters with no email condition match 863 (totalAll).
Here is how the records add up for one run of the workflow.
| Step | Call | Records |
|---|---|---|
| 2 | Count over MCP or count=true | 0 |
| 3 | People search, limit 25 | 25 |
| 4 | Enrich 10 people with reveal_email | 10 or fewer |
| 5 | Qualify and draft in Claude Code | 0 |
Rows that return a 404 on enrich cost nothing, so 35 is the ceiling for this run.
This is the response shape from the API reference for one enriched person, with placeholder values.
{
"success": true,
"data": {
"profile_data": { "...": "name, headline, current role, company, location" },
"contact_data": {
"contact_emails": [
{
"email": "jane.doe@acme.com",
"type": "work",
"status": "verified",
"verified_batch_date": "2026-09-20",
"priority": 1,
"score": 0.9
}
],
"contact_phones": [],
"has_email": true,
"has_phone": false,
"email_status": "verified",
"email_type": "work"
},
"resume_data": {
"experiences": [
{ "job_title": "Sales Director", "company_name": "<past employer>", "start_date": "2019-01", "end_date": "2023-04", "company_id": null }
],
"educations": [],
"r_skills": ["<skill>"]
},
"meta": { "fetched_at": "<date>" }
},
"meta": { "source": "leadocean", "count": 1 }
}Since 17 October 2026, resume_data carries career history: past roles with title, company name and dates, plus education, certifications and skills, each when we hold it. Past employers have no company_id yet. The current role is in contact_data.contact_current_experiences, not repeated in resume_data.experiences. A past role at one of your customers makes a better opener than the headline. Add this to the Step 5 prompt.
For each person, read resume_data.experiences. If a past employer appears in
customers.txt, put it in the reason column and build the opener on it.The CSV from Step 5 has one row per person. The row below uses placeholder values.
name,title,company,email,email_status,fit_score,reason,opener
Jane Doe,VP Sales,Acme,jane.doe@acme.com,verified,4,"VP at a 51-200 software company",""Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing, wrong or revoked key | Re-copy the key from app.leadocean.io and re-export $LEADOCEAN_API_KEY. |
| 429 | Above the plan's rate limit: 5 requests a second on Free, 50 on Solo, 200 on Agency | Wait the Retry-After seconds and send full pages of 100 instead of more calls. |
| 402 | Free plan: the 1,000 records are spent | Check GET /v1/account, then move to Solo or Agency on pricing. Free records do not reset. |
| 404 on enrich | We hold nothing for that person_id | Nothing to fix. It costs no record. Skip the row. |
| Empty results or a 400 | A filter value outside the list, such as industry: "Software" | Resolve it with leadocean_list_enum_values or GET /v1/enums/<name> and pass the exact string. |
400 export_limit_rows | limit is above 50,000 rows for one request | Split the audience by country or headcount band and run several exports. |
429 export_limit_daily | Over 500,000 export rows in a day | Wait for the reset in the response, then run the rest. |
FAQ
How many records does a prospecting run cost?
One record per person returned by a search or an enrich. Sizing is free. The example spends about 35: 25 for the pull and 10 for reveals. If you outgrow the free 1,000, Solo is $97 a month for one person and Agency is $497 a month for a team. Paid plans have a fair-use pace, not credits, and no per-record price.
How do I run it every week without repeats?
Keep the person_id of everyone you enriched in a seen.txt file, and tell Claude Code in CLAUDE.md to skip those ids before it picks the next 10. The dataset refreshes monthly and each record carries a fetched_at date, so a weekly run on the same filters mostly returns the same people.
Why enrich only 10 of the 25?
Search rows are thin and enrich is where you get addresses. Reveal only the rows that pass your own fit check, so you spend records on people you would actually email.
Does Claude Code see my API key?
Not on the MCP route. The server uses OAuth sign-in and you paste no key into the chat. The REST steps read $LEADOCEAN_API_KEY from your shell.
Can I run the same workflow for recruiting or RevOps?
Yes. Swap the ICP block in CLAUDE.md and keep the steps. See recruiting workflows with Claude, Claude Code RevOps workflows and the Claude Code sales agent guide. Plans are on pricing.
More recipes live in the blog hub.
Build your first prospecting workflow in Claude Code today
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →