Email enrichment turns a contact you half know into an address you can send to. It also adds the context to write the email. This guide runs the loop against the LeadOcean API: find, verify, profile.
LeadOcean holds 693M people, 366M of them with an email (coverage measured 2026-09-23). The loop reads from that one dataset, so each step answers from the same record.
LeadMagic's guide (September 2026) splits the term into two directions. Forward means name and company in, email out. Reverse means email in, person and company out. You will do both below.
TL;DR
- It takes about 15 minutes and 7 records: 5 people pulled by search, 1 work-email lookup, 1 profile lookup. Sizing the list in step 1 is free.
- LeadOcean has no separate verify call. The
statuscomes back with the address, from our own verifier, so find and verify are one request. - Scale it with
POST /v1/exports: up to 50,000 rows a request, 500,000 a day, 1 record per row.
Prerequisites
- A LeadOcean account. The free plan has no card and 1,000 records, one-off. Sign up at app.leadocean.io.
- An API key from the app, exported as
$LEADOCEAN_API_KEY. Free keys get both scopes, search and enrich. curlandjq.- Optional: the hosted MCP server at
https://api.leadocean.io/mcpif you would rather drive step 1 from Claude.
export LEADOCEAN_API_KEY="paste-your-key-here"Step 1: Size the list before you spend a record
Count first. A sizing call returns meta.total and no rows, so it costs nothing however often you run it.
Send count=true and limit=1 as query parameters, not in the JSON body. The body rejects unknown fields. Filters are flat arrays of exact strings.
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 '{
"jobLevel": ["VP"],
"jobFunction": ["Sales & Business Development"],
"country": ["CA"],
"emailType": ["work"],
"emailStatus": ["verified"]
}' | jq '.meta | {total, totalIsExact, countOnly}'meta.total is capped at 100,000. If totalIsExact is false, treat the number as a floor.
The same count from Claude or any MCP client, through the free leadocean_count_leads tool:
Count VPs in Sales & Business Development in Canada.
Use leadocean_count_leads with facets=false. Do not search.Enum values are exact and case-sensitive. VP works. vp does not, because jobLevel and seniority are different filters with different spellings. Look values up first with leadocean_list_enum_values.
Step 2: Pull a small sample with deliverability flags
Search returns people, not addresses. Each row carries has_email, email_status and email_type as flags, plus a person_id at meta.source_ids.person_id. You send that id back in step 3.
Each person returned costs 1 record. Keep the first run at five.
curl -s -X POST "https://api.leadocean.io/v1/people/search" \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"jobLevel": ["VP"],
"jobFunction": ["Sales & Business Development"],
"country": ["CA"],
"emailType": ["work"],
"emailStatus": ["verified"],
"limit": 5
}' > people.json
PERSON_ID=$(jq -r '.data[0].meta.source_ids.person_id' people.json)
echo "$PERSON_ID"limit runs from 1 to 100. To page, pass meta.nextCursor back unchanged as cursor. One search walks at most 10,000 rows by default.
A search-side verified is an upper bound. The API reference says enrichment applies the narrow rule per address, so a person can read verified in a search row and untested when you enrich. Trust the enriched answer.
Step 3: Find and verify the work email in one call
GET /v2/people/email/work returns one address or data: null. It only returns addresses at the person's current employer whose status is verified, catch_all_valid or catch_all. A former employer's address never comes back here.
It costs 1 record per call, including a call that finds nothing.
curl -s "https://api.leadocean.io/v2/people/email/work?person_id=$PERSON_ID" \
-H "x-api-key: $LEADOCEAN_API_KEY" > work_email.json
jq '.data' work_email.jsonGate the address in the same script. Send only what we tested ourselves or confirmed on a catch-all domain.
jq -r '.data | select(.status == "verified" or .status == "catch_all_valid") | .email' work_email.jsonAn empty line means no address qualifies. data: null means "none that qualify", not "this person has no email". Read what each status means and how catch-all domains change the odds before you send to catch_all.
There is no refund or credit-back for bounces. Read status first.
Treat the status as a rule, not a hint. Send to verified and catch_all_valid. Hold catch_all for a smaller first batch. Drop risky, role and derived unless you accept the bounce rate. Never send to invalid, spam_trap, abuse or disposable.
Step 4: Profile the address
The profile step runs the other way. Send an email, get the person. That is the inbound case: a signup, a form fill, an old CRM row.
POST /v1/people/enrich takes exactly one of linkedin_url, email, phone or person_id. A person we hold nothing for is a 404 and costs nothing. Use a placeholder address below and swap in the one from step 3.
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"}' > profile.json
jq '.data.contact_data | {has_email, has_phone, email_status, email_type}' profile.jsonWithout reveal_email, you get the profile and the flags. Add "reveal_email": true and the same record returns every email and phone we hold, at no extra charge. Phones come back in contact_phones[].phone.
Pick one door per person. /v2/people/email/work, /v2/people/reverse and enrich all resolve to the same lookup, and each call is 1 record.
Step 5: Run it on a whole list
Do not loop the API over thousands of rows. Start an export instead. Bulk CSV export is not on the Free plan. It is included from Solo ($97 a month) up. On Free, page the search 100 rows at a time instead. One export takes up to 50,000 rows. The account ceiling is 500,000 rows a day.
curl -s -X POST "https://api.leadocean.io/v1/exports" \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "vp-sales-ca-work-emails",
"filters": {
"jobLevel": ["VP"],
"jobFunction": ["Sales & Business Development"],
"country": ["CA"],
"emailType": ["work"],
"emailStatus": ["verified"]
},
"limit": 100,
"preset": "full",
"caps": {"emails": 1, "phones": 1}
}'The call answers 202 and a worker does the walk. Poll GET /v1/exports/{id}, then fetch GET /v1/exports/{id}/download.
An export reserves up to limit records at once, doubled when a column needs the full record. It bills each row it writes. The full preset has 45 columns and needs the enrich scope.
Cost is simple to plan. A 100-row export spends 100 records and reserves up to 200 while it runs. A 10,000-row export needs 10,000 free records on the account when it starts, or it is refused with a 402.
You can skip the code. The Exports page in app.leadocean.io has the filter builder, a column picker, the record price before you start, and a Download button.
What you get
Step 1, count from the free MCP tool, run 2026-10-21. It counts VPs in Sales & Business Development in Canada. The tool's default email filter applies, so the total is the mailable audience (verified, catch_all_valid or catch_all).
leadocean_count_leads jobLevel=VP jobFunction="Sales & Business Development" country=CA
total: 5574 (mailable audience, default email filter)
totalAll: 18800 (same filters, no email condition)Your count will move with the monthly refresh.
The statuses you filter and gate on, from leadocean_list_enum_values on 2026-10-21.
email_status (13 values):
verified, catch_all_valid, catch_all, risky, unknown, untested,
invalid, role, disposable, spam_trap, abuse, derived, nonecatch_all_valid is a mailbox confirmed on a catch-all domain. Safe to send: verified and catch_all_valid. Do not send: invalid, spam_trap, abuse, disposable.
Step 3, response shape from the API reference, placeholder values:
{
"success": true,
"data": {
"email": "jane.doe@acme.com",
"status": "verified",
"verified_batch_date": "2026-09-20"
}
}verified_batch_date is the verification run, not a timestamp for this one address. null means the check was not ours.
Step 4, response shape from the API reference, placeholder values (profile fields omitted):
{
"success": true,
"data": {
"profile_data": {},
"contact_data": {
"has_email": true,
"has_phone": true,
"email_status": "verified",
"email_type": "work",
"contact_emails": [
{"email": "jane.doe@acme.com", "type": "work", "status": "verified", "verified_batch_date": "2026-09-20"}
],
"contact_phones": [
{"phone": "+14155550133", "type": "mobile", "priority": 1}
]
}
},
"meta": {"source": "leadocean", "credits": 1}
}contact_emails and contact_phones appear only with reveal_email: true. The dataset is refreshed monthly, and each record carries its own fetched_at date.
Step 5, the CSV columns your send gate reads.
email_1_address,email_1_status,phone_1_number
jane.doe@acme.com,verified,+14155550133Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing or wrong x-api-key header | Send the key in the x-api-key header. Rotate it in the app if it leaked. |
| 402 | Out of records. On Free the 1,000 are a one-off and do not reset. | Check GET /v1/account and GET /v1/usage. Move to Solo ($97 a month) or Agency ($497 a month), or cut the export limit. |
| 429 | Above your plan's rate limit per account (5 a second on Free, 200 on Agency) | Wait for the Retry-After header, then retry. Batch through exports instead of loops. |
Search returns nothing or meta.unsupportedFilters | An enum value with the wrong spelling or case, such as vp for VP | Resolve values with leadocean_list_enum_values or GET /v1/enums/{name} and send them exactly. |
data: null from the work-email call | No address qualifies at the current employer. The call still cost 1 record. | Run step 4 with reveal_email to see what else we hold, or move to the next person. |
400 export_limit_rows or 429 export_limit_daily | More than 50,000 rows in one export, or past 500,000 rows today | Split by filter into several exports. The response carries remaining and resetsAt for the daily ceiling. |
FAQ
What is email enrichment?
Email enrichment adds a verified address, a status and a profile to a contact record. Forward enrichment finds the email from a person or company. Reverse enrichment finds the person from an email.
Does LeadOcean verify the emails it returns?
Yes, for business addresses. verified means our own verifier sent to the address and the server accepted it. A supplier's claim alone is published as untested. Personal addresses are the exception: we do not verify consumer domains, and GET /v2/people/email/personal publishes no status.
How many records does the loop cost?
One per person returned by search, one per work-email or reverse lookup, and one per enrichment that finds someone. A person we hold nothing for costs nothing on enrich, but a v2 contact call that finds nothing still costs one record. Sizing a list is free.
What does it cost at scale?
The free plan covers 1,000 records, once. Solo is $97 a month for one person and Agency is $497 a month for teams. Paid plans have a fair-use pace, not credits, and no per-record price. See pricing.
Where do I look for people before I have their company?
Start from the source you already hold, such as a Twitter handle, a GitHub profile or a Crunchbase list. Then run the loop above.
Enrich your first 1,000 contacts free
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →