TL;DR
- Contact discovery is three calls: a free coverage count, a search by company domain and title, then one enrichment with
reveal_emailthat returns every email and phone we hold. - It takes about 10 minutes. Each person found by domain spends about 4 LeadOcean records, or 1 if you already have their LinkedIn URL.
- You get an email with its status and a phone with its type. Read both before you send or dial.
Prerequisites
- A LeadOcean account from app.leadocean.io. The free plan has no card and 1,000 records, one-off.
curlandjq.- An API key in your shell as
$LEADOCEAN_API_KEY. Free keys get both scopes, search and enrich.
export LEADOCEAN_API_KEY="paste-your-key-here"Step 1: Check coverage before you spend anything
Ask how many people match, and how many hold a phone, before you pull a single record. A search with count=true as a query parameter and limit at 1 is a free sizing call. The body stays flat JSON with arrays.
curl -s "https://api.leadocean.io/v1/people/search?count=true" \
-H "x-api-key: $LEADOCEAN_API_KEY" -H "content-type: application/json" \
-d '{
"jobLevel": ["VP"],
"jobFunction": ["Sales & Business Development"],
"country": ["CA"],
"hasEmail": true,
"hasPhone": true,
"limit": 1
}' | jq '.meta | {total, totalIsExact, credits}'hasPhone and hasEmail are flags on the search row. They tell you a number exists, not what it is. The email flag counts any status, so this is not a mailable count.
Step 2: Find the person by company domain and title
There is no name plus domain lookup in the API, so search by domain first. Send the domain and a title keyword, and keep limit small because each person returned costs one record.
curl -s "https://api.leadocean.io/v1/people/search" \
-H "x-api-key: $LEADOCEAN_API_KEY" -H "content-type: application/json" \
-d '{
"domain": ["acme.com"],
"title": ["VP Sales"],
"limit": 3
}' | jq '{count: .meta.count, credits: .meta.credits, source_ids: .meta.source_ids, people: .data}'A search row is thin: name, headline, current role and company, plus flags such as has_email, has_phone and email_status. It carries no addresses and no numbers. Each row has a person_id at meta.source_ids.person_id, which you pass to the next call.
If you already hold a LinkedIn URL, an email or a phone number for the person, skip this step. Enrich takes exactly one of linkedin_url, email, phone or person_id.
Step 3: Enrich once and reveal both contact points
One enrichment call returns the email and the phone together. Send the person_id and set reveal_email to true. Without it you get the profile and the has_email and has_phone flags, but no addresses or numbers.
PERSON_ID="paste-person_id-from-step-2"
curl -s https://api.leadocean.io/v1/people/enrich \
-H "x-api-key: $LEADOCEAN_API_KEY" -H "content-type: application/json" \
-d "{\"person_id\": \"$PERSON_ID\", \"reveal_email\": true}" \
| jq '.data.contact_data | {has_email, has_phone, email_status, contact_emails, contact_phones}'This costs one record. reveal_email adds every email and phone we hold at no extra charge. A person we hold nothing for returns a 404 and costs nothing.
Step 4: Use a narrow lookup when you want only one field
Two v2 endpoints return a single field. They suit a workflow that needs only the cell number, or only the work email. Each call costs one record, including a call that finds nothing.
curl -s "https://api.leadocean.io/v2/people/phone?person_id=$PERSON_ID" \
-H "x-api-key: $LEADOCEAN_API_KEY" | jq '.data'
curl -s "https://api.leadocean.io/v2/people/email/work?person_id=$PERSON_ID" \
-H "x-api-key: $LEADOCEAN_API_KEY" | jq '.data'Calling both spends two records where Step 3 spent one. If you want both fields, use Step 3. The work endpoint returns only an address at the person's current employer that is verified, catch_all_valid or catch_all. A null means none qualify, not that no email exists.
Step 5: Run it for a list with an export
For more than a handful of people on Solo ($97 a month) or above, export instead of looping. Bulk CSV export is not on Free. An export writes one CSV, with numbered columns such as email_1_address, email_1_status and phone_1_number. A request takes up to 50,000 rows and a day takes up to 500,000.
curl -s https://api.leadocean.io/v1/exports \
-H "x-api-key: $LEADOCEAN_API_KEY" -H "content-type: application/json" \
-d '{
"name": "VP sales, Canada, with phone",
"filters": {
"jobLevel": ["VP"],
"jobFunction": ["Sales & Business Development"],
"country": ["CA"],
"hasPhone": true,
"emailStatus": ["verified", "catch_all_valid", "catch_all"]
},
"limit": 100,
"preset": "full"
}' | jq '.data | {id, status, requested, remaining, pollAfter}'The export reserves up to your limit when it starts, and each row costs one record. The full preset reveals emails, so the key needs the enrich scope. The app does the same on its Exports page, with a filter builder, a column picker and the record price before you start.
What you get
The Step 1 count is a real run. We ran it on 2026-10-21 with leadocean_count_leads, which spends 0 records, for VP-level sales leaders in Canada.
{
"total": 8941,
"totalIsExact": true,
"credits": 0,
"unsupportedFilters": []
}That is 8,941 people who hold any email and any phone, at any email status. It is not a mailable count. With the default mailable filter and no phone filter, the same audience was 5,574 on 2026-10-21.
The enrichment returns the shape below. This is the response shape from the API reference, with placeholder values. Phones come back at contact_phones[].phone.
{
"success": true,
"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", "dnc": null, "priority": 1 }
]
}
},
"meta": { "credits": 1 }
}Read email_status before you send. LeadOcean gives no refund or credit-back for bounced emails. Treat dnc: null as unknown, never as safe to call, because the field is published but not yet populated.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing, wrong or revoked key | Re-copy the key from app.leadocean.io and export $LEADOCEAN_API_KEY again. |
| 429 | Above your plan's rate limit per account (5 a second on Free, 50 on Solo, 200 on Agency) | Wait, then slow the loop or use an export for large batches. |
| 402 | The 1,000 free records are spent | Check your balance with GET /v1/account, or move to Solo or Agency. Free records do not reset. |
| Search returns no rows | A filter value outside the catalogue, such as a job level of "VP Sales" | Check exact strings with GET /v1/enums or leadocean_list_enum_values. Job level is VP, not a free-text title. |
| 404 on enrich | We hold no record for that identifier | Costs nothing. Try a LinkedIn URL or an email instead of a person_id. |
400 export_limit_rows | The export limit is above 50,000 rows | Split the audience by country or headcount band and run several exports. |
FAQ
How many records does contact discovery cost?
One record per person returned by a search and one per enrichment. Coverage counts are free. Finding one person by domain and title with limit 3, then enriching, spends about 4 records.
Can I find a cell phone number from a name and company?
Not in one call. There is no name plus domain lookup. Search by company domain and title, then send the person_id to enrich with reveal_email set to true.
Is the email checked before I get it?
Each address carries an email_status, such as verified, catch_all_valid or catch_all. The work-email endpoint returns only those three statuses. Bounces are not refunded, so filter on status before you send.
Does LeadOcean tell me if a number is safe to call?
No. The dnc field exists but is not populated yet, so null means unknown. Check your own do-not-call rules before dialing.
Where can I compare other tools?
See 8 contact enrichment tools that find emails and phone numbers fast. For the reverse direction, read how to find an email from a phone number and how to find someone's cell phone number for B2B outreach. More recipes are in the blog hub, and plans are on pricing.
Find your first verified email and cell number, free
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →