Personalization that gets replies is one specific line the reader recognizes as true, tied to one reason you are writing. A first-name merge tag is not that. This guide shows the queries, the output and a rule for picking the line.
TL;DR
- Size a narrow segment for free, search it with a signal filter, enrich one person, write one line around one fact.
- It takes about 25 minutes and spends about 11 records, inside the free 1,000.
- If no fact passes the swap test in Step 4, send a short plain email. Do not fake a hook.
Prerequisites
- A free LeadOcean key in
$LEADOCEAN_API_KEY(start free, no card: see pricing). curlandjq.- Any inbox or sending tool. LeadOcean returns data only. It has no sequencer or inbox.
- Related reading: personalizing outbound sales emails and 40 cold email templates. More guides are in the blog hub.
Step 1: Size a narrow segment for free
Narrow beats broad. A tight segment shares a context, and a shared context is what lets one line sound personal. Counting spends no records.
Start with the role, the level, the country and a headcount band. Add a filter only when it changes who you would write to. If the count is under a few hundred, you can read every row. If it is over 20,000, the segment is too broad for a personal line, so cut it by country, headcount or a keyword.
curl -s "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": ["Director"],
"jobFunction": ["Sales & Business Development"],
"country": ["GB"],
"employeeRange": ["51-200"]
}' | jq '.meta'On 2026-10-21 the free MCP tool leadocean_count_leads returned 6,930 people with a verified, catch_all_valid or catch_all email, across 4,642 companies. With no email condition the same filters matched 33,279. Say which number you mean when you quote one.
Step 2: Search with a signal filter
A signal is a reason to write now. The search body is flat JSON, filters are arrays and limit runs 1 to 100. Here the signal is a tool on the company website.
The technologies filter reads what a company runs on its site. Other useful signals in the same body are keywords for text in a person's own profile and minLastFundingYear for recent funding. Funding filters narrow the list at search time, but do not use them in a free count. Pick one signal per campaign so the reason for writing stays the same across the list.
curl -s https://api.leadocean.io/v1/people/search \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "content-type: application/json" \
-d '{
"jobLevel": ["Director"],
"jobFunction": ["Sales & Business Development"],
"country": ["GB"],
"employeeRange": ["51-200"],
"technologies": ["HubSpot"],
"emailStatus": ["verified"],
"limit": 10
}' > people.json
jq -r '.data[] | [.meta.source_ids.person_id,
.contact_data.contact_current_experiences[0].job_title,
.contact_data.contact_current_experiences[0].company_name,
.contact_data.email_status] | @tsv' people.jsonTen rows is ten records. Page on with cursor when you want more. One search pages to 10,000 rows, then meta.depthCapped turns true. Narrow the filters instead of walking deeper.
Step 3: Enrich one person for candidate facts
Search gives title and company. Enrich adds career history: past roles, schools, certifications and awards. Those are facts a stranger would not guess. Enrich takes exactly one of linkedin_url, email, phone or person_id.
PID=$(jq -r '.data[0].meta.source_ids.person_id' people.json)
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\": \"$PID\"}" > person.json
jq '{now: .data.contact_data.contact_current_experiences[0],
past: .data.resume_data.experiences[0:2],
awards: .data.resume_data.awards[0:1]}' person.jsonA person we hold nothing for is a 404 and costs nothing. Past employers carry no company_id yet, so compare them by name. Each section appears only when held.
Read the output before you decide anything. Look for a move between companies, a certification, a published piece, or an award. A long tenure at one employer is also a fact, and a safe one to leave alone. Skip anything that feels like you watched the person. A line that is true but creepy loses the reply.
Step 4: Pick the fact with the swap test
The swap test: paste your line into an email to the next person on the list. If it still reads true, it is a merge tag in disguise.
Fact Swap test Why
Title: Director of Sales Fails Every row has it
Company size: 51-200 Fails The whole segment shares it
Tool on site: HubSpot Weak Shared, but it explains why you write
Past role at a named employer Passes Specific to this career path
Award in resume_data.awards Passes Rare and checkablePick one fact. Use the signal from Step 2 as the reason and the career fact as the opener. Two openers read like a script.
The reason matters as much as the opener. The opener earns the next sentence, and the reason is why the email exists. If you cannot state the reason in one sentence, the email is not ready. A good opener on a weak reason still gets deleted.
Step 5: Write around the one fact
Open with the fact, give one reason, ask one question. Keep it under 90 words.
Subject: {{past_company}} to {{company}}
Hi {{first_name}},
You went from {{past_title}} at {{past_company}} to leading sales at {{company}}.
{{one_line_on_why_hubspot_matters_to_your_offer}}
I work on {{problem}}. Is routing on your list this quarter?
{{your_name}}Fill the variables in your own script or sending tool. Read each email aloud once. If a sentence only works because of a variable, cut it.
Keep the subject line short and lowercase-friendly. Name a place or a company, not a pitch. Avoid fake threads like "Re:" and avoid asking for fifteen minutes in the first email. One question that can be answered in a sentence gets more answers than a calendar link.
Step 6: Check email status before you send
Read contact_data.email_status on every row. We do not refund bounces.
Send only verified rows to start. If you add catch_all rows later, send them from a separate, smaller batch and watch bounces before you scale. Sending volume and domain health are your sending tool's job, so keep each batch small enough to read replies by hand.
jq -r '.data[] | select(.contact_data.email_status=="verified")
| .meta.source_ids.person_id' people.json | wc -lWhat you get
Response shape from the API reference, with placeholder values (not a real person):
{
"success": true,
"data": [
{
"profile_data": { "profile_full_name": "Jane Doe", "profile_headline": "Director of Sales at Acme" },
"contact_data": {
"has_email": true,
"email_status": "verified",
"contact_current_experiences": [
{ "job_title": "Director of Sales", "company_name": "Acme", "company_domain": "acme.com" }
]
},
"meta": { "source_ids": { "person_id": "1234567" } }
}
],
"meta": { "count": 1, "credits": 1, "nextCursor": null }
}Enrich adds resume_data with experiences[], educations[], certifications[] and awards[], each present only when held.
The output of Step 1 on 2026-10-21 is a free count: 6,930 mailable people, 33,279 with no email condition. Steps 2 and 3 spend records. The search page of 10 spends 10 and the enrichment spends 1, so the whole run is 11.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing or wrong x-api-key | Copy the key from app.leadocean.io and export it again |
| 429 | Above the plan rate limit (5 a second on Free, 200 on Agency) | Honor Retry-After, or move to a higher plan |
| 402 | Records spent (Free has 1,000 for life) | Check GET /v1/usage, then pick a plan on pricing |
| Empty results | A bad enum value, such as director instead of Director for jobLevel | Check exact strings with leadocean_list_enum_values |
| 404 on enrich | Nothing held for that person | No cost. Try another person_id |
| Export refused | Over 50,000 rows a request or 500,000 a day | Split the export. Bulk CSV is not on Free |
FAQ
How much personalization is enough?
One true line. More than one reads as research by script, and the extra facts are better saved for the follow-up.
Are first name and company name personalization?
No. Every record has both. Treat them as formatting.
What if the record has no usable fact?
Send a short, plain email that says why you are writing. See the pharma cold email samples for how plain copy can still land.
Can I do this for 1,000 people?
Yes, with a script. Loop over the search page, enrich each person_id, and keep the swap test as a filter. Use the Exports page in the app for bulk CSV on a paid plan.
How many records does this cost?
About 11 here. Counting is free, and an enrichment that finds nobody costs nothing.
Find the one fact that makes your next email worth answering
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →