A personalized sales email is one specific fact about the reader, stated in the first line, that you could only know by looking them up. Pull the fact from data, then write around it. The 11 letters below each use one field.
All examples use placeholders: Acme, acme.com, Jane Doe. Requests go to https://api.leadocean.io with the x-api-key header.
TL;DR
- Pick one field per letter (title, tech, funding, school, past employer), query it, then write the opening line from the value. About 40 minutes for the pull and three drafts.
- Sizing costs 0 records. A first batch of 25 to 100 people costs 25 to 100 records, inside the 1,000 on the Free plan.
- Personalization that needs a lookup (career history, tech stack) beats a first name merge. Skip any letter where the field comes back empty.
Prerequisites
- A LeadOcean key from app.leadocean.io. Free has no card.
- The key in your shell as
$LEADOCEAN_API_KEY. curlandjq.
export LEADOCEAN_API_KEY="paste-your-key-here"Step 1: Size the list before you write a word
Count first. Counting is free on every plan, through the leadocean_count_leads MCP tool or count=true as a query parameter with limit 1 on search.
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":["VP"],"jobFunction":["Sales & Business Development"],"country":["CA"]}'On 21 October 2026 the same filters through the MCP count tool returned 5,574 people. Each has a verified or catch-all email. They work at 3,873 distinct companies. Without the email condition the total is 18,800. Label your own count the same way: it describes the mailable audience, not everyone.
Enum values are exact strings (VP, not vp). Resolve them with leadocean_list_enum_values or GET /v1/enums before you guess.
Step 2: Pull the people with the field you will personalize on
Search body is flat JSON, filters are arrays, limit runs 1 to 100, and you paginate with cursor. Each filter below maps to one or more of the letters.
curl -s 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"],
"emailStatus": ["verified"],
"limit": 25
}' > batch.jsonSwap the filter to change the letter:
technologiesortechnologyCategories: letters 4 and 5.minLastFundingYearplusfundingType: letter 6. Counts with funding filters are not supported, so size the list without them and expect fewer rows at search time.education: letter 9.keywords: letter 10 (a phrase from the person's own profile text).
Step 3: Enrich the people you will write to
Search rows hold the basics. For career history, enrich one person by person_id, which every search row carries at meta.source_ids.person_id. Enrich takes exactly one of linkedin_url, email, phone or person_id. A person we hold nothing on is a 404 and costs nothing.
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}'Since 17 October 2026 the response carries resume_data: past roles, education, certifications, awards and publications, each when held. Past employers carry no company_id yet, so match them by name. The current role is in contact_data.contact_current_experiences[], with job_title, company_name and start_date.
Step 4: The 11 letters
Each letter is under 80 words and uses one field. Write the opening line from the value, keep the ask to one question, and let the subject line name the same fact. Do not stack two fields in one email. The bracketed value is the field you merge in. If the field is empty for a person, do not send that letter to them.
1. Role in the first line
Subject: Acme's sales team Jane, running sales at Acme means you own pipeline and hiring in the same quarter. We hand your reps a verified list by title so neither waits on ops. Worth a look?
Field: job_title and company_name from contact_data.contact_current_experiences[0].
2. Recent job change
Subject: New at Acme Jane, congrats on the move to Acme. New VPs usually rebuild the account list in the first 90 days. We can size yours today for free.
Field: start_date of contact_data.contact_current_experiences[0].
3. Past employer
Subject: From Globex to Acme Jane, you spent years at Globex before Acme. Teams that move between the two often keep the same list-building habit. Here is a faster way.
Field: company_name in resume_data.experiences[].
4. Tech you run
Subject: Acme and your CRM Jane, acme.com loads a CRM we detect on the site. Does your team enrich records after they land in it, or before?
Field: technologies filter, matched through the crawled-site data (about 7.8M sites).
5. Tech category gap
Subject: Email tooling at Acme Jane, we found no marketing automation on acme.com. If you are sending from a spreadsheet, there is a cleaner route.
Field: technologyCategories. A missing tag means not detected, not proven absent. Say "we found" and nothing stronger.
6. Funding round
Subject: After the round Jane, Acme raised a Series A in 2025. Companies at that stage usually add a first outbound hire. Do you have a list ready for them?
Field: fundingType and minLastFundingYear.
7. Headcount band
Subject: 51 to 200 people Jane, at Acme's size one ops person usually owns the CRM and the list. We built the API for that person.
Field: employeeRange.
8. Country or region
Subject: Selling into Canada Jane, we hold verified work emails for VPs of sales in Canada. If that is your next territory, I can send the count.
Field: country, plus the count from Step 1.
9. School
Subject: Fellow alum Jane, I saw you studied at Example University. I went there too and now work on sales data. Short question about your list process.
Field: education, or resume_data.educations[]. Only use this if it is true for you.
10. Something from their profile
Subject: Your note on outbound Jane, your profile mentions outbound for mid-market. One thing we see work for that segment: verified email only, no catch-all.
Field: keywords filter on profile text.
11. Certification or award
Subject: Congrats on the award Jane, saw the award listed on your profile. Reps who win those usually have the cleanest data. Is yours?
Field: resume_data.awards[] or certifications[].
Step 5: Merge and check before you send
Merge in a script, not by hand. Skip empty values.
import json
rows = json.load(open("batch.json"))["data"]
for r in rows:
first = (r.get("profile_data") or {}).get("profile_first_name")
roles = (r.get("contact_data") or {}).get("contact_current_experiences") or [{}]
title, company = roles[0].get("job_title"), roles[0].get("company_name")
if not (first and title and company):
continue
print(f"{first}, running {title} at {company}...")Check contact_data.email_status on each row before sending. There is no refund for bounced emails. Send only verified unless you accept catch-all risk.
What you get
The count in Step 1 is a real run. The enrich response below is the shape from the API reference, with placeholder values.
{
"success": true,
"data": {
"profile_data": { "profile_first_name": "Jane", "profile_full_name": "Jane Doe" },
"contact_data": {
"email_status": "verified",
"contact_emails": [{ "email": "jane@acme.com", "status": "verified" }],
"contact_current_experiences": [
{ "job_title": "VP Sales", "company_name": "Acme", "company_domain": "acme.com", "start_date": "2026-03-01" }
]
},
"resume_data": {
"experiences": [{ "job_title": "Director of Sales", "company_name": "Globex", "start_date": "2021-01-01", "end_date": "2026-02-28" }],
"educations": [{ "educational_establishment": "Example University", "diploma": "BA" }],
"certifications": [],
"awards": []
},
"meta": { "source_ids": { "person_id": "123456789" } }
}
}Response shape from the API reference (openapi.json and the enrich docs, October 2026), with placeholder values. Empty arrays mean we hold nothing for that block.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing or wrong x-api-key | Check $LEADOCEAN_API_KEY is exported in this shell. |
| 429 | Above the plan's rate limit (5 a second on Free, 200 on Agency) | Wait for Retry-After, or move to a plan with a higher limit. |
| 402 | Out of records | The Free plan holds 1,000 for life. Upgrade or narrow the pull. |
Empty data | Bad enum value, such as vp for VP | Resolve values with GET /v1/enums and use the exact string. |
| 404 on enrich | We hold nothing for that person | Free. Skip them and move on. |
| Search stops at 10,000 rows | meta.depthCapped is true | Narrow the filters and run a second search. |
FAQ
How many personalized lines should one email have?
One. A single specific fact in the first sentence does the work. Two or three reads like a dossier.
What if the field is empty for some people?
Send a different letter. Pick the next field, or drop the person from the batch. Never write a guess.
Can I do this without paying?
Yes. Free includes 1,000 records for life, every feature, 5 requests a second and no card. See pricing.
Where do I get more templates?
Pull the field, then write the line. Get your free API key.
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →