Personalizing emails means putting one true fact about the reader in the opening line. You get that fact from data, so fetch the data before you write.
This guide pulls a small segment, enriches a few people, and turns the fields into merge values. Each step is a request you can run.
TL;DR
- Search a segment, enrich five people for career history, then fill a merge template. Skip any person whose field is empty.
- It takes about 20 minutes and spends about 30 records, inside the free 1,000.
- Sizing the list first is free, so count before you pull.
Prerequisites
- A free LeadOcean key in
$LEADOCEAN_API_KEY(start free, no card: pricing). curlandjq.- Any sending tool. LeadOcean returns data only. It has no sequencer or inbox.
- Background: email personalization and email deduplication if you merge lists.
Step 1: Size the segment for free
Count first. A count costs no records, so you can tune filters as often as you like.
curl -s -X POST "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": ["GB"],
"employeeRange": ["51-200"],
"limit": 1
}' | jq '.meta'The count=true flag goes in the query string, not the JSON body. Enum values are exact strings: check them with leadocean_list_enum_values or GET /v1/enums.
Step 2: Pull one page of people
Search with the same body and a real limit. One page of 25 spends 25 records.
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": ["GB"],
"employeeRange": ["51-200"],
"limit": 25
}' > page1.jsonFilters are arrays and the body is flat. For the next page, send the cursor from the response.
Step 3: Enrich a few people for a personal fact
Pick five people and enrich each by person_id, found on each search row at meta.source_ids.person_id. Since 2026-10-17 the response carries career history, so you can reference a past role, a school or a certification.
for id in $(jq -r '.data[:5][].meta.source_ids.person_id' page1.json); do
curl -s -X POST "https://api.leadocean.io/v1/people/enrich" \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"person_id\": \"$id\"}"
done > enriched.jsonlAn enrichment that finds nothing costs nothing. Past employers carry no company_id yet, so match them by name.
Step 4: Build merge values and skip gaps
A merge value is only safe when the field exists. Pick the best one per person and fall back to a plain opener.
jq -r '
.data as $p
| def past: ($p.resume_data.experiences // [])[0];
{
name: $p.profile_data.profile_full_name,
company: ($p.contact_data.contact_current_experiences[0].company_name // null),
hook: (if past then "your time at " + past.company_name else null end)
}
| select(.hook != null)
| "Hi \(.name), saw \(.hook). Quick question about \(.company)."
' enriched.jsonlThe API reference does not list the inner keys of resume_data or of the experience objects. Print one record and confirm experiences and company_name before you run a batch. Rule of thumb: one fact, one line, no stacking. If hook is null, send the plain version or drop the person.
What you get
Step 1 on 2026-10-21, run with the free leadocean_count_leads tool, for VP-level Sales & Business Development people in GB at companies of 51-200 employees:
{
"total": 442,
"totalIsExact": true,
"totalAll": 1349,
"distinctCompanies": 352,
"credits": 0
}The 442 counts only people whose email is verified, catch_all_valid or catch_all. The 1,349 is the same filters with no email condition. Do not call 442 everyone you can reach by other means.
A person record from enrich has this shape (response shape from the API reference, placeholder values):
{
"success": true,
"data": {
"profile_data": { "profile_full_name": "Jane Doe" },
"contact_data": {
"has_email": true,
"email_status": "verified",
"contact_current_experiences": [{ "company_name": "Acme" }]
},
"resume_data": {
"experiences": [{ "company_name": "Example Ltd" }],
"educations": []
},
"meta": { "source_ids": { "person_id": "1234567890" } }
}
}Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing or wrong x-api-key | Check $LEADOCEAN_API_KEY is exported and sent as the x-api-key header. |
| 429 | Above the plan's rate limit (5 requests a second on Free, 200 on Agency) | Honor Retry-After, or add a short sleep in the loop. |
| 402 | Out of records | The free 1,000 are spent. Pick a paid plan on pricing. |
| Empty results | A filter value is not an exact enum string | List values with leadocean_list_enum_values, for example Sales & Business Development. |
| Unknown field rejected | count=true placed in the JSON body | Move it to the query string. |
| Search stops paging | One search pages to 10,000 rows (meta.depthCapped) | Narrow the filters and run a second search. |
FAQ
How many fields should one email use?
One. A single accurate fact reads as research. Three facts read as a mail merge.
What if the personal field is empty?
Skip that line, not the email. Use a plain opener or leave the person out of the batch.
Does LeadOcean send the emails?
No. It returns people and company data. Send with any tool you already use.
How do I avoid emailing the same person twice?
Dedupe on email address before you send. See email deduplication.
Can I guess addresses from a pattern instead?
You can, but verify them first. See what is an email pattern for the risk.
Get the fields that make your first line true
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →