Personalization that earns replies is one true, specific line about the recipient. It comes from a field you can point to. Name and company merge tags do not count. This guide builds that line from API data in about 45 minutes and 25 records.
TL;DR
- Size the list for free, pull 25 people whose email status is verified, then enrich the best fits for career history and the address. Total spend is about 50 records, well inside the Free plan's 1,000.
- Write one opener per person from a single field: a past role, a current title, or the company's detected tech. If no field supports a line, skip the person.
- Send in small batches and check
email_statusfirst. Bounces are not refunded, so read the status before you send.
Prerequisites
- A LeadOcean API key from the free plan, exported as
$LEADOCEAN_API_KEY. Free allows 5 requests a second, which is plenty here. curlandjq.- A sending tool you already use (cold email software compared). LeadOcean has no sequencer, so it only supplies the list and the facts.
- Optional: the hosted MCP server at
https://api.leadocean.io/mcp, which lets an assistant run the free sizing calls for you. See the Claude recipe.
Step 1: Pick one narrow audience and size it for free
A narrow audience beats a wide one because every line you write can be specific. Count first. A count spends no records.
The example below is VPs of sales at UK companies with 51 to 200 employees. Enum values are exact strings, so check them with GET /v1/enums or the leadocean_list_enum_values tool before you guess.
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": ["GB"],
"employeeRange": ["51-200"]
}' | jq '.meta'count=true goes in the query string, not the JSON body. meta.total is capped at 100,000. If the number is under about 500, you can personalize the whole list by hand or with a prompt. If it is in the thousands, add a filter.
Step 2: Pull one page of people with verified emails
Pull a page of 25. The search body is flat JSON, filters are arrays, and limit runs 1 to 100.
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": ["GB"],
"employeeRange": ["51-200"],
"emailStatus": ["verified"],
"limit": 25
}' > page1.json
jq '.meta' page1.jsonThis spends 25 records. Filtering on verified only is the strict choice. The default count includes catch_all_valid and catch_all, which are mailable but riskier. Page further with the cursor value from meta if you need more.
Search rows carry flags such as has_email and email_status, not addresses. Each row's ID is at meta.source_ids.person_id; you send it to enrich in Step 3.
Step 3: Enrich the people you will actually write to
Search gives you a title and company. Career history gives you something to say. Since October 2026, person enrich returns resume_data: past roles, education, certifications and awards, each when held.
Enrich takes exactly one of linkedin_url, email, phone or person_id. A person we hold nothing for 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_STEP_2", "reveal_email": true}' > person.json
jq '.data | {current: .contact_data.contact_current_experiences, past: .resume_data.experiences}' person.jsonEnrich only the 25 you have shortlisted, or fewer. Past employers carry no company_id yet, so you cannot join a past role to a company record. Treat the past employer name as text. Each past role carries job_title, company_name, start_date and end_date, per the MCP docs. reveal_email returns the addresses at no extra record.
Step 4: Turn one field into one opener
Rules that keep this honest:
- One field per email. Two facts read as a dossier.
- The fact must be something the person would not mind you knowing: a past role, a school, a published talk. Skip anything that reads as surveillance.
- The opener ties the fact to your offer in one sentence. If you cannot make that link, do not send a personalized line.
A prompt you can run over person.json:
You write the first line of a cold email.
Input: a JSON record for one person.
Use exactly one fact from contact_current_experiences or resume_data.
Pick the fact most relevant to selling {{our_product}} to a VP of Sales.
Rules:
- Max 22 words. No flattery. No "I saw that".
- State the fact plainly, then the reason it matters to them.
- If no fact supports a relevant line, output exactly: SKIP
Output only the line.Here is the pattern with placeholder data. The input says Jane Doe was an account executive at Acme Cloud before becoming VP of Sales at Example Ltd. The output reads:
You carried a quota at Acme Cloud before running sales at Example Ltd, so you know what a rep loses to bad data.
That is a fact plus a reason. For multi-signal openers, see the advanced guide.
Step 5: Check status, batch, and send
Read email_status on every row before it goes to your sending tool. Drop anything that is not verified. Keep first batches small (our rule of thumb: 25 to 50 sends a day per mailbox), so you can see what the replies say before you scale.
Save each enrich response from Step 3 as enriched/PERSON_ID.json, then build the list:
jq -r '.data
| select(.contact_data.email_status == "verified")
| [.profile_data.profile_full_name, .contact_data.contact_emails[0].email] | @csv' \
enriched/*.json > send_list.csvcontact_emails[] is present only when you enriched with reveal_email. Then load send_list.csv and the openers into your sequencer and review ten emails by eye before launch.
Step 6: Read the replies and fix one thing at a time
Change one variable per batch. If you change the audience, the opener and the offer together, you learn nothing.
Keep a plain log: batch, field used, sends, replies, bounces. After three batches you will see which field earns replies for your audience. Past roles tend to work when your product solves a problem the person lived with. Tech detected on the company website works when your offer replaces or sits beside that tool.
batch,audience,field_used,sends,replies,bounces
1,GB VP Sales 51-200,past_role,25,,
2,GB VP Sales 51-200,current_title,25,,Do not publish reply rates you have not measured. Your numbers will differ from anyone else's, and the log is how you find yours.
What you get
A free sizing call on 2026-10-21 with the MCP tool leadocean_count_leads. Filters: VP level, Sales & Business Development, country GB, 51-200 employees.
{
"total": 442,
"totalIsExact": true,
"totalAll": 1349,
"distinctCompanies": 352,
"defaults": "Counted only addresses worth sending to (emailStatus: verified or catch_all_valid or catch_all)",
"credits": 0
}That is 442 people with a mailable email (verified, catch_all_valid or catch_all) across 352 companies, out of 1,349 matching people with or without an email. It is a mailable-email count, not a promise that each address will land.
The enrich output has the shape below. This is the response shape from the API reference, with placeholder values.
{
"success": true,
"data": {
"profile_data": { "profile_full_name": "Jane Doe" },
"contact_data": {
"email_status": "verified",
"contact_emails": [{ "email": "jane@example.co.uk", "type": "work", "status": "verified" }],
"contact_current_experiences": [
{ "job_title": "VP of Sales", "company_name": "Example Ltd" }
]
},
"resume_data": {
"experiences": [
{ "job_title": "Account Executive", "company_name": "Acme Cloud", "start_date": "2016-01-01", "end_date": "2019-06-30" }
],
"educations": []
}
}
}Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing or wrong x-api-key header | Re-export $LEADOCEAN_API_KEY. Keys are created in app.leadocean.io. |
| 429 | Above your plan's rate limit (5 a second on Free, 200 on Agency) | Wait for the Retry-After header, then retry. Add a short sleep between enrich calls. |
| 402 | Out of records. The Free plan's 1,000 are one-off. | Check GET /v1/usage. Upgrade to Solo ($97 a month) to continue. |
Empty data array | Wrong enum value, for example vp instead of VP in jobLevel | List valid values with GET /v1/enums or leadocean_list_enum_values. |
| 404 on enrich | We hold nothing for that person | No charge. Move to the next person. |
| Export refused | Over 50,000 rows a request or 500,000 a day | Split the export into smaller filtered runs. Bulk CSV export is not on Free. |
FAQ
How many records does this cost?
About 50 on the Free plan: 25 for the search page, up to 25 for enrichment. Counting is free, and an enrichment that finds nothing is free.
Does personalization still matter if the list is bad?
No. Read email_status first and send to verified addresses only for the first test. A great opener on a bounced address is wasted, and bounces are not refunded.
When should I skip the custom opener?
When the list is large and the offer is simple. A prompt run over thin data produces generic lines. Use a plain, relevant email for the wide list and save custom openers for the accounts that matter most.
Can I personalize without career history?
Yes. Current title, company size, country, and the technologies detected on the company's website all work as single-fact openers. Pick one and keep it true.
Does LeadOcean send the emails?
No. LeadOcean is the data and API layer. It has an app with an Exports page for CSV download, but no sequencer, dialer or CRM. Use the sending tool you already have.
What plan do I need for a bigger run?
Free covers this test. Solo at $97 a month includes bulk CSV export for one person. Agency at $497 a month is for teams. See pricing.
Write your first specific opener from real data
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →