A personal email names one fact the reader would recognize about themselves. The ten templates below each hang on a single data field, so you fill them from an API call instead of from guesswork.
TL;DR
- Pull a small list with title, company and email status, then enrich a few people for career history.
- It takes about 30 minutes and spends about 30 records, well inside the Free plan's 1,000.
- Each template names the field it needs. If the field is empty for a person, skip that template for that person.
Prerequisites
- A free LeadOcean API key in
$LEADOCEAN_API_KEY. Sign up at app.leadocean.io, no card. Plans are on pricing. curlandjq, or any HTTP client.- A sending tool of your choice. LeadOcean returns data only. It has no sequencer, inbox or CRM.
- Optional: the hosted MCP server at
https://api.leadocean.io/mcpif you would rather run the count from Claude.
Step 1: Size the list before you spend anything
Count first. A count spends no records, so run it as often as you like while you tune the filters.
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": ["Director"],
"jobFunction": ["Sales & Business Development"],
"country": ["US"],
"employeeRange": ["51-200"],
"limit": 1
}' | jq '.meta'count=true goes in the query string, because the JSON body rejects unknown fields. The response has no rows, only meta.total (capped at 100,000), meta.distinctCompanies and meta.countOnly: true.
On 2026-10-21 the same filters through the free MCP tool leadocean_count_leads returned 36,453 people with a verified, catch_all_valid or catch_all email, across 20,473 companies. Without the email condition they matched 119,788. Restricted to verified only, the count was 9,043 people at 6,241 companies. Note which number you quote and label it by the filter you used.
Step 2: Pull 25 people with a verified email
One search page of 25 is 25 records. Ask only for addresses you will send to.
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": ["Director"],
"jobFunction": ["Sales & Business Development"],
"country": ["US"],
"employeeRange": ["51-200"],
"emailStatus": ["verified"],
"limit": 25
}' > people.json
jq -r '.data[] | [.profile_data.profile_full_name, .profile_data.profile_headline, .meta.source_ids.person_id] | @tsv' people.jsonSearch is a flat JSON body, filters are arrays and limit runs 1 to 100. Page on with the meta.nextCursor value passed back as cursor. Search rows carry no contact points: you get the name, headline, email_status and a person_id to enrich.
The filters you searched with are merge data too. Every person in this file is a sales director at a US company of 51 to 200 people. Templates 4 and 10 use exactly that, with no extra call.
Step 3: Enrich five people for career history
Search gives you title and company. Enrich adds past roles, schools, certifications and awards, which are the facts that make a line feel researched. Enrich takes exactly one of linkedin_url, email, phone or person_id.
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": "PERSON_ID_FROM_SEARCH", "reveal_email": true}' > person.json
jq '.data.resume_data | {experiences: .experiences[0:2], educations: .educations[0:1], certifications, awards}' person.jsonPast roles come back in resume_data.experiences[] with title, company name, start and end dates. The current role sits in contact_data.contact_current_experiences[]. Each array appears only when held, so test for it before you merge. A person we hold nothing for is a 404 and costs nothing. Past employers carry no company_id yet, so match them by name.
Enrich five people, not 25. Read what came back, decide which templates you can fill, then enrich more only for the templates that work.
Step 4: Fill the ten templates
Each template needs one field. Keep every email under 90 words and ask one question. Merge fields are in {braces}; rename them to whatever your sending tool uses.
1. Role change Subject: {first_name}, congrats on {new_title}
Needs: contact_current_experiences[] start date, recent.
Body: "Saw you took over {team} at {company} recently. New roles often
bring a tooling review. Worth 10 minutes?"
2. Past employer Subject: From {past_company} to {company}
Needs: resume_data.experiences[].
Body: "You went from {past_title} at {past_company} to {title} here.
How different is the buying process?"
3. School Subject: {school}
Needs: resume_data.educations[]. Use only if you share it.
Body: "Another {school} grad in {function}. Quick question about how
{company} handles {problem}."
4. Company size Subject: Running {function} at {headcount} people
Needs: the employeeRange you searched with.
Body: "At your size, one person often owns {problem} on top of the
day job. Is that you?"
5. Tech stack Subject: {technology} at {company}
Needs: the technologies filter you searched with (crawled sites only).
Body: "Noticed {company} runs {technology}. Does {problem} come up
on your side?"
6. Award Subject: Congrats on {award}
Needs: resume_data.awards[].
Body: "{award} is hard to get. What changed in how your team sold
that year?"
7. Certification Subject: {certification}
Needs: resume_data.certifications[].
Body: "You hold {certification}, so {problem} is likely on your
radar. Can I send a two-line idea?"
8. Peer count Subject: Two others at {company}
Needs: GET /v1/companies/{domain}/people.
Body: "I am writing to {peer_title} too, but you own {function}.
Is that the right split?"
9. Funding Subject: After the {round}
Needs: fundingType at search time (not in free counts).
Body: "Congrats on the {round}. Is {process} on the list for the
next year at {company}?"
10. Plain ask Subject: Quick question, {first_name}
Needs: title and company only.
Body: "I help {title}s at {headcount}-person companies with
{problem}. Is it on your list this quarter?"Merge in your own script or sending tool. Skip any template whose field is empty. Never write a line the data does not support, and never claim a shared school or employer you do not share.
Which field each template trusts
Templates 1, 2, 6 and 7 come from enrich. They cost one record per person and only work when the array is held. Expect gaps, and test for the array before you merge.
Templates 4, 5 and 9 come from the search filters, not the record. If you searched for employeeRange 51-200, every row is in that band, so the line is true for all of them. The same goes for technologies and fundingType.
Template 8 needs a second call: GET /v1/companies/{domain}/people lists the other people at one domain. Each person returned is one record, so use it only on your top accounts. Template 3 needs a school you share. Do not send it otherwise.
Template 10 needs nothing but the search row. It is the control: every other template should beat it, or you drop it.
Writing the personal line
The personal line is the first sentence, and it states the fact plainly. "You went from account executive at Acme to sales director here" works. "I was so impressed by your journey" does not, because it says nothing the reader can check.
Keep the fact and the ask in separate sentences. The fact earns the read. The ask earns the reply. When one sentence tries to do both, it reads like a mail merge.
Do not stack fields. One email with a school, an award and a past employer reads as surveillance, not research. Pick the strongest field for that person and leave the others for a follow-up.
Read every filled email before it goes out for the first batch. Merge errors show up fast: an empty field leaves "at ," in the line, and a job title in capitals reads like a database export. Fix the template, not the email, so the next batch is clean.
How to test them
Send each template to at least 50 people before you judge it. Track replies per template, not per campaign. Keep the subject line and the ask the same, and change only the personal line, so you learn which field earns the reply.
Step 5: Check the email status before sending
Read email_status on every row before it reaches your sending tool. LeadOcean does not refund bounces.
jq -r '.data[] | select(.contact_data.email_status=="verified") | .meta.source_ids.person_id' people.json | wc -lSend to verified first. Treat catch_all_valid and catch_all as a second wave, after you know your bounce rate on the first.
What you get
Output of leadocean_count_leads on 2026-10-21 for jobLevel Director, jobFunction Sales & Business Development, employeeRange 51-200, country US. The total counts people whose best email is verified, catch_all_valid or catch_all.
{
"total": 36453,
"totalIsExact": true,
"totalAll": 119788,
"distinctCompanies": 20473,
"credits": 0
}Response shape from the API reference, with placeholder values. It is not a real person.
{
"success": true,
"data": [
{
"profile_data": {
"profile_full_name": "Jane Doe",
"profile_headline": "Director of Sales at Acme",
"profile_url": "https://www.linkedin.com/in/jane-doe"
},
"contact_data": {
"has_email": true,
"has_phone": false,
"email_status": "verified",
"email_type": "work"
},
"meta": { "source_ids": { "person_id": "1234567890" } }
}
],
"meta": { "count": 1, "limit": 25, "credits": 1, "nextCursor": "CURSOR" }
}Enrich returns the same person under data, plus resume_data with experiences[], educations[], certifications[] and awards[], each present only when held.
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 | 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, or on Free | Split the export. Bulk CSV is not on Free |
FAQ
Does a personal template mean writing each email by hand?
No. Write ten templates once, fill them from data and review the first 20 sends by hand.
Which template should I start with?
Start with number 10 as the control, then add one field-driven template at a time. Compare replies before you add the next.
How many records does this cost?
About 30 here: 25 for the search page and 5 enrichments. Counting costs nothing, and an enrichment that finds nobody costs nothing.
Can I get the data in a spreadsheet instead?
Yes, on a paid plan. The app has an Exports page with a filter builder, a column picker and the record price before you start. Solo is $97 a month and Agency $497 a month.
What else should I read?
Fill your next ten templates with real data
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →